1use crate::Error;
4
5const CHARS_PER_SECOND: u64 = 15;
7
8pub fn sentences(text: &str) -> Vec<String> {
15 let mut units = Vec::new();
16 let mut start = 0;
17 let mut previous_digit = false;
18 for (index, character) in text.char_indices() {
19 let end = index + character.len_utf8();
20 let next = text[end..].chars().next();
21 let ends = match character {
22 '!' | '?' => next.is_none_or(char::is_whitespace),
23 '.' => {
24 let decimal = previous_digit && next.is_some_and(|next| next.is_ascii_digit());
25 !decimal && next.is_none_or(char::is_whitespace)
26 }
27 _ => false,
28 };
29 if ends {
30 push(&mut units, &text[start..end]);
31 start = end;
32 }
33 previous_digit = character.is_ascii_digit();
34 }
35 push(&mut units, &text[start..]);
36 units
37}
38
39pub fn within_bound(text: &str, max_seconds: u64) -> Result<(), Error> {
42 let bound = max_seconds.saturating_mul(CHARS_PER_SECOND);
43 let characters = u64::try_from(text.chars().count()).unwrap_or(u64::MAX);
44 if characters > bound {
45 return Err(Error::Render(format!(
46 "the utterance is longer than [tts] max_utterance_seconds ({max_seconds})"
47 )));
48 }
49 Ok(())
50}
51
52fn push(units: &mut Vec<String>, unit: &str) {
53 let trimmed = unit.trim();
54 if !trimmed.is_empty() {
55 units.push(trimmed.to_owned());
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::{sentences, within_bound};
62
63 #[test]
64 fn three_sentences_split_into_three() {
65 assert_eq!(
66 sentences("One. Two! Three?"),
67 vec!["One.".to_owned(), "Two!".to_owned(), "Three?".to_owned()]
68 );
69 }
70
71 #[test]
72 fn a_decimal_is_not_a_sentence_end() {
73 assert_eq!(sentences("Version 1.2 shipped."), vec!["Version 1.2 shipped.".to_owned()]);
74 }
75
76 #[test]
77 fn text_with_no_terminal_punctuation_is_one_unit() {
78 assert_eq!(sentences("no punctuation here"), vec!["no punctuation here".to_owned()]);
79 }
80
81 #[test]
82 fn an_over_long_utterance_is_refused_by_name() {
83 let long = "word ".repeat(2_000);
84 let error = within_bound(&long, 120).expect_err("refused");
85 assert!(error.to_string().contains("max_utterance_seconds"));
86 }
87}