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 futures::{
18 FutureExt,
19 future::{BoxFuture, join_all},
20};
21#[cfg(any(feature = "providers", test))]
22use kcode_speaker_v3_llm_protocol::{
23 FEATURE_PACKETS, FeaturePacket, GeminiRequestPart, SpeakerFeatureEvidence,
24 TERRA_SPEAKER_LABELS_PROMPT_REVISION, TerraFinalInput, TerraSpeakerLabelsInput, ToolDefinition,
25 decode_record_speaker_analysis_arguments, decode_record_speaker_labels_arguments,
26 extract_gemini_text, gemini_feature_cached_prefix, gemini_feature_suffix,
27 gemini_transcript_request, record_speaker_analysis_tool, record_speaker_labels_tool,
28};
29#[cfg(any(feature = "providers", test))]
30use serde_json::Value;
31
32const GEMINI_MODEL_ID: &str = "gemini-3.1-pro-preview";
33const TERRA_MODEL_ID: &str = "gpt-5.6-terra";
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct GeminiCohort {
37 pub model_id: String,
38 pub transcript_prompt_revision: String,
39 pub feature_prompt_revisions: [String; 3],
40 pub feature_schema_revision: String,
41}
42
43impl GeminiCohort {
44 pub fn new(model_id: impl Into<String>) -> Self {
45 Self {
46 model_id: model_id.into(),
47 transcript_prompt_revision: GEMINI_TRANSCRIPT_PROMPT_REVISION.into(),
48 feature_prompt_revisions: GEMINI_FEATURE_PROMPT_REVISIONS.map(str::to_owned),
49 feature_schema_revision: FEATURE_SCHEMA_REVISION.into(),
50 }
51 }
52
53 pub fn validate(&self) -> Result<(), ValidationError> {
54 validate_text(&self.model_id, "gemini_model_id")?;
55 validate_text(
56 &self.transcript_prompt_revision,
57 "transcript_prompt_revision",
58 )?;
59 for revision in &self.feature_prompt_revisions {
60 validate_text(revision, "feature_prompt_revision")?;
61 }
62 validate_text(&self.feature_schema_revision, "feature_schema_revision")
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct StructurerProvenance {
68 pub model_id: String,
69 pub prompt_revision: String,
70}
71
72impl StructurerProvenance {
73 pub fn new(model_id: impl Into<String>) -> Self {
74 Self {
75 model_id: model_id.into(),
76 prompt_revision: GPT_STRUCTURING_PROMPT_REVISION.into(),
77 }
78 }
79
80 pub fn validate(&self) -> Result<(), ValidationError> {
81 validate_text(&self.model_id, "structurer_model_id")?;
82 validate_text(&self.prompt_revision, "structurer_prompt_revision")
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct AnalysisEnvelope {
88 pub audio: OggAudioMetadata,
89 pub analysis: StructuredAnalysis,
90 pub gemini: GeminiCohort,
91 pub structurer: StructurerProvenance,
92}
93
94impl AnalysisEnvelope {
95 pub fn validate(&self) -> Result<(), ValidationError> {
96 self.audio.validate()?;
97 self.analysis.validate()?;
98 self.gemini.validate()?;
99 self.structurer.validate()
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum AnalysisError {
105 Input(String),
106 GeminiTranscript(String),
107 TerraLabels(String),
108 GeminiCache(String),
109 GeminiFeature {
110 speaker: LocalSpeakerLabel,
111 packet: u8,
112 message: String,
113 },
114 TerraStructuring(String),
115 TranscriptMismatch,
116 SpeakerSetMismatch,
117}
118
119impl fmt::Display for AnalysisError {
120 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 Self::Input(message) => write!(formatter, "invalid input: {message}"),
123 Self::GeminiTranscript(message) => {
124 write!(formatter, "Gemini transcript failed: {message}")
125 }
126 Self::TerraLabels(message) => {
127 write!(
128 formatter,
129 "Terra speaker-label extraction failed: {message}"
130 )
131 }
132 Self::GeminiCache(message) => {
133 write!(formatter, "Gemini feature cache creation failed: {message}")
134 }
135 Self::GeminiFeature {
136 speaker,
137 packet,
138 message,
139 } => write!(
140 formatter,
141 "Gemini feature call failed for {speaker}, packet {packet}: {message}"
142 ),
143 Self::TerraStructuring(message) => {
144 write!(formatter, "Terra final structuring failed: {message}")
145 }
146 Self::TranscriptMismatch => {
147 formatter.write_str("Terra returned a different transcript")
148 }
149 Self::SpeakerSetMismatch => {
150 formatter.write_str("Terra returned a different speaker set")
151 }
152 }
153 }
154}
155
156impl Error for AnalysisError {}
157
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct ExecutedAnalysis {
160 pub envelope: AnalysisEnvelope,
161 pub label_extractor: StructurerProvenance,
162}
163
164#[cfg(feature = "providers")]
165pub struct Analyzer {
166 backend: ProviderBackend,
167}
168
169#[cfg(feature = "providers")]
170impl Analyzer {
171 pub fn new(
172 gemini: kcode_gemini_3_1_pro::Gemini31Pro,
173 terra: kcode_codex_terra::CodexTerra,
174 ) -> Self {
175 Self {
176 backend: ProviderBackend { gemini, terra },
177 }
178 }
179
180 pub async fn analyze_ogg(
181 &self,
182 bytes: &[u8],
183 duration_ms: u64,
184 filename: Option<String>,
185 ) -> Result<ExecutedAnalysis, AnalysisError> {
186 execute(&self.backend, bytes, duration_ms, filename).await
187 }
188}
189
190fn validate_text(value: &str, field: &'static str) -> Result<(), ValidationError> {
191 (!value.trim().is_empty())
192 .then_some(())
193 .ok_or(ValidationError::Blank(field))
194}
195
196#[cfg(any(feature = "providers", test))]
197trait AnalysisBackend: Sync {
198 type Cache: Send + Sync;
199
200 fn gemini_transcript<'a>(&'a self, audio: &'a [u8]) -> BoxFuture<'a, Result<Value, String>>;
201
202 fn terra_labels<'a>(
203 &'a self,
204 input: String,
205 tool: ToolDefinition,
206 ) -> BoxFuture<'a, Result<Value, String>>;
207
208 fn gemini_cache<'a>(
209 &'a self,
210 audio: &'a [u8],
211 transcript: &'a str,
212 ) -> BoxFuture<'a, Result<Self::Cache, String>>;
213
214 fn gemini_feature<'a>(
215 &'a self,
216 cache: &'a Self::Cache,
217 speaker: LocalSpeakerLabel,
218 packet: FeaturePacket,
219 ) -> BoxFuture<'a, Result<Value, String>>;
220
221 fn terra_final<'a>(
222 &'a self,
223 input: String,
224 tool: ToolDefinition,
225 ) -> BoxFuture<'a, Result<Value, String>>;
226}
227
228#[cfg(any(feature = "providers", test))]
229async fn execute<B: AnalysisBackend>(
230 backend: &B,
231 bytes: &[u8],
232 duration_ms: u64,
233 filename: Option<String>,
234) -> Result<ExecutedAnalysis, AnalysisError> {
235 let audio = OggAudioMetadata::from_bytes(bytes, duration_ms, filename)
236 .map_err(|error| AnalysisError::Input(error.to_string()))?;
237
238 let transcript_response = backend
239 .gemini_transcript(bytes)
240 .await
241 .map_err(AnalysisError::GeminiTranscript)?;
242 let transcript = extract_gemini_text(&transcript_response)
243 .map_err(|error| AnalysisError::GeminiTranscript(error.to_string()))?;
244
245 let labels_input = TerraSpeakerLabelsInput::new(transcript.clone())
246 .map_err(|error| AnalysisError::TerraLabels(error.to_string()))?;
247 let labels_arguments = backend
248 .terra_labels(labels_input.render(), record_speaker_labels_tool())
249 .await
250 .map_err(AnalysisError::TerraLabels)?;
251 let labels = decode_record_speaker_labels_arguments(&labels_arguments)
252 .map_err(|error| AnalysisError::TerraLabels(error.to_string()))?;
253
254 let evidence = collect_feature_evidence(backend, bytes, &transcript, &labels).await?;
255
256 let final_input = TerraFinalInput::new(transcript.clone(), evidence)
257 .map_err(|error| AnalysisError::TerraStructuring(error.to_string()))?;
258 let final_arguments = backend
259 .terra_final(final_input.render(), record_speaker_analysis_tool())
260 .await
261 .map_err(AnalysisError::TerraStructuring)?;
262 let analysis = decode_record_speaker_analysis_arguments(&final_arguments)
263 .map_err(|error| AnalysisError::TerraStructuring(error.to_string()))?;
264
265 if analysis.transcript != transcript {
266 return Err(AnalysisError::TranscriptMismatch);
267 }
268
269 let expected_speakers = labels.iter().copied().collect::<BTreeSet<_>>();
270 let returned_speakers = analysis
271 .speakers
272 .iter()
273 .map(|speaker| speaker.speaker)
274 .collect::<BTreeSet<_>>();
275 if expected_speakers != returned_speakers {
276 return Err(AnalysisError::SpeakerSetMismatch);
277 }
278
279 let envelope = AnalysisEnvelope {
280 audio,
281 analysis,
282 gemini: GeminiCohort::new(GEMINI_MODEL_ID),
283 structurer: StructurerProvenance::new(TERRA_MODEL_ID),
284 };
285 envelope
286 .validate()
287 .map_err(|error| AnalysisError::TerraStructuring(error.to_string()))?;
288
289 let label_extractor = StructurerProvenance {
290 model_id: TERRA_MODEL_ID.into(),
291 prompt_revision: TERRA_SPEAKER_LABELS_PROMPT_REVISION.into(),
292 };
293 label_extractor
294 .validate()
295 .map_err(|error| AnalysisError::TerraLabels(error.to_string()))?;
296
297 Ok(ExecutedAnalysis {
298 envelope,
299 label_extractor,
300 })
301}
302
303#[cfg(any(feature = "providers", test))]
304async fn collect_feature_evidence<B: AnalysisBackend>(
305 backend: &B,
306 bytes: &[u8],
307 transcript: &str,
308 labels: &[LocalSpeakerLabel],
309) -> Result<Vec<SpeakerFeatureEvidence>, AnalysisError> {
310 if labels.is_empty() {
311 return Ok(Vec::new());
312 }
313
314 let cache = backend
315 .gemini_cache(bytes, transcript)
316 .await
317 .map_err(AnalysisError::GeminiCache)?;
318 let jobs = labels
319 .iter()
320 .copied()
321 .flat_map(|speaker| {
322 FEATURE_PACKETS
323 .into_iter()
324 .map(move |packet| (speaker, packet))
325 })
326 .collect::<Vec<_>>();
327 let responses = join_all(
328 jobs.iter()
329 .map(|(speaker, packet)| backend.gemini_feature(&cache, *speaker, *packet)),
330 )
331 .await;
332
333 let mut texts = Vec::with_capacity(responses.len());
334 for ((speaker, packet), response) in jobs.into_iter().zip(responses) {
335 let response = response.map_err(|message| AnalysisError::GeminiFeature {
336 speaker,
337 packet: packet.index() as u8 + 1,
338 message,
339 })?;
340 let text =
341 extract_gemini_text(&response).map_err(|error| AnalysisError::GeminiFeature {
342 speaker,
343 packet: packet.index() as u8 + 1,
344 message: error.to_string(),
345 })?;
346 texts.push(text);
347 }
348
349 let mut evidence = Vec::with_capacity(labels.len());
350 for (speaker, packets) in labels.iter().copied().zip(texts.chunks_exact(3)) {
351 evidence.push(
352 SpeakerFeatureEvidence::new(
353 speaker,
354 packets[0].clone(),
355 packets[1].clone(),
356 packets[2].clone(),
357 )
358 .map_err(|error| AnalysisError::GeminiFeature {
359 speaker,
360 packet: 1,
361 message: error.to_string(),
362 })?,
363 );
364 }
365 Ok(evidence)
366}
367
368#[cfg(any(feature = "providers", test))]
369#[derive(Clone)]
370struct ProviderBackend {
371 gemini: kcode_gemini_3_1_pro::Gemini31Pro,
372 terra: kcode_codex_terra::CodexTerra,
373}
374
375#[cfg(any(feature = "providers", test))]
376impl AnalysisBackend for ProviderBackend {
377 type Cache = kcode_gemini_3_1_pro::CachedPrefix;
378
379 fn gemini_transcript<'a>(&'a self, audio: &'a [u8]) -> BoxFuture<'a, Result<Value, String>> {
380 async move {
381 let contents = vec![gemini_content(gemini_transcript_request(audio))];
382 self.gemini
383 .generate(contents, None, None)
384 .await
385 .map(|generation| generation.response)
386 .map_err(|error| error.to_string())
387 }
388 .boxed()
389 }
390
391 fn terra_labels<'a>(
392 &'a self,
393 input: String,
394 tool: ToolDefinition,
395 ) -> BoxFuture<'a, Result<Value, String>> {
396 async move { run_terra(&self.terra, input, tool).await }.boxed()
397 }
398
399 fn gemini_cache<'a>(
400 &'a self,
401 audio: &'a [u8],
402 transcript: &'a str,
403 ) -> BoxFuture<'a, Result<Self::Cache, String>> {
404 async move {
405 let contents = vec![gemini_content(gemini_feature_cached_prefix(
406 audio, transcript,
407 ))];
408 self.gemini
409 .create_cached_prefix(contents, None, std::time::Duration::from_secs(60 * 60))
410 .await
411 .map_err(|error| error.to_string())
412 }
413 .boxed()
414 }
415
416 fn gemini_feature<'a>(
417 &'a self,
418 cache: &'a Self::Cache,
419 speaker: LocalSpeakerLabel,
420 packet: FeaturePacket,
421 ) -> BoxFuture<'a, Result<Value, String>> {
422 async move {
423 let contents = vec![kcode_gemini_3_1_pro::Content {
424 parts: vec![kcode_gemini_3_1_pro::Part::Text(gemini_feature_suffix(
425 packet, speaker,
426 ))],
427 }];
428 self.gemini
429 .generate(contents, Some(cache), None)
430 .await
431 .map(|generation| generation.response)
432 .map_err(|error| error.to_string())
433 }
434 .boxed()
435 }
436
437 fn terra_final<'a>(
438 &'a self,
439 input: String,
440 tool: ToolDefinition,
441 ) -> BoxFuture<'a, Result<Value, String>> {
442 async move { run_terra(&self.terra, input, tool).await }.boxed()
443 }
444}
445
446#[cfg(any(feature = "providers", test))]
447fn gemini_content<'a>(
448 parts: impl IntoIterator<Item = GeminiRequestPart<'a>>,
449) -> kcode_gemini_3_1_pro::Content {
450 kcode_gemini_3_1_pro::Content {
451 parts: parts
452 .into_iter()
453 .map(|part| match part {
454 GeminiRequestPart::Audio { media_type, bytes } => {
455 kcode_gemini_3_1_pro::Part::InlineData {
456 mime_type: media_type.into(),
457 bytes: bytes.to_vec(),
458 }
459 }
460 GeminiRequestPart::Text(text) => kcode_gemini_3_1_pro::Part::Text(text),
461 })
462 .collect(),
463 }
464}
465
466#[cfg(any(feature = "providers", test))]
467async fn run_terra(
468 terra: &kcode_codex_terra::CodexTerra,
469 input: String,
470 tool: ToolDefinition,
471) -> Result<Value, String> {
472 terra
473 .run(kcode_codex_terra::ToolRun {
474 input,
475 tool_name: tool.name.into(),
476 tool_description: tool.description.into(),
477 input_schema: tool.input_schema,
478 })
479 .await
480 .map(|result| result.arguments)
481 .map_err(|error| error.to_string())
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use futures::{executor::block_on, future::poll_fn, join};
488 use serde_json::{Map, json};
489 use std::{
490 sync::{
491 Arc, Mutex,
492 atomic::{AtomicBool, AtomicUsize, Ordering},
493 },
494 task::Poll,
495 };
496
497 #[derive(Clone)]
498 struct FakeBackend {
499 state: Arc<FakeState>,
500 }
501
502 struct FakeState {
503 transcript: Value,
504 labels: Value,
505 final_arguments: Value,
506 transcript_error: Option<String>,
507 labels_error: Option<String>,
508 cache_error: Option<String>,
509 feature_error: Option<(LocalSpeakerLabel, FeaturePacket)>,
510 final_error: Option<String>,
511 wait_for: Option<Arc<AtomicBool>>,
512 mark_complete: Option<Arc<AtomicBool>>,
513 transcript_calls: AtomicUsize,
514 labels_calls: AtomicUsize,
515 cache_calls: AtomicUsize,
516 feature_calls: AtomicUsize,
517 final_calls: AtomicUsize,
518 active_features: AtomicUsize,
519 maximum_active_features: AtomicUsize,
520 targets: Mutex<Vec<(LocalSpeakerLabel, FeaturePacket)>>,
521 }
522
523 impl FakeBackend {
524 fn successful(speaker_count: u32) -> Self {
525 let labels = (1..=speaker_count)
526 .map(|number| format!("Speaker {number}"))
527 .collect::<Vec<_>>();
528 Self {
529 state: Arc::new(FakeState {
530 transcript: gemini_text("[high] Speaker 1: exact transcript"),
531 labels: json!({ "speakers": labels }),
532 final_arguments: final_arguments(
533 "[high] Speaker 1: exact transcript",
534 speaker_count,
535 ),
536 transcript_error: None,
537 labels_error: None,
538 cache_error: None,
539 feature_error: None,
540 final_error: None,
541 wait_for: None,
542 mark_complete: None,
543 transcript_calls: AtomicUsize::new(0),
544 labels_calls: AtomicUsize::new(0),
545 cache_calls: AtomicUsize::new(0),
546 feature_calls: AtomicUsize::new(0),
547 final_calls: AtomicUsize::new(0),
548 active_features: AtomicUsize::new(0),
549 maximum_active_features: AtomicUsize::new(0),
550 targets: Mutex::new(Vec::new()),
551 }),
552 }
553 }
554
555 fn with_state(&self, update: impl FnOnce(&FakeState) -> FakeState) -> Self {
556 Self {
557 state: Arc::new(update(&self.state)),
558 }
559 }
560 }
561
562 impl AnalysisBackend for FakeBackend {
563 type Cache = ();
564
565 fn gemini_transcript<'a>(
566 &'a self,
567 _audio: &'a [u8],
568 ) -> BoxFuture<'a, Result<Value, String>> {
569 async move {
570 self.state.transcript_calls.fetch_add(1, Ordering::SeqCst);
571 if let Some(wait_for) = &self.state.wait_for {
572 poll_fn(|context| {
573 if wait_for.load(Ordering::SeqCst) {
574 Poll::Ready(())
575 } else {
576 context.waker().wake_by_ref();
577 Poll::Pending
578 }
579 })
580 .await;
581 }
582 match &self.state.transcript_error {
583 Some(error) => Err(error.clone()),
584 None => Ok(self.state.transcript.clone()),
585 }
586 }
587 .boxed()
588 }
589
590 fn terra_labels<'a>(
591 &'a self,
592 _input: String,
593 _tool: ToolDefinition,
594 ) -> BoxFuture<'a, Result<Value, String>> {
595 async move {
596 self.state.labels_calls.fetch_add(1, Ordering::SeqCst);
597 match &self.state.labels_error {
598 Some(error) => Err(error.clone()),
599 None => Ok(self.state.labels.clone()),
600 }
601 }
602 .boxed()
603 }
604
605 fn gemini_cache<'a>(
606 &'a self,
607 _audio: &'a [u8],
608 _transcript: &'a str,
609 ) -> BoxFuture<'a, Result<Self::Cache, String>> {
610 async move {
611 self.state.cache_calls.fetch_add(1, Ordering::SeqCst);
612 match &self.state.cache_error {
613 Some(error) => Err(error.clone()),
614 None => Ok(()),
615 }
616 }
617 .boxed()
618 }
619
620 fn gemini_feature<'a>(
621 &'a self,
622 _cache: &'a Self::Cache,
623 speaker: LocalSpeakerLabel,
624 packet: FeaturePacket,
625 ) -> BoxFuture<'a, Result<Value, String>> {
626 async move {
627 self.state.feature_calls.fetch_add(1, Ordering::SeqCst);
628 self.state.targets.lock().unwrap().push((speaker, packet));
629 let active = self.state.active_features.fetch_add(1, Ordering::SeqCst) + 1;
630 self.state
631 .maximum_active_features
632 .fetch_max(active, Ordering::SeqCst);
633 let mut yielded = false;
634 poll_fn(|context| {
635 if yielded {
636 Poll::Ready(())
637 } else {
638 yielded = true;
639 context.waker().wake_by_ref();
640 Poll::Pending
641 }
642 })
643 .await;
644 self.state.active_features.fetch_sub(1, Ordering::SeqCst);
645 if self.state.feature_error == Some((speaker, packet)) {
646 Err("feature failure".into())
647 } else {
648 Ok(gemini_text(&format!(
649 "{speaker} packet {}",
650 packet.index() + 1
651 )))
652 }
653 }
654 .boxed()
655 }
656
657 fn terra_final<'a>(
658 &'a self,
659 _input: String,
660 _tool: ToolDefinition,
661 ) -> BoxFuture<'a, Result<Value, String>> {
662 async move {
663 self.state.final_calls.fetch_add(1, Ordering::SeqCst);
664 match &self.state.final_error {
665 Some(error) => Err(error.clone()),
666 None => {
667 if let Some(mark_complete) = &self.state.mark_complete {
668 mark_complete.store(true, Ordering::SeqCst);
669 }
670 Ok(self.state.final_arguments.clone())
671 }
672 }
673 }
674 .boxed()
675 }
676 }
677
678 fn copy_state(state: &FakeState) -> FakeState {
679 FakeState {
680 transcript: state.transcript.clone(),
681 labels: state.labels.clone(),
682 final_arguments: state.final_arguments.clone(),
683 transcript_error: state.transcript_error.clone(),
684 labels_error: state.labels_error.clone(),
685 cache_error: state.cache_error.clone(),
686 feature_error: state.feature_error,
687 final_error: state.final_error.clone(),
688 wait_for: state.wait_for.clone(),
689 mark_complete: state.mark_complete.clone(),
690 transcript_calls: AtomicUsize::new(0),
691 labels_calls: AtomicUsize::new(0),
692 cache_calls: AtomicUsize::new(0),
693 feature_calls: AtomicUsize::new(0),
694 final_calls: AtomicUsize::new(0),
695 active_features: AtomicUsize::new(0),
696 maximum_active_features: AtomicUsize::new(0),
697 targets: Mutex::new(Vec::new()),
698 }
699 }
700
701 fn gemini_text(text: &str) -> Value {
702 json!({
703 "candidates": [{
704 "content": {
705 "parts": [{ "text": text }]
706 }
707 }]
708 })
709 }
710
711 fn null_features() -> Map<String, Value> {
712 FEATURE_NAMES
713 .into_iter()
714 .map(|name| (name.into(), Value::Null))
715 .collect()
716 }
717
718 fn final_arguments(transcript: &str, speaker_count: u32) -> Value {
719 let speakers = (1..=speaker_count)
720 .map(|number| {
721 json!({
722 "speaker": format!("Speaker {number}"),
723 "language": "English",
724 "features": null_features(),
725 "features_usable_for_training": false
726 })
727 })
728 .collect::<Vec<_>>();
729 json!({
730 "transcript": transcript,
731 "speakers": speakers
732 })
733 }
734
735 fn ogg() -> Vec<u8> {
736 let mut bytes = vec![0; 28];
737 bytes[..4].copy_from_slice(b"OggS");
738 bytes[4] = 0;
739 bytes[26] = 1;
740 bytes[27] = 0;
741 bytes
742 }
743
744 #[test]
745 fn zero_one_and_many_speaker_workflows_have_exact_calls() {
746 for speaker_count in [0, 1, 40] {
747 let backend = FakeBackend::successful(speaker_count);
748 let result = block_on(execute(&backend, &ogg(), 1, Some("voice.ogg".into()))).unwrap();
749 assert_eq!(
750 result.envelope.analysis.speakers.len(),
751 speaker_count as usize
752 );
753 assert_eq!(
754 result.label_extractor,
755 StructurerProvenance {
756 model_id: TERRA_MODEL_ID.into(),
757 prompt_revision: TERRA_SPEAKER_LABELS_PROMPT_REVISION.into(),
758 }
759 );
760 assert_eq!(backend.state.transcript_calls.load(Ordering::SeqCst), 1);
761 assert_eq!(backend.state.labels_calls.load(Ordering::SeqCst), 1);
762 assert_eq!(
763 backend.state.cache_calls.load(Ordering::SeqCst),
764 usize::from(speaker_count > 0)
765 );
766 assert_eq!(
767 backend.state.feature_calls.load(Ordering::SeqCst),
768 speaker_count as usize * 3
769 );
770 assert_eq!(backend.state.final_calls.load(Ordering::SeqCst), 1);
771 }
772 }
773
774 #[test]
775 fn terra_labels_control_targets_and_features_overlap() {
776 let backend = FakeBackend::successful(3);
777 block_on(execute(&backend, &ogg(), 1, None)).unwrap();
778 let expected = (1..=3)
779 .flat_map(|number| {
780 FEATURE_PACKETS
781 .into_iter()
782 .map(move |packet| (LocalSpeakerLabel::new(number).unwrap(), packet))
783 })
784 .collect::<Vec<_>>();
785 assert_eq!(*backend.state.targets.lock().unwrap(), expected);
786 assert!(backend.state.maximum_active_features.load(Ordering::SeqCst) > 1);
787 }
788
789 #[test]
790 fn input_and_each_provider_stage_fail_without_retry() {
791 let backend = FakeBackend::successful(1);
792 assert!(matches!(
793 block_on(execute(&backend, b"bad", 1, None)),
794 Err(AnalysisError::Input(_))
795 ));
796 assert_eq!(backend.state.transcript_calls.load(Ordering::SeqCst), 0);
797
798 let transcript_failure = backend.with_state(|state| {
799 let mut state = copy_state(state);
800 state.transcript_error = Some("transcript".into());
801 state
802 });
803 assert!(matches!(
804 block_on(execute(&transcript_failure, &ogg(), 1, None)),
805 Err(AnalysisError::GeminiTranscript(_))
806 ));
807 assert_eq!(
808 transcript_failure
809 .state
810 .transcript_calls
811 .load(Ordering::SeqCst),
812 1
813 );
814 assert_eq!(
815 transcript_failure.state.labels_calls.load(Ordering::SeqCst),
816 0
817 );
818
819 let labels_failure = backend.with_state(|state| {
820 let mut state = copy_state(state);
821 state.labels_error = Some("labels".into());
822 state
823 });
824 assert!(matches!(
825 block_on(execute(&labels_failure, &ogg(), 1, None)),
826 Err(AnalysisError::TerraLabels(_))
827 ));
828 assert_eq!(labels_failure.state.labels_calls.load(Ordering::SeqCst), 1);
829 assert_eq!(labels_failure.state.cache_calls.load(Ordering::SeqCst), 0);
830
831 let cache_failure = backend.with_state(|state| {
832 let mut state = copy_state(state);
833 state.cache_error = Some("cache".into());
834 state
835 });
836 assert!(matches!(
837 block_on(execute(&cache_failure, &ogg(), 1, None)),
838 Err(AnalysisError::GeminiCache(_))
839 ));
840 assert_eq!(cache_failure.state.cache_calls.load(Ordering::SeqCst), 1);
841 assert_eq!(cache_failure.state.feature_calls.load(Ordering::SeqCst), 0);
842
843 let feature_failure = backend.with_state(|state| {
844 let mut state = copy_state(state);
845 state.feature_error = Some((LocalSpeakerLabel::new(1).unwrap(), FeaturePacket::Two));
846 state
847 });
848 assert!(matches!(
849 block_on(execute(&feature_failure, &ogg(), 1, None)),
850 Err(AnalysisError::GeminiFeature { packet: 2, .. })
851 ));
852 assert_eq!(
853 feature_failure.state.feature_calls.load(Ordering::SeqCst),
854 3
855 );
856 assert_eq!(feature_failure.state.final_calls.load(Ordering::SeqCst), 0);
857
858 let final_failure = backend.with_state(|state| {
859 let mut state = copy_state(state);
860 state.final_error = Some("final".into());
861 state
862 });
863 assert!(matches!(
864 block_on(execute(&final_failure, &ogg(), 1, None)),
865 Err(AnalysisError::TerraStructuring(_))
866 ));
867 assert_eq!(final_failure.state.final_calls.load(Ordering::SeqCst), 1);
868 }
869
870 #[test]
871 fn protocol_failures_and_cross_stage_mismatches_are_rejected() {
872 let backend = FakeBackend::successful(1);
873
874 let invalid_transcript = backend.with_state(|state| {
875 let mut state = copy_state(state);
876 state.transcript = json!({});
877 state
878 });
879 assert!(matches!(
880 block_on(execute(&invalid_transcript, &ogg(), 1, None)),
881 Err(AnalysisError::GeminiTranscript(_))
882 ));
883
884 let invalid_labels = backend.with_state(|state| {
885 let mut state = copy_state(state);
886 state.labels = json!({ "speakers": ["Unknown"] });
887 state
888 });
889 assert!(matches!(
890 block_on(execute(&invalid_labels, &ogg(), 1, None)),
891 Err(AnalysisError::TerraLabels(_))
892 ));
893
894 let invalid_final = backend.with_state(|state| {
895 let mut state = copy_state(state);
896 state.final_arguments = json!({});
897 state
898 });
899 assert!(matches!(
900 block_on(execute(&invalid_final, &ogg(), 1, None)),
901 Err(AnalysisError::TerraStructuring(_))
902 ));
903
904 let transcript_mismatch = backend.with_state(|state| {
905 let mut state = copy_state(state);
906 state.final_arguments = final_arguments("different", 1);
907 state
908 });
909 assert_eq!(
910 block_on(execute(&transcript_mismatch, &ogg(), 1, None)),
911 Err(AnalysisError::TranscriptMismatch)
912 );
913
914 let speaker_mismatch = backend.with_state(|state| {
915 let mut state = copy_state(state);
916 state.final_arguments = final_arguments("[high] Speaker 1: exact transcript", 2);
917 state
918 });
919 assert_eq!(
920 block_on(execute(&speaker_mismatch, &ogg(), 1, None)),
921 Err(AnalysisError::SpeakerSetMismatch)
922 );
923 }
924
925 #[test]
926 fn a_blocked_analysis_does_not_block_an_unrelated_analysis() {
927 let completed = Arc::new(AtomicBool::new(false));
928 let fast = FakeBackend::successful(0).with_state(|state| {
929 let mut state = copy_state(state);
930 state.mark_complete = Some(completed.clone());
931 state
932 });
933 let slow = FakeBackend::successful(0).with_state(|state| {
934 let mut state = copy_state(state);
935 state.wait_for = Some(completed.clone());
936 state
937 });
938 let slow_audio = ogg();
939 let fast_audio = ogg();
940 let (slow_result, fast_result) = block_on(async {
941 join!(
942 execute(&slow, &slow_audio, 1, None),
943 execute(&fast, &fast_audio, 1, None)
944 )
945 });
946 slow_result.unwrap();
947 fast_result.unwrap();
948 assert!(completed.load(Ordering::SeqCst));
949 }
950
951 #[test]
952 fn provenance_preserves_the_previous_public_contract() {
953 let cohort = GeminiCohort::new("gemini-model");
954 assert_eq!(
955 cohort.feature_prompt_revisions,
956 GEMINI_FEATURE_PROMPT_REVISIONS.map(str::to_owned)
957 );
958 cohort.validate().unwrap();
959 StructurerProvenance::new("gpt-5.6").validate().unwrap();
960 assert_eq!(
961 GeminiCohort::new(" ").validate(),
962 Err(ValidationError::Blank("gemini_model_id"))
963 );
964 }
965
966 #[test]
967 fn concrete_provider_backend_compiles() {
968 fn require_backend<B: AnalysisBackend>() {}
969 require_backend::<ProviderBackend>();
970 }
971}