aria_kit/speaker/turns/
gate.rs1use std::collections::VecDeque;
4use std::ops::Range;
5use std::time::Duration;
6
7use super::{Tally, Timeline};
8use crate::audio::canonical_samples;
9use crate::speaker::Label;
10use crate::transcription::Heard;
11use crate::voice_activity::SpeechMap;
12
13const MIN_SPEECH_SAMPLES: u64 = canonical_samples(Duration::from_millis(200));
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Admission {
21 OwnerOnly,
23 Everyone,
25 Spoken,
27}
28
29pub struct Gate {
31 admission: Admission,
32 from: u64,
34 held: VecDeque<Heard>,
35 provisional: Option<Provisional>,
36 previous_speech: u64,
40}
41
42struct Provisional {
44 text: String,
45 source_window: Option<Range<u64>>,
46 read_to: u64,
48}
49
50pub struct Released {
52 pub heard: Heard,
53 pub speaker: Option<Label>,
54 pub embedding: Option<Vec<f32>>,
56}
57
58impl Released {
59 fn new(heard: Heard, timeline: &Timeline) -> Self {
60 Self {
61 speaker: timeline.label(&heard.window).cloned(),
62 embedding: timeline.embedding(&heard.window),
63 heard,
64 }
65 }
66}
67
68#[derive(Default)]
70pub struct Decision {
71 pub released: Vec<Released>,
72 pub dropped: Vec<Heard>,
74 pub preview: Option<String>,
77}
78
79impl Gate {
80 pub fn new(admission: Admission, from: u64) -> Self {
82 Self {
83 admission,
84 from,
85 held: VecDeque::new(),
86 provisional: None,
87 previous_speech: 0,
88 }
89 }
90
91 pub fn admission(&self) -> Admission {
92 self.admission
93 }
94
95 pub fn undecided_from(&self) -> Option<u64> {
97 self.held.front().map(|heard| heard.window.start)
98 }
99
100 pub fn hold(&mut self, heard: Heard) {
102 self.provisional = None;
103 self.held.push_back(heard);
104 }
105
106 pub fn propose(&mut self, text: String, source_window: Option<Range<u64>>, read_to: u64) {
108 self.provisional = Some(Provisional {
109 text,
110 source_window,
111 read_to,
112 });
113 }
114
115 pub fn decide(&mut self, timeline: &Timeline, speech: Option<&SpeechMap>) -> Decision {
120 let mut decision = Decision {
121 preview: self.preview(timeline),
122 ..Decision::default()
123 };
124 match self.admission {
125 Admission::Everyone => decision.released.extend(
126 self.held
127 .drain(..)
128 .map(|heard| Released::new(heard, timeline)),
129 ),
130 Admission::OwnerOnly => self.release_owner(timeline, &mut decision),
131 Admission::Spoken => self.release_spoken(timeline, speech, &mut decision),
132 }
133 decision
134 }
135
136 fn release_spoken(
139 &mut self,
140 timeline: &Timeline,
141 speech: Option<&SpeechMap>,
142 decision: &mut Decision,
143 ) {
144 while let Some(heard) = self.held.pop_front() {
145 let spoken = match speech.filter(|speech| !speech.is_broken()) {
147 None => true,
148 Some(speech) => {
149 if heard.window.end > speech.reached() && !speech.is_finished() {
150 self.held.push_front(heard);
151 break;
152 }
153 let heard_speech = speech.speech(&heard.window);
154 let enough = heard_speech.max(self.previous_speech) >= MIN_SPEECH_SAMPLES;
155 self.previous_speech = heard_speech;
156 enough
157 }
158 };
159 if spoken {
160 decision.released.push(Released::new(heard, timeline));
161 } else {
162 tracing::debug!("a segment with no speech under it was dropped");
163 decision.dropped.push(heard);
164 }
165 }
166 }
167
168 fn release_owner(&mut self, timeline: &Timeline, decision: &mut Decision) {
170 while let Some(mut heard) = self.held.pop_front() {
171 if timeline.is_broken() {
172 tracing::debug!("segment not delivered: nothing can be judged");
173 decision.dropped.push(heard);
174 continue;
175 }
176 let Some(keep) = word_admissions(timeline, &heard, self.from) else {
177 self.held.push_front(heard);
178 break;
179 };
180 let Some(segment) = heard.segment.keeping(&keep) else {
181 tracing::debug!("segment in another voice not delivered");
182 decision.dropped.push(heard);
183 continue;
184 };
185 if segment.word_spans().is_some() {
186 heard.span = segment.span();
187 heard.window = segment.span().unwrap_or(heard.window);
188 }
189 heard.segment = segment;
190 decision.released.push(Released::new(heard, timeline));
191 }
192 }
193
194 fn preview(&self, timeline: &Timeline) -> Option<String> {
195 let provisional = self.provisional.as_ref()?;
196 Some(match self.admission {
197 Admission::Everyone | Admission::Spoken => provisional.text.clone(),
198 Admission::OwnerOnly => self
199 .owner_preview(timeline, provisional)
200 .unwrap_or_default(),
201 })
202 }
203
204 fn owner_preview(&self, timeline: &Timeline, provisional: &Provisional) -> Option<String> {
207 let read_to = provisional.read_to;
208 if timeline.is_broken() || timeline.reached < read_to {
209 return None;
210 }
211 let fallback = self.from..read_to;
212 let source = provisional.source_window.as_ref().unwrap_or(&fallback);
213 if source.start < self.from || source.start > source.end || source.end > read_to {
214 return None;
215 }
216 let provisional_owner = provisional.text.is_empty() || timeline.admitted_so_far(source);
217 let held_owner = self
218 .held
219 .iter()
220 .all(|heard| timeline.admitted_so_far(&heard.window));
221 if !provisional_owner || !held_owner {
222 return None;
223 }
224 let words: Vec<&str> = self
225 .held
226 .iter()
227 .map(|heard| heard.segment.as_str())
228 .chain([provisional.text.as_str()])
229 .filter(|text| !text.is_empty())
230 .collect();
231 Some(words.join(" "))
232 }
233}
234
235fn word_admissions(timeline: &Timeline, heard: &Heard, from: u64) -> Option<Vec<bool>> {
236 let source = heard.segment.source_window();
237 match heard.segment.word_spans() {
238 Some(words) => words
239 .iter()
240 .map(|word| word_admission(timeline, word, source.as_ref(), from))
241 .collect(),
242 None => timeline
243 .wholly_admitted(&heard.window)
244 .map(|owner| vec![owner]),
245 }
246}
247
248fn word_admission(
249 timeline: &Timeline,
250 word: &Range<u64>,
251 source: Option<&Range<u64>>,
252 from: u64,
253) -> Option<bool> {
254 let admitted = timeline.admission(word)?;
255 if admitted
256 || word.start >= timeline.reached
257 || word.end > timeline.reached
258 || !timeline.tally(word).is_some_and(Tally::is_empty)
259 {
260 return Some(admitted);
261 }
262 let Some(source) = source.filter(|source| source.start >= from && covers(source, word)) else {
263 return Some(false);
264 };
265 timeline.wholly_admitted(source)
267}
268
269fn covers(source: &Range<u64>, word: &Range<u64>) -> bool {
270 source.start <= word.start && word.start < source.end && word.end <= source.end
271}