1use serde::Serialize;
2use serde_json::{Map, Value, json};
3use std::{collections::BTreeSet, error::Error, fmt};
4
5pub use kcode_speaker_v3_schema::{
6 FEATURE_NAMES, FeatureVector24, LocalSpeakerLabel, StructuredAnalysis, StructuredSpeaker,
7 ValidationError as AnalysisValidationError, VocalGenderPresentation,
8};
9
10pub const GEMINI_TRANSCRIPT_PROMPT_REVISION: &str = "speaker-v3-gemini-transcript-r1";
11pub const GEMINI_FEATURE_PROMPT_ONE_REVISION: &str = "speaker-v3-gemini-feature-1-r2";
12pub const GEMINI_FEATURE_PROMPT_TWO_REVISION: &str = "speaker-v3-gemini-feature-2-r2";
13pub const GEMINI_FEATURE_PROMPT_THREE_REVISION: &str = "speaker-v3-gemini-feature-3-r2";
14pub const GPT_STRUCTURING_PROMPT_REVISION: &str = "speaker-v3-gpt-structure-r2";
15pub const TERRA_SPEAKER_LABELS_PROMPT_REVISION: &str = "speaker-v3-terra-labels-r1";
16
17pub const GEMINI_FEATURE_PROMPT_REVISIONS: [&str; 3] = [
18 GEMINI_FEATURE_PROMPT_ONE_REVISION,
19 GEMINI_FEATURE_PROMPT_TWO_REVISION,
20 GEMINI_FEATURE_PROMPT_THREE_REVISION,
21];
22
23pub const GEMINI_TRANSCRIPT_PROMPT: &str = include_str!("gemini-transcript-prompt.txt");
24pub const GEMINI_FEATURE_PROMPT_ONE: &str = include_str!("gemini-feature-1-prompt.txt");
25pub const GEMINI_FEATURE_PROMPT_TWO: &str = include_str!("gemini-feature-2-prompt.txt");
26pub const GEMINI_FEATURE_PROMPT_THREE: &str = include_str!("gemini-feature-3-prompt.txt");
27pub const GPT_STRUCTURING_PROMPT: &str = include_str!("gpt-structuring-prompt.txt");
28pub const TERRA_SPEAKER_LABELS_PROMPT: &str = include_str!("terra-speaker-labels-prompt.txt");
29
30pub const RECORD_SPEAKER_LABELS_TOOL_NAME: &str = "record_speaker_labels";
31pub const RECORD_SPEAKER_LABELS_TOOL_DESCRIPTION: &str = "Record every exact local Speaker N label from the supplied Gemini transcript in first-appearance order.";
32pub const RECORD_SPEAKER_ANALYSIS_TOOL_NAME: &str = "record_speaker_analysis";
33pub const RECORD_SPEAKER_ANALYSIS_TOOL_DESCRIPTION: &str = "Record the exact transcript and complete structured 24-feature analysis for every local speaker.";
34
35pub const FEATURE_PACKETS: [FeaturePacket; 3] =
36 [FeaturePacket::One, FeaturePacket::Two, FeaturePacket::Three];
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum GeminiRequestPart<'a> {
40 Audio {
41 media_type: &'static str,
42 bytes: &'a [u8],
43 },
44 Text(String),
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub enum FeaturePacket {
49 One,
50 Two,
51 Three,
52}
53
54impl FeaturePacket {
55 pub fn index(self) -> usize {
56 match self {
57 Self::One => 0,
58 Self::Two => 1,
59 Self::Three => 2,
60 }
61 }
62
63 pub fn prompt(self) -> &'static str {
64 match self {
65 Self::One => GEMINI_FEATURE_PROMPT_ONE,
66 Self::Two => GEMINI_FEATURE_PROMPT_TWO,
67 Self::Three => GEMINI_FEATURE_PROMPT_THREE,
68 }
69 }
70
71 pub fn revision(self) -> &'static str {
72 match self {
73 Self::One => GEMINI_FEATURE_PROMPT_ONE_REVISION,
74 Self::Two => GEMINI_FEATURE_PROMPT_TWO_REVISION,
75 Self::Three => GEMINI_FEATURE_PROMPT_THREE_REVISION,
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum ProtocolError {
82 Blank(&'static str),
83 InvalidGeminiResponse(String),
84 GeminiTextCandidateCount(usize),
85 InvalidSpeakerLabelsArguments(String),
86 DuplicateSpeakerLabel(LocalSpeakerLabel),
87 InvalidFinalArguments(String),
88 InvalidStructuredAnalysis(AnalysisValidationError),
89}
90
91impl fmt::Display for ProtocolError {
92 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93 match self {
94 Self::Blank(field) => write!(formatter, "{field} is blank"),
95 Self::InvalidGeminiResponse(message) => {
96 write!(formatter, "invalid Gemini response: {message}")
97 }
98 Self::GeminiTextCandidateCount(count) => {
99 write!(
100 formatter,
101 "Gemini returned {count} nonblank textual candidates"
102 )
103 }
104 Self::InvalidSpeakerLabelsArguments(message) => {
105 write!(formatter, "invalid speaker-label arguments: {message}")
106 }
107 Self::DuplicateSpeakerLabel(label) => {
108 write!(formatter, "duplicate speaker label: {label}")
109 }
110 Self::InvalidFinalArguments(message) => {
111 write!(formatter, "invalid final analysis arguments: {message}")
112 }
113 Self::InvalidStructuredAnalysis(error) => {
114 write!(formatter, "invalid structured analysis: {error}")
115 }
116 }
117 }
118}
119
120impl Error for ProtocolError {
121 fn source(&self) -> Option<&(dyn Error + 'static)> {
122 match self {
123 Self::InvalidStructuredAnalysis(error) => Some(error),
124 _ => None,
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct ToolDefinition {
131 pub name: &'static str,
132 pub description: &'static str,
133 pub input_schema: Value,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
137pub struct TerraSpeakerLabelsInput {
138 transcript: String,
139}
140
141impl TerraSpeakerLabelsInput {
142 pub fn new(transcript: String) -> Result<Self, ProtocolError> {
143 require_nonblank(&transcript, "transcript")?;
144 Ok(Self { transcript })
145 }
146
147 pub fn instruction(&self) -> &'static str {
148 TERRA_SPEAKER_LABELS_PROMPT
149 }
150
151 pub fn transcript(&self) -> &str {
152 &self.transcript
153 }
154
155 pub fn render(&self) -> String {
156 format!(
157 "{}Input:\n{}",
158 self.instruction(),
159 serde_json::to_string(self).expect("serializing strings cannot fail")
160 )
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
165pub struct SpeakerFeatureEvidence {
166 speaker: LocalSpeakerLabel,
167 feature_packets: [String; 3],
168}
169
170impl SpeakerFeatureEvidence {
171 pub fn new(
172 speaker: LocalSpeakerLabel,
173 packet_one: String,
174 packet_two: String,
175 packet_three: String,
176 ) -> Result<Self, ProtocolError> {
177 require_nonblank(&packet_one, "feature_packet_one")?;
178 require_nonblank(&packet_two, "feature_packet_two")?;
179 require_nonblank(&packet_three, "feature_packet_three")?;
180 Ok(Self {
181 speaker,
182 feature_packets: [packet_one, packet_two, packet_three],
183 })
184 }
185
186 pub fn speaker(&self) -> LocalSpeakerLabel {
187 self.speaker
188 }
189
190 pub fn packet(&self, packet: FeaturePacket) -> &str {
191 &self.feature_packets[packet.index()]
192 }
193
194 pub fn packets(&self) -> [&str; 3] {
195 self.feature_packets.each_ref().map(String::as_str)
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
200pub struct TerraFinalInput {
201 transcript: String,
202 speakers: Vec<SpeakerFeatureEvidence>,
203}
204
205impl TerraFinalInput {
206 pub fn new(
207 transcript: String,
208 speakers: Vec<SpeakerFeatureEvidence>,
209 ) -> Result<Self, ProtocolError> {
210 require_nonblank(&transcript, "transcript")?;
211 let mut labels = BTreeSet::new();
212 for speaker in &speakers {
213 if !labels.insert(speaker.speaker()) {
214 return Err(ProtocolError::DuplicateSpeakerLabel(speaker.speaker()));
215 }
216 }
217 Ok(Self {
218 transcript,
219 speakers,
220 })
221 }
222
223 pub fn instruction(&self) -> &'static str {
224 GPT_STRUCTURING_PROMPT
225 }
226
227 pub fn transcript(&self) -> &str {
228 &self.transcript
229 }
230
231 pub fn speakers(&self) -> &[SpeakerFeatureEvidence] {
232 &self.speakers
233 }
234
235 pub fn render(&self) -> String {
236 format!(
237 "{}Input:\n{}",
238 self.instruction(),
239 serde_json::to_string(self).expect("serializing strings cannot fail")
240 )
241 }
242}
243
244pub fn gemini_transcript_request(audio: &[u8]) -> [GeminiRequestPart<'_>; 2] {
245 [
246 GeminiRequestPart::Audio {
247 media_type: "audio/ogg",
248 bytes: audio,
249 },
250 GeminiRequestPart::Text(GEMINI_TRANSCRIPT_PROMPT.to_owned()),
251 ]
252}
253
254pub fn gemini_feature_cached_prefix<'a>(
255 audio: &'a [u8],
256 transcript: &str,
257) -> [GeminiRequestPart<'a>; 2] {
258 let (shared_prefix, _) = feature_prompt_parts(FeaturePacket::One);
259 [
260 GeminiRequestPart::Audio {
261 media_type: "audio/ogg",
262 bytes: audio,
263 },
264 GeminiRequestPart::Text(format!("{shared_prefix}{transcript}")),
265 ]
266}
267
268pub fn gemini_feature_suffix(packet: FeaturePacket, target: LocalSpeakerLabel) -> String {
269 let (_, suffix) = feature_prompt_parts(packet);
270 suffix.replace("{{TARGET_SPEAKER}}", &target.to_string())
271}
272
273pub fn extract_gemini_text(response: &Value) -> Result<String, ProtocolError> {
274 let candidates = response
275 .get("candidates")
276 .and_then(Value::as_array)
277 .ok_or_else(|| ProtocolError::InvalidGeminiResponse("candidates is not an array".into()))?;
278 let mut textual_candidates = Vec::new();
279 for (candidate_index, candidate) in candidates.iter().enumerate() {
280 let parts = candidate
281 .get("content")
282 .and_then(|content| content.get("parts"))
283 .and_then(Value::as_array)
284 .ok_or_else(|| {
285 ProtocolError::InvalidGeminiResponse(format!(
286 "candidate {candidate_index} has no content parts array"
287 ))
288 })?;
289 let mut text = String::new();
290 for (part_index, part) in parts.iter().enumerate() {
291 if let Some(value) = part.get("text") {
292 let value = value.as_str().ok_or_else(|| {
293 ProtocolError::InvalidGeminiResponse(format!(
294 "candidate {candidate_index} part {part_index} text is not a string"
295 ))
296 })?;
297 text.push_str(value);
298 }
299 }
300 if !text.trim().is_empty() {
301 textual_candidates.push(text);
302 }
303 }
304 if textual_candidates.len() != 1 {
305 return Err(ProtocolError::GeminiTextCandidateCount(
306 textual_candidates.len(),
307 ));
308 }
309 Ok(textual_candidates.pop().expect("length checked"))
310}
311
312pub fn record_speaker_labels_tool() -> ToolDefinition {
313 ToolDefinition {
314 name: RECORD_SPEAKER_LABELS_TOOL_NAME,
315 description: RECORD_SPEAKER_LABELS_TOOL_DESCRIPTION,
316 input_schema: json!({
317 "type": "object",
318 "additionalProperties": false,
319 "required": ["speakers"],
320 "properties": {
321 "speakers": {
322 "type": "array",
323 "items": {
324 "type": "string",
325 "pattern": "^Speaker [1-9][0-9]*$"
326 }
327 }
328 }
329 }),
330 }
331}
332
333pub fn decode_record_speaker_labels_arguments(
334 arguments: &Value,
335) -> Result<Vec<LocalSpeakerLabel>, ProtocolError> {
336 let object = arguments.as_object().ok_or_else(|| {
337 ProtocolError::InvalidSpeakerLabelsArguments("arguments is not an object".into())
338 })?;
339 require_exact_keys(object, &["speakers"])
340 .map_err(ProtocolError::InvalidSpeakerLabelsArguments)?;
341 let values = object
342 .get("speakers")
343 .and_then(Value::as_array)
344 .ok_or_else(|| {
345 ProtocolError::InvalidSpeakerLabelsArguments("speakers is not an array".into())
346 })?;
347 let mut labels = Vec::with_capacity(values.len());
348 let mut unique = BTreeSet::new();
349 for (index, value) in values.iter().enumerate() {
350 let raw = value.as_str().ok_or_else(|| {
351 ProtocolError::InvalidSpeakerLabelsArguments(format!("speaker {index} is not a string"))
352 })?;
353 let label = parse_canonical_label(raw).map_err(|message| {
354 ProtocolError::InvalidSpeakerLabelsArguments(format!("speaker {index}: {message}"))
355 })?;
356 if !unique.insert(label) {
357 return Err(ProtocolError::DuplicateSpeakerLabel(label));
358 }
359 labels.push(label);
360 }
361 Ok(labels)
362}
363
364pub fn record_speaker_analysis_tool() -> ToolDefinition {
365 let mut feature_properties = Map::new();
366 for name in FEATURE_NAMES {
367 feature_properties.insert(name.into(), feature_property(name));
368 }
369 ToolDefinition {
370 name: RECORD_SPEAKER_ANALYSIS_TOOL_NAME,
371 description: RECORD_SPEAKER_ANALYSIS_TOOL_DESCRIPTION,
372 input_schema: json!({
373 "type": "object",
374 "additionalProperties": false,
375 "required": ["transcript", "speakers"],
376 "properties": {
377 "transcript": {
378 "type": "string"
379 },
380 "speakers": {
381 "type": "array",
382 "items": {
383 "type": "object",
384 "additionalProperties": false,
385 "required": [
386 "speaker",
387 "language",
388 "features",
389 "features_usable_for_training"
390 ],
391 "properties": {
392 "speaker": {
393 "type": "string",
394 "pattern": "^Speaker [1-9][0-9]*$"
395 },
396 "language": {
397 "type": "string"
398 },
399 "features": {
400 "type": "object",
401 "additionalProperties": false,
402 "required": FEATURE_NAMES,
403 "properties": feature_properties
404 },
405 "features_usable_for_training": {
406 "type": "boolean"
407 }
408 }
409 }
410 }
411 }
412 }),
413 }
414}
415
416pub fn decode_record_speaker_analysis_arguments(
417 arguments: &Value,
418) -> Result<StructuredAnalysis, ProtocolError> {
419 validate_final_shape(arguments)?;
420 let analysis: StructuredAnalysis = serde_json::from_value(arguments.clone())
421 .map_err(|error| ProtocolError::InvalidFinalArguments(error.to_string()))?;
422 analysis
423 .validate()
424 .map_err(ProtocolError::InvalidStructuredAnalysis)?;
425 Ok(analysis)
426}
427
428fn require_nonblank(value: &str, field: &'static str) -> Result<(), ProtocolError> {
429 if value.trim().is_empty() {
430 return Err(ProtocolError::Blank(field));
431 }
432 Ok(())
433}
434
435fn feature_prompt_parts(packet: FeaturePacket) -> (&'static str, &'static str) {
436 packet
437 .prompt()
438 .split_once("{{TRANSCRIPT}}")
439 .expect("frozen feature prompt contains transcript placeholder")
440}
441
442fn parse_canonical_label(value: &str) -> Result<LocalSpeakerLabel, String> {
443 let label = value
444 .parse::<LocalSpeakerLabel>()
445 .map_err(|error| error.to_string())?;
446 if label.to_string() != value {
447 return Err(format!("noncanonical speaker label: {value}"));
448 }
449 Ok(label)
450}
451
452fn require_exact_keys(object: &Map<String, Value>, expected: &[&str]) -> Result<(), String> {
453 if object.len() != expected.len() || expected.iter().any(|key| !object.contains_key(*key)) {
454 let found = object.keys().cloned().collect::<Vec<_>>().join(", ");
455 return Err(format!(
456 "object keys must be exactly [{}], found [{found}]",
457 expected.join(", ")
458 ));
459 }
460 Ok(())
461}
462
463fn feature_property(name: &str) -> Value {
464 match name {
465 "dominant_rhotic_realization" | "dominant_lateral_realization" => {
466 json!({ "type": ["string", "null"] })
467 }
468 "vocal_gender_presentation" => json!({
469 "type": ["string", "null"],
470 "enum": [
471 "strongly_feminine",
472 "feminine",
473 "androgynous",
474 "masculine",
475 "strongly_masculine",
476 null
477 ]
478 }),
479 _ => json!({ "type": ["number", "null"] }),
480 }
481}
482
483fn validate_final_shape(arguments: &Value) -> Result<(), ProtocolError> {
484 let object = arguments
485 .as_object()
486 .ok_or_else(|| ProtocolError::InvalidFinalArguments("arguments is not an object".into()))?;
487 require_exact_keys(object, &["transcript", "speakers"])
488 .map_err(ProtocolError::InvalidFinalArguments)?;
489 let speakers = object
490 .get("speakers")
491 .and_then(Value::as_array)
492 .ok_or_else(|| ProtocolError::InvalidFinalArguments("speakers is not an array".into()))?;
493 for (speaker_index, speaker) in speakers.iter().enumerate() {
494 let speaker_object = speaker.as_object().ok_or_else(|| {
495 ProtocolError::InvalidFinalArguments(format!(
496 "speaker {speaker_index} is not an object"
497 ))
498 })?;
499 require_exact_keys(
500 speaker_object,
501 &[
502 "speaker",
503 "language",
504 "features",
505 "features_usable_for_training",
506 ],
507 )
508 .map_err(|message| {
509 ProtocolError::InvalidFinalArguments(format!("speaker {speaker_index}: {message}"))
510 })?;
511 let raw_label = speaker_object
512 .get("speaker")
513 .and_then(Value::as_str)
514 .ok_or_else(|| {
515 ProtocolError::InvalidFinalArguments(format!(
516 "speaker {speaker_index} label is not a string"
517 ))
518 })?;
519 parse_canonical_label(raw_label).map_err(|message| {
520 ProtocolError::InvalidFinalArguments(format!("speaker {speaker_index}: {message}"))
521 })?;
522 let features = speaker_object
523 .get("features")
524 .and_then(Value::as_object)
525 .ok_or_else(|| {
526 ProtocolError::InvalidFinalArguments(format!(
527 "speaker {speaker_index} features is not an object"
528 ))
529 })?;
530 require_exact_keys(features, &FEATURE_NAMES).map_err(|message| {
531 ProtocolError::InvalidFinalArguments(format!(
532 "speaker {speaker_index} features: {message}"
533 ))
534 })?;
535 }
536 Ok(())
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use std::time::Instant;
543
544 fn label(number: u32) -> LocalSpeakerLabel {
545 LocalSpeakerLabel::new(number).unwrap()
546 }
547
548 fn candidate(parts: Vec<Value>) -> Value {
549 json!({ "content": { "parts": parts } })
550 }
551
552 fn null_features() -> Map<String, Value> {
553 FEATURE_NAMES
554 .into_iter()
555 .map(|name| (name.into(), Value::Null))
556 .collect()
557 }
558
559 fn final_arguments() -> Value {
560 json!({
561 "transcript": "[high] Speaker 1: exact words",
562 "speakers": [{
563 "speaker": "Speaker 1",
564 "language": "English",
565 "features": null_features(),
566 "features_usable_for_training": true
567 }]
568 })
569 }
570
571 #[test]
572 fn revisions_and_frozen_prompts_cover_the_workflow() {
573 assert_eq!(
574 GEMINI_FEATURE_PROMPT_REVISIONS,
575 [
576 "speaker-v3-gemini-feature-1-r2",
577 "speaker-v3-gemini-feature-2-r2",
578 "speaker-v3-gemini-feature-3-r2",
579 ]
580 );
581 assert_eq!(
582 TERRA_SPEAKER_LABELS_PROMPT_REVISION,
583 "speaker-v3-terra-labels-r1"
584 );
585 assert!(GEMINI_TRANSCRIPT_PROMPT.contains("[high] Speaker N:"));
586 assert!(GPT_STRUCTURING_PROMPT.contains("record_speaker_analysis"));
587 assert!(TERRA_SPEAKER_LABELS_PROMPT.contains("record_speaker_labels"));
588 for (packet, names) in FEATURE_PACKETS.into_iter().zip([
589 &FEATURE_NAMES[..8],
590 &FEATURE_NAMES[8..16],
591 &FEATURE_NAMES[16..],
592 ]) {
593 assert!(packet.prompt().contains("{{TRANSCRIPT}}"));
594 assert!(packet.prompt().contains("{{TARGET_SPEAKER}}"));
595 for name in names {
596 assert!(packet.prompt().contains(name));
597 }
598 }
599 }
600
601 #[test]
602 fn gemini_requests_keep_audio_prefix_and_target_last() {
603 let audio = b"OggS bytes";
604 let transcript_request = gemini_transcript_request(audio);
605 assert_eq!(
606 transcript_request[0],
607 GeminiRequestPart::Audio {
608 media_type: "audio/ogg",
609 bytes: audio,
610 }
611 );
612 assert_eq!(
613 transcript_request[1],
614 GeminiRequestPart::Text(GEMINI_TRANSCRIPT_PROMPT.into())
615 );
616
617 let transcript = "literal {{TARGET_SPEAKER}}\ntranscript";
618 let prefix = gemini_feature_cached_prefix(audio, transcript);
619 assert_eq!(
620 prefix[0],
621 GeminiRequestPart::Audio {
622 media_type: "audio/ogg",
623 bytes: audio,
624 }
625 );
626 let GeminiRequestPart::Text(prefix_text) = &prefix[1] else {
627 panic!()
628 };
629 let (shared, _) = feature_prompt_parts(FeaturePacket::One);
630 assert_eq!(prefix_text, &format!("{shared}{transcript}"));
631
632 for packet in FEATURE_PACKETS {
633 let suffix = gemini_feature_suffix(packet, label(12));
634 assert!(!suffix.contains("{{TARGET_SPEAKER}}"));
635 assert!(suffix.ends_with("Speaker 12\n"));
636 assert_eq!(
637 packet.revision(),
638 GEMINI_FEATURE_PROMPT_REVISIONS[packet.index()]
639 );
640 assert_eq!(
641 feature_prompt_parts(packet).0,
642 feature_prompt_parts(FeaturePacket::One).0
643 );
644 }
645 }
646
647 #[test]
648 fn gemini_extraction_preserves_one_textual_candidate() {
649 let response = json!({
650 "candidates": [
651 candidate(vec![
652 json!({"text": "a\n"}),
653 json!({"inlineData": {}}),
654 json!({"text": " b"})
655 ]),
656 candidate(vec![json!({"text": " "})])
657 ]
658 });
659 assert_eq!(extract_gemini_text(&response).unwrap(), "a\n b");
660
661 let ambiguous = json!({
662 "candidates": [
663 candidate(vec![json!({"text": "one"})]),
664 candidate(vec![json!({"text": "two"})])
665 ]
666 });
667 assert_eq!(
668 extract_gemini_text(&ambiguous),
669 Err(ProtocolError::GeminiTextCandidateCount(2))
670 );
671 assert!(matches!(
672 extract_gemini_text(&json!({})),
673 Err(ProtocolError::InvalidGeminiResponse(_))
674 ));
675 }
676
677 #[test]
678 fn label_tool_and_decode_are_strict_ordered_and_unbounded() {
679 let tool = record_speaker_labels_tool();
680 assert_eq!(tool.name, "record_speaker_labels");
681 assert_eq!(tool.input_schema["additionalProperties"], false);
682 assert!(
683 tool.input_schema["properties"]["speakers"]
684 .get("maxItems")
685 .is_none()
686 );
687
688 let arguments = json!({
689 "speakers": (1..=1000)
690 .map(|number| format!("Speaker {number}"))
691 .collect::<Vec<_>>()
692 });
693 let decoded = decode_record_speaker_labels_arguments(&arguments).unwrap();
694 assert_eq!(decoded.len(), 1000);
695 assert_eq!(decoded[0], label(1));
696 assert_eq!(decoded[999], label(1000));
697
698 assert!(matches!(
699 decode_record_speaker_labels_arguments(
700 &json!({"speakers": ["Speaker 1", "Speaker 1"]})
701 ),
702 Err(ProtocolError::DuplicateSpeakerLabel(_))
703 ));
704 for invalid in ["Unknown", "Speaker 0", "Speaker 01", "speaker 1"] {
705 assert!(
706 decode_record_speaker_labels_arguments(&json!({"speakers": [invalid]})).is_err()
707 );
708 }
709 assert!(
710 decode_record_speaker_labels_arguments(&json!({"speakers": [], "extra": true}))
711 .is_err()
712 );
713
714 let input = TerraSpeakerLabelsInput::new("x\n\"Input:\\n\" {{raw}}".into()).unwrap();
715 let rendered = input.render();
716 let serialized = rendered
717 .strip_prefix(TERRA_SPEAKER_LABELS_PROMPT)
718 .unwrap()
719 .strip_prefix("Input:\n")
720 .unwrap();
721 let decoded_input: Value = serde_json::from_str(serialized).unwrap();
722 assert_eq!(decoded_input["transcript"], input.transcript());
723 }
724
725 #[test]
726 fn evidence_and_final_input_preserve_adversarial_text_and_order() {
727 let first = SpeakerFeatureEvidence::new(
728 label(2),
729 "one\n\"x\"".into(),
730 "two {{raw}}".into(),
731 "three Input:\n".into(),
732 )
733 .unwrap();
734 assert_eq!(
735 first.packets(),
736 ["one\n\"x\"", "two {{raw}}", "three Input:\n"]
737 );
738 let second =
739 SpeakerFeatureEvidence::new(label(1), "four".into(), "five".into(), "six".into())
740 .unwrap();
741 let input = TerraFinalInput::new(
742 "transcript\n\"quoted\"".into(),
743 vec![first.clone(), second.clone()],
744 )
745 .unwrap();
746 assert_eq!(input.speakers()[0].speaker(), label(2));
747 assert_eq!(input.speakers()[1].speaker(), label(1));
748 let rendered = input.render();
749 let serialized = rendered
750 .strip_prefix(GPT_STRUCTURING_PROMPT)
751 .unwrap()
752 .strip_prefix("Input:\n")
753 .unwrap();
754 let decoded: Value = serde_json::from_str(serialized).unwrap();
755 assert_eq!(decoded["transcript"], input.transcript());
756 assert_eq!(decoded["speakers"][0]["speaker"], "Speaker 2");
757 assert_eq!(
758 decoded["speakers"][0]["feature_packets"][0],
759 first.packet(FeaturePacket::One)
760 );
761 assert_eq!(
762 decoded["speakers"][1]["feature_packets"][2],
763 second.packet(FeaturePacket::Three)
764 );
765 assert!(TerraFinalInput::new("x".into(), vec![first.clone(), first]).is_err());
766 }
767
768 #[test]
769 fn final_tool_schema_is_closed_complete_and_nullable() {
770 let tool = record_speaker_analysis_tool();
771 assert_eq!(tool.name, "record_speaker_analysis");
772 assert_eq!(tool.input_schema["additionalProperties"], false);
773 let speaker = &tool.input_schema["properties"]["speakers"]["items"];
774 assert_eq!(speaker["additionalProperties"], false);
775 let features = &speaker["properties"]["features"];
776 assert_eq!(features["additionalProperties"], false);
777 assert_eq!(features["required"].as_array().unwrap().len(), 24);
778 assert_eq!(features["properties"].as_object().unwrap().len(), 24);
779 for name in FEATURE_NAMES {
780 assert!(features["properties"][name].to_string().contains("null"));
781 }
782 }
783
784 #[test]
785 fn final_decode_requires_complete_closed_valid_analysis() {
786 let arguments = final_arguments();
787 let analysis = decode_record_speaker_analysis_arguments(&arguments).unwrap();
788 assert_eq!(analysis.transcript, "[high] Speaker 1: exact words");
789 assert_eq!(analysis.speakers.len(), 1);
790 assert!(analysis.speakers[0].features_usable_for_training);
791
792 let mut extra = arguments.clone();
793 extra
794 .as_object_mut()
795 .unwrap()
796 .insert("extra".into(), json!(true));
797 assert!(decode_record_speaker_analysis_arguments(&extra).is_err());
798
799 let mut missing = arguments.clone();
800 missing["speakers"][0]["features"]
801 .as_object_mut()
802 .unwrap()
803 .remove("median_f0_hz");
804 assert!(decode_record_speaker_analysis_arguments(&missing).is_err());
805
806 let mut nested_extra = arguments.clone();
807 nested_extra["speakers"][0]["features"]
808 .as_object_mut()
809 .unwrap()
810 .insert("extra".into(), Value::Null);
811 assert!(decode_record_speaker_analysis_arguments(&nested_extra).is_err());
812
813 let mut noncanonical = arguments;
814 noncanonical["speakers"][0]["speaker"] = json!("Speaker 01");
815 assert!(decode_record_speaker_analysis_arguments(&noncanonical).is_err());
816 }
817
818 #[test]
819 fn reference_scale_canary_completes_local_work() {
820 let started = Instant::now();
821 let text = "x".repeat(1_048_576);
822 let response = json!({
823 "candidates": [
824 candidate(vec![json!({"text": text})])
825 ]
826 });
827 assert_eq!(extract_gemini_text(&response).unwrap().len(), 1_048_576);
828
829 let arguments = json!({
830 "speakers": (1..=1000)
831 .map(|number| format!("Speaker {number}"))
832 .collect::<Vec<_>>()
833 });
834 assert_eq!(
835 decode_record_speaker_labels_arguments(&arguments)
836 .unwrap()
837 .len(),
838 1000
839 );
840
841 let speakers = (1..=1000)
842 .map(|number| {
843 SpeakerFeatureEvidence::new(
844 label(number),
845 "a".repeat(1024),
846 "b".repeat(1024),
847 "c".repeat(1024),
848 )
849 .unwrap()
850 })
851 .collect();
852 let input = TerraFinalInput::new("x".repeat(1_048_576), speakers).unwrap();
853 assert!(input.render().len() > 4_000_000);
854 assert!(started.elapsed().as_secs() < 10);
855 }
856}