Files
999/scripts/rebuild_zodiac_intersections.py

333 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Chinese zodiac: hanzi, earthly branches, lore, lexicon intersections."""
from __future__ import annotations
import json
import re
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CANON = ROOT / "oracle_enricher" / "data" / "traditions_canon.json"
ASSET = ROOT / "assets" / "data" / "traditions_canon.json"
LEX = ROOT / "oracle_enricher" / "data" / "lexicon.json"
DART = ROOT / "lib" / "data" / "words_data.dart"
HTML = ROOT / "lexicon_viewer.html"
MD = ROOT / "words_alphabetical.md"
YAML = ROOT / "oracle_enricher" / "data" / "sources" / "chinese_zodiac.yaml"
ZODIAC_TAG_RE = re.compile(r"\[Китайский гороскоп:[^\]]*\]\s*")
DART_RESTORE = {
"вызвать": "обезьяна",
}
RESTORE_META = {
"обезьяна": (
"существительное",
"Тотем / Архетип",
"Ловкий ум ветвей, игра и трюк, существо, что снимает маску с порядка и собирает её заново.",
),
}
# n, id, ru, pinyin, animal_hanzi, branch_hanzi, branch_py, emoji, element, polarity, primary, cluster, lore
STATIONS = [
(
1, "zodiac_1", "Крыса", "Шу", "鼠", "子", "Цзы", "🐀", "Вода", "Ян",
"крыса", ["начало", "цикл", "вода"],
"Первая земная ветвь, полночь и север: янская вода, что просачивается в любую щель. "
"Крыса открывает двенадцатилетний круг — нюх на перемену, запас на чёрный день, ум, "
"который выживает там, где сила ломается. В раскладе — чуткость к скрытой выгоде и скрытой "
"утечке; тень знака — мелочность, подозрительность и грызня запасов.",
),
(
2, "zodiac_2", "Бык", "Ню", "牛", "丑", "Чоу", "🐂", "Земля", "Инь",
"бык", ["труд", "земля", "сила"],
"Вторая ветвь, иньская земля плуга. Бык тянет борозду, пока другие уже спят: упорство, "
"долг, выносливость, которая не нуждается в зрителях. Знак обещает плод только труду и "
"времени. Тень — косность, упрямство и неспособность вовремя остановить плуг.",
),
(
3, "zodiac_3", "Тигр", "Ху", "虎", "寅", "Инь", "🐅", "Дерево", "Ян",
"тигр", ["охотник", "воля", "страсть"],
"Третья ветвь, янское дерево весны: тигр сходит с горы. Доблесть, царственный прыжок, "
"пыл, который рвёт преграду, а не обходит её. В раскладе — лидерский порыв и право на риск. "
"Тень — вспышка без расчёта, охота ради охоты, ярость, что жрёт своих.",
),
(
4, "zodiac_4", "Кролик", "Ту", "兔", "卯", "Мао", "🐇", "Дерево", "Инь",
"заяц", ["мир", "луна", "дерево"],
"Четвёртая ветвь, иньское дерево рассвета. Кролик (заяц восточного круга) живёт чуткостью: "
"грация, дипломатия, умение не попасть под удар. Знак советует мягкий ход и верный момент. "
"Тень — трусость, вечный обход и бегство в нору, когда пора стоять.",
),
(
5, "zodiac_5", "Дракон", "Лун", "龍", "辰", "Чэнь", "🐉", "Земля", "Ян",
"дракон", ["царь", "власть", "небо"],
"Пятая ветвь, янская земля, но животное небесное. Дракон — единственный из двенадцати, "
"кто не ходит по пыли: удача, харизма, преображение, дождь, который зовут, когда земля трескается. "
"Знак даёт размах и требует меры. Тень — мания величия и власть без служения.",
),
(
6, "zodiac_6", "Змея", "Шэ", "蛇", "巳", "Сы", "🐍", "Огонь", "Инь",
"змей", ["мудрость", "тайна", "яд"],
"Шестая ветвь, иньский огонь, что тлеет под кожей, а не пылает в лицо. Змея сбрасывает "
"кожу и помнит нору: проницательность, мистика, холодный ум трансформации. В раскладе — "
"тайна, которую нельзя брать силой. Тень — яд, ревность и коварство без мудрости.",
),
(
7, "zodiac_7", "Лошадь", "Ма", "馬", "午", "У", "🐎", "Огонь", "Ян",
"конь", ["свобода", "путь", "огонь"],
"Седьмая ветвь, полдень, янский огонь скачки. Конь не стоит в стойле: свобода, путь, "
"благородный ход, энтузиазм, который сжигает версту. Знак велит двигаться и не отдавать "
"поводья страху. Тень — нетерпение, бег от себя и ветер, который не знает яслей.",
),
(
8, "zodiac_8", "Коза", "Ян", "羊", "未", "Вэй", "🐐", "Земля", "Инь",
"коза", ["мир", "плод", "земля"],
"Восьмая ветвь, иньская земля пастбища. Коза (овца круга) держит вкус, стадо и мягкий нрав: "
"художество, сочувствие, гармония, которую не куют мечом. Знак просит беречь хрупкое. "
"Тень — каприз, жалость к себе и пассивность, когда пастбище уже вытоптано.",
),
(
9, "zodiac_9", "Обезьяна", "Хоу", "猴", "申", "Шэнь", "🐒", "Металл", "Ян",
"обезьяна", ["игра", "маска", "смех"],
"Девятая ветвь, янский металл ветвей. Обезьяна — трикстер круга: ловкость, быстрый ум, "
"игра, что снимает корону с идола и ставит её обратно уже как шутку. Знак даёт выход "
"из тупика через смекалку. Тень — лукавство, насмешка вместо дела и верность, которая длится до скуки.",
),
(
10, "zodiac_10", "Петух", "Цзи", "雞", "酉", "Ю", "🐓", "Металл", "Инь",
"петух", ["рассвет", "железо", "страж"],
"Десятая ветвь, иньский металл зари. Петух кричит, когда ещё темно: бдительность, прямота, "
"порядок двора, встреча света. Знак велит назвать вещь своим именем и стоять на часах. "
"Тень — хвастовство, придирка и крик, который будит дом впустую.",
),
(
11, "zodiac_11", "Собака", "Гоу", "狗", "戌", "Сюй", "🐕", "Земля", "Ян",
"собака", ["друг", "страж", "защита"],
"Одиннадцатая ветвь, янская земля порога. Собака сторожит своих: верность, честность, "
"чутьё на неправду, служба, которая не торгуется. В раскладе — союз, которому можно верить. "
"Тень — тревога, пессимизм и преданность, уже слепая к тому, кого сторожит.",
),
(
12, "zodiac_12", "Свинья", "Чжу", "豬", "亥", "Хай", "🐗", "Вода", "Инь",
"кабан", ["изобилие", "пир", "вода"],
"Двенадцатая ветвь, иньская вода, что закрывает круг и кормит зиму. Свинья (кабан) — "
"прямодушие, щедрость, достаток без позы, напор того, кто не играет в тонкость. Знак "
"даёт сытость и искренность. Тень — наивность, чревоугодие и неумение сказать «нет».",
),
]
def restore_dart(text: str) -> str:
for old, new in DART_RESTORE.items():
pos, cat, desc = RESTORE_META[new]
desc_esc = desc.replace("\\", "\\\\").replace('"', '\\"')
def repl(m: re.Match, new=new, pos=pos, cat=cat, desc_esc=desc_esc) -> str:
return (
f"{m.group(1)}{new}{m.group(2)}"
f"{m.group(3)}{pos}{m.group(4)}"
f"{m.group(5)}{cat}{m.group(6)}"
f"{m.group(7)}{desc_esc}{m.group(8)}"
)
pat = re.compile(
rf'(word: "){re.escape(old)}(",\s*\n)'
rf'(\s*partOfSpeech: ")[^"]+(",\s*\n)'
rf'(\s*frequency: [^\n]+\n\s*category: ")[^"]+(",\s*\n)'
rf'(\s*description: ")[^"]*(")',
)
if f'word: "{new}",' in text and f'word: "{old}",' not in text:
continue
text2, n = pat.subn(repl, text, count=1)
if n != 1:
raise SystemExit(f"failed to restore {old} -> {new} (n={n})")
text = text2
return text
def payload(word: str, dart_by_word: dict, lex_by_word: dict) -> dict:
lex = lex_by_word.get(word) or {}
return {
"id": dart_by_word[word],
"word": word,
"emoji": lex.get("emoji") or "",
"pos": lex.get("pos") or "существительное",
"category": lex.get("category") or "",
"desc": "",
}
def main() -> None:
dart_text = restore_dart(DART.read_text(encoding="utf-8"))
DART.write_text(dart_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)
}
lex_by_word = {e["word"]: e for e in json.loads(LEX.read_text(encoding="utf-8"))}
canon = json.loads(CANON.read_text(encoding="utf-8"))
word_to_tags: dict[str, list[str]] = defaultdict(list)
missing = []
stations = []
for rec in STATIONS:
(
n, sid, ru, py, hanzi, branch, bpy, emoji, element, pol,
prim, extra, lore,
) = rec
if prim not in dart_by_word:
missing.append(f"primary {sid}/{prim}")
continue
cluster = []
seen = {prim}
for w in extra:
if w in seen:
continue
if w not in dart_by_word:
missing.append(f"cluster {sid}/{w}")
continue
seen.add(w)
cluster.append(payload(w, dart_by_word, lex_by_word))
name = f"{n} {ru} ({py})"
st = {
"id": sid,
"name": name,
"symbol": emoji,
"hanzi": f"{hanzi} · {branch}",
"group": f"Земные ветви · {branch} {bpy} · {pol}-{element}",
"lore": lore,
"primary": payload(prim, dart_by_word, lex_by_word),
"cluster": cluster,
}
stations.append(st)
tag = f"[Китайский гороскоп: {n}/12 {ru} {hanzi} ({py} / {bpy})]"
word_to_tags[prim].append(tag)
for c in cluster:
word_to_tags[c["word"]].append(tag)
if missing:
print("WARN", missing)
z = canon["zodiac"]
z["name"] = "Китайский Зодиак (十二生肖)"
z["count"] = len(stations)
z["description"] = (
"Двенадцать земных ветвей (地支): животные восточного круга с классическими иероглифами, "
"стихией У-син, полярностью инь/ян и пересечениями словаря 999."
)
z["stations"] = stations
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)
nstamp = 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 = ZODIAC_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"
nstamp += 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 = HTML.read_text(encoding="utf-8")
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:]
)
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))
dart_descs = dict(re.findall(r'word: "([^"]+)"[\s\S]*?description: "([^"]*)"', dart2))
for w in words:
wd = w.get("word")
if wd in dart_descs:
w["description"] = dart_descs[wd]
html = html[: m.start(1)] + json.dumps(words, ensure_ascii=False) + html[m.end(1) :]
HTML.write_text(html, encoding="utf-8")
if MD.exists():
md = MD.read_text(encoding="utf-8")
def repl_block(mm: re.Match) -> str:
word = mm.group(1)
body = mm.group(3)
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(2)}`\n{body2}"
md2 = re.sub(
r"### ([^\s`]+) `(#[^`]+)`\n([\s\S]*?)(?=\n### |\n## |\Z)",
repl_block,
md,
)
MD.write_text(md2, encoding="utf-8")
yaml_lines = [
"# 12 Animals of Chinese Zodiac (十二生肖 / 12 земных ветвей)",
"system: chinese_zodiac",
'version: "2.0"',
"animals:",
]
yaml_ids = [
"rat", "ox", "tiger", "rabbit", "dragon", "snake",
"horse", "goat", "monkey", "rooster", "dog", "pig",
]
for rec, yid in zip(STATIONS, yaml_ids):
n, sid, ru, py, hanzi, branch, bpy, emoji, element, pol, prim, extra, lore = rec
yaml_lines.append(f" {yid}:")
yaml_lines.append(f" number: {n}")
yaml_lines.append(f' name: "{ru}"')
yaml_lines.append(f' hanzi: "{hanzi}"')
yaml_lines.append(f' pinyin: "{py}"')
yaml_lines.append(f' branch: "{bpy} ({branch})"')
yaml_lines.append(f' element: "{element}"')
yaml_lines.append(f' polarity: "{pol}"')
yaml_lines.append(f' lore: "{lore}"')
yaml_lines.append(" names_ru:")
yaml_lines.append(f" - {prim}")
for w in extra:
yaml_lines.append(f" - {w}")
YAML.write_text("\n".join(yaml_lines) + "\n", encoding="utf-8")
print(f"zodiac {len(stations)} stamped {nstamp} tagged {len(word_to_tags)}")
for st in stations:
cl = [c["word"] for c in st["cluster"]]
print(f" {st['id']:10} {st['hanzi']:8} {st['name']:22} prim={st['primary']['word']:10} {cl}")
if __name__ == "__main__":
main()