feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -23,7 +23,8 @@ public interface ISttProxyService
|
|||
SttTranscribeRequest request,
|
||||
byte[]? audioBytes = null,
|
||||
string? contentType = null,
|
||||
string? fileName = null
|
||||
string? fileName = null,
|
||||
bool recordUsage = true
|
||||
);
|
||||
|
||||
Task<SttTestResultDto> TestEndpointAsync(int endpointId, string? testApiKey = null, string? testEndpointUrl = null);
|
||||
|
|
@ -35,6 +36,17 @@ public interface ISttProxyService
|
|||
Task<SttUsageReportDto> GetUsageReportAsync();
|
||||
}
|
||||
|
||||
public sealed class SttProviderUnavailableException : Exception
|
||||
{
|
||||
public bool ProviderAttempted { get; }
|
||||
|
||||
public SttProviderUnavailableException(bool providerAttempted)
|
||||
: base("STT service is temporarily unavailable.")
|
||||
{
|
||||
ProviderAttempted = providerAttempted;
|
||||
}
|
||||
}
|
||||
|
||||
public class SttProxyService : ISttProxyService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -54,7 +66,8 @@ public class SttProxyService : ISttProxyService
|
|||
SttTranscribeRequest request,
|
||||
byte[]? audioBytes = null,
|
||||
string? contentType = null,
|
||||
string? fileName = null
|
||||
string? fileName = null,
|
||||
bool recordUsage = true
|
||||
)
|
||||
{
|
||||
// 1. Resolve Audio Bytes
|
||||
|
|
@ -94,23 +107,12 @@ public class SttProxyService : ISttProxyService
|
|||
var candidates = await GetCandidateEndpointsAsync(request.Provider, request.ModelId);
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
// Seed a dynamic fallback endpoint in memory
|
||||
candidates.Add(new SttProviderEndpoint
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Default Groq Whisper Fallback",
|
||||
ProviderType = "groq",
|
||||
EndpointUrl = "https://api.groq.com/openai/v1/audio/transcriptions",
|
||||
ModelId = "whisper-large-v3-turbo",
|
||||
Method = "multipart",
|
||||
Language = request.Language ?? "ko",
|
||||
CostPerMinute = 0.000500m,
|
||||
IsActive = true
|
||||
});
|
||||
throw new SttProviderUnavailableException(providerAttempted: false);
|
||||
}
|
||||
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
Exception? lastException = null;
|
||||
var providerAttempted = false;
|
||||
|
||||
// 3. Failover Execution Chain
|
||||
foreach (var endpoint in candidates)
|
||||
|
|
@ -118,6 +120,14 @@ public class SttProxyService : ISttProxyService
|
|||
var epSw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
if (!HasRequiredProviderConfiguration(endpoint))
|
||||
{
|
||||
_logger.LogWarning("Skipping STT provider {Provider}: required credentials are not configured",
|
||||
endpoint.ProviderType);
|
||||
continue;
|
||||
}
|
||||
|
||||
providerAttempted = true;
|
||||
_logger.LogInformation("Attempting STT transcription via provider {Provider} ({Name}, Model: {Model})",
|
||||
endpoint.ProviderType, endpoint.Name, endpoint.ModelId);
|
||||
|
||||
|
|
@ -136,29 +146,33 @@ public class SttProxyService : ISttProxyService
|
|||
var durationMinutes = (decimal)(finalDuration / 60.0);
|
||||
var cost = Math.Max(0.000001m, durationMinutes * endpoint.CostPerMinute);
|
||||
|
||||
// Record Usage Log
|
||||
try
|
||||
// Supabase stt-proxy owns user quota and usage for internal
|
||||
// provider calls. Direct legacy callers may still record here.
|
||||
if (recordUsage)
|
||||
{
|
||||
var usageLog = new SttUsageLog
|
||||
try
|
||||
{
|
||||
UserId = userId,
|
||||
UserEmail = userEmail,
|
||||
EndpointId = endpoint.Id,
|
||||
Provider = endpoint.ProviderType,
|
||||
ModelId = endpoint.ModelId,
|
||||
AudioDurationSeconds = Math.Round(finalDuration, 2),
|
||||
CalculatedCost = cost,
|
||||
LatencyMs = (int)epSw.ElapsedMilliseconds,
|
||||
StatusCode = 200,
|
||||
TranscriptPreview = transcript.Length > 200 ? transcript.Substring(0, 200) + "..." : transcript,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
_db.SttUsageLogs.Add(usageLog);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception dbEx)
|
||||
{
|
||||
_logger.LogWarning(dbEx, "Failed to save STT usage log to database");
|
||||
var usageLog = new SttUsageLog
|
||||
{
|
||||
UserId = userId,
|
||||
UserEmail = userEmail,
|
||||
EndpointId = endpoint.Id,
|
||||
Provider = endpoint.ProviderType,
|
||||
ModelId = endpoint.ModelId,
|
||||
AudioDurationSeconds = Math.Round(finalDuration, 2),
|
||||
CalculatedCost = cost,
|
||||
LatencyMs = (int)epSw.ElapsedMilliseconds,
|
||||
StatusCode = 200,
|
||||
TranscriptPreview = transcript.Length > 200 ? transcript.Substring(0, 200) + "..." : transcript,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
_db.SttUsageLogs.Add(usageLog);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception dbEx)
|
||||
{
|
||||
_logger.LogWarning(dbEx, "Failed to save STT usage log to database");
|
||||
}
|
||||
}
|
||||
|
||||
return new SttTranscribeResponse(
|
||||
|
|
@ -176,18 +190,19 @@ public class SttProxyService : ISttProxyService
|
|||
{
|
||||
epSw.Stop();
|
||||
lastException = ex;
|
||||
_logger.LogWarning(ex, "Provider {Provider} ({Name}) transcription failed after {Ms}ms. Trying fallback...",
|
||||
endpoint.ProviderType, endpoint.Name, epSw.ElapsedMilliseconds);
|
||||
var failureType = GetSafeFailureType(ex);
|
||||
_logger.LogWarning("Provider {Provider} transcription failed after {Ms}ms ({FailureType}). Trying fallback...",
|
||||
endpoint.ProviderType, epSw.ElapsedMilliseconds, failureType);
|
||||
|
||||
// Record error to ErrorLogs
|
||||
try
|
||||
{
|
||||
_db.ErrorLogs.Add(new ServerErrorLog
|
||||
{
|
||||
ErrorType = $"SttProviderError:{endpoint.ProviderType}",
|
||||
Message = $"STT failed on endpoint {endpoint.Name} ({endpoint.EndpointUrl}): {ex.Message}",
|
||||
StackTrace = ex.StackTrace,
|
||||
Endpoint = endpoint.EndpointUrl,
|
||||
ErrorType = "SttProviderError",
|
||||
Message = $"STT provider request failed ({failureType}).",
|
||||
StackTrace = null,
|
||||
Endpoint = null,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
|
@ -200,19 +215,9 @@ public class SttProxyService : ISttProxyService
|
|||
}
|
||||
|
||||
totalSw.Stop();
|
||||
_logger.LogError(lastException, "All STT candidate endpoints failed. Total duration: {Ms}ms", totalSw.ElapsedMilliseconds);
|
||||
|
||||
// Standalone graceful mock echo response if no cloud API keys configured or network is isolated
|
||||
return new SttTranscribeResponse(
|
||||
Text: $"[D3RO Cloud STT — Voice Transcribed] 음성 전사 완료 ({effectiveFileName}, {Math.Round(durationSeconds, 1)}초)",
|
||||
Confidence: 0.95,
|
||||
Language: request.Language ?? "ko",
|
||||
DurationSeconds: Math.Round(durationSeconds, 2),
|
||||
Provider: "fallback-local",
|
||||
ModelId: "whisper-local",
|
||||
LatencyMs: totalSw.ElapsedMilliseconds,
|
||||
Cost: 0m
|
||||
);
|
||||
_logger.LogError("All STT candidate endpoints failed. Total duration: {Ms}ms; last failure type: {FailureType}",
|
||||
totalSw.ElapsedMilliseconds, GetSafeFailureType(lastException));
|
||||
throw new SttProviderUnavailableException(providerAttempted);
|
||||
}
|
||||
|
||||
private async Task<(string Transcript, double Confidence, string? Language, double Duration)> ExecuteProviderTranscriptionAsync(
|
||||
|
|
@ -229,15 +234,9 @@ public class SttProxyService : ISttProxyService
|
|||
var providerType = endpoint.ProviderType.ToLowerInvariant();
|
||||
var apiKey = endpoint.ApiKey?.Trim() ?? "";
|
||||
|
||||
// If no API key configured or local mock
|
||||
if (string.IsNullOrWhiteSpace(apiKey) && providerType != "local-sidecar" && providerType != "custom")
|
||||
if (RequiresApiKey(providerType) && string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
return (
|
||||
$"[D3RO Online Cloud STT - {endpoint.Name}] 음성 인식이 성공적으로 처리되었습니다.",
|
||||
0.99,
|
||||
request.Language ?? endpoint.Language ?? "ko",
|
||||
EstimateAudioDuration(audioBytes, contentType)
|
||||
);
|
||||
throw new InvalidOperationException("STT provider credentials are not configured.");
|
||||
}
|
||||
|
||||
switch (providerType)
|
||||
|
|
@ -325,17 +324,17 @@ public class SttProxyService : ISttProxyService
|
|||
|
||||
if (!httpResponse.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"STT Upstream {endpoint.ProviderType} returned HTTP {httpResponse.StatusCode}: {responseBody}");
|
||||
throw new HttpRequestException($"STT upstream returned HTTP {httpResponse.StatusCode}.");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var text = "";
|
||||
if (root.TryGetProperty("text", out var textProp))
|
||||
if (!root.TryGetProperty("text", out var textProp) || textProp.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
text = textProp.GetString() ?? "";
|
||||
throw new JsonException("STT upstream response did not contain a transcript.");
|
||||
}
|
||||
var text = textProp.GetString() ?? "";
|
||||
|
||||
var duration = 0.0;
|
||||
if (root.TryGetProperty("duration", out var durProp))
|
||||
|
|
@ -385,28 +384,29 @@ public class SttProxyService : ISttProxyService
|
|||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Deepgram returned HTTP {response.StatusCode}: {responseBody}");
|
||||
throw new HttpRequestException($"STT upstream returned HTTP {response.StatusCode}.");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var transcript = "";
|
||||
string transcript;
|
||||
var confidence = 0.95;
|
||||
var duration = 0.0;
|
||||
|
||||
if (root.TryGetProperty("results", out var results) &&
|
||||
results.TryGetProperty("channels", out var channels) &&
|
||||
channels.GetArrayLength() > 0)
|
||||
if (!root.TryGetProperty("results", out var results) ||
|
||||
!results.TryGetProperty("channels", out var channels) ||
|
||||
channels.GetArrayLength() == 0 ||
|
||||
!channels[0].TryGetProperty("alternatives", out var alternatives) ||
|
||||
alternatives.GetArrayLength() == 0 ||
|
||||
!alternatives[0].TryGetProperty("transcript", out var transcriptProperty) ||
|
||||
transcriptProperty.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
var ch0 = channels[0];
|
||||
if (ch0.TryGetProperty("alternatives", out var alts) && alts.GetArrayLength() > 0)
|
||||
{
|
||||
var alt0 = alts[0];
|
||||
if (alt0.TryGetProperty("transcript", out var tProp)) transcript = tProp.GetString() ?? "";
|
||||
if (alt0.TryGetProperty("confidence", out var cProp)) confidence = cProp.GetDouble();
|
||||
}
|
||||
throw new JsonException("STT upstream response did not contain a transcript.");
|
||||
}
|
||||
transcript = transcriptProperty.GetString() ?? "";
|
||||
if (alternatives[0].TryGetProperty("confidence", out var confidenceProperty))
|
||||
confidence = confidenceProperty.GetDouble();
|
||||
|
||||
if (root.TryGetProperty("metadata", out var meta) && meta.TryGetProperty("duration", out var dProp))
|
||||
{
|
||||
|
|
@ -431,9 +431,6 @@ public class SttProxyService : ISttProxyService
|
|||
if (endpoint.EndpointUrl.Contains("generativelanguage.googleapis.com") || endpoint.ModelId.Contains("gemini"))
|
||||
{
|
||||
var apiKey = endpoint.ApiKey;
|
||||
var url = endpoint.EndpointUrl.Contains("?")
|
||||
? $"{endpoint.EndpointUrl}&key={apiKey}"
|
||||
: $"{endpoint.EndpointUrl}?key={apiKey}";
|
||||
|
||||
var geminiPayload = new
|
||||
{
|
||||
|
|
@ -460,32 +457,33 @@ public class SttProxyService : ISttProxyService
|
|||
}
|
||||
};
|
||||
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, endpoint.EndpointUrl)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(geminiPayload), Encoding.UTF8, "application/json")
|
||||
};
|
||||
httpRequest.Headers.Add("X-Goog-Api-Key", apiKey);
|
||||
|
||||
var response = await client.SendAsync(httpRequest);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Google Gemini STT returned HTTP {response.StatusCode}: {responseBody}");
|
||||
throw new HttpRequestException($"STT upstream returned HTTP {response.StatusCode}.");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody);
|
||||
var root = doc.RootElement;
|
||||
var text = "";
|
||||
if (root.TryGetProperty("candidates", out var cands) && cands.GetArrayLength() > 0)
|
||||
if (!root.TryGetProperty("candidates", out var cands) ||
|
||||
cands.GetArrayLength() == 0 ||
|
||||
!cands[0].TryGetProperty("content", out var content) ||
|
||||
!content.TryGetProperty("parts", out var parts) ||
|
||||
parts.GetArrayLength() == 0 ||
|
||||
!parts[0].TryGetProperty("text", out var textProperty) ||
|
||||
textProperty.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
var cand0 = cands[0];
|
||||
if (cand0.TryGetProperty("content", out var content) &&
|
||||
content.TryGetProperty("parts", out var parts) &&
|
||||
parts.GetArrayLength() > 0)
|
||||
{
|
||||
text = parts[0].GetProperty("text").GetString() ?? "";
|
||||
}
|
||||
throw new JsonException("STT upstream response did not contain a transcript.");
|
||||
}
|
||||
var text = textProperty.GetString() ?? "";
|
||||
|
||||
return (text.Trim(), 0.98, lang, EstimateAudioDuration(audioBytes, contentType));
|
||||
}
|
||||
|
|
@ -493,9 +491,6 @@ public class SttProxyService : ISttProxyService
|
|||
{
|
||||
// Google Cloud Speech-to-Text v1
|
||||
var apiKey = endpoint.ApiKey;
|
||||
var url = endpoint.EndpointUrl.Contains("?")
|
||||
? $"{endpoint.EndpointUrl}&key={apiKey}"
|
||||
: $"{endpoint.EndpointUrl}?key={apiKey}";
|
||||
|
||||
var gcpPayload = new
|
||||
{
|
||||
|
|
@ -512,17 +507,18 @@ public class SttProxyService : ISttProxyService
|
|||
}
|
||||
};
|
||||
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, endpoint.EndpointUrl)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(gcpPayload), Encoding.UTF8, "application/json")
|
||||
};
|
||||
httpRequest.Headers.Add("X-Goog-Api-Key", apiKey);
|
||||
|
||||
var response = await client.SendAsync(httpRequest);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Google Cloud STT returned HTTP {response.StatusCode}: {responseBody}");
|
||||
throw new HttpRequestException($"STT upstream returned HTTP {response.StatusCode}.");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody);
|
||||
|
|
@ -530,7 +526,12 @@ public class SttProxyService : ISttProxyService
|
|||
var transcript = "";
|
||||
var confidence = 0.95;
|
||||
|
||||
if (root.TryGetProperty("results", out var results) && results.GetArrayLength() > 0)
|
||||
if (!root.TryGetProperty("results", out var results) || results.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw new JsonException("STT upstream response did not contain transcription results.");
|
||||
}
|
||||
|
||||
if (results.GetArrayLength() > 0)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var res in results.EnumerateArray())
|
||||
|
|
@ -568,7 +569,7 @@ public class SttProxyService : ISttProxyService
|
|||
var uploadBody = await uploadResp.Content.ReadAsStringAsync();
|
||||
if (!uploadResp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"AssemblyAI upload failed: {uploadBody}");
|
||||
throw new HttpRequestException($"STT upstream upload returned HTTP {uploadResp.StatusCode}.");
|
||||
}
|
||||
|
||||
using var uploadDoc = JsonDocument.Parse(uploadBody);
|
||||
|
|
@ -594,7 +595,7 @@ public class SttProxyService : ISttProxyService
|
|||
var transBody = await transResp.Content.ReadAsStringAsync();
|
||||
if (!transResp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"AssemblyAI transcript job failed: {transBody}");
|
||||
throw new HttpRequestException($"STT upstream job returned HTTP {transResp.StatusCode}.");
|
||||
}
|
||||
|
||||
using var transDoc = JsonDocument.Parse(transBody);
|
||||
|
|
@ -610,12 +611,22 @@ public class SttProxyService : ISttProxyService
|
|||
var pollResp = await client.SendAsync(pollReq);
|
||||
var pollBody = await pollResp.Content.ReadAsStringAsync();
|
||||
|
||||
if (!pollResp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"STT upstream poll returned HTTP {pollResp.StatusCode}.");
|
||||
}
|
||||
|
||||
using var pollDoc = JsonDocument.Parse(pollBody);
|
||||
var status = pollDoc.RootElement.GetProperty("status").GetString();
|
||||
|
||||
if (status == "completed")
|
||||
{
|
||||
var text = pollDoc.RootElement.GetProperty("text").GetString() ?? "";
|
||||
if (!pollDoc.RootElement.TryGetProperty("text", out var textProperty) ||
|
||||
textProperty.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw new JsonException("STT upstream response did not contain a transcript.");
|
||||
}
|
||||
var text = textProperty.GetString() ?? "";
|
||||
var confidence = 0.95;
|
||||
if (pollDoc.RootElement.TryGetProperty("confidence", out var c)) confidence = c.GetDouble();
|
||||
var duration = 0.0;
|
||||
|
|
@ -624,8 +635,7 @@ public class SttProxyService : ISttProxyService
|
|||
}
|
||||
if (status == "error")
|
||||
{
|
||||
var err = pollDoc.RootElement.TryGetProperty("error", out var e) ? e.GetString() : "Unknown error";
|
||||
throw new HttpRequestException($"AssemblyAI processing error: {err}");
|
||||
throw new HttpRequestException("STT upstream processing failed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -663,24 +673,32 @@ public class SttProxyService : ISttProxyService
|
|||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Azure Speech STT returned HTTP {response.StatusCode}: {responseBody}");
|
||||
throw new HttpRequestException($"STT upstream returned HTTP {response.StatusCode}.");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody);
|
||||
var root = doc.RootElement;
|
||||
var text = "";
|
||||
string text;
|
||||
var confidence = 0.95;
|
||||
|
||||
if (root.TryGetProperty("DisplayText", out var dt))
|
||||
if (root.TryGetProperty("DisplayText", out var dt) && dt.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
text = dt.GetString() ?? "";
|
||||
}
|
||||
else if (root.TryGetProperty("NBest", out var nbest) && nbest.GetArrayLength() > 0)
|
||||
{
|
||||
var best = nbest[0];
|
||||
if (best.TryGetProperty("Display", out var d)) text = d.GetString() ?? "";
|
||||
if (!best.TryGetProperty("Display", out var display) || display.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw new JsonException("STT upstream response did not contain a transcript.");
|
||||
}
|
||||
text = display.GetString() ?? "";
|
||||
if (best.TryGetProperty("Confidence", out var c)) confidence = c.GetDouble();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new JsonException("STT upstream response did not contain a transcript.");
|
||||
}
|
||||
|
||||
return (text, confidence, lang, EstimateAudioDuration(audioBytes, contentType));
|
||||
}
|
||||
|
|
@ -717,14 +735,17 @@ public class SttProxyService : ISttProxyService
|
|||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Local STT Sidecar returned HTTP {response.StatusCode}: {responseBody}");
|
||||
throw new HttpRequestException($"STT upstream returned HTTP {response.StatusCode}.");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var text = "";
|
||||
if (root.TryGetProperty("text", out var tProp)) text = tProp.GetString() ?? "";
|
||||
if (!root.TryGetProperty("text", out var tProp) || tProp.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw new JsonException("STT upstream response did not contain a transcript.");
|
||||
}
|
||||
var text = tProp.GetString() ?? "";
|
||||
var duration = EstimateAudioDuration(audioBytes, contentType);
|
||||
if (root.TryGetProperty("duration", out var dProp)) duration = dProp.GetDouble();
|
||||
|
||||
|
|
@ -789,7 +810,7 @@ public class SttProxyService : ISttProxyService
|
|||
sw.Stop();
|
||||
return new SttTestResultDto(
|
||||
Success: false,
|
||||
Message: $"연결 실패: {ex.Message}",
|
||||
Message: $"연결 실패: {GetSafeFailureType(ex)}",
|
||||
LatencyMs: sw.ElapsedMilliseconds,
|
||||
TranscriptPreview: null,
|
||||
Provider: endpoint.ProviderType,
|
||||
|
|
@ -980,6 +1001,32 @@ public class SttProxyService : ISttProxyService
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static bool HasRequiredProviderConfiguration(SttProviderEndpoint endpoint)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(endpoint.EndpointUrl)) return false;
|
||||
|
||||
var providerType = endpoint.ProviderType.Trim().ToLowerInvariant();
|
||||
return !RequiresApiKey(providerType) || !string.IsNullOrWhiteSpace(endpoint.ApiKey);
|
||||
}
|
||||
|
||||
private static bool RequiresApiKey(string providerType)
|
||||
{
|
||||
return providerType is "groq" or "openai" or "deepgram" or "google" or "assemblyai" or "azure";
|
||||
}
|
||||
|
||||
private static string GetSafeFailureType(Exception? exception)
|
||||
{
|
||||
return exception switch
|
||||
{
|
||||
TimeoutException or TaskCanceledException => "timeout",
|
||||
HttpRequestException => "upstream_http_error",
|
||||
JsonException => "invalid_upstream_response",
|
||||
InvalidOperationException => "provider_not_configured",
|
||||
null => "not_configured",
|
||||
_ => "provider_error"
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplyExtraHeaders(HttpRequestMessage req, string? extraHeadersJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(extraHeadersJson)) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue