Public Access
181 lines
6.2 KiB
C#
181 lines
6.2 KiB
C#
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);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MatcherReturnsNullWhenDiarizationTimesOut()
|
|
{
|
|
var matcher = CreateMatcher(
|
|
new HangingSpeakerIdentityDiarizationClient(),
|
|
TimeSpan.FromMilliseconds(20));
|
|
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,
|
|
TimeSpan? matchTimeout = null)
|
|
{
|
|
return new AzureSpeechSpeakerIdentityMatcher(
|
|
diarizationClient,
|
|
Options.Create(new MeetingAssistantOptions
|
|
{
|
|
SpeakerIdentification = new SpeakerIdentificationOptions
|
|
{
|
|
SilenceBetweenSnippetsSeconds = 1,
|
|
MatchTimeout = matchTimeout ?? TimeSpan.FromMinutes(1)
|
|
}
|
|
}),
|
|
NullLogger<AzureSpeechSpeakerIdentityMatcher>.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<TranscriptionSegment> segments;
|
|
|
|
public FakeSpeakerIdentityDiarizationClient(IReadOnlyList<TranscriptionSegment> segments)
|
|
{
|
|
this.segments = segments;
|
|
}
|
|
|
|
public string? WavPath { get; private set; }
|
|
|
|
public Task<IReadOnlyList<TranscriptionSegment>> DiarizeAsync(
|
|
string wavPath,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
WavPath = wavPath;
|
|
Assert.True(File.Exists(wavPath));
|
|
return Task.FromResult(segments);
|
|
}
|
|
}
|
|
|
|
private sealed class HangingSpeakerIdentityDiarizationClient : ISpeakerIdentityDiarizationClient
|
|
{
|
|
public async Task<IReadOnlyList<TranscriptionSegment>> DiarizeAsync(
|
|
string wavPath,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
|
|
return [];
|
|
}
|
|
}
|
|
}
|