1pub use kcode_speaker_types::{FeatureVector, Key};
2
3use kcode_speaker_types::FEATURE_COUNT;
4use serde::Deserialize;
5use std::fmt;
6use std::sync::OnceLock;
7
8const SINGLE_SEGMENT_LIMIT_MS: u64 = 240_000;
9const MAX_SEGMENT_MS: u128 = 239_999;
10const OVERLAP_MS: u128 = 5_000;
11const UNIQUE_CAPACITY_MS: u128 = MAX_SEGMENT_MS - OVERLAP_MS;
12
13pub struct SegmentPlan {
14 pub policy: Key,
15 pub source_duration_ms: u64,
16 pub segments: Vec<PlannedSegment>,
17}
18
19pub struct PlannedSegment {
20 pub ordinal: u16,
21 pub start_ms: u64,
22 pub end_ms: u64,
23}
24
25pub struct ExtractionContract {
26 pub schema_key: Key,
27 pub prompt: &'static str,
28 pub response_mime: &'static str,
29}
30
31pub enum ExtractionOutcome {
32 Scored(ScoredClip),
33 Unscorable { reason: String },
34}
35
36pub struct ScoredClip {
37 pub recording_quality: u8,
38 pub speakers: Vec<SpeakerSample>,
39}
40
41pub struct SpeakerSample {
42 pub speaker_ordinal: u16,
43 pub primary_language: Key,
44 pub closest_dialect: String,
45 pub usable_speech_ms: u32,
46 pub features: FeatureVector,
47}
48
49#[derive(Debug, PartialEq, Eq)]
50pub enum ExtractError {
51 ZeroDuration,
52 SegmentCountExceedsU16 { required: u128 },
53 InvalidJson(String),
54 InvalidResponse(&'static str),
55}
56
57impl fmt::Display for ExtractError {
58 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Self::ZeroDuration => formatter.write_str("source duration must be positive"),
61 Self::SegmentCountExceedsU16 { required } => {
62 write!(formatter, "required segment count {required} exceeds u16")
63 }
64 Self::InvalidJson(message) => write!(formatter, "invalid response JSON: {message}"),
65 Self::InvalidResponse(message) => write!(formatter, "invalid response: {message}"),
66 }
67 }
68}
69
70impl std::error::Error for ExtractError {}
71
72pub fn plan_segments(duration_ms: u64) -> Result<SegmentPlan, ExtractError> {
73 if duration_ms == 0 {
74 return Err(ExtractError::ZeroDuration);
75 }
76
77 if duration_ms < SINGLE_SEGMENT_LIMIT_MS {
78 return Ok(SegmentPlan {
79 policy: segment_policy(),
80 source_duration_ms: duration_ms,
81 segments: vec![PlannedSegment {
82 ordinal: 0,
83 start_ms: 0,
84 end_ms: duration_ms,
85 }],
86 });
87 }
88
89 let duration = u128::from(duration_ms);
90 let unique_duration = duration - OVERLAP_MS;
91 let required = unique_duration.div_ceil(UNIQUE_CAPACITY_MS);
92 if required > u128::from(u16::MAX) {
93 return Err(ExtractError::SegmentCountExceedsU16 { required });
94 }
95
96 let count = usize::try_from(required).expect("u16-bounded count fits usize");
97 let total_segment_duration = duration + (required - 1) * OVERLAP_MS;
98 let short_length = total_segment_duration / required;
99 let longer_count = total_segment_duration % required;
100 let mut segments = Vec::with_capacity(count);
101 let mut start = 0_u128;
102
103 for index in 0..count {
104 let index_u128 = u128::try_from(index).expect("u16-bounded index fits u128");
105 let length = short_length + if index_u128 < longer_count { 1 } else { 0 };
106 let end = start + length;
107 segments.push(PlannedSegment {
108 ordinal: u16::try_from(index).expect("count is bounded by u16::MAX"),
109 start_ms: u64::try_from(start).expect("segment start is within source duration"),
110 end_ms: u64::try_from(end).expect("segment end is within source duration"),
111 });
112 start = end - OVERLAP_MS;
113 }
114
115 debug_assert_eq!(
116 segments.last().map(|segment| segment.end_ms),
117 Some(duration_ms)
118 );
119 debug_assert!(
120 segments
121 .iter()
122 .all(|segment| u128::from(segment.end_ms - segment.start_ms) <= MAX_SEGMENT_MS)
123 );
124
125 Ok(SegmentPlan {
126 policy: segment_policy(),
127 source_duration_ms: duration_ms,
128 segments,
129 })
130}
131
132pub fn contract() -> &'static ExtractionContract {
133 static CONTRACT: OnceLock<ExtractionContract> = OnceLock::new();
134 CONTRACT.get_or_init(|| ExtractionContract {
135 schema_key: frozen_key("gemini-speaker-35/1"),
136 prompt: include_str!("../prompt-v1.txt"),
137 response_mime: "application/json",
138 })
139}
140
141pub fn parse(response: &str, clip_duration_ms: u64) -> Result<ExtractionOutcome, ExtractError> {
142 let response: WireOutcome = serde_json::from_str(response)
143 .map_err(|error| ExtractError::InvalidJson(error.to_string()))?;
144
145 match response {
146 WireOutcome::Unscorable { reason } => {
147 if reason.is_empty() || reason.len() > 240 {
148 return Err(ExtractError::InvalidResponse(
149 "unscorable reason must contain 1..=240 bytes",
150 ));
151 }
152 Ok(ExtractionOutcome::Unscorable { reason })
153 }
154 WireOutcome::Scored {
155 recording_quality,
156 speakers,
157 } => {
158 if recording_quality > 100 {
159 return Err(ExtractError::InvalidResponse(
160 "recording quality must be in 0..=100",
161 ));
162 }
163 if speakers.is_empty() {
164 return Err(ExtractError::InvalidResponse(
165 "scored response must contain at least one speaker",
166 ));
167 }
168
169 let mut validated = Vec::with_capacity(speakers.len());
170 for (index, speaker) in speakers.into_iter().enumerate() {
171 if usize::from(speaker.speaker_ordinal) != index {
172 return Err(ExtractError::InvalidResponse(
173 "speaker ordinals must be unique and contiguous from zero",
174 ));
175 }
176 if speaker.closest_dialect.is_empty() || speaker.closest_dialect.len() > 128 {
177 return Err(ExtractError::InvalidResponse(
178 "closest dialect must contain 1..=128 bytes",
179 ));
180 }
181 if speaker.usable_speech_ms == 0
182 || u64::from(speaker.usable_speech_ms) > clip_duration_ms
183 {
184 return Err(ExtractError::InvalidResponse(
185 "usable speech must be positive and no greater than clip duration",
186 ));
187 }
188
189 let primary_language = Key::parse(&speaker.primary_language).map_err(|_| {
190 ExtractError::InvalidResponse("primary language is not a valid shared key")
191 })?;
192 let feature_values: [u8; FEATURE_COUNT] =
193 speaker.features.try_into().map_err(|_| {
194 ExtractError::InvalidResponse("features must contain exactly 35 integers")
195 })?;
196 let features = FeatureVector::new(feature_values).map_err(|_| {
197 ExtractError::InvalidResponse("feature value is outside shared validation")
198 })?;
199
200 validated.push(SpeakerSample {
201 speaker_ordinal: speaker.speaker_ordinal,
202 primary_language,
203 closest_dialect: speaker.closest_dialect,
204 usable_speech_ms: speaker.usable_speech_ms,
205 features,
206 });
207 }
208
209 Ok(ExtractionOutcome::Scored(ScoredClip {
210 recording_quality,
211 speakers: validated,
212 }))
213 }
214 }
215}
216
217fn segment_policy() -> Key {
218 frozen_key("speaker-segments/1")
219}
220
221fn frozen_key(value: &str) -> Key {
222 match Key::parse(value) {
223 Ok(key) => key,
224 Err(_) => panic!("invalid frozen key"),
225 }
226}
227
228#[derive(Deserialize)]
229#[serde(tag = "status", deny_unknown_fields)]
230enum WireOutcome {
231 #[serde(rename = "scored")]
232 Scored {
233 #[serde(rename = "recordingQuality")]
234 recording_quality: u8,
235 speakers: Vec<WireSpeaker>,
236 },
237 #[serde(rename = "unscorable")]
238 Unscorable { reason: String },
239}
240
241#[derive(Deserialize)]
242#[serde(deny_unknown_fields)]
243struct WireSpeaker {
244 #[serde(rename = "speakerOrdinal")]
245 speaker_ordinal: u16,
246 #[serde(rename = "primaryLanguage")]
247 primary_language: String,
248 #[serde(rename = "closestDialect")]
249 closest_dialect: String,
250 #[serde(rename = "usableSpeechMs")]
251 usable_speech_ms: u32,
252 features: Vec<u8>,
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use serde_json::{Value, json};
259
260 fn base_features() -> Vec<u8> {
261 let mut values = vec![50; FEATURE_COUNT];
262 values[11] = 60;
263 values
264 }
265
266 fn speaker(ordinal: u16, usable_speech_ms: u32) -> Value {
267 json!({
268 "speakerOrdinal": ordinal,
269 "primaryLanguage": "eng",
270 "closestDialect": "General American English",
271 "usableSpeechMs": usable_speech_ms,
272 "features": base_features(),
273 })
274 }
275
276 fn scored(speakers: Vec<Value>) -> Value {
277 json!({
278 "status": "scored",
279 "recordingQuality": 82,
280 "speakers": speakers,
281 })
282 }
283
284 fn assert_rejected(value: Value, clip_duration_ms: u64) {
285 assert!(parse(&value.to_string(), clip_duration_ms).is_err());
286 }
287
288 fn assert_plan_invariants(duration_ms: u64) -> SegmentPlan {
289 let plan = plan_segments(duration_ms).expect("plan should succeed");
290 assert_eq!(plan.source_duration_ms, duration_ms);
291 assert_eq!(plan.policy.as_ref(), "speaker-segments/1");
292 assert_eq!(plan.segments.first().expect("segment").start_ms, 0);
293 assert_eq!(plan.segments.last().expect("segment").end_ms, duration_ms);
294
295 let lengths: Vec<u64> = plan
296 .segments
297 .iter()
298 .enumerate()
299 .map(|(index, segment)| {
300 assert_eq!(usize::from(segment.ordinal), index);
301 assert!(segment.start_ms < segment.end_ms);
302 let length = segment.end_ms - segment.start_ms;
303 assert!(length <= u64::try_from(MAX_SEGMENT_MS).unwrap());
304 length
305 })
306 .collect();
307
308 for (index, pair) in plan.segments.windows(2).enumerate() {
309 assert_eq!(pair[0].end_ms - pair[1].start_ms, 5_000);
310 assert_eq!(usize::from(pair[1].ordinal), index + 1);
311 }
312
313 let minimum = *lengths.iter().min().expect("length");
314 let maximum = *lengths.iter().max().expect("length");
315 assert!(maximum - minimum <= 1);
316
317 if plan.segments.len() > 1 {
318 let fewer = u128::try_from(plan.segments.len() - 1).unwrap();
319 let fewer_capacity = fewer * UNIQUE_CAPACITY_MS + OVERLAP_MS;
320 assert!(u128::from(duration_ms) > fewer_capacity);
321 }
322
323 plan
324 }
325
326 #[test]
327 fn rejects_zero_duration() {
328 assert_eq!(plan_segments(0).err(), Some(ExtractError::ZeroDuration));
329 }
330
331 #[test]
332 fn plans_below_equal_and_above_four_minutes() {
333 let below = assert_plan_invariants(239_999);
334 assert_eq!(below.segments.len(), 1);
335 assert_eq!(below.segments[0].end_ms, 239_999);
336
337 let equal = assert_plan_invariants(240_000);
338 assert_eq!(equal.segments.len(), 2);
339
340 let above = assert_plan_invariants(240_001);
341 assert_eq!(above.segments.len(), 2);
342 }
343
344 #[test]
345 fn plans_long_and_remainder_sources() {
346 for duration in [474_998, 474_999, 475_000, 1_000_003, 9_876_543] {
347 assert_plan_invariants(duration);
348 }
349 }
350
351 #[test]
352 fn plans_exact_capacity_with_minimum_count() {
353 let duration = 7 * 234_999 + 5_000;
354 let plan = assert_plan_invariants(duration);
355 assert_eq!(plan.segments.len(), 7);
356 assert!(
357 plan.segments
358 .iter()
359 .all(|segment| segment.end_ms - segment.start_ms == 239_999)
360 );
361 }
362
363 #[test]
364 fn rejects_count_that_does_not_fit_u16() {
365 let duration = u64::from(u16::MAX) * 234_999 + 5_001;
366 assert!(matches!(
367 plan_segments(duration),
368 Err(ExtractError::SegmentCountExceedsU16 { required: 65_536 })
369 ));
370 }
371
372 #[test]
373 fn maximum_supported_count_is_valid() {
374 let duration = u64::from(u16::MAX) * 234_999 + 5_000;
375 let plan = assert_plan_invariants(duration);
376 assert_eq!(plan.segments.len(), usize::from(u16::MAX));
377 }
378
379 #[test]
380 fn planning_is_deterministic() {
381 let first = plan_segments(1_234_567).unwrap();
382 let second = plan_segments(1_234_567).unwrap();
383 assert_eq!(first.policy.as_ref(), second.policy.as_ref());
384 assert_eq!(first.source_duration_ms, second.source_duration_ms);
385 assert_eq!(first.segments.len(), second.segments.len());
386 for (left, right) in first.segments.iter().zip(&second.segments) {
387 assert_eq!(left.ordinal, right.ordinal);
388 assert_eq!(left.start_ms, right.start_ms);
389 assert_eq!(left.end_ms, right.end_ms);
390 }
391 }
392
393 #[test]
394 fn contract_has_frozen_identity_and_prompt() {
395 let value = contract();
396 assert_eq!(value.schema_key.as_ref(), "gemini-speaker-35/1");
397 assert_eq!(value.response_mime, "application/json");
398 assert!(
399 value
400 .prompt
401 .starts_with("Analyze the attached audio directly.")
402 );
403 assert!(value.prompt.contains("0 filler_preference"));
404 assert!(value.prompt.contains("11 perceived_age"));
405 assert!(value.prompt.contains("34 aspiration_intensity"));
406 assert!(
407 value
408 .prompt
409 .trim_end()
410 .ends_with("Return the JSON object only.")
411 );
412 assert!(std::ptr::eq(value, contract()));
413 }
414
415 #[test]
416 fn accepts_reordered_scored_fields() {
417 let response = format!(
418 r#"{{"speakers":[{}],"recordingQuality":0,"status":"scored"}}"#,
419 speaker(0, 1_000)
420 );
421 let outcome = parse(&response, 1_000).expect("reordered keys are valid");
422 let ExtractionOutcome::Scored(clip) = outcome else {
423 panic!("expected scored response");
424 };
425 assert_eq!(clip.recording_quality, 0);
426 assert_eq!(clip.speakers.len(), 1);
427 }
428
429 #[test]
430 fn accepts_multi_speaker_scored_with_overlapping_durations() {
431 let mut second = speaker(1, 1_000);
432 second["primaryLanguage"] = json!("spa");
433 second["closestDialect"] = json!("Mexican Spanish");
434 let response = scored(vec![speaker(0, 1_000), second]).to_string();
435 let outcome = parse(&response, 1_000).expect("overlap is valid");
436 let ExtractionOutcome::Scored(clip) = outcome else {
437 panic!("expected scored response");
438 };
439 assert_eq!(clip.recording_quality, 82);
440 assert_eq!(clip.speakers.len(), 2);
441 assert_eq!(clip.speakers[0].speaker_ordinal, 0);
442 assert_eq!(clip.speakers[1].speaker_ordinal, 1);
443 assert_eq!(clip.speakers[1].primary_language.as_ref(), "spa");
444 }
445
446 #[test]
447 fn accepts_whole_clip_unscorable() {
448 let outcome = parse(
449 r#"{"reason":"Insufficient substantive speech.","status":"unscorable"}"#,
450 0,
451 )
452 .expect("whole-clip abstention is valid");
453 let ExtractionOutcome::Unscorable { reason } = outcome else {
454 panic!("expected unscorable response");
455 };
456 assert_eq!(reason, "Insufficient substantive speech.");
457 }
458
459 #[test]
460 fn accepts_quality_and_feature_boundaries() {
461 for quality in [0, 100] {
462 for maximum in [false, true] {
463 let mut values = if maximum {
464 vec![100; FEATURE_COUNT]
465 } else {
466 vec![0; FEATURE_COUNT]
467 };
468 if maximum {
469 values[11] = 120;
470 }
471 let mut value = scored(vec![speaker(0, 1)]);
472 value["recordingQuality"] = json!(quality);
473 value["speakers"][0]["features"] = json!(values);
474 let outcome = parse(&value.to_string(), 1).expect("boundary is valid");
475 let ExtractionOutcome::Scored(clip) = outcome else {
476 panic!("expected scored response");
477 };
478 let actual: &[u8; FEATURE_COUNT] = clip.speakers[0].features.as_ref();
479 assert_eq!(actual[11], if maximum { 120 } else { 0 });
480 }
481 }
482 }
483
484 #[test]
485 fn rejects_each_feature_above_its_upper_boundary() {
486 for index in 0..FEATURE_COUNT {
487 let mut values = base_features();
488 values[index] = if index == 11 { 121 } else { 101 };
489 let mut value = scored(vec![speaker(0, 1)]);
490 value["speakers"][0]["features"] = json!(values);
491 assert_rejected(value, 1);
492 }
493 }
494
495 #[test]
496 fn rejects_wrong_feature_counts() {
497 for count in [0, 1, FEATURE_COUNT - 1, FEATURE_COUNT + 1] {
498 let mut value = scored(vec![speaker(0, 1)]);
499 value["speakers"][0]["features"] = json!(vec![50; count]);
500 assert_rejected(value, 1);
501 }
502 }
503
504 #[test]
505 fn rejects_quality_and_duration_ranges() {
506 let mut quality = scored(vec![speaker(0, 1)]);
507 quality["recordingQuality"] = json!(101);
508 assert_rejected(quality, 1);
509
510 let mut zero = scored(vec![speaker(0, 1)]);
511 zero["speakers"][0]["usableSpeechMs"] = json!(0);
512 assert_rejected(zero, 1);
513
514 assert_rejected(scored(vec![speaker(0, 2)]), 1);
515 assert_rejected(scored(vec![speaker(0, 1)]), 0);
516 }
517
518 #[test]
519 fn validates_dialect_byte_lengths() {
520 let mut accepted = scored(vec![speaker(0, 1)]);
521 accepted["speakers"][0]["closestDialect"] = json!("é".repeat(64));
522 assert!(parse(&accepted.to_string(), 1).is_ok());
523
524 for dialect in [
525 "".to_owned(),
526 "a".repeat(129),
527 format!("{}a", "é".repeat(64)),
528 ] {
529 let mut value = scored(vec![speaker(0, 1)]);
530 value["speakers"][0]["closestDialect"] = json!(dialect);
531 assert_rejected(value, 1);
532 }
533 }
534
535 #[test]
536 fn validates_reason_byte_lengths() {
537 for reason in ["a".to_owned(), "a".repeat(240), "é".repeat(120)] {
538 let value = json!({"status": "unscorable", "reason": reason});
539 assert!(parse(&value.to_string(), 0).is_ok());
540 }
541
542 for reason in [
543 "".to_owned(),
544 "a".repeat(241),
545 format!("{}a", "é".repeat(120)),
546 ] {
547 let value = json!({"status": "unscorable", "reason": reason});
548 assert_rejected(value, 0);
549 }
550 }
551
552 #[test]
553 fn rejects_invalid_primary_language_keys() {
554 let languages = ["".to_owned(), "en g".to_owned(), "a".repeat(129)];
555 for language in languages {
556 let mut value = scored(vec![speaker(0, 1)]);
557 value["speakers"][0]["primaryLanguage"] = json!(language);
558 assert_rejected(value, 1);
559 }
560 }
561
562 #[test]
563 fn rejects_empty_and_noncontiguous_speaker_sets() {
564 assert_rejected(scored(vec![]), 1);
565 assert_rejected(scored(vec![speaker(1, 1)]), 1);
566 assert_rejected(scored(vec![speaker(0, 1), speaker(0, 1)]), 1);
567 assert_rejected(scored(vec![speaker(0, 1), speaker(2, 1)]), 1);
568 }
569
570 #[test]
571 fn rejects_malformed_and_non_object_json() {
572 for response in [
573 "",
574 "{",
575 "null",
576 "[]",
577 "true",
578 "42",
579 r#""scored""#,
580 r#"{"status":"unknown"}"#,
581 r#"{"status":null}"#,
582 r#"{"status":"unscorable","reason":"x"} trailing"#,
583 r#"{"status":"scored","recordingQuality":NaN,"speakers":[]}"#,
584 r#"{"status":"scored","recordingQuality":Infinity,"speakers":[]}"#,
585 ] {
586 assert!(parse(response, 1).is_err(), "{response}");
587 }
588 }
589
590 #[test]
591 fn rejects_missing_unknown_extra_and_mixed_top_level_fields() {
592 for value in [
593 json!({}),
594 json!({"status": "scored"}),
595 json!({"status": "scored", "recordingQuality": 50}),
596 json!({"status": "scored", "speakers": [speaker(0, 1)]}),
597 json!({"status": "unscorable"}),
598 json!({"status": "unscorable", "reason": "x", "extra": 1}),
599 json!({"status": "scored", "recordingQuality": 50, "speakers": [speaker(0, 1)], "extra": 1}),
600 json!({"status": "scored", "recordingQuality": 50, "speakers": [speaker(0, 1)], "reason": "x"}),
601 json!({"status": "unscorable", "reason": "x", "recordingQuality": 50}),
602 json!({"status": "unscorable", "reason": "x", "speakers": []}),
603 ] {
604 assert_rejected(value, 1);
605 }
606 }
607
608 #[test]
609 fn rejects_duplicate_json_fields() {
610 for response in [
611 r#"{"status":"unscorable","reason":"a","reason":"b"}"#,
612 r#"{"status":"unscorable","status":"unscorable","reason":"a"}"#,
613 r#"{"status":"scored","recordingQuality":50,"recordingQuality":51,"speakers":[]}"#,
614 r#"{"status":"scored","recordingQuality":50,"speakers":[{"speakerOrdinal":0,"speakerOrdinal":0,"primaryLanguage":"eng","closestDialect":"x","usableSpeechMs":1,"features":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}"#,
615 ] {
616 assert!(parse(response, 1).is_err(), "{response}");
617 }
618 }
619
620 #[test]
621 fn rejects_missing_and_extra_speaker_fields() {
622 for field in [
623 "speakerOrdinal",
624 "primaryLanguage",
625 "closestDialect",
626 "usableSpeechMs",
627 "features",
628 ] {
629 let mut value = scored(vec![speaker(0, 1)]);
630 value["speakers"][0].as_object_mut().unwrap().remove(field);
631 assert_rejected(value, 1);
632 }
633
634 let mut extra = scored(vec![speaker(0, 1)]);
635 extra["speakers"][0]["confidence"] = json!(50);
636 assert_rejected(extra, 1);
637 }
638
639 #[test]
640 fn rejects_wrong_top_level_types() {
641 for quality in [json!(null), json!("50"), json!(50.5), json!(-1)] {
642 let mut value = scored(vec![speaker(0, 1)]);
643 value["recordingQuality"] = quality;
644 assert_rejected(value, 1);
645 }
646
647 for speakers in [json!(null), json!("speaker"), json!({}), json!(50)] {
648 let mut value = scored(vec![speaker(0, 1)]);
649 value["speakers"] = speakers;
650 assert_rejected(value, 1);
651 }
652
653 for reason in [json!(null), json!(50), json!([]), json!({})] {
654 assert_rejected(json!({"status": "unscorable", "reason": reason}), 1);
655 }
656 }
657
658 #[test]
659 fn rejects_wrong_speaker_field_types() {
660 let cases = [
661 ("speakerOrdinal", json!(null)),
662 ("speakerOrdinal", json!("0")),
663 ("speakerOrdinal", json!(0.5)),
664 ("speakerOrdinal", json!(-1)),
665 ("primaryLanguage", json!(null)),
666 ("primaryLanguage", json!(1)),
667 ("closestDialect", json!(null)),
668 ("closestDialect", json!(1)),
669 ("usableSpeechMs", json!(null)),
670 ("usableSpeechMs", json!("1")),
671 ("usableSpeechMs", json!(1.5)),
672 ("usableSpeechMs", json!(-1)),
673 ("features", json!(null)),
674 ("features", json!("values")),
675 ("features", json!({})),
676 ];
677
678 for (field, replacement) in cases {
679 let mut value = scored(vec![speaker(0, 1)]);
680 value["speakers"][0][field] = replacement;
681 assert_rejected(value, 1);
682 }
683 }
684
685 #[test]
686 fn rejects_non_integer_partial_and_abstaining_features() {
687 for replacement in [
688 json!(null),
689 json!("50"),
690 json!(50.5),
691 json!(-1),
692 json!({"value": 50, "confidence": 80}),
693 json!([50, 60]),
694 ] {
695 let mut value = scored(vec![speaker(0, 1)]);
696 value["speakers"][0]["features"][4] = replacement;
697 assert_rejected(value, 1);
698 }
699
700 let raw = format!(
701 r#"{{"status":"scored","recordingQuality":50,"speakers":[{{"speakerOrdinal":0,"primaryLanguage":"eng","closestDialect":"x","usableSpeechMs":1,"features":[NaN,{}]}}]}}"#,
702 vec!["0"; FEATURE_COUNT - 1].join(",")
703 );
704 assert!(parse(&raw, 1).is_err());
705 }
706
707 #[test]
708 fn rejects_out_of_representation_numeric_values() {
709 let responses = [
710 r#"{"status":"scored","recordingQuality":256,"speakers":[]}"#.to_owned(),
711 format!(
712 r#"{{"status":"scored","recordingQuality":50,"speakers":[{{"speakerOrdinal":65536,"primaryLanguage":"eng","closestDialect":"x","usableSpeechMs":1,"features":[{}]}}]}}"#,
713 vec!["0"; FEATURE_COUNT].join(",")
714 ),
715 format!(
716 r#"{{"status":"scored","recordingQuality":50,"speakers":[{{"speakerOrdinal":0,"primaryLanguage":"eng","closestDialect":"x","usableSpeechMs":4294967296,"features":[{}]}}]}}"#,
717 vec!["0"; FEATURE_COUNT].join(",")
718 ),
719 format!(
720 r#"{{"status":"scored","recordingQuality":50,"speakers":[{{"speakerOrdinal":0,"primaryLanguage":"eng","closestDialect":"x","usableSpeechMs":1,"features":[256,{}]}}]}}"#,
721 vec!["0"; FEATURE_COUNT - 1].join(",")
722 ),
723 ];
724
725 for response in responses {
726 assert!(parse(&response, u64::MAX).is_err(), "{response}");
727 }
728 }
729}