using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using DramaLing.Api.Models.DTOs; using DramaLing.Api.Contracts.Services.Speech; using DramaLing.Api.Services; namespace DramaLing.Api.Controllers; [Route("api/speech")] [AllowAnonymous] // 暫時開放測試,之後可以加上認證 [ApiExplorerSettings(IgnoreApi = true)] // 暫時從 Swagger 排除,避免 IFormFile 相關問題 public class SpeechController : BaseController { private readonly IPronunciationAssessmentService _assessmentService; public SpeechController( IPronunciationAssessmentService assessmentService, IAuthService authService, ILogger logger) : base(logger, authService) { _assessmentService = assessmentService; } /// /// 發音評估 - 上傳音頻檔案並獲得 AI 發音評估結果 /// /// 音頻檔案 (WAV/WebM/MP3 格式,最大 10MB) /// 參考文本 - 用戶應該說出的目標句子 /// 詞卡 ID /// 語言代碼 (預設: en-US) /// 包含準確度、流暢度等多維度評分的評估結果 [HttpPost("pronunciation-assessment")] [Consumes("multipart/form-data")] [ProducesResponseType(typeof(PronunciationResult), 200)] [ProducesResponseType(400)] [ProducesResponseType(500)] public async Task EvaluatePronunciation( [FromForm] IFormFile audio, [FromForm] string referenceText, [FromForm] string flashcardId, [FromForm] string language = "en-US") { try { // 1. 驗證請求 if (audio == null || audio.Length == 0) { return ErrorResponse("AUDIO_REQUIRED", "音頻檔案不能為空", null, 400); } if (audio.Length > 10 * 1024 * 1024) // 10MB 限制 { return ErrorResponse("AUDIO_TOO_LARGE", "音頻檔案過大,請限制在 10MB 以內", new { maxSize = "10MB", actualSize = $"{audio.Length / 1024 / 1024}MB" }, 400); } if (string.IsNullOrWhiteSpace(referenceText)) { return ErrorResponse("REFERENCE_TEXT_REQUIRED", "參考文本不能為空", null, 400); } if (string.IsNullOrWhiteSpace(flashcardId)) { return ErrorResponse("FLASHCARD_ID_REQUIRED", "詞卡 ID 不能為空", null, 400); } // 2. 驗證音頻格式 var contentType = audio.ContentType?.ToLowerInvariant(); var allowedTypes = new[] { "audio/wav", "audio/webm", "audio/mp3", "audio/mpeg", "audio/ogg" }; if (string.IsNullOrEmpty(contentType) || !allowedTypes.Contains(contentType)) { return ErrorResponse("INVALID_AUDIO_FORMAT", "不支援的音頻格式", new { supportedFormats = allowedTypes }, 400); } // 3. 驗證音頻時長 (簡單檢查檔案大小作為時長估算) if (audio.Length < 1000) // 小於 1KB 可能太短 { return ErrorResponse("AUDIO_TOO_SHORT", "錄音時間太短,請至少錄製 1 秒", new { minDuration = "1秒" }, 400); } _logger.LogInformation("開始處理發音評估: FlashcardId={FlashcardId}, Size={Size}MB", flashcardId, audio.Length / 1024.0 / 1024.0); // 4. 處理音頻流並呼叫 Azure Speech Services using var audioStream = audio.OpenReadStream(); var result = await _assessmentService.EvaluatePronunciationAsync( audioStream, referenceText, flashcardId, language); _logger.LogInformation("發音評估完成: Score={Score}, ProcessingTime={Time}ms", result.Scores.Overall, result.ProcessingTime); return SuccessResponse(result, "發音評估完成"); } catch (InvalidOperationException ex) { _logger.LogWarning(ex, "發音評估業務邏輯錯誤: FlashcardId={FlashcardId}", flashcardId); return ErrorResponse("SPEECH_PROCESSING_ERROR", ex.Message, null, 400); } catch (Exception ex) { _logger.LogError(ex, "發音評估系統錯誤: FlashcardId={FlashcardId}", flashcardId); return ErrorResponse("INTERNAL_ERROR", "發音評估失敗,請稍後再試", null, 500); } } /// /// 檢查語音服務狀態 /// /// Azure Speech Services 的可用性狀態 [HttpGet("service-status")] [ProducesResponseType(typeof(object), 200)] [ProducesResponseType(500)] public async Task GetServiceStatus() { try { var isAvailable = await _assessmentService.IsServiceAvailableAsync(); var status = new { IsAvailable = isAvailable, ServiceName = "Azure Speech Services", CheckTime = DateTime.UtcNow, Message = isAvailable ? "服務正常運行" : "服務不可用" }; return SuccessResponse(status); } catch (Exception ex) { _logger.LogError(ex, "檢查語音服務狀態時發生錯誤"); return ErrorResponse("SERVICE_CHECK_ERROR", "無法檢查服務狀態", null, 500); } } }