Files
999/scripts/rebuild_western_zodiac.py
T

348 lines
17 KiB
Python

#!/usr/bin/env python3
"""Classical Western zodiac as its own atlas tradition (Додекада)."""
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"
TAG_RE = re.compile(r"\[Додекада Созвездий:[^\]]*\]\s*")
DART_RESTORE = {
"попросить": "овен",
"выпить": "телец",
"близнец": "близнецы",
"сесть": "рак",
"смочь": "стрелец",
"отдать": "козерог",
"ожидать": "водолей",
}
RESTORE_META = {
"овен": (
"существительное",
"Космический Знак",
"Первородный импульс весны, рог пробивающий скалу, ярость пробуждения. "
"Овен начинает цикл без оглядки на риск — это пламя первого шага и воля вырваться из чрева зимы. "
"В раскладе требует прямой смелости, лидерства и готовности идти на таран, предостерегая от слепой импульсивности.",
),
"телец": (
"существительное",
"Космический Знак",
"Непоколебимая опора материи, плодородие, терпеливое накопление и сила плоти. "
"Телец воплощает стойкость духа в физической форме, любовь к земным благам и упорный созидательный труд. "
"Знак советует твердо стоять на земле, беречь ресурсы и строить на века.",
),
"близнецы": (
"существительное",
"Космический Знак",
"Воздушный перекресток двух умов, зеркало полярностей, неразрывный союз братьев света и тени. "
"Как знак зодиака — ведает гибкостью мысли, скоростью слова и связью миров. "
"Требует диалога и интеграции противоположностей.",
),
"рак": (
"существительное",
"Космический Знак",
"Лоно истока, панцирь родовой памяти, охраняющий нежнейшую уязвимость сердца. "
"Рак движется боком, оберегая священный очаг и чувствуя приливы невидимой Луны. "
"В раскладе говорит о глубинных родовых корнях, защите близких и верности памяти.",
),
"стрелец": (
"существительное",
"Космический Знак",
"Кентавр духа, натянутый лук и стрела, устремленная за горизонт видимого мира. "
"Стрелец объединяет животную силу земли с поиском божественной мудрости. "
"Знак сулит дальнее странствие, наставника и веру в высокую цель.",
),
"козерог": (
"существительное",
"Космический Знак",
"Одинокое восхождение на ледяную вершину, аскеза времени и кристаллизация высшего закона. "
"Козерог преодолевает преграды через терпение, дисциплину и долг. "
"Знак сулит вершину тем, кто не боится холода на пути к мастерству.",
),
"водолей": (
"существительное",
"Космический Знак",
"Излияние кувшина живых вод будущего, сокрушение отживших плотин и братство свободных сердец. "
"Водолей черпает знание из грядущих веков. Знак призывает сбросить оковы рутины и ценить дружбу.",
),
}
# n, id, ru, latin, glyph, element, planet, primary, cluster, lore
STATIONS = [
(
1, "aries", "Овен", "Aries", "♈", "Огонь", "Марс",
"овен", ["баран", "начало", "рог"],
"Первый знак тропического круга, точка овна, огонь Марса. Рог тарана, весенний прорыв, "
"воля начать без карты. В раскладе — лидерский шаг и риск первого удара. "
"Тень — слепая ярость и разрушение без плана.",
),
(
2, "taurus", "Телец", "Taurus", "♉", "Земля", "Венера",
"телец", ["бык", "земля", "труд"],
"Второй знак, земля Венеры, бык, держащий сад. Плоть, накопление, верность форме, "
"труд, который строит на века. Знак велит стоять и растить. Тень — косность и жадность застоя.",
),
(
3, "gemini", "Близнецы", "Gemini", "♊", "Воздух", "Меркурий",
"близнецы", ["вестник", "слово", "воздух"],
"Третий знак, воздух Меркурия, двое в одном дыхании. Диалог, скорость мысли, вестник "
"между берегами. Знак просит говорить и слушать. Тень — двуличие и рассеивание сил.",
),
(
4, "cancer", "Рак", "Cancer", "♋", "Вода", "Луна",
"рак", ["луна", "дом", "вода"],
"Четвёртый знак, вода Луны, панцирь рода. Лоно, очаг, память, ход боком вокруг того, "
"что нельзя отдать. Знак бережёт близких. Тень — уход в раковину и цепляние за прошлое.",
),
(
5, "leo", "Лев", "Leo", "♌", "Огонь", "Солнце",
"лев", ["солнце", "сердце", "царь"],
"Пятый знак, огонь Солнца в сердце круга. Грива, трон, щедрость сияния, право быть видимым. "
"Знак зовёт явить своё. Тень — тирания эго и театр вместо сердца.",
),
(
6, "virgo", "Дева", "Virgo", "♍", "Земля", "Меркурий",
"дева", ["чистота", "хлеб", "мера"],
"Шестой знак, земля Меркурия, колос и серп. Различение зерна от плевел, служение детали, "
"порядок как забота. Знак велит навести меру. Тень — педантизм и страх осквернения.",
),
(
7, "libra", "Весы", "Libra", "♎", "Воздух", "Венера",
"весы", ["союз", "мир", "мера"],
"Седьмой знак, воздух Венеры, чаши договора. Равновесие, красота союза, суд без меча. "
"Знак просит взвесить. Тень — вечные качели и ложный компромисс.",
),
(
8, "scorpio", "Скорпион", "Scorpio", "♏", "Вода", "Плутон",
"скорпион", ["яд", "тайна", "смерть"],
"Восьмой знак, вода глубин, жало и феникс. Кризис, который сжигает личину, тайна недр, "
"яд, способный стать лекарством. Знак сулит перерождение. Тень — месть и саморазрушение.",
),
(
9, "sagittarius", "Стрелец", "Sagittarius", "♐", "Огонь", "Юпитер",
"стрелец", ["путь", "вера", "лук"],
"Девятый знак, огонь Юпитера, кентавр и лук. Стрела за горизонт, странствие, наставник, "
"вера в цель, которой ещё нет на карте. Тень — догма и бегство в даль вместо дела.",
),
(
10, "capricorn", "Козерог", "Capricorn", "♑", "Земля", "Сатурн",
"козерог", ["гора", "закон", "холод"],
"Десятый знак, земля Сатурна, козёл на ледяном гребне. Долг, время, восхождение без зрителей. "
"Знак сулит вершину за аскезу. Тень — черствость и страх срыва.",
),
(
11, "aquarius", "Водолей", "Aquarius", "♒", "Воздух", "Уран",
"водолей", ["свобода", "друг", "воздух"],
"Одиннадцатый знак, воздух Урана, кувшин вод будущего. Братья по духу, слом плотины, "
"мысль, которая не помещается в обычай. Знак зовёт к свободе. Тень — холод утопии и бунт ради бунта.",
),
(
12, "pisces", "Рыбы", "Pisces", "♓", "Вода", "Нептун",
"рыбы", ["вода", "сон", "вера"],
"Двенадцатый знак, вода Нептуна, две рыбы в разные стороны. Растворение границ, "
"сострадание, интуиция океана, конец круга, уже содержащий исток. Тень — иллюзия и утрата берега.",
),
]
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, latin, glyph, element, planet, 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))
st = {
"id": sid,
"name": f"{n} {ru} ({latin})",
"symbol": glyph,
"group": f"Додекада · {element} · {planet}",
"lore": lore,
"primary": payload(prim, dart_by_word, lex_by_word),
"cluster": cluster,
}
stations.append(st)
tag = f"[Додекада Созвездий: {n}/12 {ru} ({latin} / {element} / {planet})]"
word_to_tags[prim].append(tag)
for c in cluster:
word_to_tags[c["word"]].append(tag)
if missing:
print("WARN", missing)
west = {
"id": "western_zodiac",
"name": "Классический гороскоп (Додекада)",
"icon": "♈",
"count": len(stations),
"description": (
"12 знаков тропического зодиака: огонь, земля, воздух, вода; "
"управители от Марса до Нептуна; пересечения со словарём 999."
),
"stations": stations,
}
ordered = {}
for k, v in canon.items():
if k == "zodiac":
ordered["western_zodiac"] = west
ordered[k] = v
if "western_zodiac" not in ordered:
ordered["western_zodiac"] = west
canon = ordered
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 = 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")
print(f"western {len(stations)} stamped {nstamp} tagged {len(word_to_tags)}")
for st in stations:
cl = [c["word"] for c in st["cluster"]]
print(f" {st['symbol']} {st['name']:24} prim={st['primary']['word']:12} {cl}")
if __name__ == "__main__":
main()