Skip to main content

kcode_speaker_v3_analysis/
lib.rs

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}