feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stamp I Ching station intersections onto the 1000-word catalog — like runes.
|
||||
|
||||
Does not add words to the 1000. Hexagrams stay atlas stations; each intersecting
|
||||
lexicon word gets an [И Цзин: …] tag. Station clusters pick up extra YAML names
|
||||
that already exist in the lexicon.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CANON = ROOT / "oracle_enricher" / "data" / "traditions_canon.json"
|
||||
ASSET = ROOT / "assets" / "data" / "traditions_canon.json"
|
||||
YAML_PATH = ROOT / "oracle_enricher" / "data" / "sources" / "iching.yaml"
|
||||
DART = ROOT / "lib" / "data" / "words_data.dart"
|
||||
HTML = ROOT / "lexicon_viewer.html"
|
||||
MD = ROOT / "words_alphabetical.md"
|
||||
LEX = ROOT / "oracle_enricher" / "data" / "lexicon.json"
|
||||
|
||||
ICHING_TAG_RE = re.compile(r"\[И Цзин:[^\]]*\]\s*")
|
||||
|
||||
# Ba Gua images. A hexagram that merely *contains* ☲ does not own «огонь».
|
||||
# Keep the trigram itself and the doubled hexagram (same gua twice).
|
||||
ELEMENT_OWNERS = {
|
||||
"огонь": {"li", "hex_30_li_hex"},
|
||||
"вода": {"kan", "hex_29_kan_hex"},
|
||||
"земля": {"kun", "hex_02_kun"},
|
||||
"воздух": {"xun", "hex_57_xun_hex"},
|
||||
"ветер": {"xun", "hex_57_xun_hex"},
|
||||
"озеро": {"dui", "hex_58_dui_hex"},
|
||||
"небо": {"qian", "hex_01_qian"},
|
||||
"гора": {"gen", "hex_52_gen_hex"},
|
||||
"гром": {"zhen", "hex_51_zhen_hex"},
|
||||
}
|
||||
|
||||
|
||||
def allowed_element(word: str, station_id: str) -> bool:
|
||||
owners = ELEMENT_OWNERS.get(word)
|
||||
if owners is None:
|
||||
return True
|
||||
return station_id in owners
|
||||
|
||||
|
||||
def tag_for(station: dict) -> str:
|
||||
name = station["name"]
|
||||
if station["id"].startswith("hex_"):
|
||||
# "Гексаграмма 01 ䷀ Цянь (Творчество)"
|
||||
inner = name.replace("Гексаграмма ", "", 1)
|
||||
return f"[И Цзин: Гексаграмма {inner}]"
|
||||
return f"[И Цзин: {name}]"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
canon = json.loads(CANON.read_text(encoding="utf-8"))
|
||||
lex = json.loads(LEX.read_text(encoding="utf-8"))
|
||||
by_word = {e["word"]: e for e in lex}
|
||||
yaml_cards = {e["id"]: e for e in yaml.safe_load(YAML_PATH.read_text(encoding="utf-8"))}
|
||||
|
||||
dart_text = DART.read_text(encoding="utf-8")
|
||||
dart_by_word = {
|
||||
w: int(i)
|
||||
for i, w in re.findall(r'WordItem\(\s*id:\s*(\d+),\s*word:\s*"([^"]+)"', dart_text)
|
||||
}
|
||||
|
||||
def payload(word: str) -> dict:
|
||||
entry = by_word.get(word) or {}
|
||||
return {
|
||||
"id": dart_by_word.get(word, entry.get("id", 0)),
|
||||
"word": word,
|
||||
"emoji": entry.get("emoji", ""),
|
||||
"pos": entry.get("pos", ""),
|
||||
"category": entry.get("category", ""),
|
||||
"desc": "",
|
||||
}
|
||||
|
||||
word_to_tags: dict[str, list[str]] = defaultdict(list)
|
||||
extra_cluster = 0
|
||||
stripped = 0
|
||||
|
||||
for st in canon["iching"]["stations"]:
|
||||
sid = st["id"]
|
||||
tag = tag_for(st)
|
||||
prim = (st.get("primary") or {}).get("word")
|
||||
if prim and not allowed_element(prim, sid):
|
||||
prim = None
|
||||
keep: list[str] = []
|
||||
seen = {prim} if prim else set()
|
||||
|
||||
def consider(word: str, from_yaml: bool = False) -> None:
|
||||
nonlocal extra_cluster
|
||||
if not word or word in seen:
|
||||
return
|
||||
if word not in by_word and word not in dart_by_word:
|
||||
return
|
||||
if not allowed_element(word, sid):
|
||||
return
|
||||
seen.add(word)
|
||||
keep.append(word)
|
||||
if from_yaml:
|
||||
extra_cluster += 1
|
||||
|
||||
for c in st.get("cluster") or []:
|
||||
consider(c.get("word") or "")
|
||||
y = yaml_cards.get(sid, {})
|
||||
for name in y.get("names_ru") or []:
|
||||
consider(name, from_yaml=True)
|
||||
stripped += max(0, len(st.get("cluster") or []) - len(keep))
|
||||
if not prim and keep:
|
||||
prim = keep.pop(0)
|
||||
seen = {prim, *keep}
|
||||
if prim:
|
||||
st["primary"] = payload(prim)
|
||||
st["cluster"] = [payload(w) for w in keep]
|
||||
|
||||
linked = []
|
||||
if prim:
|
||||
linked.append(prim)
|
||||
linked.extend(keep)
|
||||
for w in linked:
|
||||
if tag not in word_to_tags[w]:
|
||||
word_to_tags[w].append(tag)
|
||||
|
||||
text = json.dumps(canon, ensure_ascii=False, indent=2) + "\n"
|
||||
CANON.write_text(text, encoding="utf-8")
|
||||
ASSET.write_text(text, encoding="utf-8")
|
||||
|
||||
dart_lines = dart_text.splitlines(keepends=True)
|
||||
n = 0
|
||||
i = 0
|
||||
while i < len(dart_lines):
|
||||
wm = re.match(r'(\s*word: ")([^"]+)(",\s*)$', dart_lines[i])
|
||||
if wm:
|
||||
word = wm.group(2)
|
||||
for j in range(i + 1, min(i + 12, len(dart_lines))):
|
||||
raw = dart_lines[j].rstrip("\n")
|
||||
dm = re.match(r'^(\s*description: ")(.*)(",)\s*$', raw)
|
||||
if dm:
|
||||
body = ICHING_TAG_RE.sub("", dm.group(2)).strip()
|
||||
tags = word_to_tags.get(word)
|
||||
if tags:
|
||||
dart_lines[j] = f"{dm.group(1)}{' '.join(tags)} {body}{dm.group(3)}\n"
|
||||
n += 1
|
||||
else:
|
||||
dart_lines[j] = f"{dm.group(1)}{body}{dm.group(3)}\n"
|
||||
break
|
||||
if dart_lines[j].strip().startswith("WordItem("):
|
||||
break
|
||||
i += 1
|
||||
dart2 = "".join(dart_lines)
|
||||
DART.write_text(dart2, encoding="utf-8")
|
||||
|
||||
# HTML WORDS_DATA: update description by word
|
||||
html = HTML.read_text(encoding="utf-8")
|
||||
m = re.search(r"const WORDS_DATA = (\[.*?\]);", html, flags=re.DOTALL)
|
||||
if not m:
|
||||
raise SystemExit("WORDS_DATA not found")
|
||||
words = json.loads(m.group(1))
|
||||
updated_html_words = 0
|
||||
dart_descs = dict(
|
||||
re.findall(r'word: "([^"]+)"[\s\S]*?description: "([^"]*)"', dart2)
|
||||
)
|
||||
for w in words:
|
||||
wd = w.get("word")
|
||||
if wd in dart_descs and w.get("description") != dart_descs[wd]:
|
||||
w["description"] = dart_descs[wd]
|
||||
updated_html_words += 1
|
||||
html = html[: m.start(1)] + json.dumps(words, ensure_ascii=False) + html[m.end(1) :]
|
||||
|
||||
# HTML TRADITIONS_CANON
|
||||
start = html.find(" const TRADITIONS_CANON = ")
|
||||
end = html.find(" const WORDS_DATA = ")
|
||||
if start > 0 and end > start:
|
||||
html = (
|
||||
html[:start]
|
||||
+ " const TRADITIONS_CANON = "
|
||||
+ json.dumps(canon, ensure_ascii=False, indent=2)
|
||||
+ ";\n\n"
|
||||
+ html[end:]
|
||||
)
|
||||
HTML.write_text(html, encoding="utf-8")
|
||||
|
||||
if MD.exists():
|
||||
md = MD.read_text(encoding="utf-8")
|
||||
|
||||
def repl_md(mm: re.Match) -> str:
|
||||
word = mm.group(1)
|
||||
rest_head = mm.group(2)
|
||||
old = mm.group(3)
|
||||
if word not in dart_descs:
|
||||
return mm.group(0)
|
||||
return f"### {word} `#{mm.group(0).split('`#')[1].split('`')[0] if False else ''}"
|
||||
|
||||
# simpler: replace "* **Толкование:** ..." after ### word
|
||||
def repl_block(mm: re.Match) -> str:
|
||||
word = mm.group(1)
|
||||
body = mm.group(2)
|
||||
if word not in dart_descs:
|
||||
return mm.group(0)
|
||||
body2 = re.sub(
|
||||
r"\* \*\*Толкование:\*\* .+",
|
||||
f"* **Толкование:** {dart_descs[word]}",
|
||||
body,
|
||||
count=1,
|
||||
)
|
||||
return f"### {word} `{mm.group(0).split('`')[1]}`\n{body2}"
|
||||
|
||||
md2 = re.sub(
|
||||
r"### ([^\s`]+) `(#[^`]+)`\n([\s\S]*?)(?=\n### |\n## |\Z)",
|
||||
repl_block,
|
||||
md,
|
||||
)
|
||||
MD.write_text(md2, encoding="utf-8")
|
||||
|
||||
print(
|
||||
f"stations={len(canon['iching']['stations'])} "
|
||||
f"words_stamped={len(word_to_tags)} extra_cluster={extra_cluster} "
|
||||
f"stripped_generic={stripped} dart_blocks={n} html_words={updated_html_words}"
|
||||
)
|
||||
for w in ("огонь", "вода", "земля", "воздух", "озеро", "небо", "ветер", "гора", "гром"):
|
||||
print(f" {w:8} {word_to_tags.get(w)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user