Files
999/scripts/rebuild_tarot_intersections.py
T

247 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""Fix Tarot grouping and rebuild lexicon + cross-system parallels.
Колесо Фортуны было с другим group key. У «Мира» раздутый cluster.
"""
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"
ROMAN = ["0", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X",
"XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX", "XXI"]
# station_id -> extra lexicon words (primary is already on the station)
CLUSTERS = {
"fool": ["путь", "начало", "маска", "воздух"],
"magician": ["творец", "воля", "огонь", "вода", "земля", "воздух"],
"high_priestess": ["тайна", "завеса", "луна", "книга", "порог"],
"empress": ["земля", "рост", "росток", "сад", "плод"],
"emperor": ["закон", "корона", "сила"],
"hierophant": ["храм", "закон", "книга"],
"lovers": ["союз", "сердце", "связь", "кольцо"],
"chariot": ["путь", "дорога", "воин", "воля"],
"strength": ["смирение", "лев", "огонь"],
"hermit": ["мудрец", "лампа", "путь", "покой"],
"wheel_of_fortune": ["цикл", "судьба", "карма", "круг"],
"justice": ["закон", "весы"],
"hanged_man": ["покой", "путь"],
"death": ["гроб", "тень", "цикл", "коса"],
"temperance": ["вода", "мера"],
"devil": ["цепь", "змей", "маска"],
"tower": ["удар", "молния"],
"star": ["надежда", "искра", "свет"],
"moon": ["зеркало", "туман", "затмение", "вода"],
"sun": ["свет", "золото", "космос"],
"judgement": ["ангел", "свет"],
"world": ["космос", "земля", "ось", "круг", "покой"],
}
# station_id -> [(system, station_id)]
PARALLELS = {
"fool": [("runes", "raidho")],
"magician": [("runes", "kenaz"), ("iching", "qian"), ("alchemy", "fire")],
"high_priestess": [("lenormand", "card_26"), ("lenormand", "card_32")],
"empress": [("runes", "berkano"), ("alchemy", "earth"), ("lenormand", "card_20")],
"emperor": [("runes", "tiwaz"), ("runes", "othala")],
"hierophant": [("runes", "ansuz"), ("lenormand", "card_26")],
"lovers": [("runes", "gebo"), ("lenormand", "card_24"), ("lenormand", "card_25")],
"chariot": [("runes", "raidho"), ("runes", "ehwaz")],
"strength": [("runes", "uruz")],
"hermit": [("runes", "isa"), ("runes", "perthro"), ("lenormand", "card_33")],
"wheel_of_fortune": [("runes", "jera"), ("runes", "raidho"), ("iching", "hex_24_fu")],
"justice": [("runes", "tiwaz"), ("iching", "hex_06_song")],
"hanged_man": [("runes", "eihwaz")],
"death": [("alchemy", "nigredo"), ("lenormand", "card_8"), ("lenormand", "card_10")],
"temperance": [("runes", "laguz"), ("alchemy", "water")],
"devil": [("runes", "nauthiz"), ("runes", "thurisaz")],
"tower": [("runes", "hagalaz"), ("lenormand", "card_19")],
"star": [("runes", "sowilo"), ("lenormand", "card_16")],
"moon": [("lenormand", "card_32"), ("runes", "laguz")],
"sun": [("runes", "sowilo"), ("lenormand", "card_31"), ("alchemy", "citrinitas")],
"judgement": [("runes", "dagaz"), ("iching", "hex_06_song")],
"world": [("runes", "othala"), ("alchemy", "earth"), ("iching", "hex_11_tai")],
}
SYS_TITLE = {
"runes": "Руны",
"lenormand": "Ленорман",
"iching": "И Цзин",
"alchemy": "Алхимия",
"tarot": "Таро",
}
TAROT_TAG_RE = re.compile(r"\[Таро:[^\]]*\]\s*")
def payload(entry: dict) -> dict:
return {
"id": entry["id"],
"word": entry["word"],
"emoji": entry.get("emoji", ""),
"pos": entry.get("pos", ""),
"category": entry.get("category", ""),
"desc": "",
}
def find_station(canon: dict, system: str, sid: str) -> dict | None:
for st in canon.get(system, {}).get("stations") or []:
if st.get("id") == sid:
return st
return None
def main() -> None:
canon = json.loads(CANON.read_text(encoding="utf-8"))
lex = {e["word"]: e for e in json.loads(LEX.read_text(encoding="utf-8"))}
tarot = canon["tarot"]
# normalize ids / groups
for i, st in enumerate(tarot["stations"]):
if st["id"] == "wheel":
st["id"] = "wheel_of_fortune"
rest = re.sub(r"^(?:[0-9]+|[IVXLCDM]+)\s+", "", st["name"])
st["group"] = f"Старшие Арканы · {ROMAN[i]} {rest}"
word_to_tags: dict[str, list[str]] = defaultdict(list)
for n, st in enumerate(tarot["stations"]):
sid = st["id"]
prim_w = (st.get("primary") or {}).get("word")
cluster_words = []
for w in CLUSTERS.get(sid, []):
if w == prim_w or w not in lex:
continue
if w not in cluster_words:
cluster_words.append(w)
st["cluster"] = [payload(lex[w]) for w in cluster_words[:6]]
parallels = []
for sys, psid in PARALLELS.get(sid, []):
other = find_station(canon, sys, psid)
if not other:
print("missing parallel", sys, psid)
continue
parallels.append(
{
"system": sys,
"system_name": SYS_TITLE.get(sys, sys),
"id": other["id"],
"name": other["name"],
"symbol": other.get("symbol", ""),
}
)
st["parallels"] = parallels
tag = f"[Таро: {st['name']}]"
if prim_w:
word_to_tags[prim_w].append(tag)
for w in cluster_words[:6]:
word_to_tags[w].append(tag)
tarot["description"] = (
"22 Старших Аркана одной колодой: сводное значение, сочетания, "
"пересечения со словарём 999 и параллели с рунами, Ленорман, Ицзином, алхимией."
)
text = json.dumps(canon, ensure_ascii=False, indent=2) + "\n"
CANON.write_text(text, encoding="utf-8")
ASSET.write_text(text, encoding="utf-8")
# stamp dart
dart_lines = DART.read_text(encoding="utf-8").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 = TAROT_TAG_RE.sub("", dm.group(2)).strip()
tags = word_to_tags.get(word)
if tags:
prefix = " ".join(tags)
dart_lines[j] = f"{dm.group(1)}{prefix} {body}{dm.group(3)}\n"
else:
dart_lines[j] = f"{dm.group(1)}{body}{dm.group(3)}\n"
nstamp += 1
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 m:
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")
dart_descs = dict(re.findall(r'word: "([^"]+)"[\s\S]*?description: "([^"]*)"', dart2))
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")
print("tarot stations", len(tarot["stations"]))
print("words tagged", len(word_to_tags), "dart desc lines", nstamp)
for st in tarot["stations"]:
print(
f" {st['id']:20} group={st['group']!r:42} "
f"cl={len(st['cluster'])} par={len(st['parallels'])} "
f"prim={(st.get('primary') or {}).get('word')}"
)
if __name__ == "__main__":
main()