1//! The lines Whiskers says that are the same every time, so a cache of speech can hold them. 2//! 3//! A line is fixed when its words are decided in code and depend on nothing but the child's profile (and, for 4//! a voice's introduction, the voice's name): the greetings, the character chooser's hello. What came from a 5//! memory or from the model is never here, and the speech cache keeps nothing that was not declared fixed (see 6//! the service README), so forgetting a memory leaves nothing behind. 7 8use crate::greeting::{self, Variant}; 9use crate::profile::Audience; 10 11/// The longest voice name said in a line: a vendor's names are sometimes descriptions. 12const VOICE_NAME_SPOKEN_MAX: usize = 40; 13 14/// What she hears when a character changes: "Hi Ada", or "Hi there" with no name. Said in the new character's 15/// own voice. 16pub fn hi_line(audience: &Audience) -> String { 17 format!("Hi {}", audience.name().map_or("there", |n| n.as_str())) 18} 19 20/// What she hears when a voice is chosen: "Hi Ada, this voice is called Jennifer". A voice with no name the 21/// service gave is introduced without one. Said in that voice. 22pub fn voice_intro_line(audience: &Audience, voice_name: Option<&str>) -> String { 23 let hi = hi_line(audience); 24 match voice_name.map(str::trim).filter(|n| !n.is_empty()) { 25 Some(name) => { 26 // A name is cut at a word boundary if it is long, so the line stays a line. 27 let spoken: String = if name.chars().count() > VOICE_NAME_SPOKEN_MAX { 28 let cut: String = name.chars().take(VOICE_NAME_SPOKEN_MAX).collect(); 29 cut.rsplit_once(' ').map_or(cut.clone(), |(head, _)| head.to_owned()) 30 } else { 31 name.to_owned() 32 }; 33 format!("{hi}, this voice is called {spoken}") 34 } 35 None => format!("{hi}, this is my usual voice"), 36 } 37} 38 39/// Every fixed line the core knows for this audience, once each. The app adds its own (page names, hints, the 40/// time-left lines, the undo toast) to the list it hands to a pre-render. 41pub fn fixed_lines(audience: &Audience) -> Vec<String> { 42 let mut lines: Vec<String> = Variant::ALL.into_iter().map(|v| greeting::say(audience, v)).collect(); 43 lines.push(hi_line(audience)); 44 lines.sort(); 45 lines.dedup(); 46 lines 47} 48 49#[cfg(test)] 50mod tests { 51 use super::*; 52 use crate::profile::{Age, Child, ChildName}; 53 54 fn ada() -> Audience { 55 Audience::new(Some(&Child { name: ChildName::new("Ada").unwrap(), age: Age::new(6).unwrap() })) 56 } 57 58 #[test] 59 fn the_hello_and_the_introduction_are_exactly_what_she_was_promised() { 60 assert_eq!(hi_line(&ada()), "Hi Ada"); 61 assert_eq!(voice_intro_line(&ada(), Some("Jennifer")), "Hi Ada, this voice is called Jennifer"); 62 assert_eq!(hi_line(&Audience::new(None)), "Hi there", "with no profile the wording is neutral"); 63 assert_eq!(voice_intro_line(&Audience::new(None), Some(" Jennifer ")), "Hi there, this voice is called Jennifer"); 64 assert_eq!(voice_intro_line(&ada(), None), "Hi Ada, this is my usual voice"); 65 assert_eq!(voice_intro_line(&ada(), Some(" ")), "Hi Ada, this is my usual voice"); 66 } 67 68 #[test] 69 fn a_long_vendor_name_is_cut_at_a_word() { 70 let line = voice_intro_line(&ada(), Some("Brian - Deep, Resonant and Comforting, Perfect for Narration")); 71 assert_eq!(line, "Hi Ada, this voice is called Brian - Deep, Resonant and Comforting,"); 72 } 73 74 #[test] 75 fn the_fixed_lines_are_the_greetings_and_the_hello_without_repeats() { 76 let lines = fixed_lines(&ada()); 77 assert_eq!(lines.len(), 7); 78 assert!(lines.contains(&"Hi Ada".to_owned())); 79 assert!(lines.iter().all(|l| l.contains("Ada"))); 80 } 81}