using GermanApp.Domain.Interfaces; using Microsoft.Extensions.Logging; namespace GermanApp.Application.Services; /// /// Application service for generating audio from text using Coqui TTS. /// This is part of the Application layer. /// public class AudioGenerationService { private readonly ITtsService _ttsService; private readonly ILogger _logger; /// /// Creates a new AudioGenerationService. /// /// The Coqui TTS service /// Logger for service operations public AudioGenerationService( ITtsService ttsService, ILogger logger) { _ttsService = ttsService; _logger = logger; } /// /// Generates audio from text. /// /// The text to convert to speech /// The language code (default: "de" for German) /// Optional speaker ID /// Cancellation token /// Audio data as byte array public virtual async Task GenerateAudioAsync( string text, string language = "de", string? speaker = null, CancellationToken cancellationToken = default) { _logger.LogInformation( "Generating audio: TextLength={Length}, Language={Language}, Speaker={Speaker}", text?.Length ?? 0, language, speaker ?? "default"); try { var audio = await _ttsService.GenerateAudioAsync( text, speaker, language, cancellationToken); _logger.LogInformation("Audio generated successfully: {ByteCount} bytes", audio.Length); return audio; } catch (Exception ex) { _logger.LogError(ex, "Failed to generate audio"); throw new AiServiceException( "Failed to generate audio: " + ex.Message, AiErrorCode.Temporary); } } /// /// Generates audio and saves to a file. /// /// The text to convert to speech /// Path to save the audio file /// The language code (default: "de") /// Optional speaker ID /// Cancellation token /// Path to the generated audio file public virtual async Task GenerateAudioToFileAsync( string text, string outputPath, string language = "de", string? speaker = null, CancellationToken cancellationToken = default) { _logger.LogInformation( "Generating audio to file: TextLength={Length}, Output={Output}", text?.Length ?? 0, outputPath); try { await _ttsService.GenerateAudioToFileAsync( text, outputPath, speaker, language, cancellationToken); _logger.LogInformation("Audio saved to: {OutputPath}", outputPath); return outputPath; } catch (Exception ex) { _logger.LogError(ex, "Failed to generate audio to file"); throw; } } /// /// Generates audio as a stream. /// /// The text to convert to speech /// The language code (default: "de") /// Optional speaker ID /// Cancellation token /// Stream containing audio data public virtual async Task GenerateAudioStreamAsync( string text, string language = "de", string? speaker = null, CancellationToken cancellationToken = default) { _logger.LogInformation("Generating audio stream: TextLength={Length}", text?.Length ?? 0); try { return await _ttsService.GenerateAudioStreamAsync( text, speaker, language, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Failed to generate audio stream"); throw; } } /// /// Generates audio for a vocabulary word. /// /// The vocabulary word /// The language code (default: "de") /// Optional speaker ID /// Cancellation token /// Audio data as byte array public virtual async Task GenerateVocabularyAudioAsync( string word, string language = "de", string? speaker = null, CancellationToken cancellationToken = default) { _logger.LogInformation("Generating vocabulary audio: Word={Word}", word); // For vocabulary, we might want to add a slight pause or emphasis // For now, just generate the word directly return await GenerateAudioAsync(word, language, speaker, cancellationToken); } /// /// Generates audio for a complete lesson including vocabulary and example sentences. /// /// The lesson text to narrate /// Vocabulary words to include /// The language code (default: "de") /// Cancellation token /// Audio data as byte array public virtual async Task GenerateLessonAudioAsync( string lessonText, IReadOnlyList vocabularyWords, string language = "de", CancellationToken cancellationToken = default) { _logger.LogInformation( "Generating lesson audio: TextLength={Length}, VocabularyCount={Count}", lessonText?.Length ?? 0, vocabularyWords?.Count ?? 0); // Combine lesson text with vocabulary var fullText = BuildLessonNarration(lessonText, vocabularyWords); return await GenerateAudioAsync(fullText, language, null, cancellationToken); } /// /// Generates audio for a story. /// /// The story text to narrate /// The language code (default: "de") /// Optional speaker ID /// Cancellation token /// Audio data as byte array public virtual async Task GenerateStoryAudioAsync( string storyText, string language = "de", string? speaker = null, CancellationToken cancellationToken = default) { _logger.LogInformation("Generating story audio: TextLength={Length}", storyText?.Length ?? 0); // Stories might be long, so we use the TTS service's built-in text splitting return await GenerateAudioAsync(storyText, language, speaker, cancellationToken); } /// /// Generates audio for a quiz question. /// /// The quiz question text /// The answer options /// The language code (default: "de") /// Cancellation token /// Audio data as byte array public virtual async Task GenerateQuizAudioAsync( string questionText, IReadOnlyList options, string language = "de", CancellationToken cancellationToken = default) { _logger.LogInformation( "Generating quiz audio: QuestionLength={Length}, OptionCount={Count}", questionText?.Length ?? 0, options?.Count ?? 0); // Build the full quiz narration var fullText = BuildQuizNarration(questionText, options); return await GenerateAudioAsync(fullText, language, null, cancellationToken); } /// /// Generates multiple audio files for a batch of texts. /// /// Dictionary mapping IDs to texts /// Directory to save audio files /// The language code (default: "de") /// Cancellation token /// Dictionary mapping IDs to output file paths public virtual async Task> GenerateBatchAudioAsync( IDictionary texts, string outputDirectory, string language = "de", CancellationToken cancellationToken = default) { _logger.LogInformation("Generating batch audio: Count={Count}", texts?.Count ?? 0); var results = new Dictionary(); // Ensure output directory exists Directory.CreateDirectory(outputDirectory); foreach (var kvp in texts) { var id = kvp.Key; var text = kvp.Value; var outputPath = Path.Combine(outputDirectory, $"{id}.wav"); try { await GenerateAudioToFileAsync(text, outputPath, language, null, cancellationToken); results[id] = outputPath; _logger.LogInformation("Generated audio for: {Id}", id); } catch (Exception ex) { _logger.LogError(ex, "Failed to generate audio for: {Id}", id); results[id] = string.Empty; } } return results; } /// /// Gets available voices for the current TTS model. /// /// Cancellation token /// List of available speaker IDs public virtual async Task> GetAvailableVoicesAsync( CancellationToken cancellationToken = default) { try { return await _ttsService.GetAvailableSpeakersAsync(cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Failed to get available voices"); return new List { "default" }; } } /// /// Gets information about the current TTS model. /// /// Model information tuple public virtual async Task<(string ModelName, string? ModelPath)> GetModelInfoAsync() { try { return await _ttsService.GetModelInfoAsync(); } catch (Exception ex) { _logger.LogError(ex, "Failed to get model info"); return ("unknown", null); } } /// /// Tests the audio generation service. /// /// Cancellation token /// True if service is working, false otherwise public virtual async Task TestServiceAsync(CancellationToken cancellationToken = default) { try { // Test with a simple phrase var audio = await GenerateAudioAsync("Hallo Welt", "de", null, cancellationToken); return audio != null && audio.Length > 0; } catch (Exception ex) { _logger.LogError(ex, "Audio generation service test failed"); return false; } } /// /// Builds narration text for a lesson including vocabulary words. /// private string BuildLessonNarration(string lessonText, IReadOnlyList vocabularyWords) { // Start with the lesson text var narration = lessonText; // Append vocabulary words with pauses if (vocabularyWords != null && vocabularyWords.Count > 0) { narration += "\n\n"; narration += "Vocabulary words: "; narration += string.Join(", ", vocabularyWords); } return narration; } /// /// Builds narration text for a quiz question with options. /// private string BuildQuizNarration(string questionText, IReadOnlyList options) { var narration = questionText; if (options != null && options.Count > 0) { narration += "\n"; for (int i = 0; i < options.Count; i++) { narration += $"Option {i + 1}: {options[i]}"; if (i < options.Count - 1) narration += ". "; } } return narration; } }