Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ hasami は入力を文字単位の double-array trie で辞書引きし、未知
## 読みと字幅

読みが欠けた仮名のトークンは表層形から補う。半角カナと結合濁点・半濁点も扱う
(`ガッコウ` → ガッコウ)。全角英字も ASCII と同じ綴り読み・数字の後の単位読みの対象になる。
(`ガッコウ` → ガッコウ)。全角英字も ASCII と同じ綴り読み・数字と隣接する場合の単位読みの対象になる。
入力と辞書キーは書き換えず、表層形と UTF-8 バイト位置を保つ。
結合文字が別のトークンに分かれた場合まで入力全体を正規化する機能ではない。

英字の文脈補正では、数字の `end` と英字の `start` が一致するときだけ単位読みを付ける。
`3F` の F はファラド、`3 F`・タブや改行で離れた F はエフになる。
`100km`・`3GB`・`5V` は単位読み、`100 km`・`3 GB`・`5 V` はケーエム・ジービー・ブイの綴り読みになる。
既知語で 3 文字以上の英字にカタカナの読みがある場合は、従来どおり辞書の読みを優先する(`NASA` → ナサ)。

補助平面の CJK 拡張漢字・互換漢字も、char.def の明示指定がなければ漢字として未知語候補を作る。
60 changes: 58 additions & 2 deletions src/lattice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ fn apply_decimal_counter_override(token: &mut Token, after_decimal_point: bool)
/// 辞書の読みを文脈で補正し、読みが欠けているトークンを補完する。
/// - 品詞で読みが決まる語: `POS_READING_OVERRIDES`(「他」の名詞/接頭詞)
/// - 後続で読みが決まる語: 「数」(数詞が続けば「スウ」、続かなければ「カズ」)
/// - アルファベット: 数字トークンの直後 → 単位読み(該当する場合)、なければアルファベット読み
/// - アルファベット: 数字トークンと隙間なく隣接 → 単位読み(該当する場合)、なければアルファベット読み
/// - 繰り返し記号「々」: 直前のトークンの読み(そこだけ無音になるのを防ぐ)
/// - 仮名のみの語: 表層形をカタカナ化した読み(未知語で読みが空になるケースの救済)
fn apply_contextual_readings(tokens: &mut [Token]) {
Expand Down Expand Up @@ -404,7 +404,8 @@ fn apply_contextual_readings(tokens: &mut [Token]) {
continue;
}

let preceded_by_number = i > 0 && is_number_like(&tokens[i - 1]);
let preceded_by_number =
i > 0 && tokens[i - 1].end == tokens[i].start && is_number_like(&tokens[i - 1]);

if preceded_by_number && let Some(reading) = unit_reading(&tokens[i].surface) {
let arc: Arc<str> = Arc::from(reading);
Expand Down Expand Up @@ -1911,6 +1912,8 @@ mod tests {
reading_token("3", "名詞,数,*,*", "サン"),
reading_token(surface, NOUN, ""),
];
tokens[1].start = tokens[0].end;
tokens[1].end += tokens[1].start;
apply_contextual_readings(&mut tokens);
assert_eq!(&*tokens[1].reading, expected);
}
Expand All @@ -1919,6 +1922,59 @@ mod tests {
assert_eq!(&*tokens[0].reading, "ナサ");
}

#[test]
fn test_alphabet_unit_readings_require_adjacent_number_spans() {
let words = [
("第", 0, NOUN),
("3", 0, "名詞,数,*,*"),
("3", 0, "名詞,数,*,*"),
("会議室", 0, NOUN),
(" ", 0, "記号,空白,*,*"),
];
for (surface, unit, spelled) in [
("F", "ファラド", "エフ"),
("A", "アンペア", "エー"),
("W", "ワット", "ダブリュー"),
("V", "ボルト", "ブイ"),
("km", "キロメートル", "ケーエム"),
("GB", "ギガバイト", "ジービー"),
("F", "ファラド", "エフ"),
("km", "キロメートル", "ケーエム"),
] {
for known in [false, true] {
let mut entries = words.to_vec();
if known {
entries.push((surface, 0, NOUN));
}
let dict = cost_dict(&entries);
let mut workspace = LatticeWorkspace::new();
for number in ["3", "3"] {
for gap in ["", " ", " ", "\t", "\n", "\r\n", " "] {
let text = format!("第{number}{gap}{surface}会議室");
let offset = 17;
// 既存の出力へ追加する経路でも、元入力の位置で隣接を判定する。
let mut out = vec![reading_token("3", "名詞,数,*,*", "サン")];
workspace
.tokenize_into(&text, &dict, offset, &mut out)
.unwrap();
let token = out.iter().find(|t| &*t.surface == surface).unwrap();
let expected = if gap.is_empty() { unit } else { spelled };
assert_eq!(&*token.reading, expected, "{text:?}, known={known}");
assert_eq!(&*token.pronunciation, expected, "{text:?}");
let start = offset + "第".len() + number.len() + gap.len();
assert_eq!((token.start, token.end), (start, start + surface.len()));
assert_eq!(token.is_known, known);
}
}
let mut out = vec![reading_token("3", "名詞,数,*,*", "サン")];
workspace
.tokenize_into(surface, &dict, 1, &mut out)
.unwrap();
assert_eq!(&*out[1].reading, spelled);
}
}
}

#[test]
fn test_ten_minutes_euphony_requires_numeric_ten_and_minute_suffix() {
for (ten_pos, counter, counter_pos, expected) in [
Expand Down
Loading