From 6aab4bc299b6e60525e1c446068e7601ffe4da49 Mon Sep 17 00:00:00 2001 From: Yohei <20108042+owayo@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:26:18 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=95=B0=E5=AD=97=E3=81=A8=E9=9B=A2?= =?UTF-8?q?=E3=82=8C=E3=81=9F=E8=8B=B1=E5=AD=97=E3=82=92=E5=8D=98=E4=BD=8D?= =?UTF-8?q?=E8=AA=AD=E3=81=BF=E3=81=97=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 空白を読み飛ばすと、離れた数字が英字の直前のトークンになるため、 階や部屋の英字記号を単位として読んでいた。 入力のバイト位置が連続する場合だけ単位読みを付ける。 既知語・未知語、全角英字、空白類と offset の回帰テストを追加する。 --- docs/architecture.md | 7 +++++- src/lattice.rs | 60 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 34d3dbe..0fef534 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 の明示指定がなければ漢字として未知語候補を作る。 diff --git a/src/lattice.rs b/src/lattice.rs index 5ef1172..5529b37 100644 --- a/src/lattice.rs +++ b/src/lattice.rs @@ -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]) { @@ -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 = Arc::from(reading); @@ -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); } @@ -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 [