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