use std::path::Path;
use std::sync::Arc;
use super::tests::{lookup, syllables, table, Table};
use super::*;
use crate::lattice::octagram::{encode, GramConfig, GramDb};
fn fixture_db(dir: &Path) -> Arc<GramDb> {
let path = crate::lattice::octagram::fixture::gram_file(
dir,
"collocation.gram",
&[([encode("一名"), encode("医")].concat(), 200_000)],
);
GramDb::shared(&path).expect("fixture .gram loads")
}
fn rows() -> Table {
table(&[
("yi'ming", &[("一名", 10_000)]),
("yi'sheng", &[("医生", 1_000), ("一生", 2_000)]),
])
}
fn decode_with(
rows: &Table,
scorer: Option<&CollocationScorer<'_>>,
weight: f64,
) -> Vec<SentencePath> {
let options = LatticeOptions {
collocation_scorer: scorer,
collocation_weight: weight,
..LatticeOptions::default()
};
decode_sentences(&syllables("yi'ming'yi'sheng"), &mut lookup(rows), &options)
}
#[test]
fn a_gram_model_corrects_the_frequency_winner() {
let dir = tempfile::tempdir().unwrap();
let rows = rows();
let baseline = decode_with(&rows, None, 0.0);
assert_eq!(
baseline[0].sentence, "一名一生",
"没有语法模型时词频首选是串错句"
);
let db = fixture_db(dir.path());
let config = GramConfig::default();
let scorer = |tail: &str, word: &str, is_rear: bool| {
db.query(tail, word, is_rear, &config) * std::f64::consts::LOG10_E
};
let scored = decode_with(&rows, Some(&scorer), 1.0);
assert_eq!(
scored[0].sentence, "一名医生",
"「一名+医」的搭配项应把医生顶回首位"
);
}
#[test]
fn no_gram_model_decodes_byte_identical_to_baseline() {
let rows = rows();
let baseline = decode_with(&rows, None, 0.0);
let scorer = |_: &str, _: &str, _: bool| 9.9;
let weightless = decode_with(&rows, Some(&scorer), 0.0);
assert_eq!(
baseline, weightless,
"scorer 在场但 weight=0 时输出须逐字节一致"
);
}
#[test]
fn missing_or_corrupt_gram_falls_back_to_baseline() {
let dir = tempfile::tempdir().unwrap();
let rows = rows();
let baseline = decode_with(&rows, None, 0.0);
let missing = dir.path().join("absent.gram");
assert!(GramDb::load(&missing).is_none());
let corrupt = dir.path().join("corrupt.gram");
std::fs::write(&corrupt, b"Rime::Grammar/\xFF\xFF\xFF").unwrap();
assert!(GramDb::load(&corrupt).is_none());
let db = GramDb::shared(&missing);
let config = GramConfig::default();
let scorer = db.map(|db| {
move |tail: &str, word: &str, is_rear: bool| {
db.query(tail, word, is_rear, &config) * std::f64::consts::LOG10_E
}
});
let scorer = scorer.as_ref().map(|f| f as &CollocationScorer<'_>);
let fallen = decode_with(&rows, scorer, 1.0);
assert_eq!(baseline, fallen, "模型缺失时解码输出与基线逐字节一致");
GramDb::evict_shared(&missing);
}
#[test]
fn a_real_downloaded_gram_model_loads_and_scores() {
let Some(path) = option_env!("MSIME_REAL_GRAM") else {
eprintln!("skipping: set MSIME_REAL_GRAM=/path/to/model.gram to run this");
return;
};
let db = GramDb::shared(Path::new(path)).expect("real .gram loads");
let config = GramConfig::default();
let pairs = [
("我们", "一起"),
("已经", "完成"),
("非常", "奇怪"),
("这个", "年纪"),
];
let scores: Vec<f64> = pairs
.iter()
.map(|(a, b)| db.query(a, b, false, &config))
.collect();
assert!(
scores.iter().all(|s| s.is_finite()),
"real model must give finite scores: {scores:?}"
);
assert!(
scores.iter().any(|s| *s > config.non_collocation_penalty),
"at least one known bigram should beat the miss penalty {}. scores={scores:?}",
config.non_collocation_penalty
);
}
#[test]
fn a_real_baguwen_model_corrects_the_frequency_winner() {
let Some(path) = option_env!("MSIME_REAL_GRAM") else {
eprintln!("skipping: set MSIME_REAL_GRAM=/path/to/zh-hans-t-essay-bgw-compact.gram");
return;
};
if !path.contains("bgw") {
eprintln!("skipping: {path} has no 北京+大学 entry; run this with a baguwen model");
return;
}
let db = GramDb::shared(Path::new(path)).expect("real .gram loads");
let config = GramConfig::default();
let scorer = |tail: &str, word: &str, is_rear: bool| {
db.query(tail, word, is_rear, &config) * std::f64::consts::LOG10_E
};
let rows = table(&[
("bei'jing", &[("北京", 10_000)]),
("da'xue", &[("大学", 1_000), ("大雪", 2_000)]),
]);
let text = syllables("bei'jing'da'xue");
let plain = LatticeOptions::default();
let baseline = decode_sentences(&text, &mut lookup(&rows), &plain);
assert_eq!(
baseline[0].sentence, "北京大雪",
"没有语法模型时词频首选是串错句"
);
let scored_options = LatticeOptions {
collocation_scorer: Some(&scorer),
collocation_weight: 1.0,
..LatticeOptions::default()
};
let scored = decode_sentences(&text, &mut lookup(&rows), &scored_options);
assert_eq!(
scored[0].sentence, "北京大学",
"真实模型要把「北京+大学」拉回首位"
);
}
#[test]
fn a_stiffer_collocation_penalty_surrenders_to_the_frequency_winner() {
let dir = tempfile::tempdir().unwrap();
let rows = rows();
let db = fixture_db(dir.path());
let config = GramConfig {
collocation_penalty: -30.0,
..GramConfig::default()
};
let scorer = |tail: &str, word: &str, is_rear: bool| {
db.query(tail, word, is_rear, &config) * std::f64::consts::LOG10_E
};
let scored = decode_with(&rows, Some(&scorer), 1.0);
assert_eq!(
scored[0].sentence, "一名一生",
"命中惩罚收紧后词频首选应回到首位"
);
}