1use serde::{Deserialize, Serialize};
2use std::fmt::{Display, Formatter};
3use std::path::{Component, Path, PathBuf};
4
5pub use kcode_k1_transaction_id::TxId;
6pub use kcode_speaker_v3_analysis::{ExecutedAnalysis, FeatureVector24, LocalSpeakerLabel};
7
8const EVENT_VERSION: u8 = 1;
9const QUEUE_TAG: u8 = 1;
10const PROCESSED_TAG: u8 = 2;
11const FAILED_TAG: u8 = 3;
12const DISCARDED_TAG: u8 = 4;
13const CONFIRMED_TAG: u8 = 5;
14const FRAGMENT_VERSION: u8 = 1;
15const STAGED_KIND: u8 = 1;
16const FINAL_KIND: u8 = 2;
17const HEADER_LEN: usize = 16;
18const ANALYSIS_SLOT_OFFSET: usize = 16;
19const CONFIRMATION_SLOT_OFFSET: usize = 32;
20const BODY_OFFSET: usize = 48;
21const BASE64_ALPHABET: &[u8; 64] =
22 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum AnalysisStageV1 {
26 GeminiTranscript,
27 TerraLabels,
28 GeminiFeatures,
29 TerraStructuring,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct QueueV1 {
34 #[serde(with = "txid_serde")]
35 pub audio_object_id: TxId,
36 pub duration_ms: u64,
37 pub filename: Option<String>,
38 #[serde(with = "optional_txid_serde")]
39 pub retry_of: Option<TxId>,
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct ProcessedV1 {
44 #[serde(with = "txid_serde")]
45 pub queue_id: TxId,
46 #[serde(with = "txid_serde")]
47 pub audio_object_id: TxId,
48 pub analysis: ExecutedAnalysis,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct FailedV1 {
53 #[serde(with = "txid_serde")]
54 pub queue_id: TxId,
55 #[serde(with = "txid_serde")]
56 pub audio_object_id: TxId,
57 pub final_stage: AnalysisStageV1,
58 pub final_error: String,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct DiscardedV1 {
63 #[serde(with = "txid_serde")]
64 pub failed_queue_id: TxId,
65 #[serde(with = "txid_serde")]
66 pub audio_object_id: TxId,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct ConfirmedSpeakerV1 {
71 pub speaker: LocalSpeakerLabel,
72 pub person_id: String,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct ConfirmedV1 {
77 #[serde(with = "txid_serde")]
78 pub queue_id: TxId,
79 #[serde(with = "txid_serde")]
80 pub audio_object_id: TxId,
81 #[serde(with = "txid_serde")]
82 pub analysis_txid: TxId,
83 pub speakers: Vec<ConfirmedSpeakerV1>,
84}
85
86#[allow(clippy::large_enum_variant)]
87#[derive(Debug, Clone, PartialEq)]
88pub enum AudioClassificationEventV1 {
89 Queue(QueueV1),
90 Processed(ProcessedV1),
91 Failed(FailedV1),
92 Discarded(DiscardedV1),
93 Confirmed(ConfirmedV1),
94}
95
96#[derive(Debug, Clone, PartialEq)]
97pub struct StagedSpeakerV1 {
98 pub speaker: LocalSpeakerLabel,
99 pub language: String,
100 pub features: FeatureVector24,
101 pub usable_for_training: bool,
102}
103
104#[derive(Debug, Clone, PartialEq)]
105pub struct StagedFragmentV1 {
106 pub analysis_txid: TxId,
107 pub transcript: String,
108 pub speakers: Vec<StagedSpeakerV1>,
109}
110
111#[derive(Debug, Clone, PartialEq)]
112pub struct FinalSpeakerV1 {
113 pub speaker: LocalSpeakerLabel,
114 pub person_id: String,
115 pub language: String,
116 pub features: FeatureVector24,
117 pub usable_for_training: bool,
118}
119
120#[derive(Debug, Clone, PartialEq)]
121pub struct FinalFragmentV1 {
122 pub analysis_txid: TxId,
123 pub confirmation_txid: TxId,
124 pub transcript: String,
125 pub speakers: Vec<FinalSpeakerV1>,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum FormatError {
130 Truncated,
131 LengthOverflow,
132 UnsupportedVersion(u8),
133 UnknownEventTag(u8),
134 InvalidEventBody,
135 InvalidFragmentKind(u8),
136 NonZeroReserved,
137 NonZeroPadding,
138 NonZeroStagedConfirmation,
139 InvalidTxIdSlotLength(usize),
140 InvalidUtf8,
141 InvalidSpeakerLabel,
142 InvalidFeatureBody,
143 InvalidBoolean(u8),
144 TrailingBytes,
145 InvalidPath,
146}
147
148impl Display for FormatError {
149 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
150 match self {
151 Self::Truncated => formatter.write_str("truncated input"),
152 Self::LengthOverflow => formatter.write_str("encoded length overflow"),
153 Self::UnsupportedVersion(version) => {
154 write!(formatter, "unsupported format version {version}")
155 }
156 Self::UnknownEventTag(tag) => write!(formatter, "unknown event tag {tag}"),
157 Self::InvalidEventBody => formatter.write_str("invalid event body"),
158 Self::InvalidFragmentKind(kind) => {
159 write!(formatter, "invalid fragment kind {kind}")
160 }
161 Self::NonZeroReserved => formatter.write_str("nonzero reserved bytes"),
162 Self::NonZeroPadding => formatter.write_str("nonzero transaction ID slot padding"),
163 Self::NonZeroStagedConfirmation => {
164 formatter.write_str("nonzero staged confirmation slot")
165 }
166 Self::InvalidTxIdSlotLength(length) => {
167 write!(formatter, "invalid transaction ID slot length {length}")
168 }
169 Self::InvalidUtf8 => formatter.write_str("invalid UTF-8"),
170 Self::InvalidSpeakerLabel => formatter.write_str("invalid speaker label"),
171 Self::InvalidFeatureBody => formatter.write_str("invalid feature body"),
172 Self::InvalidBoolean(value) => write!(formatter, "invalid boolean byte {value}"),
173 Self::TrailingBytes => formatter.write_str("trailing bytes"),
174 Self::InvalidPath => formatter.write_str("invalid transaction ID path"),
175 }
176 }
177}
178
179impl std::error::Error for FormatError {}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct TxIdSlot {
183 txid: TxId,
184}
185
186impl TxIdSlot {
187 pub const LEN: usize = 16;
188 pub const PADDING_LEN: usize = 4;
189
190 pub const fn new(txid: TxId) -> Self {
191 Self { txid }
192 }
193
194 pub const fn txid(self) -> TxId {
195 self.txid
196 }
197
198 pub fn encode(self) -> [u8; Self::LEN] {
199 let mut encoded = [0_u8; Self::LEN];
200 encoded[..12].copy_from_slice(self.txid.as_bytes());
201 encoded
202 }
203
204 pub fn decode(bytes: &[u8]) -> Result<Self, FormatError> {
205 if bytes.len() != Self::LEN {
206 return Err(FormatError::InvalidTxIdSlotLength(bytes.len()));
207 }
208 if bytes[12..].iter().any(|byte| *byte != 0) {
209 return Err(FormatError::NonZeroPadding);
210 }
211 let mut txid_bytes = [0_u8; 12];
212 txid_bytes.copy_from_slice(&bytes[..12]);
213 Ok(Self::new(TxId::from_bytes(txid_bytes)))
214 }
215}
216
217pub fn encode_event(event: &AudioClassificationEventV1) -> Result<Vec<u8>, FormatError> {
218 let (tag, body) = match event {
219 AudioClassificationEventV1::Queue(value) => (QUEUE_TAG, postcard::to_allocvec(value)),
220 AudioClassificationEventV1::Processed(value) => {
221 (PROCESSED_TAG, postcard::to_allocvec(value))
222 }
223 AudioClassificationEventV1::Failed(value) => (FAILED_TAG, postcard::to_allocvec(value)),
224 AudioClassificationEventV1::Discarded(value) => {
225 (DISCARDED_TAG, postcard::to_allocvec(value))
226 }
227 AudioClassificationEventV1::Confirmed(value) => {
228 (CONFIRMED_TAG, postcard::to_allocvec(value))
229 }
230 };
231 let body = body.map_err(|_| FormatError::InvalidEventBody)?;
232 let mut encoded = Vec::with_capacity(2 + body.len());
233 encoded.push(EVENT_VERSION);
234 encoded.push(tag);
235 encoded.extend_from_slice(&body);
236 Ok(encoded)
237}
238
239pub fn decode_event(bytes: &[u8]) -> Result<AudioClassificationEventV1, FormatError> {
240 if bytes.len() < 2 {
241 return Err(FormatError::Truncated);
242 }
243 if bytes[0] != EVENT_VERSION {
244 return Err(FormatError::UnsupportedVersion(bytes[0]));
245 }
246 let body = &bytes[2..];
247 match bytes[1] {
248 QUEUE_TAG => decode_event_body(body).map(AudioClassificationEventV1::Queue),
249 PROCESSED_TAG => decode_event_body(body).map(AudioClassificationEventV1::Processed),
250 FAILED_TAG => decode_event_body(body).map(AudioClassificationEventV1::Failed),
251 DISCARDED_TAG => decode_event_body(body).map(AudioClassificationEventV1::Discarded),
252 CONFIRMED_TAG => decode_event_body(body).map(AudioClassificationEventV1::Confirmed),
253 tag => Err(FormatError::UnknownEventTag(tag)),
254 }
255}
256
257pub fn encode_staged_fragment(value: &StagedFragmentV1) -> Result<Vec<u8>, FormatError> {
258 let mut encoded = encode_fragment_prefix(STAGED_KIND, value.analysis_txid, None);
259 append_string(&mut encoded, &value.transcript)?;
260 append_length(&mut encoded, value.speakers.len())?;
261 for speaker in &value.speakers {
262 append_string(&mut encoded, &speaker.speaker.to_string())?;
263 append_string(&mut encoded, &speaker.language)?;
264 append_features(&mut encoded, &speaker.features)?;
265 encoded.push(u8::from(speaker.usable_for_training));
266 }
267 Ok(encoded)
268}
269
270pub fn decode_staged_fragment(bytes: &[u8]) -> Result<StagedFragmentV1, FormatError> {
271 let (analysis_txid, _) = decode_fragment_header(bytes, STAGED_KIND, false)?;
272 let mut decoder = BodyDecoder::new(&bytes[BODY_OFFSET..]);
273 let transcript = decoder.read_string()?;
274 let speaker_count = decoder.read_count()?;
275 let mut speakers = Vec::new();
276 for _ in 0..speaker_count {
277 speakers.push(StagedSpeakerV1 {
278 speaker: decoder.read_speaker_label()?,
279 language: decoder.read_string()?,
280 features: decoder.read_features()?,
281 usable_for_training: decoder.read_boolean()?,
282 });
283 }
284 decoder.finish()?;
285 Ok(StagedFragmentV1 {
286 analysis_txid,
287 transcript,
288 speakers,
289 })
290}
291
292pub fn encode_final_fragment(value: &FinalFragmentV1) -> Result<Vec<u8>, FormatError> {
293 let mut encoded = encode_fragment_prefix(
294 FINAL_KIND,
295 value.analysis_txid,
296 Some(value.confirmation_txid),
297 );
298 append_string(&mut encoded, &value.transcript)?;
299 append_length(&mut encoded, value.speakers.len())?;
300 for speaker in &value.speakers {
301 append_string(&mut encoded, &speaker.speaker.to_string())?;
302 append_string(&mut encoded, &speaker.person_id)?;
303 append_string(&mut encoded, &speaker.language)?;
304 append_features(&mut encoded, &speaker.features)?;
305 encoded.push(u8::from(speaker.usable_for_training));
306 }
307 Ok(encoded)
308}
309
310pub fn decode_final_fragment(bytes: &[u8]) -> Result<FinalFragmentV1, FormatError> {
311 let (analysis_txid, confirmation_txid) = decode_fragment_header(bytes, FINAL_KIND, true)?;
312 let mut decoder = BodyDecoder::new(&bytes[BODY_OFFSET..]);
313 let transcript = decoder.read_string()?;
314 let speaker_count = decoder.read_count()?;
315 let mut speakers = Vec::new();
316 for _ in 0..speaker_count {
317 speakers.push(FinalSpeakerV1 {
318 speaker: decoder.read_speaker_label()?,
319 person_id: decoder.read_string()?,
320 language: decoder.read_string()?,
321 features: decoder.read_features()?,
322 usable_for_training: decoder.read_boolean()?,
323 });
324 }
325 decoder.finish()?;
326 Ok(FinalFragmentV1 {
327 analysis_txid,
328 confirmation_txid: confirmation_txid.expect("final header has a confirmation slot"),
329 transcript,
330 speakers,
331 })
332}
333
334pub fn txid_path(txid: TxId) -> PathBuf {
335 let encoded = encode_txid_base64(txid);
336 let first = String::from_utf8(encoded[..1].to_vec()).expect("base64 is ASCII");
337 let remaining = String::from_utf8(encoded[1..].to_vec()).expect("base64 is ASCII");
338 PathBuf::from(first).join(format!("{remaining}.dat"))
339}
340
341pub fn txid_from_path(path: impl AsRef<Path>) -> Result<TxId, FormatError> {
342 let mut components = path.as_ref().components();
343 let first = normal_utf8_component(components.next())?;
344 let filename = normal_utf8_component(components.next())?;
345 if components.next().is_some() || first.len() != 1 {
346 return Err(FormatError::InvalidPath);
347 }
348 let remaining = filename
349 .strip_suffix(".dat")
350 .ok_or(FormatError::InvalidPath)?;
351 if remaining.len() != 15 {
352 return Err(FormatError::InvalidPath);
353 }
354 let mut encoded = [0_u8; 16];
355 encoded[0] = first.as_bytes()[0];
356 encoded[1..].copy_from_slice(remaining.as_bytes());
357 decode_txid_base64(encoded)
358}
359
360fn decode_event_body<T>(bytes: &[u8]) -> Result<T, FormatError>
361where
362 T: for<'de> Deserialize<'de>,
363{
364 let (value, remaining) =
365 postcard::take_from_bytes(bytes).map_err(|_| FormatError::InvalidEventBody)?;
366 if !remaining.is_empty() {
367 return Err(FormatError::TrailingBytes);
368 }
369 Ok(value)
370}
371
372fn encode_fragment_prefix(kind: u8, analysis: TxId, confirmation: Option<TxId>) -> Vec<u8> {
373 let mut encoded = vec![0_u8; BODY_OFFSET];
374 encoded[0] = FRAGMENT_VERSION;
375 encoded[1] = kind;
376 encoded[ANALYSIS_SLOT_OFFSET..ANALYSIS_SLOT_OFFSET + TxIdSlot::LEN]
377 .copy_from_slice(&TxIdSlot::new(analysis).encode());
378 if let Some(confirmation) = confirmation {
379 encoded[CONFIRMATION_SLOT_OFFSET..CONFIRMATION_SLOT_OFFSET + TxIdSlot::LEN]
380 .copy_from_slice(&TxIdSlot::new(confirmation).encode());
381 }
382 encoded
383}
384
385fn decode_fragment_header(
386 bytes: &[u8],
387 expected_kind: u8,
388 needs_confirmation: bool,
389) -> Result<(TxId, Option<TxId>), FormatError> {
390 if bytes.len() < BODY_OFFSET {
391 return Err(FormatError::Truncated);
392 }
393 if bytes[0] != FRAGMENT_VERSION {
394 return Err(FormatError::UnsupportedVersion(bytes[0]));
395 }
396 if bytes[1] != expected_kind {
397 return Err(FormatError::InvalidFragmentKind(bytes[1]));
398 }
399 if bytes[2..HEADER_LEN].iter().any(|byte| *byte != 0) {
400 return Err(FormatError::NonZeroReserved);
401 }
402 let analysis =
403 TxIdSlot::decode(&bytes[ANALYSIS_SLOT_OFFSET..ANALYSIS_SLOT_OFFSET + TxIdSlot::LEN])?
404 .txid();
405 let confirmation_bytes =
406 &bytes[CONFIRMATION_SLOT_OFFSET..CONFIRMATION_SLOT_OFFSET + TxIdSlot::LEN];
407 let confirmation = if needs_confirmation {
408 Some(TxIdSlot::decode(confirmation_bytes)?.txid())
409 } else {
410 if confirmation_bytes.iter().any(|byte| *byte != 0) {
411 return Err(FormatError::NonZeroStagedConfirmation);
412 }
413 None
414 };
415 Ok((analysis, confirmation))
416}
417
418fn append_length(encoded: &mut Vec<u8>, length: usize) -> Result<(), FormatError> {
419 let length = u64::try_from(length).map_err(|_| FormatError::LengthOverflow)?;
420 encoded.extend_from_slice(&length.to_le_bytes());
421 Ok(())
422}
423
424fn append_bytes(encoded: &mut Vec<u8>, bytes: &[u8]) -> Result<(), FormatError> {
425 append_length(encoded, bytes.len())?;
426 encoded.extend_from_slice(bytes);
427 Ok(())
428}
429
430fn append_string(encoded: &mut Vec<u8>, value: &str) -> Result<(), FormatError> {
431 append_bytes(encoded, value.as_bytes())
432}
433
434fn append_features(encoded: &mut Vec<u8>, features: &FeatureVector24) -> Result<(), FormatError> {
435 let feature_bytes =
436 postcard::to_allocvec(features).map_err(|_| FormatError::InvalidFeatureBody)?;
437 append_bytes(encoded, &feature_bytes)
438}
439
440struct BodyDecoder<'a> {
441 bytes: &'a [u8],
442 position: usize,
443}
444
445impl<'a> BodyDecoder<'a> {
446 fn new(bytes: &'a [u8]) -> Self {
447 Self { bytes, position: 0 }
448 }
449
450 fn take(&mut self, length: usize) -> Result<&'a [u8], FormatError> {
451 let end = self
452 .position
453 .checked_add(length)
454 .ok_or(FormatError::LengthOverflow)?;
455 let value = self
456 .bytes
457 .get(self.position..end)
458 .ok_or(FormatError::Truncated)?;
459 self.position = end;
460 Ok(value)
461 }
462
463 fn read_u64(&mut self) -> Result<u64, FormatError> {
464 let bytes = self.take(8)?;
465 let mut value = [0_u8; 8];
466 value.copy_from_slice(bytes);
467 Ok(u64::from_le_bytes(value))
468 }
469
470 fn read_length(&mut self) -> Result<usize, FormatError> {
471 usize::try_from(self.read_u64()?).map_err(|_| FormatError::LengthOverflow)
472 }
473
474 fn read_count(&mut self) -> Result<usize, FormatError> {
475 self.read_length()
476 }
477
478 fn read_bytes(&mut self) -> Result<&'a [u8], FormatError> {
479 let length = self.read_length()?;
480 self.take(length)
481 }
482
483 fn read_string(&mut self) -> Result<String, FormatError> {
484 let bytes = self.read_bytes()?;
485 let value = std::str::from_utf8(bytes).map_err(|_| FormatError::InvalidUtf8)?;
486 Ok(value.to_owned())
487 }
488
489 fn read_speaker_label(&mut self) -> Result<LocalSpeakerLabel, FormatError> {
490 self.read_string()?
491 .parse()
492 .map_err(|_| FormatError::InvalidSpeakerLabel)
493 }
494
495 fn read_features(&mut self) -> Result<FeatureVector24, FormatError> {
496 let bytes = self.read_bytes()?;
497 let (features, remaining) =
498 postcard::take_from_bytes(bytes).map_err(|_| FormatError::InvalidFeatureBody)?;
499 if !remaining.is_empty() {
500 return Err(FormatError::InvalidFeatureBody);
501 }
502 Ok(features)
503 }
504
505 fn read_boolean(&mut self) -> Result<bool, FormatError> {
506 match self.take(1)?[0] {
507 0 => Ok(false),
508 1 => Ok(true),
509 value => Err(FormatError::InvalidBoolean(value)),
510 }
511 }
512
513 fn finish(self) -> Result<(), FormatError> {
514 if self.position == self.bytes.len() {
515 Ok(())
516 } else {
517 Err(FormatError::TrailingBytes)
518 }
519 }
520}
521
522fn normal_utf8_component(component: Option<Component<'_>>) -> Result<&str, FormatError> {
523 match component {
524 Some(Component::Normal(value)) => value.to_str().ok_or(FormatError::InvalidPath),
525 _ => Err(FormatError::InvalidPath),
526 }
527}
528
529fn encode_txid_base64(txid: TxId) -> [u8; 16] {
530 let bytes = txid.into_bytes();
531 let mut encoded = [0_u8; 16];
532 for group in 0..4 {
533 let input = group * 3;
534 let output = group * 4;
535 encoded[output] = BASE64_ALPHABET[(bytes[input] >> 2) as usize];
536 encoded[output + 1] = BASE64_ALPHABET
537 [(((bytes[input] & 0b0000_0011) << 4) | (bytes[input + 1] >> 4)) as usize];
538 encoded[output + 2] = BASE64_ALPHABET
539 [(((bytes[input + 1] & 0b0000_1111) << 2) | (bytes[input + 2] >> 6)) as usize];
540 encoded[output + 3] = BASE64_ALPHABET[(bytes[input + 2] & 0b0011_1111) as usize];
541 }
542 encoded
543}
544
545fn decode_txid_base64(encoded: [u8; 16]) -> Result<TxId, FormatError> {
546 let mut bytes = [0_u8; 12];
547 for group in 0..4 {
548 let input = group * 4;
549 let output = group * 3;
550 let first = decode_base64_character(encoded[input])?;
551 let second = decode_base64_character(encoded[input + 1])?;
552 let third = decode_base64_character(encoded[input + 2])?;
553 let fourth = decode_base64_character(encoded[input + 3])?;
554 bytes[output] = (first << 2) | (second >> 4);
555 bytes[output + 1] = (second << 4) | (third >> 2);
556 bytes[output + 2] = (third << 6) | fourth;
557 }
558 Ok(TxId::from_bytes(bytes))
559}
560
561fn decode_base64_character(value: u8) -> Result<u8, FormatError> {
562 match value {
563 b'A'..=b'Z' => Ok(value - b'A'),
564 b'a'..=b'z' => Ok(value - b'a' + 26),
565 b'0'..=b'9' => Ok(value - b'0' + 52),
566 b'-' => Ok(62),
567 b'_' => Ok(63),
568 _ => Err(FormatError::InvalidPath),
569 }
570}
571
572mod txid_serde {
573 use super::TxId;
574 use serde::{Deserialize, Deserializer, Serialize, Serializer};
575
576 pub fn serialize<S>(value: &TxId, serializer: S) -> Result<S::Ok, S::Error>
577 where
578 S: Serializer,
579 {
580 value.into_bytes().serialize(serializer)
581 }
582
583 pub fn deserialize<'de, D>(deserializer: D) -> Result<TxId, D::Error>
584 where
585 D: Deserializer<'de>,
586 {
587 let bytes = <[u8; 12]>::deserialize(deserializer)?;
588 Ok(TxId::from_bytes(bytes))
589 }
590}
591
592mod optional_txid_serde {
593 use super::TxId;
594 use serde::{Deserialize, Deserializer, Serialize, Serializer};
595
596 pub fn serialize<S>(value: &Option<TxId>, serializer: S) -> Result<S::Ok, S::Error>
597 where
598 S: Serializer,
599 {
600 value.map(TxId::into_bytes).serialize(serializer)
601 }
602
603 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<TxId>, D::Error>
604 where
605 D: Deserializer<'de>,
606 {
607 let bytes = Option::<[u8; 12]>::deserialize(deserializer)?;
608 Ok(bytes.map(TxId::from_bytes))
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use kcode_speaker_v3_analysis::{
616 AnalysisEnvelope, GeminiCohort, OggAudioMetadata, StructuredAnalysis, StructuredSpeaker,
617 StructurerProvenance,
618 };
619
620 fn txid(seed: u8) -> TxId {
621 let mut bytes = [0_u8; 12];
622 for (index, byte) in bytes.iter_mut().enumerate() {
623 *byte = seed.wrapping_add(index as u8);
624 }
625 TxId::from_bytes(bytes)
626 }
627
628 fn features(seed: f64, name: Option<&str>) -> FeatureVector24 {
629 FeatureVector24 {
630 median_f0_hz: Some(seed),
631 high_front_vowel_f1_hz: None,
632 dominant_rhotic_realization: name.map(str::to_owned),
633 hypernasality_0_to_4: Some(seed / 100.0),
634 ..FeatureVector24::default()
635 }
636 }
637
638 fn executed_analysis() -> ExecutedAnalysis {
639 let mut ogg = vec![0_u8; 29];
640 ogg[..4].copy_from_slice(b"OggS");
641 ogg[4] = 0;
642 ogg[26] = 1;
643 ogg[27] = 1;
644 ogg[28] = 0;
645 let audio = OggAudioMetadata::from_bytes(&ogg, 1_250, Some("sample.ogg".to_owned()))
646 .expect("valid Ogg metadata");
647 let analysis = StructuredAnalysis {
648 transcript: "Speaker 1: Héllo from 東京".to_owned(),
649 speakers: vec![StructuredSpeaker {
650 speaker: LocalSpeakerLabel::new(1).expect("valid label"),
651 language: "English / 日本語".to_owned(),
652 features: features(182.5, Some("approximant")),
653 features_usable_for_training: true,
654 }],
655 };
656 ExecutedAnalysis {
657 envelope: AnalysisEnvelope {
658 audio,
659 analysis,
660 gemini: GeminiCohort::new("gemini-3.1-pro"),
661 structurer: StructurerProvenance::new("terra-structurer"),
662 },
663 label_extractor: StructurerProvenance::new("terra-labeler"),
664 }
665 }
666
667 fn staged_fragment() -> StagedFragmentV1 {
668 StagedFragmentV1 {
669 analysis_txid: txid(20),
670 transcript: "Élodie: bonjour 🌍\n話者 2: こんにちは".to_owned(),
671 speakers: vec![
672 StagedSpeakerV1 {
673 speaker: LocalSpeakerLabel::new(1).expect("valid label"),
674 language: "français".to_owned(),
675 features: features(201.25, Some("uvulaire")),
676 usable_for_training: true,
677 },
678 StagedSpeakerV1 {
679 speaker: LocalSpeakerLabel::new(2).expect("valid label"),
680 language: "日本語".to_owned(),
681 features: FeatureVector24 {
682 low_vowel_f2_hz: Some(1_234.5),
683 dominant_lateral_realization: Some("明瞭".to_owned()),
684 ..FeatureVector24::default()
685 },
686 usable_for_training: false,
687 },
688 ],
689 }
690 }
691
692 fn final_fragment() -> FinalFragmentV1 {
693 FinalFragmentV1 {
694 analysis_txid: txid(30),
695 confirmation_txid: txid(40),
696 transcript: "Élodie: bonjour 🌍\n話者 2: こんにちは".to_owned(),
697 speakers: vec![
698 FinalSpeakerV1 {
699 speaker: LocalSpeakerLabel::new(1).expect("valid label"),
700 person_id: "person-éloïse".to_owned(),
701 language: "français".to_owned(),
702 features: features(201.25, Some("uvulaire")),
703 usable_for_training: false,
704 },
705 FinalSpeakerV1 {
706 speaker: LocalSpeakerLabel::new(2).expect("valid label"),
707 person_id: "人物-東京".to_owned(),
708 language: "日本語".to_owned(),
709 features: FeatureVector24::default(),
710 usable_for_training: true,
711 },
712 ],
713 }
714 }
715
716 fn read_test_length(bytes: &[u8], position: &mut usize) -> usize {
717 let end = *position + 8;
718 let mut value = [0_u8; 8];
719 value.copy_from_slice(&bytes[*position..end]);
720 *position = end;
721 usize::try_from(u64::from_le_bytes(value)).expect("test length fits")
722 }
723
724 fn first_staged_feature_and_boolean_offsets(bytes: &[u8]) -> (usize, usize) {
725 let mut position = BODY_OFFSET;
726 let transcript_length = read_test_length(bytes, &mut position);
727 position += transcript_length;
728 let speaker_count = read_test_length(bytes, &mut position);
729 assert!(speaker_count > 0);
730 let label_length = read_test_length(bytes, &mut position);
731 position += label_length;
732 let language_length = read_test_length(bytes, &mut position);
733 position += language_length;
734 let feature_length = read_test_length(bytes, &mut position);
735 let feature_offset = position;
736 position += feature_length;
737 (feature_offset, position)
738 }
739
740 #[test]
741 fn txid_slot_has_exact_bytes_and_padding() {
742 let id = txid(3);
743 let encoded = TxIdSlot::new(id).encode();
744 assert_eq!(encoded.len(), TxIdSlot::LEN);
745 assert_eq!(TxIdSlot::PADDING_LEN, 4);
746 assert_eq!(&encoded[..12], id.as_bytes());
747 assert_eq!(&encoded[12..], &[0_u8; 4]);
748 assert_eq!(TxIdSlot::decode(&encoded).expect("slot").txid(), id);
749 assert_eq!(
750 TxIdSlot::decode(&encoded[..15]),
751 Err(FormatError::InvalidTxIdSlotLength(15))
752 );
753 let mut corrupt = encoded;
754 corrupt[12] = 1;
755 assert_eq!(TxIdSlot::decode(&corrupt), Err(FormatError::NonZeroPadding));
756 }
757
758 #[test]
759 fn synthetic_slots_start_on_sixteen_byte_boundaries() {
760 let mut bytes = [0_u8; 64];
761 for (index, offset) in (0..64).step_by(TxIdSlot::LEN).enumerate() {
762 assert_eq!(offset % TxIdSlot::LEN, 0);
763 let slot = TxIdSlot::new(txid(index as u8)).encode();
764 bytes[offset..offset + TxIdSlot::LEN].copy_from_slice(&slot);
765 }
766 for (index, offset) in (0..64).step_by(TxIdSlot::LEN).enumerate() {
767 let slot =
768 TxIdSlot::decode(&bytes[offset..offset + TxIdSlot::LEN]).expect("aligned slot");
769 assert_eq!(slot.txid(), txid(index as u8));
770 }
771 }
772
773 #[test]
774 fn fragment_slots_are_at_fixed_offsets() {
775 let staged = staged_fragment();
776 let staged_bytes = encode_staged_fragment(&staged).expect("encode staged");
777 assert_eq!(
778 &staged_bytes[16..32],
779 &TxIdSlot::new(staged.analysis_txid).encode()
780 );
781 assert_eq!(&staged_bytes[32..48], &[0_u8; 16]);
782
783 let final_value = final_fragment();
784 let final_bytes = encode_final_fragment(&final_value).expect("encode final");
785 assert_eq!(
786 &final_bytes[16..32],
787 &TxIdSlot::new(final_value.analysis_txid).encode()
788 );
789 assert_eq!(
790 &final_bytes[32..48],
791 &TxIdSlot::new(final_value.confirmation_txid).encode()
792 );
793 }
794
795 #[test]
796 fn every_shard_character_roundtrips() {
797 for shard in 0..64 {
798 let mut bytes = [0_u8; 12];
799 bytes[0] = (shard as u8) << 2;
800 bytes[11] = shard as u8;
801 let id = TxId::from_bytes(bytes);
802 let path = txid_path(id);
803 let first = path
804 .components()
805 .next()
806 .expect("first component")
807 .as_os_str()
808 .to_str()
809 .expect("UTF-8");
810 assert_eq!(first.as_bytes(), &BASE64_ALPHABET[shard..shard + 1]);
811 assert_eq!(txid_from_path(&path), Ok(id));
812 }
813 }
814
815 #[test]
816 fn malformed_paths_are_rejected() {
817 for path in [
818 "",
819 "A",
820 "A/B.dat",
821 "AA/AAAAAAAAAAAAAAA.dat",
822 "A/AAAAAAAAAAAAAAA.bin",
823 "A/AAAAAAAAAAAAAA!.dat",
824 "/A/AAAAAAAAAAAAAAA.dat",
825 "A/../B.dat",
826 "A/AAAAAAAAAAAAAAA.dat/extra",
827 ] {
828 assert_eq!(txid_from_path(path), Err(FormatError::InvalidPath));
829 }
830 }
831
832 #[test]
833 fn every_event_variant_roundtrips_with_stable_tags() {
834 let events = vec![
835 AudioClassificationEventV1::Queue(QueueV1 {
836 audio_object_id: txid(1),
837 duration_ms: 98_765,
838 filename: Some("réunion.ogg".to_owned()),
839 retry_of: Some(txid(2)),
840 }),
841 AudioClassificationEventV1::Processed(ProcessedV1 {
842 queue_id: txid(3),
843 audio_object_id: txid(4),
844 analysis: executed_analysis(),
845 }),
846 AudioClassificationEventV1::Failed(FailedV1 {
847 queue_id: txid(5),
848 audio_object_id: txid(6),
849 final_stage: AnalysisStageV1::GeminiFeatures,
850 final_error: "packet 2 unavailable".to_owned(),
851 }),
852 AudioClassificationEventV1::Discarded(DiscardedV1 {
853 failed_queue_id: txid(7),
854 audio_object_id: txid(8),
855 }),
856 AudioClassificationEventV1::Confirmed(ConfirmedV1 {
857 queue_id: txid(9),
858 audio_object_id: txid(10),
859 analysis_txid: txid(11),
860 speakers: vec![ConfirmedSpeakerV1 {
861 speaker: LocalSpeakerLabel::new(1).expect("valid label"),
862 person_id: "person-α".to_owned(),
863 }],
864 }),
865 ];
866 for (index, event) in events.into_iter().enumerate() {
867 let encoded = encode_event(&event).expect("encode event");
868 assert_eq!(encoded[0], EVENT_VERSION);
869 assert_eq!(encoded[1], index as u8 + 1);
870 assert_eq!(decode_event(&encoded), Ok(event));
871 }
872 }
873
874 #[test]
875 fn every_analysis_stage_roundtrips() {
876 for stage in [
877 AnalysisStageV1::GeminiTranscript,
878 AnalysisStageV1::TerraLabels,
879 AnalysisStageV1::GeminiFeatures,
880 AnalysisStageV1::TerraStructuring,
881 ] {
882 let event = AudioClassificationEventV1::Failed(FailedV1 {
883 queue_id: txid(1),
884 audio_object_id: txid(2),
885 final_stage: stage,
886 final_error: "failure".to_owned(),
887 });
888 let encoded = encode_event(&event).expect("encode event");
889 assert_eq!(decode_event(&encoded), Ok(event));
890 }
891 }
892
893 #[test]
894 fn event_corruption_is_rejected() {
895 let event = AudioClassificationEventV1::Queue(QueueV1 {
896 audio_object_id: txid(1),
897 duration_ms: 500,
898 filename: None,
899 retry_of: None,
900 });
901 let encoded = encode_event(&event).expect("encode event");
902 assert_eq!(decode_event(&[]), Err(FormatError::Truncated));
903 let mut wrong_version = encoded.clone();
904 wrong_version[0] = 2;
905 assert_eq!(
906 decode_event(&wrong_version),
907 Err(FormatError::UnsupportedVersion(2))
908 );
909 let mut wrong_tag = encoded.clone();
910 wrong_tag[1] = 99;
911 assert_eq!(
912 decode_event(&wrong_tag),
913 Err(FormatError::UnknownEventTag(99))
914 );
915 assert_eq!(
916 decode_event(&encoded[..2]),
917 Err(FormatError::InvalidEventBody)
918 );
919 let mut trailing = encoded;
920 trailing.push(0);
921 assert_eq!(decode_event(&trailing), Err(FormatError::TrailingBytes));
922 }
923
924 #[test]
925 fn staged_and_final_fragments_roundtrip_utf8_speakers() {
926 let staged = staged_fragment();
927 let staged_bytes = encode_staged_fragment(&staged).expect("encode staged");
928 assert_eq!(decode_staged_fragment(&staged_bytes), Ok(staged));
929
930 let final_value = final_fragment();
931 let final_bytes = encode_final_fragment(&final_value).expect("encode final");
932 assert_eq!(decode_final_fragment(&final_bytes), Ok(final_value));
933 }
934
935 #[test]
936 fn fragment_header_and_slot_corruption_is_rejected() {
937 let staged = staged_fragment();
938 let encoded = encode_staged_fragment(&staged).expect("encode staged");
939 for cut in [0, 15, 47, encoded.len() - 1] {
940 assert!(decode_staged_fragment(&encoded[..cut]).is_err());
941 }
942 let mut wrong_version = encoded.clone();
943 wrong_version[0] = 2;
944 assert_eq!(
945 decode_staged_fragment(&wrong_version),
946 Err(FormatError::UnsupportedVersion(2))
947 );
948 let mut wrong_kind = encoded.clone();
949 wrong_kind[1] = FINAL_KIND;
950 assert_eq!(
951 decode_staged_fragment(&wrong_kind),
952 Err(FormatError::InvalidFragmentKind(FINAL_KIND))
953 );
954 let mut reserved = encoded.clone();
955 reserved[2] = 1;
956 assert_eq!(
957 decode_staged_fragment(&reserved),
958 Err(FormatError::NonZeroReserved)
959 );
960 let mut analysis_padding = encoded.clone();
961 analysis_padding[28] = 1;
962 assert_eq!(
963 decode_staged_fragment(&analysis_padding),
964 Err(FormatError::NonZeroPadding)
965 );
966 let mut staged_confirmation = encoded;
967 staged_confirmation[32] = 1;
968 assert_eq!(
969 decode_staged_fragment(&staged_confirmation),
970 Err(FormatError::NonZeroStagedConfirmation)
971 );
972
973 let final_value = final_fragment();
974 let mut final_bytes = encode_final_fragment(&final_value).expect("encode final");
975 final_bytes[44] = 1;
976 assert_eq!(
977 decode_final_fragment(&final_bytes),
978 Err(FormatError::NonZeroPadding)
979 );
980 }
981
982 #[test]
983 fn fragment_body_corruption_is_rejected() {
984 let value = StagedFragmentV1 {
985 analysis_txid: txid(1),
986 transcript: "hello".to_owned(),
987 speakers: vec![StagedSpeakerV1 {
988 speaker: LocalSpeakerLabel::new(1).expect("valid label"),
989 language: "English".to_owned(),
990 features: FeatureVector24::default(),
991 usable_for_training: true,
992 }],
993 };
994 let encoded = encode_staged_fragment(&value).expect("encode staged");
995
996 let mut length_overflow = encoded.clone();
997 length_overflow[BODY_OFFSET..BODY_OFFSET + 8].copy_from_slice(&u64::MAX.to_le_bytes());
998 assert_eq!(
999 decode_staged_fragment(&length_overflow),
1000 Err(FormatError::LengthOverflow)
1001 );
1002
1003 let mut invalid_utf8 = encoded.clone();
1004 invalid_utf8[BODY_OFFSET + 8] = 0xff;
1005 assert_eq!(
1006 decode_staged_fragment(&invalid_utf8),
1007 Err(FormatError::InvalidUtf8)
1008 );
1009
1010 let (feature_offset, boolean_offset) = first_staged_feature_and_boolean_offsets(&encoded);
1011 let mut invalid_feature = encoded.clone();
1012 invalid_feature[feature_offset] = 2;
1013 assert_eq!(
1014 decode_staged_fragment(&invalid_feature),
1015 Err(FormatError::InvalidFeatureBody)
1016 );
1017
1018 let mut invalid_boolean = encoded.clone();
1019 invalid_boolean[boolean_offset] = 2;
1020 assert_eq!(
1021 decode_staged_fragment(&invalid_boolean),
1022 Err(FormatError::InvalidBoolean(2))
1023 );
1024
1025 let mut trailing = encoded;
1026 trailing.push(0);
1027 assert_eq!(
1028 decode_staged_fragment(&trailing),
1029 Err(FormatError::TrailingBytes)
1030 );
1031 }
1032
1033 #[test]
1034 fn malformed_label_and_internal_feature_trailing_bytes_are_rejected() {
1035 let value = StagedFragmentV1 {
1036 analysis_txid: txid(1),
1037 transcript: "hello".to_owned(),
1038 speakers: vec![StagedSpeakerV1 {
1039 speaker: LocalSpeakerLabel::new(1).expect("valid label"),
1040 language: "English".to_owned(),
1041 features: FeatureVector24::default(),
1042 usable_for_training: true,
1043 }],
1044 };
1045 let encoded = encode_staged_fragment(&value).expect("encode staged");
1046 let mut position = BODY_OFFSET;
1047 let transcript_length = read_test_length(&encoded, &mut position);
1048 position += transcript_length;
1049 let _ = read_test_length(&encoded, &mut position);
1050 let label_length = read_test_length(&encoded, &mut position);
1051 assert_eq!(label_length, "Speaker 1".len());
1052 let mut invalid_label = encoded.clone();
1053 invalid_label[position] = b'X';
1054 assert_eq!(
1055 decode_staged_fragment(&invalid_label),
1056 Err(FormatError::InvalidSpeakerLabel)
1057 );
1058
1059 let (feature_offset, boolean_offset) = first_staged_feature_and_boolean_offsets(&encoded);
1060 let mut feature_trailing = encoded;
1061 feature_trailing.insert(boolean_offset, 0);
1062 let feature_length_offset = feature_offset - 8;
1063 let feature_length = boolean_offset - feature_offset + 1;
1064 feature_trailing[feature_length_offset..feature_length_offset + 8]
1065 .copy_from_slice(&(feature_length as u64).to_le_bytes());
1066 assert_eq!(
1067 decode_staged_fragment(&feature_trailing),
1068 Err(FormatError::InvalidFeatureBody)
1069 );
1070 }
1071}