using GermanApp.Domain.Interfaces; using Microsoft.Extensions.Logging; namespace GermanApp.Application.Services; /// /// Application service for managing speech exercises using Vosk speech recognition. /// This is part of the Application layer. /// public class SpeechExerciseService { private readonly IVoskService _voskService; private readonly ILogger _logger; /// /// Creates a new SpeechExerciseService. /// /// The Vosk speech recognition service /// Logger for service operations public SpeechExerciseService( IVoskService voskService, ILogger logger) { _voskService = voskService; _logger = logger; } /// /// Recognizes speech from audio bytes and returns the transcribed text. /// /// The audio data in bytes /// Optional expected text for verification /// Cancellation token /// Result containing recognized text and accuracy public virtual async Task RecognizeSpeechAsync( byte[] audioBytes, string? expectedText = null, CancellationToken cancellationToken = default) { _logger.LogInformation("Recognizing speech from audio: {ByteCount} bytes", audioBytes?.Length ?? 0); try { var recognizedText = await _voskService.RecognizeSpeechAsync( audioBytes, _voskService.GetType().Name == "VoskService" ? 16000 : 16000, null, cancellationToken); // Calculate accuracy if expected text is provided var accuracy = expectedText != null ? CalculateAccuracy(recognizedText, expectedText) : 0.0; _logger.LogInformation( "Speech recognized: Text={Text}, Accuracy={Accuracy:P0}", recognizedText, accuracy); return new SpeechRecognitionResult { RecognizedText = recognizedText, ExpectedText = expectedText, Accuracy = accuracy, IsCorrect = accuracy >= 0.8 }; } catch (Exception ex) { _logger.LogError(ex, "Failed to recognize speech"); throw new AiServiceException( "Failed to recognize speech: " + ex.Message, AiErrorCode.Temporary); } } /// /// Recognizes speech from an audio file. /// /// Path to the audio file /// Optional expected text for verification /// Cancellation token /// Result containing recognized text and accuracy public virtual async Task RecognizeSpeechFromFileAsync( string audioFilePath, string? expectedText = null, CancellationToken cancellationToken = default) { _logger.LogInformation("Recognizing speech from file: {FilePath}", audioFilePath); try { var recognizedText = await _voskService.RecognizeSpeechFromFileAsync( audioFilePath, cancellationToken); var accuracy = expectedText != null ? CalculateAccuracy(recognizedText, expectedText) : 0.0; return new SpeechRecognitionResult { RecognizedText = recognizedText, ExpectedText = expectedText, Accuracy = accuracy, IsCorrect = accuracy >= 0.8 }; } catch (Exception ex) { _logger.LogError(ex, "Failed to recognize speech from file"); throw; } } /// /// Verifies if spoken audio matches expected text. /// /// The audio data in bytes /// The expected text /// Cancellation token /// Result with accuracy and correctness public virtual async Task VerifySpeechAsync( byte[] audioBytes, string expectedText, CancellationToken cancellationToken = default) { _logger.LogInformation("Verifying speech: Expected={Expected}", expectedText); var result = await RecognizeSpeechAsync(audioBytes, expectedText, cancellationToken); // Additional validation for speech exercises result.PronunciationScore = CalculatePronunciationScore(result.RecognizedText, expectedText); result.FluencyScore = CalculateFluencyScore(audioBytes); return result; } /// /// Creates a speech exercise for practicing a specific phrase. /// /// The phrase to practice /// Exercise difficulty level /// Optional pronunciation hints /// Speech exercise with metadata public virtual SpeechExercise CreateExercise( string phrase, string difficulty = "medium", IReadOnlyList? hints = null) { _logger.LogInformation("Creating speech exercise: Phrase={Phrase}, Difficulty={Difficulty}", phrase, difficulty); return new SpeechExercise { Id = Guid.NewGuid().ToString(), Phrase = phrase, Difficulty = difficulty, Hints = hints ?? new List(), CreatedAt = DateTime.UtcNow }; } /// /// Evaluates a user's attempt at a speech exercise. /// /// The exercise ID /// The user's audio attempt /// The expected phrase (from exercise) /// Cancellation token /// Evaluation result with scores public virtual async Task EvaluateExerciseAttemptAsync( string exerciseId, byte[] audioBytes, string expectedPhrase, CancellationToken cancellationToken = default) { _logger.LogInformation("Evaluating exercise attempt: ExerciseId={ExerciseId}", exerciseId); var result = await VerifySpeechAsync(audioBytes, expectedPhrase, cancellationToken); return new SpeechExerciseEvaluation { ExerciseId = exerciseId, ExpectedPhrase = expectedPhrase, RecognizedText = result.RecognizedText, Accuracy = result.Accuracy, PronunciationScore = result.PronunciationScore, FluencyScore = result.FluencyScore, IsPassed = result.IsCorrect && result.Accuracy >= 0.8, AttemptedAt = DateTime.UtcNow }; } /// /// Tests the speech exercise service. /// /// Cancellation token /// True if service is working, false otherwise public virtual async Task TestServiceAsync(CancellationToken cancellationToken = default) { try { // We can't test without actual audio, so just verify the service is initialized var modelInfo = await _voskService.GetModelInfoAsync(); return modelInfo.ModelName != null; } catch (Exception ex) { _logger.LogError(ex, "Speech exercise service test failed"); return false; } } /// /// Calculates text matching accuracy between recognized and expected text. /// /// The recognized text /// The expected text /// Accuracy score (0.0 to 1.0) private double CalculateAccuracy(string recognized, string expected) { if (string.IsNullOrWhiteSpace(expected)) return 1.0; if (string.IsNullOrWhiteSpace(recognized)) return 0.0; // Normalize both strings var recognizedNormalized = recognized.Trim().ToLower(); var expectedNormalized = expected.Trim().ToLower(); // Simple exact match if (recognizedNormalized == expectedNormalized) return 1.0; // Calculate Levenshtein distance for similarity var distance = LevenshteinDistance(recognizedNormalized, expectedNormalized); var maxLength = Math.Max(recognizedNormalized.Length, expectedNormalized.Length); if (maxLength == 0) return 1.0; var similarity = 1.0 - (distance / (double)maxLength); return Math.Max(0.0, Math.Min(1.0, similarity)); } /// /// Calculates pronunciation score based on text matching. /// private double CalculatePronunciationScore(string recognized, string expected) { // For now, use accuracy as pronunciation score // In a real implementation, you might use audio analysis return CalculateAccuracy(recognized, expected); } /// /// Calculates fluency score based on audio characteristics. /// private double CalculateFluencyScore(byte[] audioBytes) { // Simple heuristic: longer audio with consistent speaking rate // In a real implementation, analyze audio features if (audioBytes == null || audioBytes.Length == 0) return 0.0; // Assume 16kHz sample rate, 2 bytes per sample (16-bit) var durationSeconds = audioBytes.Length / (16000.0 * 2); // Give higher score for longer, consistent speech if (durationSeconds >= 3.0) return 1.0; if (durationSeconds >= 1.5) return 0.8; if (durationSeconds >= 0.5) return 0.6; return 0.4; } /// /// Calculates Levenshtein distance between two strings. /// private static int LevenshteinDistance(string s, string t) { if (string.IsNullOrEmpty(s)) { return string.IsNullOrEmpty(t) ? 0 : t.Length; } if (string.IsNullOrEmpty(t)) { return s.Length; } var n = s.Length; var m = t.Length; var d = new int[n + 1, m + 1]; for (int i = 0; i <= n; d[i, 0] = i++) ; for (int j = 1; j <= m; d[0, j] = j++) ; for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { var cost = (t[j - 1] == s[i - 1]) ? 0 : 1; d[i, j] = Math.Min( Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } return d[n, m]; } } /// /// Result of speech recognition. /// public class SpeechRecognitionResult { /// /// The recognized text. /// public string RecognizedText { get; set; } = string.Empty; /// /// The expected text (if provided for verification). /// public string? ExpectedText { get; set; } /// /// Accuracy score (0.0 to 1.0). /// public double Accuracy { get; set; } /// /// Pronunciation score (0.0 to 1.0). /// public double PronunciationScore { get; set; } /// /// Fluency score (0.0 to 1.0). /// public double FluencyScore { get; set; } /// /// Whether the recognition is considered correct. /// public bool IsCorrect { get; set; } } /// /// Represents a speech exercise. /// public class SpeechExercise { /// /// Unique identifier for the exercise. /// public string Id { get; set; } = string.Empty; /// /// The phrase to practice. /// public string Phrase { get; set; } = string.Empty; /// /// Difficulty level (easy, medium, hard). /// public string Difficulty { get; set; } = "medium"; /// /// Pronunciation hints. /// public IReadOnlyList Hints { get; set; } = new List(); /// /// When the exercise was created. /// public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } /// /// Result of evaluating a speech exercise attempt. /// public class SpeechExerciseEvaluation { /// /// The exercise ID. /// public string ExerciseId { get; set; } = string.Empty; /// /// The expected phrase. /// public string ExpectedPhrase { get; set; } = string.Empty; /// /// The recognized text. /// public string RecognizedText { get; set; } = string.Empty; /// /// Accuracy score (0.0 to 1.0). /// public double Accuracy { get; set; } /// /// Pronunciation score (0.0 to 1.0). /// public double PronunciationScore { get; set; } /// /// Fluency score (0.0 to 1.0). /// public double FluencyScore { get; set; } /// /// Whether the attempt passed. /// public bool IsPassed { get; set; } /// /// When the attempt was made. /// public DateTime AttemptedAt { get; set; } = DateTime.UtcNow; }