use std::collections::HashMap;
use std::sync::OnceLock;
use super::HAIKU_TABLE;
static POEM_EMBEDDINGS: OnceLock<HashMap<(&'static str, usize), Vec<f32>>> = OnceLock::new();
fn cosine(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
pub fn warm_cache(embed_fn: impl Fn(&[&str]) -> anyhow::Result<Vec<Vec<f32>>>) -> anyhow::Result<()> {
if POEM_EMBEDDINGS.get().is_some() {
return Ok(());
}
let mut keys: Vec<(&'static str, usize)> = Vec::new();
let mut texts: Vec<&str> = Vec::new();
for h in HAIKU_TABLE.iter() {
for (slot, poem) in h.poems.iter().enumerate() {
keys.push((h.lang, slot));
texts.push(poem[1]); }
}
let vecs = embed_fn(&texts)?;
let map: HashMap<(&'static str, usize), Vec<f32>> = keys.into_iter().zip(vecs).collect();
let _ = POEM_EMBEDDINGS.set(map); Ok(())
}
pub fn select(
iso_lang: &str,
query_vec: &[f32],
tiebreak_slot: usize,
) -> Option<[&'static str; 3]> {
let cache = POEM_EMBEDDINGS.get()?;
let lang = if HAIKU_TABLE.iter().any(|h| h.lang == iso_lang) {
iso_lang
} else {
"en" };
let (best_slot, _score) = (0..5)
.filter_map(|slot| cache.get(&(lang, slot)).map(|v| (slot, cosine(query_vec, v))))
.fold((tiebreak_slot % 5, f32::NEG_INFINITY), |(bs, bsc), (s, sc)| {
if sc > bsc {
(s, sc)
} else {
(bs, bsc)
}
});
let table = HAIKU_TABLE.iter().find(|h| h.lang == lang)?;
Some(table.poems[best_slot % 5])
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_embed(texts: &[&str]) -> anyhow::Result<Vec<Vec<f32>>> {
Ok(texts
.iter()
.map(|t| {
let mut v = vec![0f32; 384];
v[0] = t.len() as f32 / 100.0;
v[1] = 1.0;
let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-9);
v.iter_mut().for_each(|x| *x /= norm);
v
})
.collect())
}
#[test]
fn warm_then_select_returns_a_valid_poem() {
warm_cache(dummy_embed).unwrap();
let query = dummy_embed(&["a test query about frost"]).unwrap().remove(0);
let lines = select("en", &query, 0).expect("warm cache selects");
assert_eq!(lines.len(), 3);
assert!(lines.iter().all(|l| !l.is_empty()));
}
#[test]
fn unknown_lang_falls_back_to_english() {
warm_cache(dummy_embed).unwrap();
let query = dummy_embed(&["query"]).unwrap().remove(0);
let lines = select("zh", &query, 2).expect("falls back to en");
assert_eq!(lines.len(), 3);
}
}