Writer power-ups (Phase 11), plus the selection-bubble vs copy/paste fix. - Find & Replace (Ctrl/Cmd+F): SearchHighlight decoration extension + FindReplace bar (match-case, replace-all back-to-front, scroll without popping the selection bubble). - Read-aloud (Web Speech, offline) on the word card and selection bubble. - Keyboard/touch access to the ESL helpers: Ctrl/Cmd+D look up word at caret, Ctrl/Cmd+J rewrite selection, touch long-press lookup. Refactored the right-click handler into a shared openWordLookup(pos). - Whole-corpus backup: GET /api/docs/export-all zips every doc (md/docx), de-dupes filenames, dated name; sidebar download links. TestExportAll. - Smart typography input rules (curly quotes/em-dash/ellipsis), ASCII-only so CJK is untouched. - Duplicate doc, sidebar sort (Recent/Title/Longest), toolbar outline popover. - English phonetic (chosen over pinyin for an English learner): ECDICT-built phonetic.json.gz (46,579 words) + Result.Phonetic + WordCard IPA line; scripts/build_phonetic.py (full build + --seed fallback). - Selection bubble no longer blocks copy/paste: deferred to pointer-up and made click-through except on its buttons. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
102 lines
4.6 KiB
Python
102 lines
4.6 KiB
Python
#!/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()
|