DeutschLernen/GermanApp/Presentation/Controllers/TtsController.cs
Lasse Rune Hansen 769c63b005
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat(backend/ai-services): Complete AI Services Phase 0-4 implementation
- Add Domain interfaces: IMistralService, IVoskService, ITtsService
- Add Configuration classes: VoskConfig, CoquiConfig
- Add Application service: MistralService (text generation with Mistral)
- Add Infrastructure services: VoskService (speech recognition), TtsService (TTS)
- Add Presentation controllers: MistralController, SpeechController, TtsController
- Fix MistralService to use correct IMistralConnector methods (CompleteAsync, ChatAsync)
- Fix TtsController to use record constructor syntax
- Fix TtsService Task.FromResult type specification
- Fix Program.cs service registration and remove merge conflict markers
- Update docs/features/ai-services.md with progress (Phases 0-4 complete)
- Update docs/ROADMAP.md with AI Services status and test metrics (296 tests)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-13 10:09:19 +02:00

193 lines
5.6 KiB
C#

using GermanApp.Domain.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace GermanApp.Presentation.Controllers;
/// <summary>
/// API controller for text-to-speech using Coqui TTS.
/// This is part of the Presentation layer.
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class TtsController : ControllerBase
{
private readonly ITtsService _ttsService;
public TtsController(ITtsService ttsService)
{
_ttsService = ttsService;
}
/// <summary>
/// Generates audio from text.
/// </summary>
/// <param name="request">TTS generation request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Audio file as bytes</returns>
[HttpPost("generate")]
[RequestSizeLimit(5_000_000)] // 5MB limit for response
public async Task<IActionResult> GenerateAudio(
[FromBody] TtsGenerationRequest request,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(request.Text))
return BadRequest("Text is required");
try
{
var audioBytes = await _ttsService.GenerateAudioAsync(
request.Text,
request.Speaker,
request.Language,
cancellationToken);
// Return audio as WAV or specified format
var contentType = GetContentType(request.Format);
return File(audioBytes, contentType);
}
catch (TimeoutException ex)
{
return StatusCode(504, new { Error = ex.Message });
}
catch (Exception ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
/// <summary>
/// Generates audio and returns a file URL.
/// </summary>
/// <param name="request">TTS generation request with filename</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Audio file URL</returns>
[HttpPost("generate-file")]
public async Task<IActionResult> GenerateAudioToFile(
[FromBody] TtsFileGenerationRequest request,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(request.Text))
return BadRequest("Text is required");
if (string.IsNullOrWhiteSpace(request.Filename))
return BadRequest("Filename is required");
try
{
var outputPath = Path.Combine("audio", "tts", request.Filename);
await _ttsService.GenerateAudioToFileAsync(
request.Text,
outputPath,
request.Speaker,
request.Language,
cancellationToken);
return Ok(new TtsFileResponse(
Url: $"/audio/tts/{request.Filename}",
Filename: request.Filename));
}
catch (Exception ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
/// <summary>
/// Tests the TTS service.
/// </summary>
/// <returns>Health check result</returns>
[HttpGet("health")]
[AllowAnonymous]
public async Task<IActionResult> HealthCheck(CancellationToken cancellationToken = default)
{
try
{
var isHealthy = await _ttsService.TestModelAsync(cancellationToken);
return Ok(new { Healthy = isHealthy });
}
catch (Exception ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
/// <summary>
/// Gets information about the current TTS model.
/// </summary>
/// <returns>Model information</returns>
[HttpGet("model-info")]
[AllowAnonymous]
public async Task<IActionResult> GetModelInfo(CancellationToken cancellationToken = default)
{
try
{
var (modelName, modelPath) = await _ttsService.GetModelInfoAsync();
return Ok(new { ModelName = modelName, ModelPath = modelPath });
}
catch (Exception ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
/// <summary>
/// Gets list of available voices/speakers.
/// </summary>
/// <returns>List of speaker IDs</returns>
[HttpGet("speakers")]
[AllowAnonymous]
public async Task<IActionResult> GetSpeakers(CancellationToken cancellationToken = default)
{
try
{
var speakers = await _ttsService.GetAvailableSpeakersAsync(cancellationToken);
return Ok(new { Speakers = speakers });
}
catch (Exception ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
/// <summary>
/// Gets the content type for the specified audio format.
/// </summary>
private string GetContentType(string? format)
{
format = format?.ToLower() ?? "wav";
return format switch
{
"wav" => "audio/wav",
"mp3" => "audio/mpeg",
"ogg" => "audio/ogg",
"flac" => "audio/flac",
_ => "audio/wav"
};
}
}
/// <summary>
/// Request DTO for TTS generation.
/// </summary>
public record TtsGenerationRequest(
string Text,
string? Speaker = null,
string Language = "de",
string? Format = null);
/// <summary>
/// Request DTO for TTS file generation.
/// </summary>
public record TtsFileGenerationRequest(
string Text,
string Filename,
string? Speaker = null,
string Language = "de");
/// <summary>
/// Response DTO for TTS file generation.
/// </summary>
public record TtsFileResponse(
string Url,
string Filename);