using MeetingAssistant.Recording; using MeetingAssistant.Transcription; using Microsoft.CognitiveServices.Speech; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace MeetingAssistant.Tests; public sealed class AzureSpeechStreamingTranscriptionProviderTests { [Fact] public async Task TranscribeFailsClearlyWhenKeyIsMissing() { var provider = new AzureSpeechStreamingTranscriptionProvider( Options.Create(new MeetingAssistantOptions { AzureSpeech = new AzureSpeechOptions { Region = "germanywestcentral", KeyEnv = $"MEETING_ASSISTANT_MISSING_AZURE_KEY_{Guid.NewGuid():N}" } }), NullLogger.Instance); var exception = await Assert.ThrowsAsync(async () => { await foreach (var _ in provider.TranscribeAsync(ReadSingleChunk(), SpeechRecognitionPipelineOptions.Default, CancellationToken.None)) { } }); Assert.Contains("Azure Speech key is not configured", exception.Message); } [Fact] public void AutoDetectLanguagesAreUsedOnlyWhenLanguageIsAuto() { Assert.Equal( ["de-DE", "en-US"], AzureSpeechStreamingTranscriptionProvider.GetAutoDetectLanguages(new AzureSpeechOptions { Language = "auto", AutoDetectLanguages = ["de-DE", "en-US", "de-DE", " "] })); Assert.Empty(AzureSpeechStreamingTranscriptionProvider.GetAutoDetectLanguages(new AzureSpeechOptions { Language = "de-DE", AutoDetectLanguages = ["de-DE", "en-US"] })); } [Fact] public void NormalizeDictationWordsTrimsAndDeduplicatesPhrases() { Assert.Equal( ["PBI", "Product Backlog Item"], AzureSpeechStreamingTranscriptionProvider.NormalizeDictationWords([ " PBI ", "pbi", "", "Product Backlog Item" ])); } [Fact] public void CreateSpeechConfigSkipsConfiguredPostProcessingOptionForConversationTranscriber() { var provider = new AzureSpeechStreamingTranscriptionProvider( Options.Create(new MeetingAssistantOptions { AzureSpeech = new AzureSpeechOptions { Endpoint = "wss://westeurope.stt.speech.microsoft.com/speech/universal/v2", Language = "de-DE", Key = "test-key", PostProcessingOption = "PostRefinement" } }), NullLogger.Instance); var speechConfig = provider.CreateSpeechConfig(); Assert.True(string.IsNullOrEmpty( speechConfig.GetProperty(PropertyId.SpeechServiceResponse_PostProcessingOption))); } [Fact] public void CreateSpeechConfigDoesNotApplyBlankPostProcessingOption() { var provider = new AzureSpeechStreamingTranscriptionProvider( Options.Create(new MeetingAssistantOptions { AzureSpeech = new AzureSpeechOptions { Endpoint = "wss://westeurope.stt.speech.microsoft.com/speech/universal/v2", Language = "de-DE", Key = "test-key", PostProcessingOption = " " } }), NullLogger.Instance); var speechConfig = provider.CreateSpeechConfig(); Assert.True(string.IsNullOrEmpty( speechConfig.GetProperty(PropertyId.SpeechServiceResponse_PostProcessingOption))); } [Fact] public void ResolveKeyFallsBackToUserEnvironment() { var keyEnv = $"MEETING_ASSISTANT_AZURE_KEY_{Guid.NewGuid():N}"; try { Environment.SetEnvironmentVariable(keyEnv, "user-key", EnvironmentVariableTarget.User); Assert.Equal("user-key", AzureSpeechStreamingTranscriptionProvider.ResolveKey(new AzureSpeechOptions { KeyEnv = keyEnv })); } finally { Environment.SetEnvironmentVariable(keyEnv, null, EnvironmentVariableTarget.User); } } [Fact] public void CreateSpeechConfigUsesSubscriptionWhenEndpointIsBlank() { var speechConfig = AzureSpeechStreamingTranscriptionProvider.CreateSpeechConfig( new AzureSpeechOptions { Endpoint = "", Region = "westeurope" }, "test-key"); Assert.NotNull(speechConfig); } [Theory] [InlineData(null, "Continuous")] [InlineData("", "Continuous")] [InlineData("continuous", "Continuous")] [InlineData("AtStart", "AtStart")] public void LanguageIdModeDefaultsToContinuous(string? configured, string expected) { Assert.Equal(expected, AzureSpeechStreamingTranscriptionProvider.NormalizeLanguageIdMode(configured)); } private static async IAsyncEnumerable ReadSingleChunk() { await Task.Yield(); yield return new AudioChunk(new byte[320], 16000, 1); } }