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