Editor: Find & Replace, read-aloud, backup, typography, phonetic, org niceties
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
This commit is contained in:
101
scripts/build_phonetic.py
Normal file
101
scripts/build_phonetic.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user