#!/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()