using System; using System.IO; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using D3ROVoice.Api.Dtos; using D3ROVoice.Api.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; namespace D3ROVoice.Api.Controllers; [ApiController] [Authorize] [Route("api/[controller]")] public class SttController : ControllerBase { private const string InternalGatewayHeader = "X-D3RO-STT-Gateway-Token"; private readonly ISttProxyService _sttService; private readonly IConfiguration _configuration; public SttController(ISttProxyService sttService, IConfiguration configuration) { _sttService = sttService; _configuration = configuration; } [HttpPost("transcribe")] [Consumes("application/json", "multipart/form-data")] public Task Transcribe() { // User-facing transcription is exclusively handled by the Supabase // stt-proxy, which owns authenticated identity, quota reservation and // usage persistence. This legacy provider path must not bypass it. return Task.FromResult(StatusCode( StatusCodes.Status410Gone, new { error = "stt_edge_gateway_required" })); } [HttpPost("internal/transcribe")] [AllowAnonymous] [Consumes("multipart/form-data")] [RequestSizeLimit(26 * 1024 * 1024)] public async Task TranscribeFromQuotaGateway() { var configuredToken = _configuration["D3RO_API_TOKEN"]?.Trim() ?? string.Empty; if (Encoding.UTF8.GetByteCount(configuredToken) < 32) { return StatusCode(StatusCodes.Status503ServiceUnavailable, new { error = "stt_gateway_not_configured" }); } var suppliedToken = Request.Headers[InternalGatewayHeader].ToString(); if (!FixedTimeTokenEquals(configuredToken, suppliedToken)) { return Unauthorized(new { error = "stt_gateway_unauthorized" }); } if (!Request.HasFormContentType) { return StatusCode(StatusCodes.Status415UnsupportedMediaType, new { error = "unsupported_media_type" }); } var form = await Request.ReadFormAsync(); var file = form.Files.GetFile("file") ?? form.Files.GetFile("audio"); if (file == null || file.Length == 0) { return BadRequest(new { error = "missing_audio" }); } await using var memoryStream = new MemoryStream(); await file.CopyToAsync(memoryStream); var language = form["language"].ToString(); var prompt = form["prompt"].ToString(); var request = new SttTranscribeRequest( AudioBase64: null, Language: string.IsNullOrWhiteSpace(language) ? "ko" : language, InitialPrompt: string.IsNullOrWhiteSpace(prompt) ? null : prompt, ModelId: null, Provider: null); try { var result = await _sttService.TranscribeAsync( userId: 0, userEmail: "edge-internal", request, memoryStream.ToArray(), file.ContentType ?? "application/octet-stream", file.FileName ?? "recording.bin", recordUsage: false); return Ok(result); } catch (SttProviderUnavailableException) { return StatusCode(StatusCodes.Status503ServiceUnavailable, new { error = "stt_provider_unavailable" }); } catch (ArgumentException) { return BadRequest(new { error = "invalid_audio" }); } catch { return StatusCode(StatusCodes.Status502BadGateway, new { error = "stt_upstream_failed" }); } } private static bool FixedTimeTokenEquals(string configured, string supplied) { var configuredDigest = SHA256.HashData(Encoding.UTF8.GetBytes(configured)); var suppliedDigest = SHA256.HashData(Encoding.UTF8.GetBytes(supplied)); return CryptographicOperations.FixedTimeEquals(configuredDigest, suppliedDigest); } [HttpGet("providers")] [Authorize(Policy = "ManagerOrAbove")] public async Task GetActiveProviders() { var endpoints = await _sttService.GetAllEndpointsAsync(); return Ok(endpoints); } [HttpPost("test")] [Authorize(Policy = "ManagerOrAbove")] public async Task TestConnection([FromQuery] int endpointId = 0) { var result = await _sttService.TestEndpointAsync(endpointId); return Ok(result); } }