Skip to main content

kcode_speaker_v3_schema/
lib.rs

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}