The speech cache: exact, bounded with the least recently used going first, and holding no words.

3use ::log::info;
4use whiskers_ports::{Audio, AudioMs, LineTiming, Millis, SpeechCache, SpeechLine, TimedAudio, VoiceId, run_ready};
6fn voice(id: &str) -> VoiceId {
7    VoiceId::new(id).unwrap()
8}
9
10fn line(text: &str) -> SpeechLine {
11    SpeechLine::new(text.into()).unwrap()
12}

A line's speech with bytes of audio, each character said for 10 ms.

15fn speech(text: &str, bytes: usize) -> TimedAudio {
16    let l = line(text);
17    let spans = (0..l.chars()).map(|i| (AudioMs::new(i * 10), AudioMs::new(i * 10 + 10))).collect();
18    TimedAudio::new(Audio(vec![7; bytes]), LineTiming::new(text.into(), spans).unwrap(), &l).unwrap()
19}
21fn now() -> Millis {
22    Millis::new(1_800_000_000_000)
23}

What one line of three characters and bytes of audio weighs against a cap.

26fn weight(bytes: usize) -> u64 {
27    whiskers_ports::speech_weight(&speech("abc", bytes))
28}

Runs every law. fresh(cap) makes an empty cache over cap bytes; restart reopens it on what it kept.

31pub fn speech_cache<C: SpeechCache>(fresh: &impl Fn(u64) -> C, restart: &impl Fn(C) -> C) {
32    info!("conformance: speech cache");
33    a_kept_line_is_found_for_its_voice_and_exact_text_only(fresh(1 << 20));
34    a_found_line_is_the_line_that_was_kept(fresh(1 << 20));
35    keeping_again_replaces(fresh(1 << 20));
36    the_least_recently_used_goes_first(fresh(3 * weight(100) + 1));
37    a_line_over_the_whole_cap_is_not_kept(fresh(weight(100)));
38    what_was_kept_survives_a_restart(fresh, restart);
39}
41pub fn a_kept_line_is_found_for_its_voice_and_exact_text_only<C: SpeechCache>(c: C) {
42    run_ready(c.keep(&voice("v1"), &line("Hello Ada"), &speech("Hello Ada", 50), now())).unwrap();
43    assert!(run_ready(c.find(&voice("v1"), &line("Hello Ada"), now())).unwrap().is_some());
44    assert!(run_ready(c.find(&voice("v2"), &line("Hello Ada"), now())).unwrap().is_none(), "another voice is another line");
45    for other in ["hello Ada", "Hello Ada ", "Hello Ad"] {
46        assert!(run_ready(c.find(&voice("v1"), &line(other), now())).unwrap().is_none(), "{other:?} is not the same text");
47    }
48}
49
50pub fn a_found_line_is_the_line_that_was_kept<C: SpeechCache>(c: C) {
51    let kept = speech("Hi h\u{e9}", 40);
52    run_ready(c.keep(&voice("v1"), &line("Hi h\u{e9}"), &kept, now())).unwrap();
53    let found = run_ready(c.find(&voice("v1"), &line("Hi h\u{e9}"), now())).unwrap().unwrap();
54    assert_eq!(found.audio(), kept.audio());
55    assert_eq!(found.timing().text(), "Hi h\u{e9}");
56    let (a, b): (Vec<_>, Vec<_>) = (found.timing().characters().collect(), kept.timing().characters().collect());
57    assert_eq!(a.len(), b.len());
58    for ((ca, sa), (cb, sb)) in a.iter().zip(&b) {
59        assert_eq!((ca, sa.start(), sa.end()), (cb, sb.start(), sb.end()));
60    }
61}
62
63pub fn keeping_again_replaces<C: SpeechCache>(c: C) {
64    run_ready(c.keep(&voice("v1"), &line("abc"), &speech("abc", 10), now())).unwrap();
65    run_ready(c.keep(&voice("v1"), &line("abc"), &speech("abc", 30), now())).unwrap();
66    let found = run_ready(c.find(&voice("v1"), &line("abc"), now())).unwrap().unwrap();
67    assert_eq!(found.audio().0.len(), 30);
68    assert_eq!(run_ready(c.size()).unwrap().entries, 1);
69}
70
71pub fn the_least_recently_used_goes_first<C: SpeechCache>(c: C) {
72    let v = voice("v1");
73    for text in ["aaa", "bbb", "ccc"] {
74        run_ready(c.keep(&v, &line(text), &speech(text, 100), now())).unwrap();
75    }
76    assert_eq!(run_ready(c.size()).unwrap().entries, 3);
77    // Using `aaa` makes `bbb` the one used longest ago.
78    assert!(run_ready(c.find(&v, &line("aaa"), now())).unwrap().is_some());
79    run_ready(c.keep(&v, &line("ddd"), &speech("ddd", 100), now())).unwrap();
80    assert!(run_ready(c.find(&v, &line("bbb"), now())).unwrap().is_none(), "the least recently used was dropped");
81    for kept in ["aaa", "ccc", "ddd"] {
82        assert!(run_ready(c.find(&v, &line(kept), now())).unwrap().is_some(), "{kept} stays");
83    }
84    let size = run_ready(c.size()).unwrap();
85    assert_eq!(size.entries, 3);
86    assert!(size.bytes <= 3 * weight(100) + 1, "the cap holds: {}", size.bytes);
87}
88
89pub fn a_line_over_the_whole_cap_is_not_kept<C: SpeechCache>(c: C) {
90    run_ready(c.keep(&voice("v1"), &line("abc"), &speech("abc", 100), now())).unwrap();
91    run_ready(c.keep(&voice("v1"), &line("xyz"), &speech("xyz", 5000), now())).unwrap();
92    assert!(run_ready(c.find(&voice("v1"), &line("xyz"), now())).unwrap().is_none());
93    assert!(run_ready(c.find(&voice("v1"), &line("abc"), now())).unwrap().is_some(), "and it did not push out what fits");
94}
95
96pub fn what_was_kept_survives_a_restart<C: SpeechCache>(fresh: &impl Fn(u64) -> C, restart: &impl Fn(C) -> C) {
97    let c = fresh(1 << 20);
98    run_ready(c.keep(&voice("v1"), &line("abc"), &speech("abc", 10), now())).unwrap();
99    let c = restart(c);
100    assert!(run_ready(c.find(&voice("v1"), &line("abc"), now())).unwrap().is_some());
101}