Spaces:
Running on Zero
Running on Zero
Download src/japanese_avatar/nlp/ruby.py from WolfDavid/japanese-learning-avatar: direct link, hf CLI and curl.
- Browser
- Download file 3.94 kB
-
https://huggingface.co/spaces/WolfDavid/japanese-learning-avatar/resolve/706670a3ece0683b9770f0b7db36e4daa955f91e/src/japanese_avatar/nlp/ruby.py
- Command line
-
hf download hf://spaces/WolfDavid/japanese-learning-avatar@706670a3ece0683b9770f0b7db36e4daa955f91e/src/japanese_avatar/nlp/ruby.py
-
curl -L -o ruby.py https://huggingface.co/spaces/WolfDavid/japanese-learning-avatar/resolve/706670a3ece0683b9770f0b7db36e4daa955f91e/src/japanese_avatar/nlp/ruby.py
3.94 kB
| """Okurigana alignment: a unit's surface + its hiragana reading -> per-kanji-run ruby spans. | |
| The reading comes from Sudachi (``units.build_units`` already converted it to hiragana with | |
| ``jaconv.kata2hira``); this module only ALIGNS it - it never guesses a reading (research | |
| § Anti-Patterns: no furigana by regex or by romaji). The standard trick: split the surface into | |
| kana runs and non-kana runs, force every kana run to match the reading verbatim, and let each | |
| non-kana run absorb whatever lies between with a non-greedy group. ``<rt>`` then sits over 食, | |
| not over 食べ. Verified on the 23 pairs in ``tests/test_ruby.py``. | |
| Ruby is per kanji run: 待ち合わせ -> [待,ま] [ち,-] [合,あ] [わせ,-]. A run's reading cannot be | |
| split further (大阪駅 -> one span おおさかえき), which is also why the kanji-axis gate (02-05) | |
| is evaluated per run. | |
| Fallback is honest: when the kana runs cannot be matched (a reading that does not fit the | |
| surface) the whole unit gets one ruby span carrying the whole reading. Never raises. | |
| ``jaconv.normalize`` must NOT be used on readings - it folds ー and small kana. Pure function | |
| over strings; no Sudachi import. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import jaconv | |
| #: Character class body for kana: hiragana ぁ-ゖ, the hiragana iteration marks ゝゞ, katakana | |
| #: ァ-ヶ, the prolonged-sound mark ー and the katakana iteration marks ヽヾ. Everything else - | |
| #: kanji, 々, digits, Latin, punctuation - is a "non-kana run" and receives ruby. | |
| KANA = "ぁ-ゖゝゞァ-ヶーヽヾ" | |
| _RUNS = re.compile(f"[{KANA}]+|[^{KANA}]+") | |
| _KANA_START = re.compile(f"[{KANA}]") | |
| #: Kanji code point ranges: CJK Unified Ideographs Extension A, CJK Unified Ideographs, CJK | |
| #: Compatibility Ideographs, the supplementary planes (Extensions B-F, e.g. 𠮷), and U+3005 々, | |
| #: the iteration mark, which repeats the kanji before it and therefore belongs to its run. | |
| _KANJI_RANGES = ( | |
| (0x3400, 0x4DBF), | |
| (0x4E00, 0x9FFF), | |
| (0xF900, 0xFAFF), | |
| (0x20000, 0x2FA1F), | |
| ) | |
| _ITERATION_MARK = 0x3005 | |
| def is_kanji(ch: str) -> bool: | |
| """True for a single kanji character (including 々). False for kana, Latin, digits, marks.""" | |
| cp = ord(ch) | |
| return cp == _ITERATION_MARK or any(lo <= cp <= hi for lo, hi in _KANJI_RANGES) | |
| def _runs(surface: str) -> list[tuple[str, bool]]: | |
| """``[(text, is_kana), ...]`` - the surface split into maximal kana / non-kana runs.""" | |
| return [(m.group(0), bool(_KANA_START.match(m.group(0)))) for m in _RUNS.finditer(surface)] | |
| def kanji_runs(surface: str) -> list[str]: | |
| """The non-kana runs of ``surface`` that contain at least one kanji, in order. | |
| These are the spans that receive ruby and that the kanji-axis gate evaluates: if any kanji in | |
| a run is above the learner's level, the whole run shows its reading. | |
| """ | |
| return [text for text, kana in _runs(surface) if not kana and any(is_kanji(c) for c in text)] | |
| def align_ruby(surface: str, reading_hira: str) -> list[list[str | None]]: | |
| """Split ``surface`` into ``[text, rt]`` spans: ``rt`` is None over kana, hiragana otherwise. | |
| ``reading_hira`` must already be hiragana (the caller converts Sudachi's katakana with | |
| ``jaconv.kata2hira``). Returns one bare span for an all-kana surface, one whole-unit ruby | |
| span when the reading cannot be aligned, and never raises. | |
| """ | |
| runs = _runs(surface) | |
| if all(kana for _, kana in runs): | |
| return [[surface, None]] | |
| if not reading_hira: | |
| return [[surface, reading_hira]] | |
| pattern = "".join( | |
| "(" + re.escape(jaconv.kata2hira(text)) + ")" if kana else "(.+?)" for text, kana in runs | |
| ) | |
| match = re.fullmatch(pattern, reading_hira) | |
| if not match: | |
| return [[surface, reading_hira]] # honest fallback: whole-unit ruby | |
| return [ | |
| [text, None if kana else group] | |
| for (text, kana), group in zip(runs, match.groups(), strict=True) | |
| ] | |