1use crate::segmentation::SegmentationError;
20use crate::types::Confidence;
21
22pub const MAX_LOCAL_SPEAKERS: usize = 3;
25
26pub const NUM_POWERSET_CLASSES: usize = 7;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum PowersetClass {
32 Silence,
33 Speaker(u8),
34 Pair(u8, u8),
35}
36
37impl PowersetClass {
38 pub const fn is_overlap(self) -> bool {
40 matches!(self, PowersetClass::Pair(_, _))
41 }
42
43 pub fn speakers(self) -> Vec<u8> {
48 match self {
49 PowersetClass::Silence => Vec::new(),
50 PowersetClass::Speaker(s) => vec![s],
51 PowersetClass::Pair(a, b) => vec![a, b],
52 }
53 }
54
55 pub(crate) const fn index(self) -> usize {
61 match self {
62 PowersetClass::Silence => 0,
63 PowersetClass::Speaker(s) => 1 + s as usize,
64 PowersetClass::Pair(a, b) => {
65 let (lo, hi) = if a < b { (a, b) } else { (b, a) };
66 match (lo, hi) {
67 (0, 1) => 4,
68 (0, 2) => 5,
69 (1, 2) => 6,
70 _ => 0,
71 }
72 }
73 }
74 }
75
76 pub(crate) fn from_speakers(speakers: &[u8]) -> Option<PowersetClass> {
82 match speakers {
83 [] => Some(PowersetClass::Silence),
84 [s] if (*s as usize) < MAX_LOCAL_SPEAKERS => Some(PowersetClass::Speaker(*s)),
85 [a, b] => {
86 let (lo, hi) = if a < b { (*a, *b) } else { (*b, *a) };
87 match (lo, hi) {
88 (0, 1) => Some(PowersetClass::Pair(0, 1)),
89 (0, 2) => Some(PowersetClass::Pair(0, 2)),
90 (1, 2) => Some(PowersetClass::Pair(1, 2)),
91 _ => None,
92 }
93 }
94 _ => None,
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq)]
101pub struct FrameLabel {
102 pub class: PowersetClass,
103 pub max_softmax: f32,
105 pub probs: [f32; NUM_POWERSET_CLASSES],
109}
110
111pub(crate) fn softmax(logits: &[f32; NUM_POWERSET_CLASSES]) -> [f32; NUM_POWERSET_CLASSES] {
118 let mut max_logit = f32::NEG_INFINITY;
119 for &l in logits {
120 if l > max_logit {
121 max_logit = l;
122 }
123 }
124 let mut probs = [0.0_f32; NUM_POWERSET_CLASSES];
125 let mut sum = 0.0_f32;
126 for (p, &l) in probs.iter_mut().zip(logits.iter()) {
127 *p = (l - max_logit).exp();
128 sum += *p;
129 }
130 let inv_sum = if sum > 0.0 { 1.0 / sum } else { 1.0 };
132 for p in probs.iter_mut() {
133 *p *= inv_sum;
134 }
135 probs
136}
137
138pub struct PowersetDecoder;
141
142impl PowersetDecoder {
143 pub const fn class_for_index(idx: usize) -> Option<PowersetClass> {
145 match idx {
146 0 => Some(PowersetClass::Silence),
147 1 => Some(PowersetClass::Speaker(0)),
148 2 => Some(PowersetClass::Speaker(1)),
149 3 => Some(PowersetClass::Speaker(2)),
150 4 => Some(PowersetClass::Pair(0, 1)),
151 5 => Some(PowersetClass::Pair(0, 2)),
152 6 => Some(PowersetClass::Pair(1, 2)),
153 _ => None,
154 }
155 }
156
157 pub fn decode_frame(logits: &[f32]) -> Result<FrameLabel, SegmentationError> {
162 if logits.len() != NUM_POWERSET_CLASSES {
163 return Err(SegmentationError::InvalidOutputShape {
164 actual_shape: vec![logits.len()],
165 });
166 }
167 let logits: &[f32; NUM_POWERSET_CLASSES] =
168 logits
169 .first_chunk()
170 .ok_or(SegmentationError::InvalidOutputShape {
171 actual_shape: vec![logits.len()],
172 })?;
173 let probs = softmax(logits);
174 let mut argmax = 0_usize;
175 let mut max_softmax = 0.0_f32;
176 for (i, &p) in probs.iter().enumerate() {
177 if p > max_softmax {
178 max_softmax = p;
179 argmax = i;
180 }
181 }
182 let class = Self::class_for_index(argmax).ok_or(SegmentationError::InvalidOutputShape {
183 actual_shape: vec![argmax],
184 })?;
185 Ok(FrameLabel {
186 class,
187 max_softmax,
188 probs,
189 })
190 }
191
192 pub fn decode_window(
197 logits_flat: &[f32],
198 num_frames: usize,
199 ) -> Result<Vec<FrameLabel>, SegmentationError> {
200 if logits_flat.len() != num_frames * NUM_POWERSET_CLASSES {
201 return Err(SegmentationError::InvalidOutputShape {
202 actual_shape: vec![logits_flat.len()],
203 });
204 }
205 let mut out = Vec::with_capacity(num_frames);
206 for i in 0..num_frames {
207 let frame = &logits_flat[i * NUM_POWERSET_CLASSES..(i + 1) * NUM_POWERSET_CLASSES];
208 out.push(Self::decode_frame(frame)?);
209 }
210 Ok(out)
211 }
212
213 pub fn frame_confidence(softmax: f32) -> Confidence {
219 let clamped = softmax.clamp(0.0, 1.0);
220 Confidence::new(clamped).unwrap_or_default()
222 }
223}
224
225#[allow(clippy::unwrap_used)]
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 fn approx(a: f32, b: f32) -> bool {
231 (a - b).abs() < 1e-6
232 }
233
234 #[test]
235 fn class_0_is_silence() {
236 let logits = [10.0_f32, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
237 let label = PowersetDecoder::decode_frame(&logits).unwrap();
238 assert_eq!(label.class, PowersetClass::Silence);
239 assert!(!label.class.is_overlap());
240 }
241
242 #[test]
243 fn class_1_is_speaker_0() {
244 let logits = [1.0_f32, 10.0, 1.0, 1.0, 1.0, 1.0, 1.0];
245 let label = PowersetDecoder::decode_frame(&logits).unwrap();
246 assert_eq!(label.class, PowersetClass::Speaker(0));
247 }
248
249 #[test]
250 fn class_3_is_speaker_2() {
251 let logits = [1.0_f32, 1.0, 1.0, 10.0, 1.0, 1.0, 1.0];
252 let label = PowersetDecoder::decode_frame(&logits).unwrap();
253 assert_eq!(label.class, PowersetClass::Speaker(2));
254 }
255
256 #[test]
257 fn class_4_is_overlap_pair_0_1() {
258 let logits = [1.0_f32, 1.0, 1.0, 1.0, 10.0, 1.0, 1.0];
259 let label = PowersetDecoder::decode_frame(&logits).unwrap();
260 assert_eq!(label.class, PowersetClass::Pair(0, 1));
261 assert!(label.class.is_overlap());
262 }
263
264 #[test]
265 fn class_5_is_overlap_pair_0_2() {
266 let logits = [1.0_f32, 1.0, 1.0, 1.0, 1.0, 10.0, 1.0];
267 let label = PowersetDecoder::decode_frame(&logits).unwrap();
268 assert_eq!(label.class, PowersetClass::Pair(0, 2));
269 }
270
271 #[test]
272 fn class_6_is_overlap_pair_1_2() {
273 let logits = [1.0_f32, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0];
274 let label = PowersetDecoder::decode_frame(&logits).unwrap();
275 assert_eq!(label.class, PowersetClass::Pair(1, 2));
276 }
277
278 #[test]
279 fn rejects_wrong_logit_count() {
280 let logits = [1.0_f32, 2.0, 3.0];
281 assert!(PowersetDecoder::decode_frame(&logits).is_err());
282 }
283
284 #[test]
285 fn max_softmax_is_softmax_of_argmax_class() {
286 let logits = [0.0_f32; 7];
287 let label = PowersetDecoder::decode_frame(&logits).unwrap();
288 assert!(approx(label.max_softmax, 1.0 / 7.0));
289 }
290
291 #[test]
292 fn confidence_clamps_to_valid_range() {
293 let logits = [-1e6_f32, -1e6, -1e6, -1e6, -1e6, -1e6, 0.0];
294 let label = PowersetDecoder::decode_frame(&logits).unwrap();
295 assert!(label.max_softmax > 0.99);
296 assert!(label.max_softmax <= 1.0 + 1e-6);
297 }
298
299 #[test]
300 fn class_method_returns_speaker_set() {
301 assert_eq!(PowersetClass::Silence.speakers(), Vec::<u8>::new());
302 assert_eq!(PowersetClass::Speaker(0).speakers(), vec![0]);
303 assert_eq!(PowersetClass::Pair(0, 2).speakers(), vec![0, 2]);
304 assert_eq!(PowersetClass::Pair(1, 2).speakers(), vec![1, 2]);
305 }
306
307 #[test]
308 fn decode_window_iterates_over_frames() {
309 let logits_flat: Vec<f32> = vec![
310 10.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 1.0, 1.0, 1.0, 1.0,
311 ];
312 let labels = PowersetDecoder::decode_window(&logits_flat, 2).unwrap();
313 assert_eq!(labels.len(), 2);
314 assert_eq!(labels[0].class, PowersetClass::Silence);
315 assert_eq!(labels[1].class, PowersetClass::Speaker(1));
316 }
317
318 #[test]
319 fn decode_window_rejects_misshaped_buffer() {
320 let logits_flat = vec![1.0_f32; 8];
321 assert!(PowersetDecoder::decode_window(&logits_flat, 1).is_err());
322 }
323
324 #[test]
325 fn confidence_construction_via_helper() {
326 let c = PowersetDecoder::frame_confidence(1.0_f32 + 1e-7);
327 assert!((c.get() - 1.0).abs() < 1e-5);
328
329 let c = PowersetDecoder::frame_confidence(-1e-7);
330 assert!(c.get() >= 0.0);
331 }
332
333 #[test]
334 fn probs_sum_to_one_and_match_max_softmax() {
335 let logits = [0.5_f32, 2.0, -1.0, 0.3, 1.0, -0.2, 0.7];
336 let label = PowersetDecoder::decode_frame(&logits).unwrap();
337 let sum: f32 = label.probs.iter().sum();
338 assert!((sum - 1.0).abs() < 1e-5, "probs must sum to 1, got {sum}");
339 let argmax = label
340 .probs
341 .iter()
342 .enumerate()
343 .max_by(|a, b| a.1.total_cmp(b.1))
344 .map(|(i, _)| i)
345 .unwrap();
346 assert!(approx(label.max_softmax, label.probs[argmax]));
347 assert_eq!(PowersetDecoder::class_for_index(argmax), Some(label.class));
348 }
349
350 #[test]
351 fn class_index_round_trips_through_class_for_index() {
352 for idx in 0..NUM_POWERSET_CLASSES {
353 let class = PowersetDecoder::class_for_index(idx).unwrap();
354 assert_eq!(class.index(), idx, "round-trip failed for {idx}");
355 assert_eq!(
356 PowersetClass::from_speakers(&class.speakers()),
357 Some(class),
358 "from_speakers round-trip failed for {idx}"
359 );
360 }
361 }
362
363 #[test]
364 fn from_speakers_normalizes_pair_order() {
365 assert_eq!(
366 PowersetClass::from_speakers(&[1, 0]),
367 Some(PowersetClass::Pair(0, 1))
368 );
369 assert_eq!(
370 PowersetClass::from_speakers(&[2, 1]),
371 Some(PowersetClass::Pair(1, 2))
372 );
373 }
374
375 #[test]
376 fn from_speakers_rejects_unexpressible_sets() {
377 assert_eq!(PowersetClass::from_speakers(&[0, 1, 2]), None);
378 assert_eq!(PowersetClass::from_speakers(&[3]), None);
379 assert_eq!(PowersetClass::from_speakers(&[1, 1]), None);
380 assert_eq!(PowersetClass::from_speakers(&[0, 3]), None);
381 }
382
383 #[test]
387 fn nan_logits_use_safe_softmax_denominator() {
388 let logits = [f32::NAN; 7];
389 let label = PowersetDecoder::decode_frame(&logits).unwrap();
390 assert_eq!(label.class, PowersetClass::Silence);
391 let probs = softmax(&logits);
392 assert_eq!(probs.len(), NUM_POWERSET_CLASSES);
393 }
394}