Files
meeting-assistant/MeetingAssistant.Tests/PyannoteSpeakerIdentityMatchValidatorTests.cs
codex a72cda0c03
PR and Push Build/Test / build-and-test (push) Successful in 7m0s
Harden speaker identity samples
2026-05-28 12:02:44 +02:00

138 lines
5.1 KiB
C#

using MeetingAssistant;
using MeetingAssistant.Recording;
using MeetingAssistant.Speakers;
using MeetingAssistant.Transcription;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NAudio.Wave;
namespace MeetingAssistant.Tests;
public sealed class PyannoteSpeakerIdentityMatchValidatorTests
{
[Fact]
public async Task ValidatorRejectsSampleWhenPyannoteReportsMultipleSpeakers()
{
var commandRunner = new CapturingCommandRunner(
"""
__MEETING_ASSISTANT_PYANNOTE_JSON_START__
[{"start":0.0,"end":10.0,"speaker":"SPEAKER_00"},{"start":10.0,"end":20.0,"speaker":"SPEAKER_01"}]
__MEETING_ASSISTANT_PYANNOTE_JSON_END__
""");
var validator = CreateValidator(commandRunner);
var valid = await validator.ValidateSampleAsync(CreateWav(TimeSpan.FromSeconds(20)), CancellationToken.None);
Assert.False(valid);
}
[Fact]
public async Task ValidatorAcceptsMatchWhenPyannoteAssignsKnownAndUnknownSamplesToSameSpeaker()
{
var commandRunner = new CapturingCommandRunner(
"""
__MEETING_ASSISTANT_PYANNOTE_JSON_START__
[{"start":0.0,"end":1.0,"speaker":"SPEAKER_00"},{"start":2.0,"end":3.0,"speaker":"SPEAKER_00"}]
__MEETING_ASSISTANT_PYANNOTE_JSON_END__
""");
var validator = CreateValidator(commandRunner);
var wav = CreateWav(TimeSpan.FromSeconds(1));
var valid = await validator.ValidateMatchAsync(
new SpeakerIdentityMatchValidationRequest("Guest03", 42, wav, [wav]),
CancellationToken.None);
Assert.True(valid);
}
[Fact]
public async Task DisabledValidatorDoesNotRunPyannote()
{
var commandRunner = new CapturingCommandRunner("");
var validator = CreateValidator(commandRunner, enabled: false);
var valid = await validator.ValidateSampleAsync(CreateWav(TimeSpan.FromSeconds(1)), CancellationToken.None);
Assert.True(valid);
Assert.Empty(commandRunner.Commands);
}
private static PyannoteSpeakerIdentityMatchValidator CreateValidator(
CapturingCommandRunner commandRunner,
bool enabled = true)
{
var finalizer = new PyannoteTranscriptFinalizer(
commandRunner,
Options.Create(new MeetingAssistantOptions()),
NullLogger<PyannoteTranscriptFinalizer>.Instance);
return new PyannoteSpeakerIdentityMatchValidator(
finalizer,
Options.Create(new MeetingAssistantOptions
{
SpeakerIdentification = new SpeakerIdentificationOptions
{
PyannoteValidation = new SpeakerIdentityPyannoteValidationOptions
{
Enabled = enabled,
MinimumSingleSpeakerCoverage = 0.90,
MinimumMatchingKnownSnippetRatio = 1,
Diarization = new PyannoteDiarizationOptions
{
Enabled = true,
BuildImage = false,
DockerCommand = "docker",
Image = "meeting-assistant-pyannote:local",
ModelsFolder = Path.Combine(
Path.GetTempPath(),
"meeting-assistant-tests",
Guid.NewGuid().ToString("N"),
"models"),
Token = "hf_test",
TokenEnv = "",
CommandTimeout = TimeSpan.FromMinutes(1),
AlignmentMode = PyannoteAlignmentMode.PyannoteTurns
}
}
}
}),
NullLogger<PyannoteSpeakerIdentityMatchValidator>.Instance);
}
private static byte[] CreateWav(TimeSpan duration)
{
const int sampleRate = 16000;
using var stream = new MemoryStream();
using (var writer = new WaveFileWriter(stream, new WaveFormat(sampleRate, 16, 1)))
{
var bytes = new byte[(int)(duration.TotalSeconds * sampleRate * sizeof(short))];
writer.Write(bytes, 0, bytes.Length);
}
return stream.ToArray();
}
private sealed class CapturingCommandRunner : ICommandRunner
{
private readonly string output;
public CapturingCommandRunner(string output)
{
this.output = output;
}
public IReadOnlyList<CapturedCommand> Commands { get; private set; } = [];
public Task<CommandResult> RunAsync(
string fileName,
IReadOnlyList<string> arguments,
CancellationToken cancellationToken,
IReadOnlyDictionary<string, string>? environment = null)
{
Commands = Commands.Append(new CapturedCommand(fileName, arguments)).ToList();
return Task.FromResult(new CommandResult(0, output, ""));
}
}
private sealed record CapturedCommand(string FileName, IReadOnlyList<string> Arguments);
}