1use serde::Serialize;
8
9use crate::error::PiperError;
10
11pub const DEFAULT_HOP_LENGTH: usize = 256;
13
14#[derive(Debug, Clone, Serialize)]
16pub struct PhonemeTimingInfo {
17 pub phoneme: String,
18 pub start_ms: f64,
19 pub end_ms: f64,
20 pub duration_ms: f64,
21}
22
23#[derive(Debug, Clone, Serialize)]
25pub struct TimingResult {
26 pub phonemes: Vec<PhonemeTimingInfo>,
27 pub total_duration_ms: f64,
28 pub sample_rate: u32,
29}
30
31impl TimingResult {
32 pub fn to_json(&self) -> Result<String, PiperError> {
34 serde_json::to_string_pretty(self).map_err(PiperError::from)
35 }
36
37 pub fn to_json_compact(&self) -> Result<String, PiperError> {
39 serde_json::to_string(self).map_err(PiperError::from)
40 }
41
42 pub fn to_tsv(&self) -> String {
44 let mut buf = String::from("start_ms\tend_ms\tduration_ms\tphoneme\n");
45 for p in &self.phonemes {
46 buf.push_str(&format!(
47 "{:.3}\t{:.3}\t{:.3}\t{}\n",
48 p.start_ms, p.end_ms, p.duration_ms, p.phoneme
49 ));
50 }
51 buf
52 }
53
54 pub fn to_srt(&self) -> String {
56 let mut buf = String::new();
57 for (i, p) in self.phonemes.iter().enumerate() {
58 let idx = i + 1;
59 let start = format_srt_timestamp(p.start_ms);
60 let end = format_srt_timestamp(p.end_ms);
61 buf.push_str(&format!("{idx}\n{start} --> {end}\n{}\n\n", p.phoneme));
62 }
63 buf
64 }
65}
66
67fn format_srt_timestamp(ms: f64) -> String {
69 let total_ms = ms.round() as u64;
70 let millis = total_ms % 1000;
71 let total_secs = total_ms / 1000;
72 let secs = total_secs % 60;
73 let total_mins = total_secs / 60;
74 let mins = total_mins % 60;
75 let hours = total_mins / 60;
76 format!("{hours:02}:{mins:02}:{secs:02},{millis:03}")
77}
78
79pub fn durations_to_timing(
90 durations: &[f32],
91 phoneme_tokens: &[String],
92 sample_rate: u32,
93 hop_length: usize,
94) -> Result<TimingResult, PiperError> {
95 if durations.len() != phoneme_tokens.len() {
96 return Err(PiperError::Inference(format!(
97 "durations length ({}) != phoneme_tokens length ({})",
98 durations.len(),
99 phoneme_tokens.len()
100 )));
101 }
102
103 if sample_rate == 0 {
104 return Err(PiperError::Inference("sample_rate must be > 0".to_string()));
105 }
106
107 if hop_length == 0 {
108 return Err(PiperError::Inference("hop_length must be > 0".to_string()));
109 }
110
111 let frame_time_s = hop_length as f64 / sample_rate as f64;
113 let frame_time_ms = frame_time_s * 1000.0;
114
115 let mut phonemes = Vec::with_capacity(durations.len());
116 let mut cursor_ms: f64 = 0.0;
117
118 for (dur, token) in durations.iter().zip(phoneme_tokens.iter()) {
119 let dur_frames = (*dur).max(0.0) as f64;
120 let duration_ms = dur_frames * frame_time_ms;
121 let start_ms = cursor_ms;
122 let end_ms = cursor_ms + duration_ms;
123
124 phonemes.push(PhonemeTimingInfo {
125 phoneme: token.clone(),
126 start_ms,
127 end_ms,
128 duration_ms,
129 });
130
131 cursor_ms = end_ms;
132 }
133
134 Ok(TimingResult {
135 total_duration_ms: cursor_ms,
136 phonemes,
137 sample_rate,
138 })
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn tokens(names: &[&str]) -> Vec<String> {
150 names.iter().map(|s| s.to_string()).collect()
151 }
152
153 #[test]
158 fn test_basic_conversion_22050() {
159 let durations = vec![10.0, 20.0, 5.0];
161 let toks = tokens(&["a", "b", "c"]);
162 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
163
164 let frame_ms = 256.0 / 22050.0 * 1000.0;
165
166 assert_eq!(result.phonemes.len(), 3);
167 assert_eq!(result.sample_rate, 22050);
168
169 assert!((result.phonemes[0].start_ms - 0.0).abs() < 1e-6);
171 assert!((result.phonemes[0].duration_ms - 10.0 * frame_ms).abs() < 1e-6);
172 assert!((result.phonemes[0].end_ms - 10.0 * frame_ms).abs() < 1e-6);
173
174 assert!((result.phonemes[1].start_ms - 10.0 * frame_ms).abs() < 1e-6);
176 assert!((result.phonemes[1].duration_ms - 20.0 * frame_ms).abs() < 1e-6);
177
178 assert!((result.phonemes[2].start_ms - 30.0 * frame_ms).abs() < 1e-6);
180
181 assert!((result.total_duration_ms - 35.0 * frame_ms).abs() < 1e-6);
183 }
184
185 #[test]
190 fn test_empty_durations() {
191 let result = durations_to_timing(&[], &[], 22050, 256).unwrap();
192 assert!(result.phonemes.is_empty());
193 assert!((result.total_duration_ms - 0.0).abs() < 1e-6);
194 }
195
196 #[test]
201 fn test_single_phoneme() {
202 let durations = vec![8.0];
203 let toks = tokens(&["k"]);
204 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
205
206 assert_eq!(result.phonemes.len(), 1);
207 assert!((result.phonemes[0].start_ms - 0.0).abs() < 1e-6);
208
209 let frame_ms = 256.0 / 22050.0 * 1000.0;
210 assert!((result.phonemes[0].duration_ms - 8.0 * frame_ms).abs() < 1e-6);
211 assert!((result.total_duration_ms - 8.0 * frame_ms).abs() < 1e-6);
212 }
213
214 #[test]
219 fn test_zero_durations() {
220 let durations = vec![0.0, 10.0, 0.0];
221 let toks = tokens(&["^", "a", "_"]);
222 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
223
224 assert_eq!(result.phonemes.len(), 3);
225
226 assert!((result.phonemes[0].duration_ms - 0.0).abs() < 1e-6);
228 assert!((result.phonemes[0].start_ms - result.phonemes[0].end_ms).abs() < 1e-6);
229
230 assert!((result.phonemes[1].start_ms - 0.0).abs() < 1e-6);
232
233 let frame_ms = 256.0 / 22050.0 * 1000.0;
235 assert!((result.phonemes[2].start_ms - 10.0 * frame_ms).abs() < 1e-6);
236 assert!((result.phonemes[2].duration_ms - 0.0).abs() < 1e-6);
237 }
238
239 #[test]
244 fn test_mismatched_lengths() {
245 let durations = vec![1.0, 2.0, 3.0];
246 let toks = tokens(&["a", "b"]);
247 let err = durations_to_timing(&durations, &toks, 22050, 256).unwrap_err();
248 let msg = err.to_string();
249 assert!(msg.contains("3"));
250 assert!(msg.contains("2"));
251 }
252
253 #[test]
258 fn test_json_roundtrip() {
259 let durations = vec![5.0, 15.0];
260 let toks = tokens(&["h", "i"]);
261 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
262
263 let json = result.to_json().unwrap();
264
265 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
267 assert!(parsed.is_object());
268 assert!(parsed["phonemes"].is_array());
269 assert_eq!(parsed["phonemes"].as_array().unwrap().len(), 2);
270 assert_eq!(parsed["sample_rate"].as_u64().unwrap(), 22050);
271
272 let first = &parsed["phonemes"][0];
273 assert_eq!(first["phoneme"].as_str().unwrap(), "h");
274 assert!((first["start_ms"].as_f64().unwrap() - 0.0).abs() < 1e-6);
275 }
276
277 #[test]
282 fn test_json_compact() {
283 let durations = vec![3.0];
284 let toks = tokens(&["x"]);
285 let result = durations_to_timing(&durations, &toks, 16000, 256).unwrap();
286
287 let json_compact = result.to_json_compact().unwrap();
288 assert!(!json_compact.contains('\n'));
290 assert!(json_compact.contains("\"phoneme\":\"x\""));
291 }
292
293 #[test]
298 fn test_tsv_format() {
299 let durations = vec![10.0, 20.0];
300 let toks = tokens(&["p", "q"]);
301 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
302
303 let tsv = result.to_tsv();
304 let lines: Vec<&str> = tsv.lines().collect();
305
306 assert_eq!(lines[0], "start_ms\tend_ms\tduration_ms\tphoneme");
308
309 assert_eq!(lines.len(), 3); assert!(lines[1].starts_with("0.000\t"));
314 assert!(lines[1].ends_with("\tp"));
315
316 assert!(lines[2].ends_with("\tq"));
318 }
319
320 #[test]
325 fn test_tsv_empty() {
326 let result = durations_to_timing(&[], &[], 22050, 256).unwrap();
327 let tsv = result.to_tsv();
328 let lines: Vec<&str> = tsv.lines().collect();
329 assert_eq!(lines.len(), 1); }
331
332 #[test]
337 fn test_srt_format() {
338 let durations = vec![500.0, 1500.0, 3000.0];
340 let toks = tokens(&["a", "bb", "c"]);
341 let result = durations_to_timing(&durations, &toks, 1000, 1).unwrap();
342
343 let srt = result.to_srt();
344 let blocks: Vec<&str> = srt.split("\n\n").filter(|b| !b.is_empty()).collect();
345 assert_eq!(blocks.len(), 3);
346
347 let lines0: Vec<&str> = blocks[0].lines().collect();
349 assert_eq!(lines0[0], "1");
350 assert_eq!(lines0[1], "00:00:00,000 --> 00:00:00,500");
351 assert_eq!(lines0[2], "a");
352
353 let lines1: Vec<&str> = blocks[1].lines().collect();
355 assert_eq!(lines1[0], "2");
356 assert_eq!(lines1[1], "00:00:00,500 --> 00:00:02,000");
357 assert_eq!(lines1[2], "bb");
358
359 let lines2: Vec<&str> = blocks[2].lines().collect();
361 assert_eq!(lines2[0], "3");
362 assert_eq!(lines2[1], "00:00:02,000 --> 00:00:05,000");
363 assert_eq!(lines2[2], "c");
364 }
365
366 #[test]
371 fn test_srt_large_timestamps() {
372 let dur_ms = 5_405_123.0_f32;
375 let durations = vec![dur_ms];
376 let toks = tokens(&["long"]);
377 let result = durations_to_timing(&durations, &toks, 1000, 1).unwrap();
378
379 let srt = result.to_srt();
380 assert!(srt.contains("00:00:00,000 --> 01:30:05,123"));
381 }
382
383 #[test]
388 fn test_sample_rate_16000() {
389 let durations = vec![16.0];
390 let toks = tokens(&["z"]);
391 let result = durations_to_timing(&durations, &toks, 16000, 256).unwrap();
392
393 let expected_ms = 16.0 * (256.0 / 16000.0 * 1000.0);
396 assert!((result.phonemes[0].duration_ms - expected_ms).abs() < 1e-6);
397 assert!((result.total_duration_ms - expected_ms).abs() < 1e-6);
398 }
399
400 #[test]
405 fn test_sample_rate_44100() {
406 let durations = vec![100.0];
407 let toks = tokens(&["w"]);
408 let result = durations_to_timing(&durations, &toks, 44100, 256).unwrap();
409
410 let frame_ms = 256.0 / 44100.0 * 1000.0;
411 let expected_ms = 100.0 * frame_ms;
412 assert!((result.phonemes[0].duration_ms - expected_ms).abs() < 1e-6);
413 assert_eq!(result.sample_rate, 44100);
414 }
415
416 #[test]
421 fn test_large_duration_values() {
422 let durations = vec![100_000.0, 200_000.0];
423 let toks = tokens(&["aa", "bb"]);
424 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
425
426 let frame_ms = 256.0 / 22050.0 * 1000.0;
427 let expected_total = 300_000.0 * frame_ms;
428 assert!((result.total_duration_ms - expected_total).abs() < 1e-3);
429
430 assert!((result.phonemes[1].start_ms - 100_000.0 * frame_ms).abs() < 1e-3);
432 }
433
434 #[test]
439 fn test_floating_point_precision() {
440 let n = 1000;
442 let durations: Vec<f32> = vec![1.0; n];
443 let toks: Vec<String> = (0..n).map(|i| format!("p{i}")).collect();
444 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
445
446 let frame_ms = 256.0 / 22050.0 * 1000.0;
447 let expected_total = n as f64 * frame_ms;
448
449 assert!(
451 (result.total_duration_ms - expected_total).abs() < 0.01,
452 "total={} expected={}",
453 result.total_duration_ms,
454 expected_total
455 );
456
457 let last = result.phonemes.last().unwrap();
459 assert!((last.end_ms - result.total_duration_ms).abs() < 1e-9);
460 }
461
462 #[test]
467 fn test_negative_durations_clamped() {
468 let durations = vec![-5.0, 10.0, -1.0];
469 let toks = tokens(&["a", "b", "c"]);
470 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
471
472 assert!((result.phonemes[0].duration_ms - 0.0).abs() < 1e-6);
474 assert!((result.phonemes[2].duration_ms - 0.0).abs() < 1e-6);
475
476 let frame_ms = 256.0 / 22050.0 * 1000.0;
478 assert!((result.total_duration_ms - 10.0 * frame_ms).abs() < 1e-6);
479 }
480
481 #[test]
486 fn test_zero_sample_rate_error() {
487 let durations = vec![1.0];
488 let toks = tokens(&["a"]);
489 let err = durations_to_timing(&durations, &toks, 0, 256).unwrap_err();
490 assert!(err.to_string().contains("sample_rate"));
491 }
492
493 #[test]
498 fn test_zero_hop_length_error() {
499 let durations = vec![1.0];
500 let toks = tokens(&["a"]);
501 let err = durations_to_timing(&durations, &toks, 22050, 0).unwrap_err();
502 assert!(err.to_string().contains("hop_length"));
503 }
504
505 #[test]
510 fn test_default_hop_length() {
511 assert_eq!(DEFAULT_HOP_LENGTH, 256);
512 }
513
514 #[test]
519 fn test_phoneme_ordering_preserved() {
520 let durations = vec![1.0, 2.0, 3.0, 4.0, 5.0];
521 let toks = tokens(&["^", "k", "o", "N", "_"]);
522 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
523
524 let names: Vec<&str> = result.phonemes.iter().map(|p| p.phoneme.as_str()).collect();
525 assert_eq!(names, vec!["^", "k", "o", "N", "_"]);
526
527 for i in 1..result.phonemes.len() {
529 assert!(
530 (result.phonemes[i].start_ms - result.phonemes[i - 1].end_ms).abs() < 1e-9,
531 "gap between phoneme {} and {}",
532 i - 1,
533 i
534 );
535 }
536 }
537
538 #[test]
543 fn test_tsv_and_json_consistency() {
544 let durations = vec![7.0, 13.0];
545 let toks = tokens(&["s", "t"]);
546 let result = durations_to_timing(&durations, &toks, 22050, 256).unwrap();
547
548 let json_str = result.to_json().unwrap();
549 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
550
551 let tsv = result.to_tsv();
552 let data_lines: Vec<&str> = tsv.lines().skip(1).collect();
553
554 for (i, line) in data_lines.iter().enumerate() {
555 let fields: Vec<&str> = line.split('\t').collect();
556 assert_eq!(fields.len(), 4);
557
558 let tsv_start: f64 = fields[0].parse().unwrap();
559 let tsv_end: f64 = fields[1].parse().unwrap();
560 let tsv_dur: f64 = fields[2].parse().unwrap();
561 let tsv_phoneme = fields[3];
562
563 let json_ph = &parsed["phonemes"][i];
564 let json_start = json_ph["start_ms"].as_f64().unwrap();
565 let json_end = json_ph["end_ms"].as_f64().unwrap();
566 let json_phoneme = json_ph["phoneme"].as_str().unwrap();
567
568 assert!((tsv_start - json_start).abs() < 0.01);
569 assert!((tsv_end - json_end).abs() < 0.01);
570 assert!(tsv_dur > 0.0 || (tsv_dur - 0.0).abs() < 1e-6);
571 assert_eq!(tsv_phoneme, json_phoneme);
572 }
573 }
574
575 #[test]
580 fn test_tsv_phoneme_with_tab() {
581 let durations = vec![5.0];
585 let toks = vec!["a\tb".to_string()];
586 let result = durations_to_timing(&durations, &toks, 1000, 1).unwrap();
587
588 let tsv = result.to_tsv();
589 let data_line = tsv.lines().nth(1).expect("expected a data line");
590 let fields: Vec<&str> = data_line.split('\t').collect();
591
592 assert_eq!(
594 fields.len(),
595 5,
596 "tab inside phoneme name produces an extra TSV column"
597 );
598 }
599
600 #[test]
605 fn test_srt_phoneme_with_newline() {
606 let durations = vec![10.0];
611 let toks = vec!["line1\nline2".to_string()];
612 let result = durations_to_timing(&durations, &toks, 1000, 1).unwrap();
613
614 let srt = result.to_srt();
615
616 assert!(srt.contains("1\n"));
620 assert!(srt.contains(" --> "));
621 assert!(srt.contains("line1\nline2"));
622 }
623
624 #[test]
629 fn test_nan_duration() {
630 let durations = vec![f32::NAN, 10.0];
631 let toks = tokens(&["nan_ph", "ok"]);
632 let result = durations_to_timing(&durations, &toks, 1000, 1).unwrap();
633
634 assert!(
637 (result.phonemes[0].duration_ms - 0.0).abs() < 1e-9,
638 "NaN duration is clamped to 0 by f32::max"
639 );
640 assert!(
641 (result.phonemes[0].start_ms - result.phonemes[0].end_ms).abs() < 1e-9,
642 "start == end for zero-duration phoneme"
643 );
644
645 assert!(
647 (result.phonemes[1].duration_ms - 10.0).abs() < 1e-6,
648 "non-NaN phoneme keeps its value"
649 );
650
651 assert!(
653 (result.total_duration_ms - 10.0).abs() < 1e-6,
654 "total reflects only the non-NaN phoneme"
655 );
656 }
657
658 #[test]
663 fn test_infinity_duration() {
664 let durations = vec![f32::INFINITY];
665 let toks = tokens(&["inf_ph"]);
666 let result = durations_to_timing(&durations, &toks, 1000, 1).unwrap();
667
668 assert!(
669 result.phonemes[0].duration_ms.is_infinite(),
670 "Infinity duration propagates"
671 );
672 assert!(
673 result.total_duration_ms.is_infinite(),
674 "total also becomes infinite"
675 );
676 }
677
678 #[test]
683 fn test_unicode_phoneme_names() {
684 let ipa_tokens = vec![
685 "\u{0251}\u{02D0}".to_string(), "\u{0283}".to_string(), "\u{014B}".to_string(), ];
689 let durations = vec![5.0, 3.0, 7.0];
690 let result = durations_to_timing(&durations, &ipa_tokens, 1000, 1).unwrap();
691
692 assert_eq!(result.phonemes[0].phoneme, "\u{0251}\u{02D0}");
694 assert_eq!(result.phonemes[1].phoneme, "\u{0283}");
695 assert_eq!(result.phonemes[2].phoneme, "\u{014B}");
696
697 let json = result.to_json().unwrap();
699 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
700 assert_eq!(
701 parsed["phonemes"][0]["phoneme"].as_str().unwrap(),
702 "\u{0251}\u{02D0}"
703 );
704
705 let tsv = result.to_tsv();
707 assert!(tsv.contains("\u{0251}\u{02D0}"));
708 assert!(tsv.contains("\u{0283}"));
709 assert!(tsv.contains("\u{014B}"));
710
711 let srt = result.to_srt();
713 assert!(srt.contains("\u{0251}\u{02D0}"));
714 assert!(srt.contains("\u{0283}"));
715 assert!(srt.contains("\u{014B}"));
716 }
717
718 #[test]
723 fn test_very_small_durations_precision() {
724 let durations = vec![0.001_f32];
726 let toks = tokens(&["tiny"]);
727 let result = durations_to_timing(&durations, &toks, 1000, 1).unwrap();
728
729 let expected = 0.001_f64;
731 assert!(
732 (result.phonemes[0].duration_ms - expected).abs() < 1e-9,
733 "very small duration: got {} expected {}",
734 result.phonemes[0].duration_ms,
735 expected
736 );
737
738 let tsv = result.to_tsv();
740 let data_line = tsv.lines().nth(1).unwrap();
741 let fields: Vec<&str> = data_line.split('\t').collect();
743 assert_eq!(fields[2], "0.001");
744 }
745
746 #[test]
751 fn test_timing_result_direct_construction() {
752 let timing = TimingResult {
753 phonemes: vec![
754 PhonemeTimingInfo {
755 phoneme: "hello".to_string(),
756 start_ms: 0.0,
757 end_ms: 100.5,
758 duration_ms: 100.5,
759 },
760 PhonemeTimingInfo {
761 phoneme: "world".to_string(),
762 start_ms: 100.5,
763 end_ms: 250.0,
764 duration_ms: 149.5,
765 },
766 ],
767 total_duration_ms: 250.0,
768 sample_rate: 48000,
769 };
770
771 assert_eq!(timing.phonemes.len(), 2);
773 assert_eq!(timing.phonemes[0].phoneme, "hello");
774 assert_eq!(timing.phonemes[1].phoneme, "world");
775 assert!((timing.phonemes[0].start_ms - 0.0).abs() < 1e-9);
776 assert!((timing.phonemes[0].end_ms - 100.5).abs() < 1e-9);
777 assert!((timing.phonemes[0].duration_ms - 100.5).abs() < 1e-9);
778 assert!((timing.phonemes[1].start_ms - 100.5).abs() < 1e-9);
779 assert!((timing.phonemes[1].end_ms - 250.0).abs() < 1e-9);
780 assert!((timing.phonemes[1].duration_ms - 149.5).abs() < 1e-9);
781 assert!((timing.total_duration_ms - 250.0).abs() < 1e-9);
782 assert_eq!(timing.sample_rate, 48000);
783
784 let cloned = timing.clone();
786 assert_eq!(cloned.phonemes.len(), timing.phonemes.len());
787 assert_eq!(cloned.sample_rate, timing.sample_rate);
788
789 let json = timing.to_json().unwrap();
791 assert!(json.contains("\"hello\""));
792 assert!(json.contains("\"world\""));
793 assert!(json.contains("48000"));
794 }
795
796 #[test]
801 fn test_json_nonfinite_serialized_as_null() {
802 let timing = TimingResult {
807 phonemes: vec![PhonemeTimingInfo {
808 phoneme: "inf".to_string(),
809 start_ms: 0.0,
810 end_ms: f64::INFINITY,
811 duration_ms: f64::INFINITY,
812 }],
813 total_duration_ms: f64::INFINITY,
814 sample_rate: 22050,
815 };
816
817 let json = timing.to_json().expect("to_json should succeed");
819 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
820
821 assert!(
823 parsed["total_duration_ms"].is_null(),
824 "Infinity total_duration_ms serialized as null"
825 );
826 assert!(
827 parsed["phonemes"][0]["end_ms"].is_null(),
828 "Infinity end_ms serialized as null"
829 );
830 assert!(
831 parsed["phonemes"][0]["duration_ms"].is_null(),
832 "Infinity duration_ms serialized as null"
833 );
834
835 assert!(
837 parsed["phonemes"][0]["start_ms"].is_number(),
838 "finite start_ms remains a number"
839 );
840
841 let compact = timing.to_json_compact().expect("compact should succeed");
843 assert!(
844 compact.contains("null"),
845 "compact JSON contains null for Infinity"
846 );
847
848 let bad_json = "{ not valid json }";
852 let serde_err: Result<serde_json::Value, _> = serde_json::from_str(bad_json);
853 let piper_err: PiperError = serde_err.unwrap_err().into();
854 let msg = piper_err.to_string();
855 assert!(
856 msg.contains("JSON"),
857 "PiperError from serde_json mentions JSON: {}",
858 msg
859 );
860 }
861}