Public Access
125 lines
4.2 KiB
C#
125 lines
4.2 KiB
C#
using MeetingAssistant.Recording;
|
|
using MeetingAssistant.Transcription;
|
|
using Microsoft.Extensions.Logging;
|
|
using NAudio.Wave;
|
|
|
|
namespace MeetingAssistant.Tests;
|
|
|
|
public sealed class SpeakerAudioSampleCollectorTests
|
|
{
|
|
[Fact]
|
|
public void CollectorWaitsForConfiguredUninterruptedSpeechDuration()
|
|
{
|
|
var collector = new SpeakerAudioSampleCollector(
|
|
TimeSpan.FromMinutes(2),
|
|
maxSamplesPerSpeaker: 3,
|
|
minimumUninterruptedSpeechDuration: TimeSpan.FromSeconds(30),
|
|
maximumSegmentGap: TimeSpan.FromSeconds(1));
|
|
collector.AppendAudio(CreateAudio(TimeSpan.FromSeconds(35)));
|
|
|
|
collector.TryAdd(Segment(0, 12, "Guest01", "one two three four five."));
|
|
collector.TryAdd(Segment(12, 24, "Guest01", "six seven eight nine ten."));
|
|
|
|
Assert.Empty(collector.Snapshot());
|
|
|
|
collector.TryAdd(Segment(24, 31, "Guest01", "eleven twelve thirteen fourteen fifteen."));
|
|
|
|
var sample = Assert.Single(collector.Snapshot());
|
|
Assert.Equal("Guest01", sample.Speaker);
|
|
Assert.Equal(TimeSpan.Zero, sample.Segment.Start);
|
|
Assert.Equal(TimeSpan.FromSeconds(31), sample.Segment.End);
|
|
Assert.True(ReadDuration(sample.WavBytes) >= TimeSpan.FromSeconds(30));
|
|
}
|
|
|
|
[Fact]
|
|
public void CollectorDoesNotCombineSpeechAcrossDifferentSpeakerInterruption()
|
|
{
|
|
var collector = new SpeakerAudioSampleCollector(
|
|
TimeSpan.FromMinutes(2),
|
|
maxSamplesPerSpeaker: 3,
|
|
minimumUninterruptedSpeechDuration: TimeSpan.FromSeconds(30),
|
|
maximumSegmentGap: TimeSpan.FromSeconds(1));
|
|
collector.AppendAudio(CreateAudio(TimeSpan.FromSeconds(50)));
|
|
|
|
collector.TryAdd(Segment(0, 20, "Guest01", "one two three four five."));
|
|
collector.TryAdd(Segment(20, 22, "Guest02", "interrupting now."));
|
|
collector.TryAdd(Segment(22, 35, "Guest01", "six seven eight nine ten."));
|
|
|
|
Assert.DoesNotContain(collector.Snapshot(), sample => sample.Speaker == "Guest01");
|
|
}
|
|
|
|
[Fact]
|
|
public void CollectorLogsWhenSampleIsDiscardedBecauseSpeechIsTooShort()
|
|
{
|
|
var logger = new CapturingLogger();
|
|
var collector = new SpeakerAudioSampleCollector(
|
|
TimeSpan.FromMinutes(2),
|
|
maxSamplesPerSpeaker: 3,
|
|
minimumUninterruptedSpeechDuration: TimeSpan.FromSeconds(30),
|
|
maximumSegmentGap: TimeSpan.FromSeconds(1),
|
|
logger: logger);
|
|
collector.AppendAudio(CreateAudio(TimeSpan.FromSeconds(35)));
|
|
|
|
collector.TryAdd(Segment(0, 12, "Guest01", "one two three four five."));
|
|
|
|
var message = Assert.Single(
|
|
logger.Messages,
|
|
message => message.Contains("Discarding speaker identity sample for Guest01", StringComparison.Ordinal));
|
|
Assert.Contains("duration", message);
|
|
Assert.Contains("minimum duration", message);
|
|
}
|
|
|
|
private static TranscriptionSegment Segment(
|
|
double start,
|
|
double end,
|
|
string speaker,
|
|
string text)
|
|
{
|
|
return new TranscriptionSegment(
|
|
TimeSpan.FromSeconds(start),
|
|
TimeSpan.FromSeconds(end),
|
|
speaker,
|
|
text);
|
|
}
|
|
|
|
private static AudioChunk CreateAudio(TimeSpan duration)
|
|
{
|
|
const int sampleRate = 16000;
|
|
const int channels = 1;
|
|
var bytes = new byte[(int)(duration.TotalSeconds * sampleRate * channels * sizeof(short))];
|
|
return new AudioChunk(bytes, sampleRate, channels);
|
|
}
|
|
|
|
private static TimeSpan ReadDuration(byte[] wavBytes)
|
|
{
|
|
using var reader = new WaveFileReader(new MemoryStream(wavBytes));
|
|
return reader.TotalTime;
|
|
}
|
|
|
|
private sealed class CapturingLogger : ILogger
|
|
{
|
|
public List<string> Messages { get; } = [];
|
|
|
|
public IDisposable? BeginScope<TState>(TState state)
|
|
where TState : notnull
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public bool IsEnabled(LogLevel logLevel)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public void Log<TState>(
|
|
LogLevel logLevel,
|
|
EventId eventId,
|
|
TState state,
|
|
Exception? exception,
|
|
Func<TState, Exception?, string> formatter)
|
|
{
|
|
Messages.Add(formatter(state, exception));
|
|
}
|
|
}
|
|
}
|