1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::{collections::BTreeSet, error::Error, fmt, str::FromStr};
3
4pub const MAX_AUDIO_DURATION_MS: u64 = 150_000;
5pub const OGG_MEDIA_TYPE: &str = "audio/ogg";
6pub const FEATURE_SCHEMA_REVISION: &str = "speaker-v3-features-24-r1";
7
8pub const FEATURE_NAMES: [&str; 24] = [
9 "median_f0_hz",
10 "high_front_vowel_f1_hz",
11 "high_back_vowel_f2_hz",
12 "spectral_tilt_db_per_octave",
13 "cepstral_peak_prominence_db",
14 "foreign_accentedness_1_to_9",
15 "dominant_rhotic_realization",
16 "unstressed_vowel_reduction_percent",
17 "high_front_vowel_f2_hz",
18 "low_vowel_f1_hz",
19 "h1_minus_h2_db",
20 "rhotic_f3_minus_f2_hz",
21 "word_initial_t_vot_ms",
22 "dominant_lateral_realization",
23 "monophthongization_percent",
24 "vocal_gender_presentation",
25 "low_vowel_f2_hz",
26 "high_back_vowel_f1_hz",
27 "mean_formant_dispersion_hz",
28 "creaky_phonation_percent",
29 "hypernasality_0_to_4",
30 "sibilant_center_of_gravity_hz",
31 "consonant_cluster_reduction_percent",
32 "perceived_vocal_age_years",
33];
34
35const NUMERIC_FEATURE_NAMES: [&str; 22] = [
36 "median_f0_hz",
37 "high_front_vowel_f1_hz",
38 "high_back_vowel_f2_hz",
39 "spectral_tilt_db_per_octave",
40 "cepstral_peak_prominence_db",
41 "foreign_accentedness_1_to_9",
42 "unstressed_vowel_reduction_percent",
43 "high_front_vowel_f2_hz",
44 "low_vowel_f1_hz",
45 "h1_minus_h2_db",
46 "rhotic_f3_minus_f2_hz",
47 "word_initial_t_vot_ms",
48 "monophthongization_percent",
49 "vocal_gender_presentation",
50 "low_vowel_f2_hz",
51 "high_back_vowel_f1_hz",
52 "mean_formant_dispersion_hz",
53 "creaky_phonation_percent",
54 "hypernasality_0_to_4",
55 "sibilant_center_of_gravity_hz",
56 "consonant_cluster_reduction_percent",
57 "perceived_vocal_age_years",
58];
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum ValidationError {
62 InvalidOgg,
63 InvalidDuration(u64),
64 ByteLengthOverflow,
65 Blank(&'static str),
66 InvalidSpeakerLabel(String),
67 DuplicateSpeakerLabel(LocalSpeakerLabel),
68 NonFiniteFeature(&'static str),
69}
70
71impl fmt::Display for ValidationError {
72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::InvalidOgg => {
75 formatter.write_str("audio is not a complete valid Ogg Opus stream")
76 }
77 Self::InvalidDuration(value) => {
78 write!(formatter, "invalid audio duration: {value} ms")
79 }
80 Self::ByteLengthOverflow => formatter.write_str("audio byte length exceeds u64"),
81 Self::Blank(field) => write!(formatter, "{field} is blank"),
82 Self::InvalidSpeakerLabel(value) => {
83 write!(formatter, "invalid speaker label: {value}")
84 }
85 Self::DuplicateSpeakerLabel(value) => {
86 write!(formatter, "duplicate speaker label: {value}")
87 }
88 Self::NonFiniteFeature(field) => write!(formatter, "{field} is not finite"),
89 }
90 }
91}
92
93impl Error for ValidationError {}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub struct OggAudioMetadata {
97 duration_ms: u64,
98 byte_length: u64,
99 filename: Option<String>,
100}
101
102impl OggAudioMetadata {
103 pub fn from_bytes(
104 bytes: &[u8],
105 duration_ms: u64,
106 filename: Option<String>,
107 ) -> Result<Self, ValidationError> {
108 validate_duration(duration_ms)?;
109 validate_optional_text(filename.as_deref(), "filename")?;
110 if bytes.len() < 27 || &bytes[..4] != b"OggS" || bytes[4] != 0 {
111 return Err(ValidationError::InvalidOgg);
112 }
113 let body_start = 27 + bytes[26] as usize;
114 if bytes.len() < body_start {
115 return Err(ValidationError::InvalidOgg);
116 }
117 let body_length: usize = bytes[27..body_start]
118 .iter()
119 .map(|value| *value as usize)
120 .sum();
121 if bytes.len() < body_start + body_length {
122 return Err(ValidationError::InvalidOgg);
123 }
124 Ok(Self {
125 duration_ms,
126 byte_length: u64::try_from(bytes.len())
127 .map_err(|_| ValidationError::ByteLengthOverflow)?,
128 filename,
129 })
130 }
131
132 pub fn from_ogg_bytes(bytes: &[u8]) -> Result<Self, ValidationError> {
133 let stream = StrictOgg::parse(bytes)?;
134 let samples = stream
135 .final_granule
136 .checked_sub(u64::from(stream.pre_skip))
137 .ok_or(ValidationError::InvalidOgg)?;
138 let duration_ms = samples / 48 + u64::from(samples % 48 != 0);
139 validate_duration(duration_ms)?;
140 Ok(Self {
141 duration_ms,
142 byte_length: u64::try_from(bytes.len())
143 .map_err(|_| ValidationError::ByteLengthOverflow)?,
144 filename: None,
145 })
146 }
147
148 pub fn validate(&self) -> Result<(), ValidationError> {
149 validate_duration(self.duration_ms)?;
150 validate_optional_text(self.filename.as_deref(), "filename")?;
151 (self.byte_length >= 27)
152 .then_some(())
153 .ok_or(ValidationError::InvalidOgg)
154 }
155
156 pub fn duration_ms(&self) -> u64 {
157 self.duration_ms
158 }
159
160 pub fn byte_length(&self) -> u64 {
161 self.byte_length
162 }
163
164 pub fn filename(&self) -> Option<&str> {
165 self.filename.as_deref()
166 }
167
168 pub fn media_type(&self) -> &'static str {
169 OGG_MEDIA_TYPE
170 }
171}
172
173struct StrictOgg {
174 pre_skip: u16,
175 final_granule: u64,
176}
177
178impl StrictOgg {
179 fn parse(bytes: &[u8]) -> Result<Self, ValidationError> {
180 let mut offset = 0usize;
181 let mut serial = None;
182 let mut expected_sequence = 0u32;
183 let mut saw_bos = false;
184 let mut saw_eos = false;
185 let mut packet = Vec::new();
186 let mut packet_open = false;
187 let mut first_packet = None;
188 let mut last_granule = None;
189 let mut final_granule = None;
190
191 while offset < bytes.len() {
192 let page = Page::read(bytes, offset)?;
193 offset = page.end;
194
195 if saw_eos || page.header_type & !0x07 != 0 {
196 return Err(ValidationError::InvalidOgg);
197 }
198 if serial
199 .replace(page.serial)
200 .is_some_and(|known| known != page.serial)
201 {
202 return Err(ValidationError::InvalidOgg);
203 }
204 if page.sequence != expected_sequence {
205 return Err(ValidationError::InvalidOgg);
206 }
207 expected_sequence = expected_sequence
208 .checked_add(1)
209 .ok_or(ValidationError::InvalidOgg)?;
210
211 let continuation = page.header_type & 0x01 != 0;
212 let bos = page.header_type & 0x02 != 0;
213 let eos = page.header_type & 0x04 != 0;
214 if bos != !saw_bos || continuation != packet_open {
215 return Err(ValidationError::InvalidOgg);
216 }
217 saw_bos = true;
218
219 if let Some(granule) = page.granule {
220 if last_granule.is_some_and(|last| granule < last) {
221 return Err(ValidationError::InvalidOgg);
222 }
223 last_granule = Some(granule);
224 }
225
226 let mut body_offset = page.body_start;
227 for lace in page.laces {
228 let end = body_offset
229 .checked_add(usize::from(*lace))
230 .ok_or(ValidationError::InvalidOgg)?;
231 packet.extend_from_slice(&bytes[body_offset..end]);
232 body_offset = end;
233 packet_open = *lace == 255;
234 if !packet_open {
235 if first_packet.is_none() {
236 first_packet = Some(std::mem::take(&mut packet));
237 } else {
238 packet.clear();
239 }
240 }
241 }
242
243 if eos {
244 if packet_open || first_packet.is_none() || page.granule.is_none() {
245 return Err(ValidationError::InvalidOgg);
246 }
247 saw_eos = true;
248 final_granule = page.granule;
249 }
250 }
251
252 if !saw_bos || !saw_eos {
253 return Err(ValidationError::InvalidOgg);
254 }
255 let head = first_packet.ok_or(ValidationError::InvalidOgg)?;
256 let pre_skip = parse_opus_head(&head)?;
257 Ok(Self {
258 pre_skip,
259 final_granule: final_granule.ok_or(ValidationError::InvalidOgg)?,
260 })
261 }
262}
263
264struct Page<'a> {
265 header_type: u8,
266 granule: Option<u64>,
267 serial: u32,
268 sequence: u32,
269 laces: &'a [u8],
270 body_start: usize,
271 end: usize,
272}
273
274impl<'a> Page<'a> {
275 fn read(bytes: &'a [u8], offset: usize) -> Result<Self, ValidationError> {
276 let header = bytes
277 .get(offset..offset + 27)
278 .ok_or(ValidationError::InvalidOgg)?;
279 if &header[..4] != b"OggS" || header[4] != 0 {
280 return Err(ValidationError::InvalidOgg);
281 }
282
283 let segments = usize::from(header[26]);
284 let laces = bytes
285 .get(offset + 27..offset + 27 + segments)
286 .ok_or(ValidationError::InvalidOgg)?;
287 let body_length: usize = laces.iter().map(|&lace| usize::from(lace)).sum();
288 let body_start = offset + 27 + segments;
289 let end = body_start
290 .checked_add(body_length)
291 .ok_or(ValidationError::InvalidOgg)?;
292 if end > bytes.len() {
293 return Err(ValidationError::InvalidOgg);
294 }
295
296 let expected_crc = u32::from_le_bytes(
297 header[22..26]
298 .try_into()
299 .map_err(|_| ValidationError::InvalidOgg)?,
300 );
301 if ogg_crc(&bytes[offset..end]) != expected_crc {
302 return Err(ValidationError::InvalidOgg);
303 }
304
305 let raw_granule = u64::from_le_bytes(
306 header[6..14]
307 .try_into()
308 .map_err(|_| ValidationError::InvalidOgg)?,
309 );
310 Ok(Self {
311 header_type: header[5],
312 granule: (raw_granule != u64::MAX).then_some(raw_granule),
313 serial: u32::from_le_bytes(
314 header[14..18]
315 .try_into()
316 .map_err(|_| ValidationError::InvalidOgg)?,
317 ),
318 sequence: u32::from_le_bytes(
319 header[18..22]
320 .try_into()
321 .map_err(|_| ValidationError::InvalidOgg)?,
322 ),
323 laces,
324 body_start,
325 end,
326 })
327 }
328}
329
330fn parse_opus_head(packet: &[u8]) -> Result<u16, ValidationError> {
331 if packet.len() != 19
332 || &packet[..8] != b"OpusHead"
333 || packet[8] != 1
334 || !(1..=2).contains(&packet[9])
335 || packet[18] != 0
336 {
337 return Err(ValidationError::InvalidOgg);
338 }
339 Ok(u16::from_le_bytes([packet[10], packet[11]]))
340}
341
342fn ogg_crc(page: &[u8]) -> u32 {
343 let mut crc = 0u32;
344 for (index, &byte) in page.iter().enumerate() {
345 let byte = if (22..26).contains(&index) { 0 } else { byte };
346 crc ^= u32::from(byte) << 24;
347 for _ in 0..8 {
348 crc = if crc & 0x8000_0000 != 0 {
349 (crc << 1) ^ 0x04c1_1db7
350 } else {
351 crc << 1
352 };
353 }
354 }
355 crc
356}
357
358fn validate_duration(duration_ms: u64) -> Result<(), ValidationError> {
359 (1..=MAX_AUDIO_DURATION_MS)
360 .contains(&duration_ms)
361 .then_some(())
362 .ok_or(ValidationError::InvalidDuration(duration_ms))
363}
364
365fn validate_optional_text(value: Option<&str>, field: &'static str) -> Result<(), ValidationError> {
366 if value.is_some_and(|text| text.trim().is_empty()) {
367 return Err(ValidationError::Blank(field));
368 }
369 Ok(())
370}
371
372fn validate_text(value: &str, field: &'static str) -> Result<(), ValidationError> {
373 (!value.trim().is_empty())
374 .then_some(())
375 .ok_or(ValidationError::Blank(field))
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
379pub struct LocalSpeakerLabel(u32);
380
381impl LocalSpeakerLabel {
382 pub fn new(number: u32) -> Result<Self, ValidationError> {
383 (number > 0)
384 .then_some(Self(number))
385 .ok_or_else(|| ValidationError::InvalidSpeakerLabel("Speaker 0".into()))
386 }
387
388 pub fn number(self) -> u32 {
389 self.0
390 }
391}
392
393impl fmt::Display for LocalSpeakerLabel {
394 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
395 write!(formatter, "Speaker {}", self.0)
396 }
397}
398
399impl FromStr for LocalSpeakerLabel {
400 type Err = ValidationError;
401
402 fn from_str(value: &str) -> Result<Self, Self::Err> {
403 value
404 .strip_prefix("Speaker ")
405 .and_then(|number| number.parse::<u32>().ok())
406 .filter(|number| *number > 0)
407 .map(Self)
408 .ok_or_else(|| ValidationError::InvalidSpeakerLabel(value.into()))
409 }
410}
411
412impl Serialize for LocalSpeakerLabel {
413 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
414 serializer.serialize_str(&self.to_string())
415 }
416}
417
418impl<'de> Deserialize<'de> for LocalSpeakerLabel {
419 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
420 String::deserialize(deserializer)?
421 .parse()
422 .map_err(serde::de::Error::custom)
423 }
424}
425
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
427#[serde(rename_all = "snake_case")]
428pub enum VocalGenderPresentation {
429 StronglyFeminine,
430 Feminine,
431 Androgynous,
432 Masculine,
433 StronglyMasculine,
434}
435
436impl VocalGenderPresentation {
437 pub fn numeric_value(self) -> f64 {
438 match self {
439 Self::StronglyFeminine => -2.0,
440 Self::Feminine => -1.0,
441 Self::Androgynous => 0.0,
442 Self::Masculine => 1.0,
443 Self::StronglyMasculine => 2.0,
444 }
445 }
446}
447
448#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
449pub struct FeatureVector24 {
450 pub median_f0_hz: Option<f64>,
451 pub high_front_vowel_f1_hz: Option<f64>,
452 pub high_back_vowel_f2_hz: Option<f64>,
453 pub spectral_tilt_db_per_octave: Option<f64>,
454 pub cepstral_peak_prominence_db: Option<f64>,
455 pub foreign_accentedness_1_to_9: Option<f64>,
456 pub dominant_rhotic_realization: Option<String>,
457 pub unstressed_vowel_reduction_percent: Option<f64>,
458 pub high_front_vowel_f2_hz: Option<f64>,
459 pub low_vowel_f1_hz: Option<f64>,
460 pub h1_minus_h2_db: Option<f64>,
461 pub rhotic_f3_minus_f2_hz: Option<f64>,
462 pub word_initial_t_vot_ms: Option<f64>,
463 pub dominant_lateral_realization: Option<String>,
464 pub monophthongization_percent: Option<f64>,
465 pub vocal_gender_presentation: Option<VocalGenderPresentation>,
466 pub low_vowel_f2_hz: Option<f64>,
467 pub high_back_vowel_f1_hz: Option<f64>,
468 pub mean_formant_dispersion_hz: Option<f64>,
469 pub creaky_phonation_percent: Option<f64>,
470 pub hypernasality_0_to_4: Option<f64>,
471 pub sibilant_center_of_gravity_hz: Option<f64>,
472 pub consonant_cluster_reduction_percent: Option<f64>,
473 pub perceived_vocal_age_years: Option<f64>,
474}
475
476impl FeatureVector24 {
477 pub fn validate(&self) -> Result<(), ValidationError> {
478 for (name, value) in NUMERIC_FEATURE_NAMES.into_iter().zip(self.numeric_values()) {
479 if value.is_some_and(|number| !number.is_finite()) {
480 return Err(ValidationError::NonFiniteFeature(name));
481 }
482 }
483 validate_optional_text(
484 self.dominant_rhotic_realization.as_deref(),
485 "dominant_rhotic_realization",
486 )?;
487 validate_optional_text(
488 self.dominant_lateral_realization.as_deref(),
489 "dominant_lateral_realization",
490 )
491 }
492
493 pub fn numeric_values(&self) -> [Option<f64>; 22] {
494 [
495 self.median_f0_hz,
496 self.high_front_vowel_f1_hz,
497 self.high_back_vowel_f2_hz,
498 self.spectral_tilt_db_per_octave,
499 self.cepstral_peak_prominence_db,
500 self.foreign_accentedness_1_to_9,
501 self.unstressed_vowel_reduction_percent,
502 self.high_front_vowel_f2_hz,
503 self.low_vowel_f1_hz,
504 self.h1_minus_h2_db,
505 self.rhotic_f3_minus_f2_hz,
506 self.word_initial_t_vot_ms,
507 self.monophthongization_percent,
508 self.vocal_gender_presentation
509 .map(VocalGenderPresentation::numeric_value),
510 self.low_vowel_f2_hz,
511 self.high_back_vowel_f1_hz,
512 self.mean_formant_dispersion_hz,
513 self.creaky_phonation_percent,
514 self.hypernasality_0_to_4,
515 self.sibilant_center_of_gravity_hz,
516 self.consonant_cluster_reduction_percent,
517 self.perceived_vocal_age_years,
518 ]
519 }
520
521 pub fn nominal_values(&self) -> [Option<&str>; 2] {
522 [
523 self.dominant_rhotic_realization.as_deref(),
524 self.dominant_lateral_realization.as_deref(),
525 ]
526 }
527
528 pub fn present_feature_count(&self) -> u8 {
529 (self
530 .numeric_values()
531 .into_iter()
532 .filter(Option::is_some)
533 .count()
534 + self
535 .nominal_values()
536 .into_iter()
537 .filter(Option::is_some)
538 .count()) as u8
539 }
540}
541
542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
543pub struct StructuredSpeaker {
544 pub speaker: LocalSpeakerLabel,
545 pub language: String,
546 pub features: FeatureVector24,
547 pub features_usable_for_training: bool,
548}
549
550impl StructuredSpeaker {
551 pub fn validate(&self) -> Result<(), ValidationError> {
552 validate_text(&self.language, "language")?;
553 self.features.validate()
554 }
555}
556
557#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
558pub struct StructuredAnalysis {
559 pub transcript: String,
560 pub speakers: Vec<StructuredSpeaker>,
561}
562
563impl StructuredAnalysis {
564 pub fn validate(&self) -> Result<(), ValidationError> {
565 validate_text(&self.transcript, "transcript")?;
566 let mut labels = BTreeSet::new();
567 for speaker in &self.speakers {
568 speaker.validate()?;
569 if !labels.insert(speaker.speaker) {
570 return Err(ValidationError::DuplicateSpeakerLabel(speaker.speaker));
571 }
572 }
573 Ok(())
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580
581 fn page(
582 serial: u32,
583 sequence: u32,
584 flags: u8,
585 granule: u64,
586 laces: &[u8],
587 body: &[u8],
588 ) -> Vec<u8> {
589 assert_eq!(
590 laces.iter().map(|&value| usize::from(value)).sum::<usize>(),
591 body.len()
592 );
593 let mut bytes = Vec::with_capacity(27 + laces.len() + body.len());
594 bytes.extend_from_slice(b"OggS");
595 bytes.push(0);
596 bytes.push(flags);
597 bytes.extend_from_slice(&granule.to_le_bytes());
598 bytes.extend_from_slice(&serial.to_le_bytes());
599 bytes.extend_from_slice(&sequence.to_le_bytes());
600 bytes.extend_from_slice(&[0; 4]);
601 bytes.push(laces.len() as u8);
602 bytes.extend_from_slice(laces);
603 bytes.extend_from_slice(body);
604 set_crc(&mut bytes);
605 bytes
606 }
607
608 fn set_crc(page: &mut [u8]) {
609 page[22..26].fill(0);
610 let crc = ogg_crc(page);
611 page[22..26].copy_from_slice(&crc.to_le_bytes());
612 }
613
614 fn head(pre_skip: u16, channels: u8, mapping: u8) -> Vec<u8> {
615 let mut value = b"OpusHead".to_vec();
616 value.push(1);
617 value.push(channels);
618 value.extend_from_slice(&pre_skip.to_le_bytes());
619 value.extend_from_slice(&48_000u32.to_le_bytes());
620 value.extend_from_slice(&0u16.to_le_bytes());
621 value.push(mapping);
622 value
623 }
624
625 fn one_page_stream(pre_skip: u16, granule: u64) -> Vec<u8> {
626 let mut body = head(pre_skip, 1, 0);
627 body.push(0);
628 page(7, 0, 0x06, granule, &[19, 1], &body)
629 }
630
631 fn two_page_stream(pre_skip: u16, granule: u64) -> Vec<u8> {
632 let head = head(pre_skip, 1, 0);
633 let mut bytes = page(7, 0, 0x02, 0, &[19], &head);
634 bytes.extend(page(7, 1, 0x04, granule, &[1], &[0]));
635 bytes
636 }
637
638 fn assert_invalid(bytes: &[u8]) {
639 assert_eq!(
640 OggAudioMetadata::from_ogg_bytes(bytes),
641 Err(ValidationError::InvalidOgg)
642 );
643 }
644
645 #[test]
646 fn strict_ogg_accepts_single_and_multiple_pages() {
647 let one = one_page_stream(0, 48);
648 assert_eq!(
649 OggAudioMetadata::from_ogg_bytes(&one)
650 .unwrap()
651 .duration_ms(),
652 1
653 );
654
655 let head = head(48, 2, 0);
656 let mut multi = page(9, 0, 0x02, 0, &[19], &head);
657 multi.extend(page(9, 1, 0, 96, &[1], &[3]));
658 multi.extend(page(9, 2, 0x04, 144, &[1], &[4]));
659 assert_eq!(
660 OggAudioMetadata::from_ogg_bytes(&multi)
661 .unwrap()
662 .duration_ms(),
663 2
664 );
665 }
666
667 #[test]
668 fn strict_ogg_uses_pre_skip_and_ceiling_duration() {
669 assert_eq!(
670 OggAudioMetadata::from_ogg_bytes(&two_page_stream(47, 48))
671 .unwrap()
672 .duration_ms(),
673 1
674 );
675
676 let exact = two_page_stream(0, MAX_AUDIO_DURATION_MS * 48);
677 assert_eq!(
678 OggAudioMetadata::from_ogg_bytes(&exact)
679 .unwrap()
680 .duration_ms(),
681 MAX_AUDIO_DURATION_MS
682 );
683
684 assert_eq!(
685 OggAudioMetadata::from_ogg_bytes(&two_page_stream(0, MAX_AUDIO_DURATION_MS * 48 + 1,)),
686 Err(ValidationError::InvalidDuration(MAX_AUDIO_DURATION_MS + 1))
687 );
688 assert_eq!(
689 OggAudioMetadata::from_ogg_bytes(&two_page_stream(48, 48)),
690 Err(ValidationError::InvalidDuration(0))
691 );
692 }
693
694 #[test]
695 fn strict_ogg_rejects_truncation_crc_and_opus_head_errors() {
696 let valid = two_page_stream(0, 48);
697 assert_invalid(&valid[..valid.len() - 1]);
698
699 let mut corrupt = valid.clone();
700 corrupt[30] ^= 1;
701 assert_invalid(&corrupt);
702
703 let mut missing = b"NotHead".to_vec();
704 missing.resize(19, 0);
705 let mut bytes = page(1, 0, 0x02, 0, &[19], &missing);
706 bytes.extend(page(1, 1, 0x04, 48, &[1], &[0]));
707 assert_invalid(&bytes);
708
709 for (version, channels, mapping) in [(2, 1, 0), (1, 0, 0), (1, 3, 0), (1, 1, 1)] {
710 let mut opus_head = head(0, channels, mapping);
711 opus_head[8] = version;
712 let mut bytes = page(1, 0, 0x02, 0, &[19], &opus_head);
713 bytes.extend(page(1, 1, 0x04, 48, &[1], &[0]));
714 assert_invalid(&bytes);
715 }
716 }
717
718 #[test]
719 fn strict_ogg_rejects_stream_structure_errors() {
720 let valid = two_page_stream(0, 48);
721
722 let mut no_bos = valid.clone();
723 no_bos[5] = 0;
724 set_crc(&mut no_bos[..47]);
725 assert_invalid(&no_bos);
726
727 let mut no_eos = valid.clone();
728 no_eos[52] = 0;
729 set_crc(&mut no_eos[47..]);
730 assert_invalid(&no_eos);
731
732 let mut wrong_serial = page(1, 0, 0x02, 0, &[19], &head(0, 1, 0));
733 wrong_serial.extend(page(2, 1, 0x04, 48, &[1], &[0]));
734 assert_invalid(&wrong_serial);
735
736 let mut wrong_sequence = page(1, 0, 0x02, 0, &[19], &head(0, 1, 0));
737 wrong_sequence.extend(page(1, 2, 0x04, 48, &[1], &[0]));
738 assert_invalid(&wrong_sequence);
739
740 let mut decreasing = page(1, 0, 0x02, 10, &[19], &head(0, 1, 0));
741 decreasing.extend(page(1, 1, 0x04, 9, &[1], &[0]));
742 assert_invalid(&decreasing);
743
744 let mut chained = valid;
745 chained.extend(page(3, 0, 0x06, 48, &[1], &[0]));
746 assert_invalid(&chained);
747 }
748
749 #[test]
750 fn strict_ogg_rejects_bad_packet_continuation() {
751 let mut unexpected_continuation = page(1, 0, 0x03, 0, &[19], &head(0, 1, 0));
752 unexpected_continuation.extend(page(1, 1, 0x04, 48, &[1], &[0]));
753 assert_invalid(&unexpected_continuation);
754
755 let mut incomplete_head = head(0, 1, 0);
756 incomplete_head.resize(255, 0);
757 let mut missing_continuation = page(1, 0, 0x02, 0, &[255], &incomplete_head);
758 missing_continuation.extend(page(1, 1, 0x04, 48, &[1], &[0]));
759 assert_invalid(&missing_continuation);
760 }
761
762 #[test]
763 fn compatibility_constructor_is_unchanged() {
764 let mut bytes = vec![0; 28];
765 bytes[..4].copy_from_slice(b"OggS");
766 bytes[26] = 1;
767 bytes[27] = 0;
768
769 let metadata = OggAudioMetadata::from_bytes(&bytes, 1, Some("voice.ogg".into())).unwrap();
770 assert_eq!(metadata.filename(), Some("voice.ogg"));
771 assert_eq!(
772 OggAudioMetadata::from_bytes(&bytes, 0, None),
773 Err(ValidationError::InvalidDuration(0))
774 );
775 }
776
777 #[test]
778 fn speaker_labels_and_features_remain_compatible() {
779 let label = LocalSpeakerLabel::new(12).unwrap();
780 assert_eq!(label.to_string(), "Speaker 12");
781 assert_eq!("Speaker 12".parse(), Ok(label));
782 assert_eq!(serde_json::to_string(&label).unwrap(), "\"Speaker 12\"");
783
784 let features = FeatureVector24 {
785 median_f0_hz: Some(100.0),
786 dominant_rhotic_realization: Some("tap".into()),
787 vocal_gender_presentation: Some(VocalGenderPresentation::Masculine),
788 ..Default::default()
789 };
790 assert_eq!(features.numeric_values()[13], Some(1.0));
791 assert_eq!(features.present_feature_count(), 3);
792 assert!(features.validate().is_ok());
793 }
794
795 #[test]
796 fn structured_analysis_remains_compatible() {
797 let speaker = StructuredSpeaker {
798 speaker: LocalSpeakerLabel::new(1).unwrap(),
799 language: "English".into(),
800 features: FeatureVector24::default(),
801 features_usable_for_training: true,
802 };
803 let analysis = StructuredAnalysis {
804 transcript: "speech".into(),
805 speakers: vec![speaker.clone()],
806 };
807 analysis.validate().unwrap();
808
809 let duplicate = StructuredAnalysis {
810 transcript: "speech".into(),
811 speakers: vec![speaker.clone(), speaker],
812 };
813 assert!(matches!(
814 duplicate.validate(),
815 Err(ValidationError::DuplicateSpeakerLabel(_))
816 ));
817 }
818}