open-standards/aac/aac-rs/tools/finder/build_ja_kana.py
Transaction Science 88317db58e WIP(aac): a form by its likeliest word only; a board noun is itself but where a verb is; ties by particles; する verbs; two words lead nowhere
H-A: by_form from a board word's fewest-endings reading, one word only;
by_readings at the fewest endings among the list's words, nowhere when
that word is on no board (つけて never つかれた, おくれた never おくる).
H-B: a board word is a noun only where its entry in its sense is one
(finder_ja.json classes "n"); the verb after を/が, or at the end after a
question word or adverb; the noun alone, after の, before a case particle.
M1: equal cost and one-kana words: more particles, then earlier.
M2: a board's <noun>する is a する verb where the list has the noun as one.
M3: two words of the list at the fewest endings lead nowhere but where
ja_inflect::DOMINANT settles it; after を, not an intransitive verb (kana
list "vi"; "adv" for adverbs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 01:17:53 -04:00

187 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""The kana word list for reading Japanese typed without spaces (aac-core
finder.rs `words_in_listed`, aac-web finder.rs).
Japanese is written without spaces, and a word typed in kana (はなや,
めがね) must be read as the word it is, not as the one-kana words of the
boards inside it (はな + や, め + が + ね). The finder reads a kana run by the
words of this list and of the boards, the fewest words first. The list is
the kana readings of JMdict's common entries — an entry with a priority tag
(news1, ichi1, spec1, spec2, gai1) on any of its spellings — each reading
that is itself marked common, or the only way the entry is written; readings
JMdict marks out-dated, search-only or rare are left out. Hiragana and
katakana, one word a line, sorted. It is fetched only by a device whose
person writes Japanese, as the pinyin index is for Chinese.
Each word that inflects carries its classes after a tab, from JMdict's parts
of speech, so a verb or adjective typed in an inflected form is read as one
word and led to its dictionary form (aac-core ja_inflect.rs). The classes are
each entry's own, the entries apart by "|": かえる is 変える (v1), 帰る (v5) and
蛙 (none), "v1|v5|", so a spelling whose entries differ is known to be more
than one word, and a form of one of them is never taken for another:
v1 Ichidan verbs (v1, v1-s, vz)
v5 Godan verbs (v5*)
vk くる
vs する itself (vs-i, vs-s)
vsn nouns that take する (vs)
adj i-adjectives (adj-i, adj-ix)
exp set phrases and interjections (exp, int): a word that may look
inflected but is the phrase it is (すみません is not すむ's)
adv adverbs (adv, adv-to): どうした, もうした — a verb's form after one
at the end of a sentence is the verb
vi a verb whose senses are all intransitive (vi, and no vt): after an
object with を it is not the verb meant (ふくをきて is not 来る's)
Writes:
data/kana/ja.<sha256-12>.txt the words, one a line (word, or word<TAB>classes,
one entry's classes apart from the next's by "|")
data/kana/index.json file, sha256, bytes, entries, source
data/kana/LICENSE.md its licence (JMdict's, CC BY-SA 4.0)
Usage:
python3 tools/finder/build_ja_kana.py JMdict_e.gz
JMdict is the property of the Electronic Dictionary Research and Development
Group (EDRDG), used under its licence (CC BY-SA 4.0,
https://www.edrdg.org/edrdg/licence.html); the list this writes is shared
under the same licence.
"""
import gzip
import hashlib
import json
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
OUT = Path(__file__).resolve().parents[2] / "data" / "kana"
PRIORITY = {"news1", "ichi1", "spec1", "spec2", "gai1"}
KANA = re.compile(r"^[ぁ-ゖァ-ヺー]+$")
SKIP_READING = {"out-dated or obsolete kana usage", "search-only kana form", "rarely used kana form"}
LICENSE = """# Kana word list — licence
Generated by `aac/aac-rs/tools/finder/build_ja_kana.py`; do not edit by hand.
`{file}` lists the kana readings of the common entries of **JMdict**
(`JMdict_e`), the Japanese–multilingual dictionary of the Electronic
Dictionary Research and Development Group (EDRDG), one word a line, so that
Japanese typed without spaces is read as its words (the finder of the AAC
reference app).
JMdict is the property of the EDRDG and is used in conformance with the
Group's licence: Creative Commons Attribution-ShareAlike 4.0 International
(CC BY-SA 4.0), https://creativecommons.org/licenses/by-sa/4.0/ ;
https://www.edrdg.org/edrdg/licence.html ;
https://www.edrdg.org/jmdict/j_jmdict.html .
The list is a work made from JMdict and is shared under the same licence,
CC BY-SA 4.0. Changes: only the readings in kana of the entries JMdict marks
common (priority tags news1, ichi1, spec1, spec2, gai1) were kept — each
reading itself marked common, or the only way its entry is written — without
readings marked out-dated, search-only or rare, each with the classes of
each of its entries' parts of speech (verb and adjective classes, whether it
is a set phrase or interjection, an adverb, or a verb that is only
intransitive, only);
nothing else of JMdict is in it.
"""
def pos_codes(jmdict: str) -> dict[str, str]:
"""JMdict's part-of-speech descriptions -> their entity names (the parser
gives the descriptions)."""
with gzip.open(jmdict, "rt", encoding="utf-8") as f:
head = f.read(300_000)
return {desc: name for name, desc in re.findall(r'<!ENTITY ([\w-]+) "([^"]*)">', head)}
def word_class(code: str) -> str | None:
if code in ("v1", "v1-s", "vz"):
return "v1"
if code.startswith("v5"):
return "v5"
if code == "vk":
return "vk"
if code in ("vs-i", "vs-s"):
return "vs"
if code == "vs":
return "vsn"
if code in ("adj-i", "adj-ix"):
return "adj"
if code in ("exp", "int"):
return "exp"
if code in ("adv", "adv-to"):
return "adv"
return None
ORDER = ["v1", "v5", "vk", "vs", "vsn", "adj", "exp", "adv", "vi"]
VERBS = {"v1", "v5", "vk", "vs"}
def entry_classes(el: ET.Element, codes: dict[str, str]) -> set[str]:
"""An entry's classes, from all its senses' parts of speech, with "vi"
for a verb none of whose senses is transitive and some intransitive."""
pos = {codes.get(p.text or "", "") for s in el.findall("sense") for p in s.findall("pos")}
found = {c for p in pos for c in [word_class(p)] if c}
if found & VERBS and "vi" in pos and "vt" not in pos:
found.add("vi")
return found
def main(jmdict: str) -> None:
codes = pos_codes(jmdict)
words: set[str] = set()
# Each spelling's entries, by their classes (a set of class sets).
classes: dict[str, set[frozenset[str]]] = {}
with gzip.open(jmdict, "rb") as f:
for _, el in ET.iterparse(f, events=("end",)):
if el.tag != "entry":
continue
kebs = [{p.text for p in k.findall("ke_pri")} for k in el.findall("k_ele")]
rebs = [
(r.findtext("reb"), {p.text for p in r.findall("re_pri")}, {i.text for i in r.findall("re_inf")})
for r in el.findall("r_ele")
]
common = any(p & PRIORITY for _, p, _ in rebs) or any(k & PRIORITY for k in kebs)
if common:
found = entry_classes(el, codes)
for reb, pri, inf in rebs:
if reb and KANA.match(reb) and not inf & SKIP_READING and (pri & PRIORITY or not kebs):
words.add(reb)
classes.setdefault(reb, set()).add(frozenset(found))
el.clear()
lines = []
for w in sorted(words):
entries = sorted(",".join(c for c in ORDER if c in found) for found in classes.get(w, ()))
if any(entries):
lines.append(f"{w}\t{'|'.join(entries)}\n")
else:
lines.append(f"{w}\n")
text = "".join(lines).encode("utf-8")
sha = hashlib.sha256(text).hexdigest()
OUT.mkdir(parents=True, exist_ok=True)
for old in OUT.glob("ja.*.txt"):
old.unlink()
name = f"ja.{sha[:12]}.txt"
(OUT / name).write_bytes(text)
index = {
"format": "aac-kana-words",
"builder": "aac/aac-rs/tools/finder/build_ja_kana.py",
"file": name,
"sha256": sha,
"bytes": len(text),
"entries": len(words),
"source": "JMdict_e (EDRDG), https://www.edrdg.org/jmdict/j_jmdict.html: kana readings of common entries, with each entry's inflecting classes from its parts of speech",
"licence": "CC-BY-SA-4.0",
}
(OUT / "index.json").write_text(json.dumps(index, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
(OUT / "LICENSE.md").write_text(LICENSE.format(file=name), encoding="utf-8")
print(f"{len(words)} kana words, {len(text)} bytes -> {OUT / name}")
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit(__doc__)
main(sys.argv[1])