feat: add local Resemblyzer speaker recognition

This commit is contained in:
2026-09-11 13:49:27 +02:00
parent 43fc8aaec0
commit f86af983e8
48 changed files with 5250 additions and 273 deletions
@@ -2899,6 +2899,65 @@ public sealed class RecordingCoordinatorTests
Assert.Equal(["Chris"], speakerIdentification.Requests.Last().MeetingNote.Frontmatter.Attendees);
}
[Fact]
public async Task LiveResemblyzerMatchingRetriesWhenSameSpeakerCollectsRequiredSamples()
{
var audioSource = new ControlledAudioSource();
var transcriptStore = new InMemoryTranscriptStore();
var speakerIdentification = new CountingSpeakerIdentificationService();
var coordinator = new MeetingRecordingCoordinator(
audioSource,
new TestSpeechRecognitionPipelineFactory(new OrderedChunkProvider()),
transcriptStore,
new InMemoryMeetingNoteStore(),
new CapturingMeetingNoteOpener(),
new InMemoryMeetingArtifactStore(),
new InMemoryRecordedAudioStore(),
new CapturingMeetingSummaryPipeline(),
Options.Create(new MeetingAssistantOptions
{
SpeakerIdentification = new SpeakerIdentificationOptions
{
InitialDelay = TimeSpan.Zero,
Interval = TimeSpan.FromMilliseconds(20),
MinimumSampleSpeechDuration = TimeSpan.Zero,
Resemblyzer = new ResemblyzerSpeakerRecognitionOptions
{
Enabled = true,
RequiredVectorsPerSpeaker = 5
}
}
}),
NullLogger<MeetingRecordingCoordinator>.Instance,
speakerIdentificationService: speakerIdentification,
speakerSampleCollectionPolicy: SpeakerSampleCollectionPolicy.IndependentVectors(1000));
await coordinator.StartAsync(CancellationToken.None);
try
{
await audioSource.WriteAsync(new AudioChunk(Samples(0, 1, 2, 3, 4, 5, 6, 7), 4, 1), CancellationToken.None);
await WaitUntilAsync(() => speakerIdentification.Requests.Count == 1);
Assert.Single(speakerIdentification.Requests.Single().Samples!);
// The provider emits overlapping two-second segments, so every second
// segment supplies another independent recognition sample.
for (var index = 1; index < 9; index++)
{
await audioSource.WriteAsync(new AudioChunk(Samples(0, 1, 2, 3, 4, 5, 6, 7), 4, 1), CancellationToken.None);
}
await WaitUntilAsync(() => speakerIdentification.Requests.Last().Samples?.Count == 5);
var attempts = speakerIdentification.Requests.Count;
await Task.Delay(100);
Assert.Equal(attempts, speakerIdentification.Requests.Count);
Assert.All(speakerIdentification.Requests.Last().Samples!, sample => Assert.Equal("Guest03", sample.Speaker));
}
finally
{
await coordinator.StopAsync(CancellationToken.None);
}
}
[Fact]
public async Task LiveSpeakerIdentificationRetriesWhenNewUnmappedSpeakerAppears()
{