- Create higher-level AI services: - StoryGenerationService (uses MistralService) - WritingFeedbackService (uses MistralService) - SpeechExerciseService (uses VoskService) - AudioGenerationService (uses TtsService) - AiFallbackService (fallback mechanisms for service failures) - Register AiFallbackService in Program.cs DI container - Add comprehensive unit tests for all Phase 5 services: - AiFallbackServiceTests (14 tests) - AudioGenerationServiceTests (16 tests) - MistralServiceTests (16 tests) - SpeechExerciseServiceTests (12 tests) - StoryGenerationServiceTests (13 tests) - WritingFeedbackServiceTests (11 tests) - VoskServiceTests (15 tests) - TtsServiceTests (21 tests) - Update feature document (ai-services.md) to mark Phase 5 as complete Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
398 lines
12 KiB
C#
398 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using GermanApp.Application.Services;
|
|
using GermanApp.Domain.Interfaces;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
using Moq;
|
|
|
|
namespace GermanApp.Tests.Unit.Application.Services;
|
|
|
|
[TestClass]
|
|
public class AudioGenerationServiceTests
|
|
{
|
|
private Mock<ITtsService> _mockTtsService;
|
|
private Mock<ILogger<AudioGenerationService>> _mockLogger;
|
|
private AudioGenerationService _service;
|
|
private readonly byte[] _sampleAudio = new byte[1000];
|
|
|
|
[TestInitialize]
|
|
public void Setup()
|
|
{
|
|
_mockTtsService = new Mock<ITtsService>();
|
|
_mockLogger = new Mock<ILogger<AudioGenerationService>>();
|
|
_service = new AudioGenerationService(
|
|
_mockTtsService.Object,
|
|
_mockLogger.Object);
|
|
|
|
// Initialize sample audio
|
|
for (int i = 0; i < _sampleAudio.Length; i++)
|
|
{
|
|
_sampleAudio[i] = (byte)(i % 256);
|
|
}
|
|
}
|
|
|
|
[TestCleanup]
|
|
public void Cleanup()
|
|
{
|
|
// Clean up any temp files
|
|
if (File.Exists("/tmp/test_output.wav"))
|
|
{
|
|
File.Delete("/tmp/test_output.wav");
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateAudioAsync_WithValidText_ReturnsAudio()
|
|
{
|
|
// Arrange
|
|
var text = "Hallo Welt";
|
|
var language = "de";
|
|
var speaker = "default";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
text, speaker, language, cancellationToken))
|
|
.ReturnsAsync(_sampleAudio);
|
|
|
|
// Act
|
|
var result = await _service.GenerateAudioAsync(
|
|
text, language, speaker, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(_sampleAudio.Length, result.Length);
|
|
_mockTtsService.Verify(s => s.GenerateAudioAsync(
|
|
text, speaker, language, cancellationToken), Times.Once);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateAudioAsync_WithDefaultLanguage_UsesGerman()
|
|
{
|
|
// Arrange
|
|
var text = "Hallo Welt";
|
|
var speaker = "default";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
text, speaker, "de", cancellationToken))
|
|
.ReturnsAsync(_sampleAudio);
|
|
|
|
// Act
|
|
var result = await _service.GenerateAudioAsync(
|
|
text, speaker: speaker, cancellationToken: cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateAudioAsync_WhenTtsThrows_ThrowsAiServiceException()
|
|
{
|
|
// Arrange
|
|
var text = "Hallo Welt";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
text, null, "de", cancellationToken))
|
|
.ThrowsAsync(new Exception("TTS Error"));
|
|
|
|
// Act & Assert
|
|
try
|
|
{
|
|
await _service.GenerateAudioAsync(text, cancellationToken: cancellationToken);
|
|
Assert.Fail("Expected AiServiceException was not thrown");
|
|
}
|
|
catch (AiServiceException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateAudioToFileAsync_WithValidText_SavesFile()
|
|
{
|
|
// Arrange
|
|
var text = "Hallo Welt";
|
|
var outputPath = "/tmp/test_output.wav";
|
|
var language = "de";
|
|
var speaker = "default";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioToFileAsync(
|
|
text, outputPath, speaker, language, cancellationToken))
|
|
.ReturnsAsync(outputPath);
|
|
|
|
// Act
|
|
var result = await _service.GenerateAudioToFileAsync(
|
|
text, outputPath, language, speaker, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.AreEqual(outputPath, result);
|
|
_mockTtsService.Verify(s => s.GenerateAudioToFileAsync(
|
|
text, outputPath, speaker, language, cancellationToken), Times.Once);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateAudioStreamAsync_WithValidText_ReturnsStream()
|
|
{
|
|
// Arrange
|
|
var text = "Hallo Welt";
|
|
var language = "de";
|
|
var speaker = "default";
|
|
var cancellationToken = CancellationToken.None;
|
|
var stream = new MemoryStream(_sampleAudio);
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioStreamAsync(
|
|
text, speaker, language, cancellationToken))
|
|
.ReturnsAsync(stream);
|
|
|
|
// Act
|
|
var result = await _service.GenerateAudioStreamAsync(
|
|
text, language, speaker, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.IsInstanceOfType(result, typeof(MemoryStream));
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateVocabularyAudioAsync_WithValidWord_ReturnsAudio()
|
|
{
|
|
// Arrange
|
|
var word = "Apfel";
|
|
var language = "de";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
word, null, language, cancellationToken))
|
|
.ReturnsAsync(_sampleAudio);
|
|
|
|
// Act
|
|
var result = await _service.GenerateVocabularyAudioAsync(
|
|
word, language, null, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(_sampleAudio.Length, result.Length);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateLessonAudioAsync_WithValidParameters_ReturnsAudio()
|
|
{
|
|
// Arrange
|
|
var lessonText = "Heute lernen wir neue Wörter.";
|
|
var vocabularyWords = new List<string> { "lernen", "Wörter", "heute" };
|
|
var language = "de";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
It.IsAny<string>(), null, language, cancellationToken))
|
|
.ReturnsAsync(_sampleAudio);
|
|
|
|
// Act
|
|
var result = await _service.GenerateLessonAudioAsync(
|
|
lessonText, vocabularyWords, language, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(_sampleAudio.Length, result.Length);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateStoryAudioAsync_WithValidText_ReturnsAudio()
|
|
{
|
|
// Arrange
|
|
var storyText = "Es war einmal ein kleiner Junge...";
|
|
var language = "de";
|
|
var speaker = "story-teller";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
storyText, speaker, language, cancellationToken))
|
|
.ReturnsAsync(_sampleAudio);
|
|
|
|
// Act
|
|
var result = await _service.GenerateStoryAudioAsync(
|
|
storyText, language, speaker, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(_sampleAudio.Length, result.Length);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateQuizAudioAsync_WithValidParameters_ReturnsAudio()
|
|
{
|
|
// Arrange
|
|
var questionText = "Was ist die Hauptstadt von Deutschland?";
|
|
var options = new List<string> { "Berlin", "München", "Hamburg", "Köln" };
|
|
var language = "de";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
It.IsAny<string>(), null, language, cancellationToken))
|
|
.ReturnsAsync(_sampleAudio);
|
|
|
|
// Act
|
|
var result = await _service.GenerateQuizAudioAsync(
|
|
questionText, options, language, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(_sampleAudio.Length, result.Length);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GenerateBatchAudioAsync_WithValidTexts_ReturnsAllFiles()
|
|
{
|
|
// Arrange
|
|
var texts = new Dictionary<string, string>
|
|
{
|
|
["1"] = "Hallo",
|
|
["2"] = "Welt",
|
|
["3"] = "Test"
|
|
};
|
|
var outputDirectory = "/tmp/audio";
|
|
var language = "de";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GenerateAudioToFileAsync(
|
|
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((string text, string outputPath, string? speaker, string language, CancellationToken ct) => outputPath);
|
|
|
|
// Act
|
|
var result = await _service.GenerateBatchAudioAsync(
|
|
texts, outputDirectory, language, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(3, result.Count);
|
|
Assert.IsTrue(result.ContainsKey("1"));
|
|
Assert.IsTrue(result.ContainsKey("2"));
|
|
Assert.IsTrue(result.ContainsKey("3"));
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GetAvailableVoicesAsync_WithVoices_ReturnsList()
|
|
{
|
|
// Arrange
|
|
var expectedVoices = new List<string> { "de-female-1", "de-male-1", "default" };
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GetAvailableSpeakersAsync(cancellationToken))
|
|
.ReturnsAsync(expectedVoices);
|
|
|
|
// Act
|
|
var result = await _service.GetAvailableVoicesAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(3, result.Count);
|
|
CollectionAssert.AreEqual(expectedVoices, new List<string>(result));
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GetAvailableVoicesAsync_WhenServiceFails_ReturnsDefault()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockTtsService.Setup(s => s.GetAvailableSpeakersAsync(cancellationToken))
|
|
.ThrowsAsync(new Exception("Error"));
|
|
|
|
// Act
|
|
var result = await _service.GetAvailableVoicesAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(1, result.Count);
|
|
Assert.AreEqual("default", result[0]);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GetModelInfoAsync_WithModelInfo_ReturnsTuple()
|
|
{
|
|
// Arrange
|
|
var expectedModelName = "tts_models/de/deu/fairseq/vits";
|
|
var expectedModelPath = "/path/to/model";
|
|
|
|
_mockTtsService.Setup(s => s.GetModelInfoAsync())
|
|
.ReturnsAsync((expectedModelName, expectedModelPath));
|
|
|
|
// Act
|
|
var result = await _service.GetModelInfoAsync();
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(expectedModelName, result.Item1);
|
|
Assert.AreEqual(expectedModelPath, result.Item2);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task GetModelInfoAsync_WhenServiceFails_ReturnsDefault()
|
|
{
|
|
// Arrange
|
|
_mockTtsService.Setup(s => s.GetModelInfoAsync())
|
|
.ThrowsAsync(new Exception("Error"));
|
|
|
|
// Act
|
|
var result = await _service.GetModelInfoAsync();
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual("unknown", result.Item1);
|
|
Assert.IsNull(result.Item2);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestServiceAsync_WithWorkingService_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
"Hallo Welt", null, "de", cancellationToken))
|
|
.ReturnsAsync(_sampleAudio);
|
|
|
|
// Act
|
|
var result = await _service.TestServiceAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsTrue(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestServiceAsync_WithEmptyAudio_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
"Hallo Welt", null, "de", cancellationToken))
|
|
.ReturnsAsync(new byte[0]);
|
|
|
|
// Act
|
|
var result = await _service.TestServiceAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestServiceAsync_WhenServiceFails_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
_mockTtsService.Setup(s => s.GenerateAudioAsync(
|
|
"Hallo Welt", null, "de", cancellationToken))
|
|
.ThrowsAsync(new Exception("Error"));
|
|
|
|
// Act
|
|
var result = await _service.TestServiceAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
}
|