#!/usr/bin/env python3 """Build the embedded English->Chinese gloss dataset from ECDICT. Petal shows an instant Chinese gloss when the writer (an English-as-a-second- language user whose first language is Mandarin) hovers or right-clicks a word. The gloss is served offline from a small map compiled into the Go binary, the same way the English definitions/synonyms are (see internal/lexicon). Source: ECDICT (https://github.com/skywind3000/ECDICT), MIT-licensed. We keep only common single words that carry a Chinese translation, trim each gloss to a couple of senses, and drop the noisy "[网络]" (internet-slang) lines — the result gzips to a few MB, in line with the other lexicon assets. Usage: curl -sL https://raw.githubusercontent.com/skywind3000/ECDICT/master/ecdict.csv -o ecdict.csv python3 scripts/build_gloss.py ecdict.csv internal/lexicon/data/gloss.json.gz """ import csv import gzip import json import re import sys # Frequency gate: keep a word only if ECDICT ranks it in the BNC or COCA # frequency lists (frq/bnc > 0). That bounds the asset to the words an ESL # writer actually meets, dropping the long tail of archaic/technical headwords. # Words below this rank but present nowhere in the frequency lists are skipped. MAX_RANK = 50000 # A gloss is at most this many sense-lines and characters, so the hover bubble # stays a glance, not a wall of text. MAX_SENSES = 3 MAX_CHARS = 80 # Single English word: letters, with internal apostrophe/hyphen (so "don't", # "well-being" survive but multi-word phrases and codes are dropped). WORD_RE = re.compile(r"^[a-z][a-z'\-]*[a-z]$|^[a-z]$") # Tag lines we drop from a translation: "[网络]" is crowd-sourced internet slang, # "[俚]" slang, "[古]" archaic — none help an ESL writer pick everyday meaning. DROP_TAG_RE = re.compile(r"^\[(网络|俚|古|罕|废)\]") def clean_gloss(translation: str) -> str: """Trim an ECDICT translation to a compact, glanceable Chinese gloss.""" # ECDICT separates senses with a literal backslash-n; normalize to real # newlines (and tolerate genuine newlines) before splitting. translation = translation.replace("\\n", "\n") senses = [] for line in translation.split("\n"): line = line.strip() if not line or DROP_TAG_RE.match(line): continue senses.append(line) if len(senses) >= MAX_SENSES: break gloss = ";".join(senses) if len(gloss) > MAX_CHARS: gloss = gloss[:MAX_CHARS].rstrip(";,;, ") + "…" return gloss def rank(row: dict) -> int: """Best (lowest, non-zero) frequency rank across COCA and BNC.""" ranks = [] for key in ("frq", "bnc"): try: v = int(row.get(key) or 0) except ValueError: v = 0 if v > 0: ranks.append(v) return min(ranks) if ranks else 0 def main(src: str, dst: str) -> None: out: dict[str, str] = {} kept_rank: dict[str, int] = {} with open(src, newline="", encoding="utf-8") as f: for row in csv.DictReader(f): word = (row.get("word") or "").strip().lower() translation = (row.get("translation") or "").strip() if not word or not translation or not WORD_RE.match(word): continue r = rank(row) if r == 0 or r > MAX_RANK: continue gloss = clean_gloss(translation) if not gloss: continue # On a duplicate headword keep the more frequent (lower-rank) entry. if word in kept_rank and kept_rank[word] <= r: continue out[word] = gloss kept_rank[word] = r payload = json.dumps(out, ensure_ascii=False, separators=(",", ":")).encode("utf-8") with gzip.open(dst, "wb", compresslevel=9) as gz: gz.write(payload) print(f"{len(out)} words -> {dst} ({len(payload)} bytes json)") if __name__ == "__main__": if len(sys.argv) != 3: sys.exit("usage: build_gloss.py ") main(sys.argv[1], sys.argv[2])