1use serde::{Deserialize, Serialize};
2use std::{collections::BTreeSet, error::Error, fmt};
3
4pub use kcode_speaker_v3_llm_protocol::{
5 GEMINI_FEATURE_PROMPT_ONE, GEMINI_FEATURE_PROMPT_ONE_REVISION, GEMINI_FEATURE_PROMPT_REVISIONS,
6 GEMINI_FEATURE_PROMPT_THREE, GEMINI_FEATURE_PROMPT_THREE_REVISION, GEMINI_FEATURE_PROMPT_TWO,
7 GEMINI_FEATURE_PROMPT_TWO_REVISION, GEMINI_TRANSCRIPT_PROMPT,
8 GEMINI_TRANSCRIPT_PROMPT_REVISION, GPT_STRUCTURING_PROMPT, GPT_STRUCTURING_PROMPT_REVISION,
9};
10pub use kcode_speaker_v3_schema::{
11 FEATURE_NAMES, FEATURE_SCHEMA_REVISION, FeatureVector24, LocalSpeakerLabel,
12 MAX_AUDIO_DURATION_MS, OGG_MEDIA_TYPE, OggAudioMetadata, StructuredAnalysis, StructuredSpeaker,
13 ValidationError, VocalGenderPresentation,
14};
15
16#[cfg(any(feature = "providers", test))]
17use kcode_speaker_v3_gemini_analysis::GeminiFeatureProgress;
18#[cfg(any(feature = "providers", test))]
19use kcode_speaker_v3_llm_protocol::SpeakerFeatureEvidence;
20#[cfg(any(feature = "providers", test))]
21use std::{future::Future, pin::Pin};
22
23const GEMINI_MODEL_ID: &str = "gemini-3.1-pro-preview";
24const TERRA_MODEL_ID: &str = "gpt-5.6-terra";
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct GeminiCohort {
28 pub model_id: String,
29 pub transcript_prompt_revision: String,
30 pub feature_prompt_revisions: [String; 3],
31 pub feature_schema_revision: String,
32}
33
34impl GeminiCohort {
35 pub fn new(model_id: impl Into<String>) -> Self {
36 Self {
37 model_id: model_id.into(),
38 transcript_prompt_revision: GEMINI_TRANSCRIPT_PROMPT_REVISION.into(),
39 feature_prompt_revisions: GEMINI_FEATURE_PROMPT_REVISIONS.map(str::to_owned),
40 feature_schema_revision: FEATURE_SCHEMA_REVISION.into(),
41 }
42 }
43
44 pub fn validate(&self) -> Result<(), ValidationError> {
45 validate_text(&self.model_id, "gemini_model_id")?;
46 validate_text(
47 &self.transcript_prompt_revision,
48 "transcript_prompt_revision",
49 )?;
50 for revision in &self.feature_prompt_revisions {
51 validate_text(revision, "feature_prompt_revision")?;
52 }
53 validate_text(&self.feature_schema_revision, "feature_schema_revision")
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct StructurerProvenance {
59 pub model_id: String,
60 pub prompt_revision: String,
61}
62
63impl StructurerProvenance {
64 pub fn new(model_id: impl Into<String>) -> Self {
65 Self {
66 model_id: model_id.into(),
67 prompt_revision: GPT_STRUCTURING_PROMPT_REVISION.into(),
68 }
69 }
70
71 pub fn validate(&self) -> Result<(), ValidationError> {
72 validate_text(&self.model_id, "structurer_model_id")?;
73 validate_text(&self.prompt_revision, "structurer_prompt_revision")
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct AnalysisEnvelope {
79 pub audio: OggAudioMetadata,
80 pub analysis: StructuredAnalysis,
81 pub gemini: GeminiCohort,
82 pub structurer: StructurerProvenance,
83}
84
85impl AnalysisEnvelope {
86 pub fn validate(&self) -> Result<(), ValidationError> {
87 self.audio.validate()?;
88 self.analysis.validate()?;
89 self.gemini.validate()?;
90 self.structurer.validate()
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum AnalysisStage {
96 Transcript,
97 SpeakerLabels,
98 SpeakerFeatures,
99 Structuring,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum AnalysisJob {
104 Transcript,
105 SpeakerLabels,
106 SpeakerFeature {
107 speaker: LocalSpeakerLabel,
108 packet: u8,
109 },
110 Structuring,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum AnalysisProgress {
115 JobStarted { sequence: u64, job: AnalysisJob },
116 JobSucceeded { sequence: u64 },
117 JobFailed { sequence: u64, error: String },
118 StageCompleted { stage: AnalysisStage },
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum AnalysisError {
123 Input(String),
124 Progress(String),
125 GeminiTranscript(String),
126 TerraLabels(String),
127 GeminiCache(String),
128 GeminiFeature {
129 speaker: LocalSpeakerLabel,
130 packet: u8,
131 message: String,
132 },
133 TerraStructuring(String),
134 TranscriptMismatch,
135 SpeakerSetMismatch,
136}
137
138impl fmt::Display for AnalysisError {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 Self::Input(message) => write!(formatter, "invalid input: {message}"),
142 Self::Progress(message) => write!(formatter, "progress reporting failed: {message}"),
143 Self::GeminiTranscript(message) => {
144 write!(formatter, "Gemini transcript failed: {message}")
145 }
146 Self::TerraLabels(message) => {
147 write!(
148 formatter,
149 "Terra speaker-label extraction failed: {message}"
150 )
151 }
152 Self::GeminiCache(message) => {
153 write!(formatter, "Gemini feature cache creation failed: {message}")
154 }
155 Self::GeminiFeature {
156 speaker,
157 packet,
158 message,
159 } => write!(
160 formatter,
161 "Gemini feature call failed for {speaker}, packet {packet}: {message}"
162 ),
163 Self::TerraStructuring(message) => {
164 write!(formatter, "Terra final structuring failed: {message}")
165 }
166 Self::TranscriptMismatch => {
167 formatter.write_str("Terra returned a different transcript")
168 }
169 Self::SpeakerSetMismatch => {
170 formatter.write_str("Terra returned a different speaker set")
171 }
172 }
173 }
174}
175
176impl Error for AnalysisError {}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct ExecutedAnalysis {
180 pub envelope: AnalysisEnvelope,
181 pub label_extractor: StructurerProvenance,
182}
183
184#[cfg(feature = "providers")]
185pub struct Analyzer {
186 operations: ProviderOperations,
187}
188
189#[cfg(feature = "providers")]
190impl Analyzer {
191 pub fn new(
192 gemini: kcode_gemini_3_1_pro::Gemini31Pro,
193 terra: kcode_codex_terra::CodexTerra,
194 ) -> Self {
195 Self {
196 operations: ProviderOperations {
197 gemini: kcode_speaker_v3_gemini_analysis::GeminiAnalysis::new(gemini),
198 terra: kcode_speaker_v3_terra_analysis::TerraAnalysis::new(terra),
199 },
200 }
201 }
202
203 pub async fn analyze_ogg_with_progress<F>(
204 &self,
205 bytes: &[u8],
206 report: F,
207 ) -> Result<ExecutedAnalysis, AnalysisError>
208 where
209 F: FnMut(AnalysisProgress) -> Result<(), String>,
210 {
211 execute_strict(&self.operations, bytes, report).await
212 }
213
214 pub async fn analyze_ogg(
215 &self,
216 bytes: &[u8],
217 duration_ms: u64,
218 filename: Option<String>,
219 ) -> Result<ExecutedAnalysis, AnalysisError> {
220 execute_legacy(&self.operations, bytes, duration_ms, filename).await
221 }
222}
223
224fn validate_text(value: &str, field: &'static str) -> Result<(), ValidationError> {
225 (!value.trim().is_empty())
226 .then_some(())
227 .ok_or(ValidationError::Blank(field))
228}
229
230#[cfg(any(feature = "providers", test))]
231type AnalysisFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
232
233#[cfg(any(feature = "providers", test))]
234trait AnalysisOperations: Sync {
235 fn transcript<'a>(
236 &'a self,
237 audio: &'a [u8],
238 ) -> AnalysisFuture<'a, Result<String, AnalysisError>>;
239
240 fn speaker_labels<'a>(
241 &'a self,
242 transcript: &'a str,
243 ) -> AnalysisFuture<'a, Result<Vec<LocalSpeakerLabel>, AnalysisError>>;
244
245 fn feature_evidence_with_progress<'a>(
246 &'a self,
247 audio: &'a [u8],
248 transcript: &'a str,
249 labels: &'a [LocalSpeakerLabel],
250 report: &'a mut dyn FnMut(GeminiFeatureProgress) -> Result<(), String>,
251 ) -> AnalysisFuture<'a, Result<Vec<SpeakerFeatureEvidence>, AnalysisError>>;
252
253 fn structured_analysis<'a>(
254 &'a self,
255 transcript: &'a str,
256 evidence: Vec<SpeakerFeatureEvidence>,
257 ) -> AnalysisFuture<'a, Result<StructuredAnalysis, AnalysisError>>;
258}
259
260#[cfg(any(feature = "providers", test))]
261struct ProviderOperations {
262 gemini: kcode_speaker_v3_gemini_analysis::GeminiAnalysis,
263 terra: kcode_speaker_v3_terra_analysis::TerraAnalysis,
264}
265
266#[cfg(any(feature = "providers", test))]
267impl AnalysisOperations for ProviderOperations {
268 fn transcript<'a>(
269 &'a self,
270 audio: &'a [u8],
271 ) -> AnalysisFuture<'a, Result<String, AnalysisError>> {
272 Box::pin(async move {
273 self.gemini.transcript(audio).await.map_err(|error| match error {
274 kcode_speaker_v3_gemini_analysis::GeminiTranscriptError::Provider(message)
275 | kcode_speaker_v3_gemini_analysis::GeminiTranscriptError::Protocol(message) => {
276 AnalysisError::GeminiTranscript(message)
277 }
278 })
279 })
280 }
281
282 fn speaker_labels<'a>(
283 &'a self,
284 transcript: &'a str,
285 ) -> AnalysisFuture<'a, Result<Vec<LocalSpeakerLabel>, AnalysisError>> {
286 Box::pin(async move {
287 self.terra
288 .speaker_labels(transcript)
289 .await
290 .map_err(|error| match error {
291 kcode_speaker_v3_terra_analysis::TerraAnalysisError::Protocol(message)
292 | kcode_speaker_v3_terra_analysis::TerraAnalysisError::Provider(message) => {
293 AnalysisError::TerraLabels(message)
294 }
295 })
296 })
297 }
298
299 fn feature_evidence_with_progress<'a>(
300 &'a self,
301 audio: &'a [u8],
302 transcript: &'a str,
303 labels: &'a [LocalSpeakerLabel],
304 report: &'a mut dyn FnMut(GeminiFeatureProgress) -> Result<(), String>,
305 ) -> AnalysisFuture<'a, Result<Vec<SpeakerFeatureEvidence>, AnalysisError>> {
306 Box::pin(async move {
307 self.gemini
308 .feature_evidence_with_progress(audio, transcript, labels, report)
309 .await
310 .map_err(|error| match error {
311 kcode_speaker_v3_gemini_analysis::GeminiFeatureError::Cache(message) => {
312 AnalysisError::GeminiCache(message)
313 }
314 kcode_speaker_v3_gemini_analysis::GeminiFeatureError::Progress(message) => {
315 AnalysisError::Progress(message)
316 }
317 kcode_speaker_v3_gemini_analysis::GeminiFeatureError::Feature {
318 speaker,
319 packet,
320 message,
321 } => AnalysisError::GeminiFeature {
322 speaker,
323 packet,
324 message,
325 },
326 })
327 })
328 }
329
330 fn structured_analysis<'a>(
331 &'a self,
332 transcript: &'a str,
333 evidence: Vec<SpeakerFeatureEvidence>,
334 ) -> AnalysisFuture<'a, Result<StructuredAnalysis, AnalysisError>> {
335 Box::pin(async move {
336 self.terra
337 .structured_analysis(transcript, evidence)
338 .await
339 .map_err(|error| match error {
340 kcode_speaker_v3_terra_analysis::TerraAnalysisError::Protocol(message)
341 | kcode_speaker_v3_terra_analysis::TerraAnalysisError::Provider(message) => {
342 AnalysisError::TerraStructuring(message)
343 }
344 })
345 })
346 }
347}
348
349#[cfg(any(feature = "providers", test))]
350async fn execute_strict<O, F>(
351 operations: &O,
352 bytes: &[u8],
353 report: F,
354) -> Result<ExecutedAnalysis, AnalysisError>
355where
356 O: AnalysisOperations,
357 F: FnMut(AnalysisProgress) -> Result<(), String>,
358{
359 let audio = OggAudioMetadata::from_ogg_bytes(bytes)
360 .map_err(|error| AnalysisError::Input(error.to_string()))?;
361 execute_admitted(operations, bytes, audio, report).await
362}
363
364#[cfg(any(feature = "providers", test))]
365async fn execute_legacy<O: AnalysisOperations>(
366 operations: &O,
367 bytes: &[u8],
368 duration_ms: u64,
369 filename: Option<String>,
370) -> Result<ExecutedAnalysis, AnalysisError> {
371 let audio = OggAudioMetadata::from_bytes(bytes, duration_ms, filename)
372 .map_err(|error| AnalysisError::Input(error.to_string()))?;
373 execute_admitted(operations, bytes, audio, |_| Ok(())).await
374}
375
376#[cfg(any(feature = "providers", test))]
377async fn execute_admitted<O, F>(
378 operations: &O,
379 bytes: &[u8],
380 audio: OggAudioMetadata,
381 mut report: F,
382) -> Result<ExecutedAnalysis, AnalysisError>
383where
384 O: AnalysisOperations,
385 F: FnMut(AnalysisProgress) -> Result<(), String>,
386{
387 report_progress(
388 &mut report,
389 AnalysisProgress::JobStarted {
390 sequence: 1,
391 job: AnalysisJob::Transcript,
392 },
393 )?;
394 let transcript = match operations.transcript(bytes).await {
395 Ok(transcript) => transcript,
396 Err(error) => {
397 report_leaf_failure(&mut report, 1, &error)?;
398 return Err(error);
399 }
400 };
401 report_progress(&mut report, AnalysisProgress::JobSucceeded { sequence: 1 })?;
402 report_progress(
403 &mut report,
404 AnalysisProgress::StageCompleted {
405 stage: AnalysisStage::Transcript,
406 },
407 )?;
408
409 report_progress(
410 &mut report,
411 AnalysisProgress::JobStarted {
412 sequence: 2,
413 job: AnalysisJob::SpeakerLabels,
414 },
415 )?;
416 let labels = match operations.speaker_labels(&transcript).await {
417 Ok(labels) => labels,
418 Err(error) => {
419 report_leaf_failure(&mut report, 2, &error)?;
420 return Err(error);
421 }
422 };
423 report_progress(&mut report, AnalysisProgress::JobSucceeded { sequence: 2 })?;
424 report_progress(
425 &mut report,
426 AnalysisProgress::StageCompleted {
427 stage: AnalysisStage::SpeakerLabels,
428 },
429 )?;
430
431 let structuring_sequence = structuring_sequence(labels.len())?;
432 let evidence = {
433 let mut feature_report = |progress| {
434 let progress = map_feature_progress(&labels, progress)?;
435 report(progress)
436 };
437 operations
438 .feature_evidence_with_progress(bytes, &transcript, &labels, &mut feature_report)
439 .await?
440 };
441 report_progress(
442 &mut report,
443 AnalysisProgress::StageCompleted {
444 stage: AnalysisStage::SpeakerFeatures,
445 },
446 )?;
447
448 report_progress(
449 &mut report,
450 AnalysisProgress::JobStarted {
451 sequence: structuring_sequence,
452 job: AnalysisJob::Structuring,
453 },
454 )?;
455 let analysis = match operations.structured_analysis(&transcript, evidence).await {
456 Ok(analysis) => analysis,
457 Err(error) => {
458 report_leaf_failure(&mut report, structuring_sequence, &error)?;
459 return Err(error);
460 }
461 };
462 report_progress(
463 &mut report,
464 AnalysisProgress::JobSucceeded {
465 sequence: structuring_sequence,
466 },
467 )?;
468
469 if analysis.transcript != transcript {
470 return Err(AnalysisError::TranscriptMismatch);
471 }
472
473 let expected_speakers = labels.iter().copied().collect::<BTreeSet<_>>();
474 let returned_speakers = analysis
475 .speakers
476 .iter()
477 .map(|speaker| speaker.speaker)
478 .collect::<BTreeSet<_>>();
479 if expected_speakers != returned_speakers {
480 return Err(AnalysisError::SpeakerSetMismatch);
481 }
482
483 let envelope = AnalysisEnvelope {
484 audio,
485 analysis,
486 gemini: GeminiCohort::new(GEMINI_MODEL_ID),
487 structurer: StructurerProvenance::new(TERRA_MODEL_ID),
488 };
489 envelope
490 .validate()
491 .map_err(|error| AnalysisError::TerraStructuring(error.to_string()))?;
492
493 let label_extractor = StructurerProvenance {
494 model_id: TERRA_MODEL_ID.into(),
495 prompt_revision: kcode_speaker_v3_llm_protocol::TERRA_SPEAKER_LABELS_PROMPT_REVISION.into(),
496 };
497 label_extractor
498 .validate()
499 .map_err(|error| AnalysisError::TerraLabels(error.to_string()))?;
500
501 let executed = ExecutedAnalysis {
502 envelope,
503 label_extractor,
504 };
505 report_progress(
506 &mut report,
507 AnalysisProgress::StageCompleted {
508 stage: AnalysisStage::Structuring,
509 },
510 )?;
511 Ok(executed)
512}
513
514#[cfg(any(feature = "providers", test))]
515fn report_progress<F>(report: &mut F, progress: AnalysisProgress) -> Result<(), AnalysisError>
516where
517 F: FnMut(AnalysisProgress) -> Result<(), String>,
518{
519 report(progress).map_err(AnalysisError::Progress)
520}
521
522#[cfg(any(feature = "providers", test))]
523fn report_leaf_failure<F>(
524 report: &mut F,
525 sequence: u64,
526 error: &AnalysisError,
527) -> Result<(), AnalysisError>
528where
529 F: FnMut(AnalysisProgress) -> Result<(), String>,
530{
531 report_progress(
532 report,
533 AnalysisProgress::JobFailed {
534 sequence,
535 error: error.to_string(),
536 },
537 )
538}
539
540#[cfg(any(feature = "providers", test))]
541fn structuring_sequence(label_count: usize) -> Result<u64, AnalysisError> {
542 u64::try_from(label_count)
543 .ok()
544 .and_then(|count| count.checked_mul(3))
545 .and_then(|count| count.checked_add(3))
546 .ok_or_else(sequence_error)
547}
548
549#[cfg(any(feature = "providers", test))]
550fn feature_sequence(
551 labels: &[LocalSpeakerLabel],
552 speaker: LocalSpeakerLabel,
553 packet: u8,
554) -> Result<u64, String> {
555 if !(1..=3).contains(&packet) {
556 return Err(format!(
557 "Gemini reported invalid feature packet {packet} for {speaker}"
558 ));
559 }
560 let label_index = labels
561 .iter()
562 .position(|candidate| *candidate == speaker)
563 .ok_or_else(|| format!("Gemini reported an unknown feature speaker {speaker}"))?;
564 u64::try_from(label_index)
565 .ok()
566 .and_then(|index| index.checked_mul(3))
567 .and_then(|index| index.checked_add(u64::from(packet)))
568 .and_then(|index| index.checked_add(2))
569 .ok_or_else(|| sequence_error().to_string())
570}
571
572#[cfg(any(feature = "providers", test))]
573fn sequence_error() -> AnalysisError {
574 AnalysisError::Progress("analysis job sequence overflow".into())
575}
576
577#[cfg(any(feature = "providers", test))]
578fn map_feature_progress(
579 labels: &[LocalSpeakerLabel],
580 progress: GeminiFeatureProgress,
581) -> Result<AnalysisProgress, String> {
582 match progress {
583 GeminiFeatureProgress::Started { speaker, packet } => Ok(AnalysisProgress::JobStarted {
584 sequence: feature_sequence(labels, speaker, packet)?,
585 job: AnalysisJob::SpeakerFeature { speaker, packet },
586 }),
587 GeminiFeatureProgress::Succeeded { speaker, packet } => {
588 Ok(AnalysisProgress::JobSucceeded {
589 sequence: feature_sequence(labels, speaker, packet)?,
590 })
591 }
592 GeminiFeatureProgress::Failed {
593 speaker,
594 packet,
595 error,
596 } => {
597 let displayed_error = AnalysisError::GeminiFeature {
598 speaker,
599 packet,
600 message: error,
601 }
602 .to_string();
603 Ok(AnalysisProgress::JobFailed {
604 sequence: feature_sequence(labels, speaker, packet)?,
605 error: displayed_error,
606 })
607 }
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use futures::{executor::block_on, future::poll_fn, join};
615 use std::{
616 sync::{
617 Arc, Mutex,
618 atomic::{AtomicBool, AtomicUsize, Ordering},
619 },
620 task::Poll,
621 time::Instant,
622 };
623
624 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
625 enum FailureStage {
626 Transcript,
627 Labels,
628 Cache,
629 Features,
630 Final,
631 }
632
633 #[derive(Clone)]
634 struct FakeConfig {
635 transcript: String,
636 labels: Vec<LocalSpeakerLabel>,
637 analysis: StructuredAnalysis,
638 failure: Option<FailureStage>,
639 feature_terminal_order: Option<Vec<(LocalSpeakerLabel, u8)>>,
640 feature_failures: Vec<(LocalSpeakerLabel, u8, String)>,
641 wait_for: Option<Arc<AtomicBool>>,
642 mark_complete: Option<Arc<AtomicBool>>,
643 }
644
645 struct FakeState {
646 config: FakeConfig,
647 transcript_calls: AtomicUsize,
648 label_calls: AtomicUsize,
649 feature_calls: AtomicUsize,
650 final_calls: AtomicUsize,
651 calls: Mutex<Vec<&'static str>>,
652 }
653
654 #[derive(Clone)]
655 struct FakeOperations {
656 state: Arc<FakeState>,
657 }
658
659 impl FakeOperations {
660 fn successful(speaker_count: u32) -> Self {
661 let transcript = "[high] Speaker 1: exact transcript".to_owned();
662 Self::from_config(FakeConfig {
663 labels: (1..=speaker_count).map(label).collect(),
664 analysis: structured_analysis(&transcript, speaker_count),
665 transcript,
666 failure: None,
667 feature_terminal_order: None,
668 feature_failures: Vec::new(),
669 wait_for: None,
670 mark_complete: None,
671 })
672 }
673
674 fn from_config(config: FakeConfig) -> Self {
675 Self {
676 state: Arc::new(FakeState {
677 config,
678 transcript_calls: AtomicUsize::new(0),
679 label_calls: AtomicUsize::new(0),
680 feature_calls: AtomicUsize::new(0),
681 final_calls: AtomicUsize::new(0),
682 calls: Mutex::new(Vec::new()),
683 }),
684 }
685 }
686
687 fn with_config(&self, update: impl FnOnce(&mut FakeConfig)) -> Self {
688 let mut config = self.state.config.clone();
689 update(&mut config);
690 Self::from_config(config)
691 }
692
693 fn feature_failure(&self, speaker: LocalSpeakerLabel, packet: u8) -> Option<String> {
694 if self.state.config.failure == Some(FailureStage::Features)
695 && speaker == label(1)
696 && packet == 2
697 {
698 return Some("features".into());
699 }
700 self.state
701 .config
702 .feature_failures
703 .iter()
704 .find(|(candidate, candidate_packet, _)| {
705 *candidate == speaker && *candidate_packet == packet
706 })
707 .map(|(_, _, message)| message.clone())
708 }
709 }
710
711 impl AnalysisOperations for FakeOperations {
712 fn transcript<'a>(
713 &'a self,
714 _audio: &'a [u8],
715 ) -> AnalysisFuture<'a, Result<String, AnalysisError>> {
716 Box::pin(async move {
717 self.state.transcript_calls.fetch_add(1, Ordering::SeqCst);
718 self.state.calls.lock().unwrap().push("transcript");
719 if let Some(wait_for) = &self.state.config.wait_for {
720 poll_fn(|context| {
721 if wait_for.load(Ordering::SeqCst) {
722 Poll::Ready(())
723 } else {
724 context.waker().wake_by_ref();
725 Poll::Pending
726 }
727 })
728 .await;
729 }
730 if self.state.config.failure == Some(FailureStage::Transcript) {
731 return Err(AnalysisError::GeminiTranscript("transcript".into()));
732 }
733 Ok(self.state.config.transcript.clone())
734 })
735 }
736
737 fn speaker_labels<'a>(
738 &'a self,
739 _transcript: &'a str,
740 ) -> AnalysisFuture<'a, Result<Vec<LocalSpeakerLabel>, AnalysisError>> {
741 Box::pin(async move {
742 self.state.label_calls.fetch_add(1, Ordering::SeqCst);
743 self.state.calls.lock().unwrap().push("labels");
744 if self.state.config.failure == Some(FailureStage::Labels) {
745 return Err(AnalysisError::TerraLabels("labels".into()));
746 }
747 Ok(self.state.config.labels.clone())
748 })
749 }
750
751 fn feature_evidence_with_progress<'a>(
752 &'a self,
753 _audio: &'a [u8],
754 _transcript: &'a str,
755 labels: &'a [LocalSpeakerLabel],
756 report: &'a mut dyn FnMut(GeminiFeatureProgress) -> Result<(), String>,
757 ) -> AnalysisFuture<'a, Result<Vec<SpeakerFeatureEvidence>, AnalysisError>> {
758 Box::pin(async move {
759 self.state.feature_calls.fetch_add(1, Ordering::SeqCst);
760 self.state.calls.lock().unwrap().push("features");
761 if self.state.config.failure == Some(FailureStage::Cache) {
762 return Err(AnalysisError::GeminiCache("cache".into()));
763 }
764
765 let jobs = labels
766 .iter()
767 .copied()
768 .flat_map(|speaker| (1..=3).map(move |packet| (speaker, packet)))
769 .collect::<Vec<_>>();
770 for &(speaker, packet) in &jobs {
771 report(GeminiFeatureProgress::Started { speaker, packet })
772 .map_err(AnalysisError::Progress)?;
773 }
774
775 let terminal_order = self
776 .state
777 .config
778 .feature_terminal_order
779 .clone()
780 .unwrap_or_else(|| jobs.clone());
781 for (speaker, packet) in terminal_order {
782 if let Some(error) = self.feature_failure(speaker, packet) {
783 report(GeminiFeatureProgress::Failed {
784 speaker,
785 packet,
786 error,
787 })
788 .map_err(AnalysisError::Progress)?;
789 } else {
790 report(GeminiFeatureProgress::Succeeded { speaker, packet })
791 .map_err(AnalysisError::Progress)?;
792 }
793 }
794
795 if let Some((speaker, packet, message)) =
796 jobs.iter().find_map(|&(speaker, packet)| {
797 self.feature_failure(speaker, packet)
798 .map(|message| (speaker, packet, message))
799 })
800 {
801 return Err(AnalysisError::GeminiFeature {
802 speaker,
803 packet,
804 message,
805 });
806 }
807
808 labels
809 .iter()
810 .copied()
811 .map(|speaker| {
812 SpeakerFeatureEvidence::new(
813 speaker,
814 format!("{speaker} packet 1"),
815 format!("{speaker} packet 2"),
816 format!("{speaker} packet 3"),
817 )
818 .map_err(|error| AnalysisError::GeminiFeature {
819 speaker,
820 packet: 1,
821 message: error.to_string(),
822 })
823 })
824 .collect()
825 })
826 }
827
828 fn structured_analysis<'a>(
829 &'a self,
830 _transcript: &'a str,
831 _evidence: Vec<SpeakerFeatureEvidence>,
832 ) -> AnalysisFuture<'a, Result<StructuredAnalysis, AnalysisError>> {
833 Box::pin(async move {
834 self.state.final_calls.fetch_add(1, Ordering::SeqCst);
835 self.state.calls.lock().unwrap().push("final");
836 if self.state.config.failure == Some(FailureStage::Final) {
837 return Err(AnalysisError::TerraStructuring("final".into()));
838 }
839 if let Some(mark_complete) = &self.state.config.mark_complete {
840 mark_complete.store(true, Ordering::SeqCst);
841 }
842 Ok(self.state.config.analysis.clone())
843 })
844 }
845 }
846
847 fn label(number: u32) -> LocalSpeakerLabel {
848 LocalSpeakerLabel::new(number).unwrap()
849 }
850
851 fn structured_analysis(transcript: &str, speaker_count: u32) -> StructuredAnalysis {
852 StructuredAnalysis {
853 transcript: transcript.into(),
854 speakers: (1..=speaker_count)
855 .map(|number| StructuredSpeaker {
856 speaker: label(number),
857 language: "English".into(),
858 features: FeatureVector24::default(),
859 features_usable_for_training: false,
860 })
861 .collect(),
862 }
863 }
864
865 fn legacy_ogg() -> Vec<u8> {
866 let mut bytes = vec![0; 28];
867 bytes[..4].copy_from_slice(b"OggS");
868 bytes[4] = 0;
869 bytes[26] = 1;
870 bytes[27] = 0;
871 bytes
872 }
873
874 fn strict_ogg(samples: u64) -> Vec<u8> {
875 let pre_skip = 312_u16;
876 let mut head = b"OpusHead".to_vec();
877 head.push(1);
878 head.push(1);
879 head.extend_from_slice(&pre_skip.to_le_bytes());
880 head.extend_from_slice(&48_000_u32.to_le_bytes());
881 head.extend_from_slice(&0_i16.to_le_bytes());
882 head.push(0);
883
884 let mut tags = b"OpusTags".to_vec();
885 tags.extend_from_slice(&0_u32.to_le_bytes());
886 tags.extend_from_slice(&0_u32.to_le_bytes());
887
888 let mut bytes = ogg_page(0x02, 0, 0, &head);
889 bytes.extend_from_slice(&ogg_page(0, 0, 1, &tags));
890 bytes.extend_from_slice(&ogg_page(
891 0x04,
892 u64::from(pre_skip) + samples,
893 2,
894 &[0xf8, 0xff, 0xfe],
895 ));
896 bytes
897 }
898
899 fn ogg_page(header_type: u8, granule: u64, sequence: u32, payload: &[u8]) -> Vec<u8> {
900 let payload_length = u8::try_from(payload.len()).unwrap();
901 let mut page = Vec::with_capacity(28 + payload.len());
902 page.extend_from_slice(b"OggS");
903 page.push(0);
904 page.push(header_type);
905 page.extend_from_slice(&granule.to_le_bytes());
906 page.extend_from_slice(&0x534b_5633_u32.to_le_bytes());
907 page.extend_from_slice(&sequence.to_le_bytes());
908 page.extend_from_slice(&0_u32.to_le_bytes());
909 page.push(1);
910 page.push(payload_length);
911 page.extend_from_slice(payload);
912 let checksum = ogg_crc(&page);
913 page[22..26].copy_from_slice(&checksum.to_le_bytes());
914 page
915 }
916
917 fn ogg_crc(bytes: &[u8]) -> u32 {
918 let mut crc = 0_u32;
919 for &byte in bytes {
920 crc ^= u32::from(byte) << 24;
921 for _ in 0..8 {
922 crc = if crc & 0x8000_0000 == 0 {
923 crc << 1
924 } else {
925 (crc << 1) ^ 0x04c1_1db7
926 };
927 }
928 }
929 crc
930 }
931
932 fn run_with_progress(
933 operations: &FakeOperations,
934 ) -> (
935 Result<ExecutedAnalysis, AnalysisError>,
936 Vec<AnalysisProgress>,
937 ) {
938 let audio = strict_ogg(48);
939 let mut events = Vec::new();
940 let result = block_on(execute_strict(operations, &audio, |event| {
941 events.push(event);
942 Ok(())
943 }));
944 (result, events)
945 }
946
947 #[test]
948 fn strict_admission_precedes_reports_and_operations() {
949 for bytes in [b"bad".to_vec(), strict_ogg(7_200_001)] {
950 let operations = FakeOperations::successful(1);
951 let reports = AtomicUsize::new(0);
952 assert!(matches!(
953 block_on(execute_strict(&operations, &bytes, |_| {
954 reports.fetch_add(1, Ordering::SeqCst);
955 Ok(())
956 })),
957 Err(AnalysisError::Input(_))
958 ));
959 assert_eq!(reports.load(Ordering::SeqCst), 0);
960 assert!(operations.state.calls.lock().unwrap().is_empty());
961 }
962 }
963
964 #[test]
965 fn zero_label_sequences_are_exact() {
966 let operations = FakeOperations::successful(0);
967 let (result, events) = run_with_progress(&operations);
968 result.unwrap();
969 assert_eq!(
970 events,
971 vec![
972 AnalysisProgress::JobStarted {
973 sequence: 1,
974 job: AnalysisJob::Transcript,
975 },
976 AnalysisProgress::JobSucceeded { sequence: 1 },
977 AnalysisProgress::StageCompleted {
978 stage: AnalysisStage::Transcript,
979 },
980 AnalysisProgress::JobStarted {
981 sequence: 2,
982 job: AnalysisJob::SpeakerLabels,
983 },
984 AnalysisProgress::JobSucceeded { sequence: 2 },
985 AnalysisProgress::StageCompleted {
986 stage: AnalysisStage::SpeakerLabels,
987 },
988 AnalysisProgress::StageCompleted {
989 stage: AnalysisStage::SpeakerFeatures,
990 },
991 AnalysisProgress::JobStarted {
992 sequence: 3,
993 job: AnalysisJob::Structuring,
994 },
995 AnalysisProgress::JobSucceeded { sequence: 3 },
996 AnalysisProgress::StageCompleted {
997 stage: AnalysisStage::Structuring,
998 },
999 ]
1000 );
1001 }
1002
1003 #[test]
1004 fn multiple_label_sequences_are_exact() {
1005 let operations = FakeOperations::successful(2);
1006 let (result, events) = run_with_progress(&operations);
1007 result.unwrap();
1008
1009 let mut expected = vec![
1010 AnalysisProgress::JobStarted {
1011 sequence: 1,
1012 job: AnalysisJob::Transcript,
1013 },
1014 AnalysisProgress::JobSucceeded { sequence: 1 },
1015 AnalysisProgress::StageCompleted {
1016 stage: AnalysisStage::Transcript,
1017 },
1018 AnalysisProgress::JobStarted {
1019 sequence: 2,
1020 job: AnalysisJob::SpeakerLabels,
1021 },
1022 AnalysisProgress::JobSucceeded { sequence: 2 },
1023 AnalysisProgress::StageCompleted {
1024 stage: AnalysisStage::SpeakerLabels,
1025 },
1026 ];
1027 for (sequence, speaker, packet) in [
1028 (3, label(1), 1),
1029 (4, label(1), 2),
1030 (5, label(1), 3),
1031 (6, label(2), 1),
1032 (7, label(2), 2),
1033 (8, label(2), 3),
1034 ] {
1035 expected.push(AnalysisProgress::JobStarted {
1036 sequence,
1037 job: AnalysisJob::SpeakerFeature { speaker, packet },
1038 });
1039 }
1040 for sequence in 3..=8 {
1041 expected.push(AnalysisProgress::JobSucceeded { sequence });
1042 }
1043 expected.extend([
1044 AnalysisProgress::StageCompleted {
1045 stage: AnalysisStage::SpeakerFeatures,
1046 },
1047 AnalysisProgress::JobStarted {
1048 sequence: 9,
1049 job: AnalysisJob::Structuring,
1050 },
1051 AnalysisProgress::JobSucceeded { sequence: 9 },
1052 AnalysisProgress::StageCompleted {
1053 stage: AnalysisStage::Structuring,
1054 },
1055 ]);
1056 assert_eq!(events, expected);
1057 }
1058
1059 #[test]
1060 fn feature_terminals_preserve_actual_completion_order() {
1061 let operations = FakeOperations::successful(2).with_config(|config| {
1062 config.feature_terminal_order = Some(vec![
1063 (label(2), 2),
1064 (label(1), 3),
1065 (label(2), 1),
1066 (label(1), 1),
1067 (label(2), 3),
1068 (label(1), 2),
1069 ]);
1070 });
1071 let (result, events) = run_with_progress(&operations);
1072 result.unwrap();
1073 let terminals = events
1074 .iter()
1075 .filter_map(|event| match event {
1076 AnalysisProgress::JobSucceeded { sequence } if (3..9).contains(sequence) => {
1077 Some(*sequence)
1078 }
1079 _ => None,
1080 })
1081 .collect::<Vec<_>>();
1082 assert_eq!(terminals, [7, 5, 6, 3, 8, 4]);
1083 }
1084
1085 #[test]
1086 fn multiple_feature_failures_are_all_reported_without_duplicate() {
1087 let operations = FakeOperations::successful(2).with_config(|config| {
1088 config.feature_terminal_order = Some(vec![
1089 (label(2), 1),
1090 (label(1), 2),
1091 (label(1), 1),
1092 (label(1), 3),
1093 (label(2), 2),
1094 (label(2), 3),
1095 ]);
1096 config.feature_failures = vec![
1097 (label(1), 2, "first deterministic failure".into()),
1098 (label(2), 1, "first completed failure".into()),
1099 ];
1100 });
1101 let (result, events) = run_with_progress(&operations);
1102 assert_eq!(
1103 result,
1104 Err(AnalysisError::GeminiFeature {
1105 speaker: label(1),
1106 packet: 2,
1107 message: "first deterministic failure".into(),
1108 })
1109 );
1110 let failures = events
1111 .iter()
1112 .filter_map(|event| match event {
1113 AnalysisProgress::JobFailed { sequence, error } => Some((*sequence, error.clone())),
1114 _ => None,
1115 })
1116 .collect::<Vec<_>>();
1117 assert_eq!(
1118 failures,
1119 vec![
1120 (
1121 6,
1122 AnalysisError::GeminiFeature {
1123 speaker: label(2),
1124 packet: 1,
1125 message: "first completed failure".into(),
1126 }
1127 .to_string(),
1128 ),
1129 (
1130 4,
1131 AnalysisError::GeminiFeature {
1132 speaker: label(1),
1133 packet: 2,
1134 message: "first deterministic failure".into(),
1135 }
1136 .to_string(),
1137 ),
1138 ]
1139 );
1140 assert!(!events.contains(&AnalysisProgress::StageCompleted {
1141 stage: AnalysisStage::SpeakerFeatures,
1142 }));
1143 assert_eq!(
1144 *operations.state.calls.lock().unwrap(),
1145 ["transcript", "labels", "features"]
1146 );
1147 }
1148
1149 #[test]
1150 fn reporter_failure_supersedes_and_stops_orchestration() {
1151 let operations = FakeOperations::successful(1);
1152 let audio = strict_ogg(48);
1153 let mut events = Vec::new();
1154 let result = block_on(execute_strict(&operations, &audio, |event| {
1155 events.push(event.clone());
1156 if event
1157 == (AnalysisProgress::JobStarted {
1158 sequence: 3,
1159 job: AnalysisJob::SpeakerFeature {
1160 speaker: label(1),
1161 packet: 1,
1162 },
1163 })
1164 {
1165 Err("reporter closed".into())
1166 } else {
1167 Ok(())
1168 }
1169 }));
1170 assert_eq!(
1171 result,
1172 Err(AnalysisError::Progress("reporter closed".into()))
1173 );
1174 assert_eq!(
1175 *operations.state.calls.lock().unwrap(),
1176 ["transcript", "labels", "features"]
1177 );
1178 assert_eq!(
1179 events.last(),
1180 Some(&AnalysisProgress::JobStarted {
1181 sequence: 3,
1182 job: AnalysisJob::SpeakerFeature {
1183 speaker: label(1),
1184 packet: 1,
1185 },
1186 })
1187 );
1188 }
1189
1190 #[test]
1191 fn provider_stages_fail_without_retry_and_leaf_failures_are_exact() {
1192 for (stage, expected_error, expected_calls) in [
1193 (
1194 FailureStage::Transcript,
1195 AnalysisError::GeminiTranscript("transcript".into()),
1196 vec!["transcript"],
1197 ),
1198 (
1199 FailureStage::Labels,
1200 AnalysisError::TerraLabels("labels".into()),
1201 vec!["transcript", "labels"],
1202 ),
1203 (
1204 FailureStage::Cache,
1205 AnalysisError::GeminiCache("cache".into()),
1206 vec!["transcript", "labels", "features"],
1207 ),
1208 (
1209 FailureStage::Features,
1210 AnalysisError::GeminiFeature {
1211 speaker: label(1),
1212 packet: 2,
1213 message: "features".into(),
1214 },
1215 vec!["transcript", "labels", "features"],
1216 ),
1217 (
1218 FailureStage::Final,
1219 AnalysisError::TerraStructuring("final".into()),
1220 vec!["transcript", "labels", "features", "final"],
1221 ),
1222 ] {
1223 let operations =
1224 FakeOperations::successful(1).with_config(|config| config.failure = Some(stage));
1225 let (result, events) = run_with_progress(&operations);
1226 assert_eq!(result, Err(expected_error.clone()));
1227 assert_eq!(*operations.state.calls.lock().unwrap(), expected_calls);
1228 let matching_failures = events
1229 .iter()
1230 .filter(|event| {
1231 matches!(
1232 event,
1233 AnalysisProgress::JobFailed { error, .. }
1234 if error == &expected_error.to_string()
1235 )
1236 })
1237 .count();
1238 if stage == FailureStage::Cache {
1239 assert_eq!(matching_failures, 0);
1240 } else {
1241 assert_eq!(matching_failures, 1);
1242 }
1243 }
1244 }
1245
1246 #[test]
1247 fn cross_stage_transcript_and_speaker_mismatches_are_rejected() {
1248 let transcript = FakeOperations::successful(1).with_config(|config| {
1249 config.analysis = structured_analysis("different", 1);
1250 });
1251 let (result, events) = run_with_progress(&transcript);
1252 assert_eq!(result, Err(AnalysisError::TranscriptMismatch));
1253 assert!(events.contains(&AnalysisProgress::JobSucceeded { sequence: 6 }));
1254 assert!(!events.contains(&AnalysisProgress::StageCompleted {
1255 stage: AnalysisStage::Structuring,
1256 }));
1257
1258 let speakers = FakeOperations::successful(1).with_config(|config| {
1259 config.analysis = structured_analysis(&config.transcript, 2);
1260 });
1261 let (result, events) = run_with_progress(&speakers);
1262 assert_eq!(result, Err(AnalysisError::SpeakerSetMismatch));
1263 assert!(events.contains(&AnalysisProgress::JobSucceeded { sequence: 6 }));
1264 assert!(!events.contains(&AnalysisProgress::StageCompleted {
1265 stage: AnalysisStage::Structuring,
1266 }));
1267 }
1268
1269 #[test]
1270 fn legacy_api_retains_metadata_and_previous_stage_order() {
1271 for speaker_count in [0, 1, 40] {
1272 let operations = FakeOperations::successful(speaker_count);
1273 let result = block_on(execute_legacy(
1274 &operations,
1275 &legacy_ogg(),
1276 1234,
1277 Some("voice.ogg".into()),
1278 ))
1279 .unwrap();
1280 assert_eq!(result.envelope.audio.duration_ms(), 1234);
1281 assert_eq!(result.envelope.audio.filename(), Some("voice.ogg"));
1282 assert_eq!(
1283 result.envelope.analysis.speakers.len(),
1284 speaker_count as usize
1285 );
1286 assert_eq!(
1287 *operations.state.calls.lock().unwrap(),
1288 ["transcript", "labels", "features", "final"]
1289 );
1290 assert_eq!(
1291 result.label_extractor,
1292 StructurerProvenance {
1293 model_id: TERRA_MODEL_ID.into(),
1294 prompt_revision:
1295 kcode_speaker_v3_llm_protocol::TERRA_SPEAKER_LABELS_PROMPT_REVISION.into(),
1296 }
1297 );
1298 }
1299
1300 let operations = FakeOperations::successful(1);
1301 assert!(matches!(
1302 block_on(execute_legacy(&operations, b"bad", 1, None)),
1303 Err(AnalysisError::Input(_))
1304 ));
1305 assert!(operations.state.calls.lock().unwrap().is_empty());
1306 }
1307
1308 #[test]
1309 fn a_blocked_analysis_does_not_block_an_unrelated_analysis() {
1310 let completed = Arc::new(AtomicBool::new(false));
1311 let fast = FakeOperations::successful(0).with_config(|config| {
1312 config.mark_complete = Some(completed.clone());
1313 });
1314 let slow = FakeOperations::successful(0).with_config(|config| {
1315 config.wait_for = Some(completed.clone());
1316 });
1317 let slow_audio = strict_ogg(48);
1318 let fast_audio = strict_ogg(48);
1319 let (slow_result, fast_result) = block_on(async {
1320 join!(
1321 execute_strict(&slow, &slow_audio, |_| Ok(())),
1322 execute_strict(&fast, &fast_audio, |_| Ok(()))
1323 )
1324 });
1325 slow_result.unwrap();
1326 fast_result.unwrap();
1327 assert!(completed.load(Ordering::SeqCst));
1328 }
1329
1330 #[test]
1331 fn provenance_preserves_the_previous_public_contract() {
1332 let cohort = GeminiCohort::new("gemini-model");
1333 assert_eq!(
1334 cohort.feature_prompt_revisions,
1335 GEMINI_FEATURE_PROMPT_REVISIONS.map(str::to_owned)
1336 );
1337 cohort.validate().unwrap();
1338 StructurerProvenance::new("gpt-5.6").validate().unwrap();
1339 assert_eq!(
1340 GeminiCohort::new(" ").validate(),
1341 Err(ValidationError::Blank("gemini_model_id"))
1342 );
1343 }
1344
1345 #[test]
1346 fn reference_scale_local_orchestration_completes_within_envelope() {
1347 let started = Instant::now();
1348 let operations = FakeOperations::successful(1000);
1349 let (result, _) = run_with_progress(&operations);
1350 assert_eq!(result.unwrap().envelope.analysis.speakers.len(), 1000);
1351 assert!(started.elapsed().as_secs() < 10);
1352 }
1353
1354 #[test]
1355 fn concrete_provider_operations_compile() {
1356 fn require_operations<O: AnalysisOperations>() {}
1357 require_operations::<ProviderOperations>();
1358 }
1359}