#!/usr/bin/env python3 """Build the embedded English-phonetic dataset. Petal's writer is a native Mandarin speaker learning English, so the useful pronunciation aid is *how to say the English word* — shown in the word popover beside the 🔊 read-aloud button. (Pinyin would annotate Chinese she already reads fluently, so it isn't built.) The phonetic spelling comes from the same ECDICT source as the Chinese gloss, which already carries a `phonetic` column. Two modes: # Full build from ECDICT (same CSV used by build_gloss.py): curl -sL https://raw.githubusercontent.com/skywind3000/ECDICT/master/ecdict.csv -o ecdict.csv python3 scripts/build_phonetic.py ecdict.csv internal/lexicon/data/phonetic.json.gz # Write just the built-in common-word seed (no CSV needed — what ships in-repo # so the feature works out of the box until a full build is run): python3 scripts/build_phonetic.py --seed internal/lexicon/data/phonetic.json.gz """ import csv import gzip import json import re import sys # Same frequency gate as the gloss build, so coverage matches the words an ESL # writer actually meets and the asset stays small. MAX_RANK = 50000 # Single English word (letters with internal apostrophe/hyphen). WORD_RE = re.compile(r"^[a-z][a-z'\-]*[a-z]$|^[a-z]$") # A small, hand-checked seed of common words → IPA. This ships in the repo so the # phonetic line is live immediately; a full ECDICT build overwrites it with broad # coverage. Kept deliberately short and correct rather than large and shaky. SEED = { "hello": "həˈloʊ", "world": "wɜːrld", "people": "ˈpiːpl", "water": "ˈwɔːtər", "river": "ˈrɪvər", "house": "haʊs", "school": "skuːl", "friend": "frɛnd", "family": "ˈfæməli", "mother": "ˈmʌðər", "father": "ˈfɑːðər", "woman": "ˈwʊmən", "language": "ˈlæŋɡwɪdʒ", "english": "ˈɪŋɡlɪʃ", "write": "raɪt", "writing": "ˈraɪtɪŋ", "read": "riːd", "word": "wɜːrd", "sentence": "ˈsɛntəns", "beautiful": "ˈbjuːtɪfl", "happy": "ˈhæpi", "love": "lʌv", "thought": "θɔːt", "through": "θruː", "though": "ðoʊ", "enough": "ɪˈnʌf", "question": "ˈkwɛstʃən", "answer": "ˈænsər", "because": "bɪˈkɔːz", "people's": "ˈpiːplz", "important": "ɪmˈpɔːrtnt", "different": "ˈdɪfərənt", "remember": "rɪˈmɛmbər", "together": "təˈɡɛðər", "morning": "ˈmɔːrnɪŋ", "tonight": "təˈnaɪt", "yesterday": "ˈjɛstərdeɪ", "tomorrow": "təˈmɒroʊ", "weather": "ˈwɛðər", "color": "ˈkʌlər", "colour": "ˈkʌlər", "favourite": "ˈfeɪvərɪt", "favorite": "ˈfeɪvərɪt", "comfortable": "ˈkʌmftəbl", "vegetable": "ˈvɛdʒtəbl", "interesting": "ˈɪntrəstɪŋ", "necessary": "ˈnɛsəsɛri", "february": "ˈfɛbruɛri", "wednesday": "ˈwɛnzdeɪ", "island": "ˈaɪlənd", "knife": "naɪf", "know": "noʊ", "knee": "niː", "hour": "ˈaʊər", "honest": "ˈɒnɪst", "listen": "ˈlɪsn", "castle": "ˈkæsl", "thank": "θæŋk", "think": "θɪŋk", "thing": "θɪŋ", "three": "θriː", "voice": "vɔɪs", "choice": "tʃɔɪs", "nature": "ˈneɪtʃər", "picture": "ˈpɪktʃər", "future": "ˈfjuːtʃər", "culture": "ˈkʌltʃər", "measure": "ˈmɛʒər", "usually": "ˈjuːʒuəli", "decision": "dɪˈsɪʒn", "delicious": "dɪˈlɪʃəs", } def clean_phonetic(p: str) -> str: """Normalize an ECDICT phonetic field: trim, strip wrapping slashes/brackets.""" p = p.strip().strip("/[]").strip() return p def build_from_csv(src: str) -> dict: out = {} with open(src, newline="", encoding="utf-8") as fh: for row in csv.DictReader(fh): word = (row.get("word") or "").strip().lower() phon = clean_phonetic(row.get("phonetic") or "") if not word or not phon or not WORD_RE.match(word): continue try: rank = int(row.get("frq") or 0) or int(row.get("bnc") or 0) except ValueError: rank = 0 if rank <= 0 or rank > MAX_RANK: continue out[word] = phon # Make sure the curated seed is always present (and authoritative). out.update(SEED) return out def main() -> None: args = sys.argv[1:] if not args: sys.exit(__doc__) if args[0] == "--seed": data, dest = dict(SEED), args[1] else: src, dest = args[0], args[1] data = build_from_csv(src) with gzip.open(dest, "wt", encoding="utf-8") as fh: json.dump(data, fh, ensure_ascii=False, separators=(",", ":")) print(f"wrote {len(data)} phonetic entries to {dest}") if __name__ == "__main__": main()