dramaling-vocab-learning/backend/DramaLing.Api/Controllers/SpeechController.cs

138 lines
5.6 KiB
C#

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<SpeechController> logger) : base(logger, authService)
{
_assessmentService = assessmentService;
}
/// <summary>
/// 發音評估 - 上傳音頻檔案並獲得 AI 發音評估結果
/// </summary>
/// <param name="audio">音頻檔案 (WAV/WebM/MP3 格式,最大 10MB)</param>
/// <param name="referenceText">參考文本 - 用戶應該說出的目標句子</param>
/// <param name="flashcardId">詞卡 ID</param>
/// <param name="language">語言代碼 (預設: en-US)</param>
/// <returns>包含準確度、流暢度等多維度評分的評估結果</returns>
[HttpPost("pronunciation-assessment")]
[Consumes("multipart/form-data")]
[ProducesResponseType(typeof(PronunciationResult), 200)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public async Task<IActionResult> 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);
}
}
/// <summary>
/// 檢查語音服務狀態
/// </summary>
/// <returns>Azure Speech Services 的可用性狀態</returns>
[HttpGet("service-status")]
[ProducesResponseType(typeof(object), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> 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);
}
}
}