Public Access
115 lines
3.3 KiB
C#
115 lines
3.3 KiB
C#
using MeetingAssistant.Speakers;
|
|
using MeetingAssistant.Transcription;
|
|
|
|
namespace MeetingAssistant.Recording;
|
|
|
|
internal sealed class SpeakerAudioSampleCollector
|
|
{
|
|
private readonly object gate = new();
|
|
private readonly RollingAudioBuffer audioBuffer;
|
|
private readonly Dictionary<string, List<SpeakerAudioSample>> samplesBySpeaker = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly int maxSamplesPerSpeaker;
|
|
|
|
public SpeakerAudioSampleCollector(TimeSpan bufferDuration, int maxSamplesPerSpeaker)
|
|
{
|
|
audioBuffer = new RollingAudioBuffer(bufferDuration);
|
|
this.maxSamplesPerSpeaker = Math.Max(1, maxSamplesPerSpeaker);
|
|
}
|
|
|
|
public void AppendAudio(AudioChunk chunk)
|
|
{
|
|
audioBuffer.Append(chunk);
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
lock (gate)
|
|
{
|
|
samplesBySpeaker.Clear();
|
|
audioBuffer.Reset();
|
|
}
|
|
}
|
|
|
|
public void TryAdd(TranscriptionSegment segment)
|
|
{
|
|
if (!IsDiarizedSpeaker(segment.Speaker))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var score = Score(segment);
|
|
if (score <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var wavBytes = audioBuffer.TryExtractWav(segment.Start, segment.End);
|
|
if (wavBytes.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var sample = new SpeakerAudioSample(segment.Speaker, segment, wavBytes, score);
|
|
lock (gate)
|
|
{
|
|
if (!samplesBySpeaker.TryGetValue(segment.Speaker, out var samples))
|
|
{
|
|
samples = [];
|
|
samplesBySpeaker[segment.Speaker] = samples;
|
|
}
|
|
|
|
samples.Add(sample);
|
|
var bestSamples = samples
|
|
.OrderByDescending(candidate => candidate.Score)
|
|
.Take(maxSamplesPerSpeaker)
|
|
.ToList();
|
|
samples.Clear();
|
|
samples.AddRange(bestSamples);
|
|
}
|
|
}
|
|
|
|
public IReadOnlyList<SpeakerAudioSample> Snapshot()
|
|
{
|
|
lock (gate)
|
|
{
|
|
return samplesBySpeaker.Values
|
|
.SelectMany(samples => samples)
|
|
.OrderBy(sample => sample.Speaker, StringComparer.OrdinalIgnoreCase)
|
|
.ThenByDescending(sample => sample.Score)
|
|
.ToList();
|
|
}
|
|
}
|
|
|
|
private static bool IsDiarizedSpeaker(string speaker)
|
|
{
|
|
return !string.IsNullOrWhiteSpace(speaker) &&
|
|
!string.Equals(speaker, "Unknown", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static double Score(TranscriptionSegment segment)
|
|
{
|
|
var durationSeconds = (segment.End - segment.Start).TotalSeconds;
|
|
if (durationSeconds < 2 || durationSeconds > 30)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var words = segment.Text
|
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Length;
|
|
if (words < 3 || words > 80)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var durationScore = 1 - Math.Min(Math.Abs(durationSeconds - 8) / 8, 1);
|
|
var wordScore = 1 - Math.Min(Math.Abs(words - 18) / 18.0, 1);
|
|
var sentenceBonus = segment.Text.TrimEnd().EndsWith('.') ||
|
|
segment.Text.TrimEnd().EndsWith('?') ||
|
|
segment.Text.TrimEnd().EndsWith('!')
|
|
? 5
|
|
: 0;
|
|
return durationScore * 70 + wordScore * 30 + sentenceBonus;
|
|
}
|
|
}
|