using MeetingAssistant; using MeetingAssistant.Speakers; using MeetingAssistant.Transcription; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using NAudio.Wave; namespace MeetingAssistant.Tests; public sealed class AzureSpeechSpeakerIdentityMatcherTests { [Fact] public void IdentityDiarizationLanguageUsesFirstAutoDetectLanguageWhenConfiguredLanguageIsAuto() { Assert.Equal( "de-DE", AzureSpeechSpeakerIdentityDiarizationClient.ResolveRecognitionLanguage(new AzureSpeechOptions { Language = "auto", AutoDetectLanguages = ["de-DE", "en-US"] })); } [Fact] public void IdentityDiarizationLanguageUsesConfiguredConcreteLanguage() { Assert.Equal( "en-US", AzureSpeechSpeakerIdentityDiarizationClient.ResolveRecognitionLanguage(new AzureSpeechOptions { Language = "en-US", AutoDetectLanguages = ["de-DE"] })); } [Fact] public async Task MatcherReturnsIdentityWhenAzureDiarizationAssignsUnknownSnippetToKnownSpeaker() { var diarizationClient = new FakeSpeakerIdentityDiarizationClient( [ new TranscriptionSegment(TimeSpan.Zero, TimeSpan.FromSeconds(1), "Guest-1", "known"), new TranscriptionSegment(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3), "Guest-1", "unknown") ]); var matcher = CreateMatcher(diarizationClient); var snippet = CreateWavSnippet(); var match = await matcher.MatchAsync( new SpeakerIdentityMatchRequest( "Guest03", snippet, [new SpeakerIdentityMatchCandidate(42, "Chris", 5, [snippet])]), CancellationToken.None); Assert.NotNull(match); Assert.Equal(42, match.IdentityId); Assert.NotNull(diarizationClient.WavPath); Assert.False(File.Exists(diarizationClient.WavPath)); } [Fact] public async Task MatcherRequiresTwoMatchingKnownSnippetsWhenIdentityHasMultipleSnippets() { var diarizationClient = new FakeSpeakerIdentityDiarizationClient( [ new TranscriptionSegment(TimeSpan.Zero, TimeSpan.FromSeconds(1), "Guest-1", "known one"), new TranscriptionSegment(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3), "Guest-2", "known two"), new TranscriptionSegment(TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(5), "Guest-1", "unknown") ]); var matcher = CreateMatcher(diarizationClient); var snippet = CreateWavSnippet(); var match = await matcher.MatchAsync( new SpeakerIdentityMatchRequest( "Guest03", snippet, [new SpeakerIdentityMatchCandidate(42, "Chris", 5, [snippet, snippet])]), CancellationToken.None); Assert.Null(match); } [Fact] public async Task MatcherReturnsNullWhenAzureDiarizationAssignsDifferentSpeakers() { var diarizationClient = new FakeSpeakerIdentityDiarizationClient( [ new TranscriptionSegment(TimeSpan.Zero, TimeSpan.FromSeconds(1), "Guest-1", "known"), new TranscriptionSegment(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3), "Guest-2", "unknown") ]); var matcher = CreateMatcher(diarizationClient); var snippet = CreateWavSnippet(); var match = await matcher.MatchAsync( new SpeakerIdentityMatchRequest( "Guest03", snippet, [new SpeakerIdentityMatchCandidate(42, "Chris", 5, [snippet])]), CancellationToken.None); Assert.Null(match); } private static AzureSpeechSpeakerIdentityMatcher CreateMatcher(ISpeakerIdentityDiarizationClient diarizationClient) { return new AzureSpeechSpeakerIdentityMatcher( diarizationClient, Options.Create(new MeetingAssistantOptions { SpeakerIdentification = new SpeakerIdentificationOptions { SilenceBetweenSnippetsSeconds = 1 } }), NullLogger.Instance); } private static byte[] CreateWavSnippet() { using var stream = new MemoryStream(); using (var writer = new WaveFileWriter(stream, new WaveFormat(16000, 16, 1))) { writer.Write(new byte[16000 * 2], 0, 16000 * 2); } return stream.ToArray(); } private sealed class FakeSpeakerIdentityDiarizationClient : ISpeakerIdentityDiarizationClient { private readonly IReadOnlyList segments; public FakeSpeakerIdentityDiarizationClient(IReadOnlyList segments) { this.segments = segments; } public string? WavPath { get; private set; } public Task> DiarizeAsync( string wavPath, CancellationToken cancellationToken) { WavPath = wavPath; Assert.True(File.Exists(wavPath)); return Task.FromResult(segments); } } }