348 lines
17 KiB
Python
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()
|