use std::ops::Range;
use std::time::Duration;
use super::{Diarizer, Embedder};
use crate::audio::{RecordingReader, canonical_duration, canonical_samples};
use crate::transcription::{
Layer, Operation, Transcriber, TranscriptSegment, TranscriptUpdate, TranscriptionError,
};
use crate::voice_activity::SileroVad;
const MIN_SPEECH_SAMPLES: usize = canonical_samples(Duration::from_millis(200)) as usize;
const TURN_GAP_SAMPLES: u64 = canonical_samples(Duration::from_millis(700));
const TURN_VOICE_SAMPLES: usize = canonical_samples(Duration::from_secs(8)) as usize;
const TURN_JUDGED_SAMPLES: usize = canonical_samples(Duration::from_secs(3)) as usize;
pub const OWN_VOICE_THRESHOLD: f32 = 0.4;
pub struct SegmentLabeler<E> {
vad: Option<SileroVad>,
diarizer: Option<Diarizer<E>>,
previous: Option<String>,
previous_speech: usize,
previous_trailing: usize,
only_own_voice: bool,
turn: Vec<f32>,
turn_voice: Option<Vec<f32>>,
}
pub enum Verdict {
Drop,
Stranger,
Keep {
label: Option<String>,
embedding: Option<Vec<f32>>,
},
}
impl<E: Embedder> SegmentLabeler<E> {
pub fn new(vad: Option<SileroVad>, diarizer: Option<Diarizer<E>>) -> Self {
Self {
vad,
diarizer,
previous: None,
previous_speech: 0,
previous_trailing: 0,
only_own_voice: false,
turn: Vec::new(),
turn_voice: None,
}
}
pub fn diarizes(&self) -> bool {
self.diarizer.is_some()
}
pub fn set_only_own_voice(&mut self, only_own_voice: bool) {
if only_own_voice && self.diarizer.as_ref().is_some_and(|d| d.owners() == 0) {
tracing::warn!("no owner is enrolled; own-voice filtering keeps every voice");
}
self.only_own_voice = only_own_voice;
}
pub fn only_own_voice(&self) -> bool {
self.only_own_voice
}
pub fn judges_own_voice(&self) -> bool {
self.only_own_voice && self.diarizer.as_ref().is_some_and(|d| d.owners() > 0)
}
pub fn set_enrolled(&mut self, enrolled: Vec<super::Profile>) {
if let Some(diarizer) = self.diarizer.as_mut() {
diarizer.set_enrolled(enrolled);
}
}
fn own_voice(&self) -> Option<bool> {
self.judge(self.turn.len(), self.turn_voice.as_deref()?)
}
pub fn own_voice_audio(&mut self, samples: &[f32]) -> Option<Vec<f32>> {
if !self.only_own_voice {
return None;
}
self.diarizer.as_ref()?;
let regions = match self.vad.as_mut() {
Some(vad) => vad.speech_regions(samples),
None => std::iter::once(0..samples.len()).collect(),
};
let mut kept = Vec::new();
for turn in turns(®ions) {
let voice: Vec<f32> = turn
.iter()
.flat_map(|region| samples[region.clone()].iter().copied())
.collect();
let heard = &voice[voice.len().saturating_sub(TURN_VOICE_SAMPLES)..];
let embedding = self
.diarizer
.as_mut()
.and_then(|diarizer| diarizer.embed(heard));
let stranger = embedding
.as_deref()
.and_then(|embedding| self.judge(voice.len(), embedding))
== Some(false);
if !stranger {
kept.extend_from_slice(&voice);
}
}
Some(kept)
}
fn judge(&self, turn_samples: usize, voice: &[f32]) -> Option<bool> {
let diarizer = self.diarizer.as_ref()?;
if turn_samples < TURN_JUDGED_SAMPLES {
return None;
}
let similarity = diarizer.owner_similarity(voice)?;
tracing::debug!(
similarity,
turn_ms = canonical_duration(turn_samples as u64).as_millis(),
"own-voice check"
);
Some(similarity >= OWN_VOICE_THRESHOLD)
}
pub fn observe(&mut self, window: &[f32], silence_before: u64) -> Verdict {
let mut gap = silence_before as usize;
let speech = self.vad.as_mut().map(|vad| vad.speech(window));
if let Some(speech) = &speech {
let regions = &speech.regions;
let leading = regions.first().map_or(window.len(), |run| run.start);
gap += self.previous_trailing + leading;
self.previous_trailing = regions.last().map_or(0, |run| window.len() - run.end);
}
let paused = gap >= TURN_GAP_SAMPLES as usize;
let speech = speech.map(|speech| speech.samples);
if let Some(speech) = &speech {
let heard = speech.len().max(self.previous_speech);
self.previous_speech = speech.len();
if heard < MIN_SPEECH_SAMPLES {
tracing::debug!(
speech_samples = heard,
window_samples = window.len(),
"the speech gate dropped a segment"
);
return Verdict::Drop;
}
}
let voice = speech.as_deref().unwrap_or(window);
self.hear_turn(voice, paused);
let embedding = self
.diarizer
.as_mut()
.and_then(|diarizer| diarizer.embed(voice));
let label = match (&embedding, self.diarizer.as_mut()) {
(Some(embedding), Some(diarizer)) => {
Some(diarizer.label(embedding, self.previous.as_deref()))
}
_ if paused => None,
_ => self.previous.clone(),
};
self.previous.clone_from(&label);
if self.only_own_voice && self.own_voice() == Some(false) {
return Verdict::Stranger;
}
Verdict::Keep { label, embedding }
}
fn hear_turn(&mut self, voice: &[f32], paused: bool) {
let Some(diarizer) = self.diarizer.as_mut() else {
return;
};
if paused {
self.turn.clear();
}
self.turn.extend_from_slice(voice);
let excess = self.turn.len().saturating_sub(TURN_VOICE_SAMPLES);
self.turn.drain(..excess);
self.turn_voice = diarizer.embed(&self.turn);
}
pub fn relabel(&self, paragraphs: &[Option<Vec<f32>>]) -> Vec<Option<String>> {
match &self.diarizer {
Some(diarizer) => diarizer.relabel(paragraphs),
None => vec![None; paragraphs.len()],
}
}
}
pub struct SegmentReader<E> {
labeler: SegmentLabeler<E>,
mark: u64,
before: u64,
awaiting_finalized: bool,
heard_through: Option<u64>,
warned_untimed: bool,
}
impl<E: Embedder> SegmentReader<E> {
pub fn new(labeler: SegmentLabeler<E>) -> Self {
Self {
labeler,
mark: 0,
before: 0,
awaiting_finalized: false,
heard_through: None,
warned_untimed: false,
}
}
pub fn labeler(&self) -> &SegmentLabeler<E> {
&self.labeler
}
pub fn rewind(&mut self) {
self.mark = 0;
self.before = 0;
self.awaiting_finalized = false;
self.heard_through = None;
}
pub fn labeler_mut(&mut self) -> &mut SegmentLabeler<E> {
&mut self.labeler
}
pub fn next<T: Transcriber, L: Layer>(
&mut self,
operation: &mut Operation<'_, T, L>,
recording: &mut RecordingReader,
) -> Result<Option<Heard>, TranscriptionError> {
match self.read(operation, recording, false)? {
Some(HeardUpdate::Finalized(heard)) => Ok(Some(heard)),
Some(HeardUpdate::Provisional(_)) => unreachable!("previews are disabled"),
None => Ok(None),
}
}
pub fn next_update<T: Transcriber, L: Layer>(
&mut self,
operation: &mut Operation<'_, T, L>,
recording: &mut RecordingReader,
) -> Result<Option<HeardUpdate>, TranscriptionError> {
self.read(operation, recording, true)
}
fn read<T: Transcriber, L: Layer>(
&mut self,
operation: &mut Operation<'_, T, L>,
recording: &mut RecordingReader,
previews: bool,
) -> Result<Option<HeardUpdate>, TranscriptionError> {
if !self.awaiting_finalized {
self.before = recording.held_to();
self.awaiting_finalized = true;
}
let segment = match operation.next(recording, previews)? {
Some(TranscriptUpdate::Finalized(segment)) => segment,
Some(TranscriptUpdate::Provisional(text)) => {
return Ok(Some(HeardUpdate::Provisional(text)));
}
None => {
self.awaiting_finalized = false;
return Ok(None);
}
};
self.awaiting_finalized = false;
let silence = self.silence_before(&segment);
let span = segment.span();
let window = span.clone().unwrap_or(self.mark..self.before);
let at = window.start;
self.mark = self.before;
let verdict = self.labeler.observe(recording.span(window), silence);
recording.release_before(operation.retained_from());
Ok(Some(HeardUpdate::Finalized(Heard {
segment,
verdict,
span,
at,
})))
}
fn silence_before(&mut self, segment: &TranscriptSegment) -> u64 {
let Some(span) = segment.span() else {
if !self.warned_untimed {
self.warned_untimed = true;
tracing::info!(
"the engine reports no word timings; speaker turns are not detected by pause"
);
}
return 0;
};
let gap = self
.heard_through
.map_or(0, |end| span.start.saturating_sub(end));
self.heard_through = Some(span.end);
gap
}
}
fn turns(regions: &[Range<usize>]) -> Vec<Vec<Range<usize>>> {
let mut turns: Vec<Vec<Range<usize>>> = Vec::new();
for region in regions {
let gap = turns
.last()
.and_then(|turn| turn.last())
.map_or(0, |last| region.start.saturating_sub(last.end));
if turns.is_empty() || gap >= TURN_GAP_SAMPLES as usize {
turns.push(Vec::new());
}
turns
.last_mut()
.expect("a turn was just opened")
.push(region.clone());
}
turns
}
pub enum HeardUpdate {
Finalized(Heard),
Provisional(String),
}
pub struct Heard {
pub segment: TranscriptSegment,
pub verdict: Verdict,
pub span: Option<std::ops::Range<u64>>,
pub at: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audio::CANONICAL_SAMPLE_RATE;
use crate::speaker::SpeakerError;
#[test]
fn preview_reads_preserve_finalized_labeling_audio_and_completion() {
use crate::audio::testing::{Pace, paced};
use crate::cancel::CancelToken;
use crate::transcription::{
Cadence, Hypothesis, Language, TimedWord, Transcription, TranscriptionAudio,
};
use std::sync::{Arc, Mutex};
struct Script {
pace: Pace,
calls: Mutex<usize>,
timed: bool,
}
impl Transcriber for Script {
fn validate_language(
&self,
_: &Language,
_: &CancelToken,
) -> Result<(), TranscriptionError> {
Ok(())
}
fn transcribe_with(
&self,
audio: TranscriptionAudio<'_>,
_: &Language,
_: &CancelToken,
) -> Result<Hypothesis, TranscriptionError> {
let mut calls = self.calls.lock().expect("no poison");
let text = ["one two", "one two three", "three four", "three four five"][*calls];
*calls += 1;
self.pace.advance();
if self.timed {
let duration = audio.require_samples("script")?.len() as f32
/ CANONICAL_SAMPLE_RATE as f32;
let words = text.split_whitespace().count();
Ok(Hypothesis::with_words(
text,
(0..words)
.map(|index| TimedWord {
start: duration * index as f32 / words as f32,
end: duration * (index + 1) as f32 / words as f32,
})
.collect(),
))
} else {
Ok(Hypothesis::new(text))
}
}
}
struct Observed(Arc<Mutex<Vec<usize>>>);
impl Embedder for Observed {
fn embed(&mut self, window: &[f32]) -> Result<Option<Vec<f32>>, SpeakerError> {
self.0.lock().expect("no poison").push(window.len());
Ok(Some(vec![1.0, 0.0]))
}
fn dimension(&self) -> usize {
2
}
}
let run = |previews, timed| {
let (_directory, mut recording, pace) = paced(vec![vec![8192_i16; 1600]; 4]);
let transcription = Transcription::agreement(
Cadence::from_millis(100, 200),
Script {
pace,
calls: Mutex::new(0),
timed,
},
);
let mut operation = transcription.start(&Language::Automatic).expect("starts");
let observations = Arc::new(Mutex::new(Vec::new()));
let mut reader = SegmentReader::new(SegmentLabeler::new(
None,
Some(Diarizer::new(
Observed(Arc::clone(&observations)),
Vec::new(),
)),
));
let mut rows = Vec::new();
let mut preview_count = 0;
loop {
let update = if previews {
reader.next_update(&mut operation, &mut recording)
} else {
reader
.next(&mut operation, &mut recording)
.map(|heard| heard.map(HeardUpdate::Finalized))
}
.expect("reads");
match update {
Some(HeardUpdate::Finalized(heard)) => {
assert!(matches!(heard.verdict, Verdict::Keep { .. }));
rows.push((
heard.segment.into_text(),
heard.at,
reader.mark,
reader.before,
));
}
Some(HeardUpdate::Provisional(_)) => preview_count += 1,
None => break,
}
}
let final_text = operation
.finish(&mut recording)
.expect("transcript")
.into_text();
let windows = observations.lock().expect("no poison").clone();
(rows, final_text, preview_count, windows)
};
for timed in [false, true] {
let (baseline, baseline_text, _, baseline_windows) = run(false, timed);
let (previewed, preview_text, count, preview_windows) = run(true, timed);
assert!(count > 0);
assert_eq!(previewed, baseline);
assert_eq!(preview_text, baseline_text);
assert_eq!(preview_windows, baseline_windows);
}
}
const OWN: [f32; 2] = [1.0, 0.0];
const OTHER: [f32; 2] = [0.0, 1.0];
struct Voice([f32; 2]);
impl Embedder for Voice {
fn embed(&mut self, _window: &[f32]) -> Result<Option<Vec<f32>>, SpeakerError> {
Ok(Some(self.0.to_vec()))
}
fn dimension(&self) -> usize {
2
}
}
fn labeler(voice: [f32; 2]) -> SegmentLabeler<Voice> {
let enrolled = vec![super::super::enrolled_voice(
"alex",
super::super::Role::Owner,
OWN.to_vec(),
)];
let mut labeler = SegmentLabeler::new(None, Some(Diarizer::new(Voice(voice), enrolled)));
labeler.set_only_own_voice(true);
labeler
}
#[test]
fn a_stranger_is_kept_out_only_when_asked() {
let mut stranger = labeler(OTHER);
stranger.observe(&[0.0; 48_000], 0);
assert!(matches!(
stranger.observe(&[0.0; 1_600], 0),
Verdict::Stranger
));
stranger.set_only_own_voice(false);
assert!(matches!(
stranger.observe(&[0.0; 1_600], 0),
Verdict::Keep { .. }
));
}
#[test]
fn the_turn_is_judged_once_enough_of_it_was_heard() {
let mut own = labeler(OWN);
own.observe(&[0.0; 32_000], 0);
assert_eq!(own.own_voice(), None, "two seconds are too little to judge");
own.observe(&[0.0; 16_000], 0);
assert_eq!(own.own_voice(), Some(true));
let mut other = labeler(OTHER);
other.observe(&[0.0; 48_000], 0);
assert_eq!(other.own_voice(), Some(false));
}
#[test]
fn a_pause_opens_a_new_turn() {
let mut labeler = labeler(OTHER);
labeler.observe(&[0.0; 48_000], 0);
assert_eq!(labeler.own_voice(), Some(false));
labeler.observe(&[0.0; 16_000], TURN_GAP_SAMPLES);
assert_eq!(labeler.own_voice(), None, "the new turn is a second old");
}
#[test]
fn a_whole_recording_keeps_the_enrolled_voice_and_drops_a_stranger() {
let audio = vec![0.0; 64_000];
let kept = labeler(OWN).own_voice_audio(&audio);
assert_eq!(kept.map(|kept| kept.len()), Some(64_000));
let kept = labeler(OTHER).own_voice_audio(&audio);
assert_eq!(kept.map(|kept| kept.len()), Some(0));
let mut everyone = labeler(OTHER);
everyone.set_only_own_voice(false);
assert!(everyone.own_voice_audio(&audio).is_none());
}
#[test]
fn a_recording_too_short_to_judge_is_kept_on_trust() {
let kept = labeler(OTHER).own_voice_audio(&[0.0; 16_000]);
assert_eq!(kept.map(|kept| kept.len()), Some(16_000));
}
#[test]
fn regions_a_turn_gap_apart_are_separate_turns() {
let gap = TURN_GAP_SAMPLES as usize;
let grouped = turns(&[0..100, 200..300, 300 + gap..400 + gap]);
assert_eq!(grouped.len(), 2);
assert_eq!(grouped[0], [0..100, 200..300]);
assert_eq!(grouped[1].len(), 1);
assert_eq!(grouped[1][0], 300 + gap..400 + gap);
}
#[test]
fn without_a_speaker_model_no_turn_is_judged() {
let mut labeler = SegmentLabeler::<Voice>::new(None, None);
labeler.observe(&[0.0; 48_000], 0);
assert_eq!(labeler.own_voice(), None);
}
}