Public Access
Add meeting assistant speech and summary automation
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user