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> 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 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; } }