feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild Lenormand lexicon intersections and stamp [Ленорман:] tags."""
|
||||
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"
|
||||
|
||||
LENO_TAG_RE = re.compile(r"\[Ленорман:[^\]]*\]\s*")
|
||||
|
||||
DART_RESTORE = {
|
||||
"пилигрим": "всадник",
|
||||
"выйти": "клевер",
|
||||
"войти": "корабль",
|
||||
"прийти": "туча",
|
||||
"уйти": "гроб",
|
||||
"ответить": "букет",
|
||||
"потерять": "коса",
|
||||
"согласиться": "метла",
|
||||
"объяснить": "аист",
|
||||
"позволить": "крыса",
|
||||
"успеть": "лилии",
|
||||
"поднять": "птицы",
|
||||
"рыба": "рыбы",
|
||||
}
|
||||
|
||||
RESTORE_META = {
|
||||
"всадник": (
|
||||
"существительное",
|
||||
"Тотем / Архетип",
|
||||
"Гонец скорых вестей, импульс в пути, тот кто выводит застой в движение.",
|
||||
),
|
||||
"клевер": (
|
||||
"существительное",
|
||||
"Сакральный Символ",
|
||||
"Малая удача, короткий шанс, лёгкость удачного мига, который нельзя удержать силой.",
|
||||
),
|
||||
"корабль": (
|
||||
"существительное",
|
||||
"Священный Предмет",
|
||||
"Дальняя дорога, торговля, новые горизонты, ковчег перехода через воду.",
|
||||
),
|
||||
"туча": (
|
||||
"существительное",
|
||||
"Образ / Архетип",
|
||||
"Неясность, сомнение, грозовой фронт, помрачение взора перед решением.",
|
||||
),
|
||||
"гроб": (
|
||||
"существительное",
|
||||
"Священный Предмет",
|
||||
"Окончание цикла, глубокая пауза, саркофаг старой формы, место где отмирает отжившее.",
|
||||
),
|
||||
"букет": (
|
||||
"существительное",
|
||||
"Священный Предмет",
|
||||
"Дар, признание, красота визита, радостный знак внимания.",
|
||||
),
|
||||
"коса": (
|
||||
"существительное",
|
||||
"Священный Предмет",
|
||||
"Внезапный срез, жатва, резкое решение, опасность отсечения лишнего.",
|
||||
),
|
||||
"метла": (
|
||||
"существительное",
|
||||
"Священный Предмет",
|
||||
"Ссора и очищение, спор, выметание сора, наказание, что метёт дом.",
|
||||
),
|
||||
"аист": (
|
||||
"существительное",
|
||||
"Тотем / Архетип",
|
||||
"Перемена места, рождение, возврат цикла, вестник семейного очага.",
|
||||
),
|
||||
"крыса": (
|
||||
"существительное",
|
||||
"Тотем / Архетип",
|
||||
"Утрата по капле, разъедание запасов, стресс скрытой утечки.",
|
||||
),
|
||||
"лилии": (
|
||||
"существительное",
|
||||
"Сакральный Символ",
|
||||
"Чистота, благородство, зрелый покой, покровительство без жара страсти.",
|
||||
),
|
||||
"птицы": (
|
||||
"существительное",
|
||||
"Тотем / Архетип",
|
||||
"Щебет речей, звонки и суета переговоров, обмен мнениями на лету.",
|
||||
),
|
||||
"рыбы": (
|
||||
"существительное",
|
||||
"Тотем / Архетип",
|
||||
"Изобилие, поток средств, глубина чувства, плодородие воды.",
|
||||
),
|
||||
}
|
||||
|
||||
# Extra lexicon words on top of the station primary.
|
||||
CLUSTERS = {
|
||||
"card_1": ["вестник", "путь", "дорога"],
|
||||
"card_2": ["радость", "успех", "смех"],
|
||||
"card_3": ["путь", "вода", "море", "дорога"],
|
||||
"card_4": ["семья", "род", "очаг"],
|
||||
"card_5": ["корень", "жизнь", "род", "древо"],
|
||||
"card_6": ["туман", "тревога", "сомнение", "тьма"],
|
||||
"card_7": ["искушение", "яд", "враг", "мудрость"],
|
||||
"card_8": ["смерть", "конец", "тьма", "пепел"],
|
||||
"card_9": ["дар", "радость", "сад", "пир"],
|
||||
"card_10": ["удар", "смерть", "урожай"],
|
||||
"card_11": ["борьба", "гнев", "чистота"],
|
||||
"card_12": ["птицы", "голос", "слово", "вестник"],
|
||||
"card_13": ["дитя", "начало", "чистота", "рождение"],
|
||||
"card_14": ["ложь", "тайна", "маска"],
|
||||
"card_15": ["сила", "власть", "защита"],
|
||||
"card_16": ["надежда", "свет", "небо"],
|
||||
"card_17": ["рождение", "дом", "весна", "цикл"],
|
||||
"card_18": ["друг", "страж"],
|
||||
"card_19": ["закон", "высота", "покой"],
|
||||
"card_20": ["встреча", "пир"],
|
||||
"card_21": ["камень", "холод", "граница"],
|
||||
"card_22": ["путь", "дорога", "ключ"],
|
||||
"card_23": ["нищета", "голод"],
|
||||
"card_24": ["любовь", "страсть", "союз"],
|
||||
"card_25": ["союз", "обет", "клятва"],
|
||||
"card_26": ["тайна", "знание", "мудрость"],
|
||||
"card_27": ["слово", "вестник", "ключ"],
|
||||
"card_28": ["отец", "воин", "воля"],
|
||||
"card_29": ["мать", "сердце", "луна"],
|
||||
"card_30": ["чистота", "покой", "белый"],
|
||||
"card_31": ["свет", "радость", "успех", "жизнь"],
|
||||
"card_32": ["сон", "тайна", "вода"],
|
||||
"card_33": ["дверь", "тайна", "выход"],
|
||||
"card_34": ["изобилие", "вода", "золото"],
|
||||
"card_35": ["дом", "покой", "море"],
|
||||
"card_36": ["судьба", "карма", "жертва", "вера"],
|
||||
}
|
||||
|
||||
|
||||
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, old: dict | None = None) -> dict:
|
||||
lex = lex_by_word.get(word) or {}
|
||||
old = old or {}
|
||||
return {
|
||||
"id": dart_by_word[word],
|
||||
"word": word,
|
||||
"emoji": lex.get("emoji") or old.get("emoji") or "",
|
||||
"pos": lex.get("pos") or old.get("pos") or "существительное",
|
||||
"category": lex.get("category") or old.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"))
|
||||
ln = canon["lenormand"]
|
||||
word_to_tags: dict[str, list[str]] = defaultdict(list)
|
||||
missing = []
|
||||
|
||||
for st in ln["stations"]:
|
||||
sid = st["id"]
|
||||
prim_old = st.get("primary") or {}
|
||||
prim_w = prim_old.get("word")
|
||||
if prim_w == "рыба":
|
||||
prim_w = "рыбы"
|
||||
if prim_w not in dart_by_word:
|
||||
missing.append(f"primary {sid}/{prim_w}")
|
||||
continue
|
||||
st["primary"] = payload(prim_w, dart_by_word, lex_by_word, prim_old)
|
||||
extra = []
|
||||
seen = {prim_w}
|
||||
for w in CLUSTERS.get(sid, []):
|
||||
if w in seen or w not in dart_by_word:
|
||||
if w not in dart_by_word:
|
||||
missing.append(f"cluster {sid}/{w}")
|
||||
continue
|
||||
seen.add(w)
|
||||
extra.append(w)
|
||||
st["cluster"] = [payload(w, dart_by_word, lex_by_word) for w in extra]
|
||||
tag = f"[Ленорман: {st['name']}]"
|
||||
word_to_tags[prim_w].append(tag)
|
||||
for w in extra:
|
||||
word_to_tags[w].append(tag)
|
||||
|
||||
if missing:
|
||||
print("WARN", missing)
|
||||
|
||||
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 = LENO_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"stamped {nstamp} tagged {len(word_to_tags)}")
|
||||
empty = 0
|
||||
for st in ln["stations"]:
|
||||
cl = [c["word"] for c in st.get("cluster") or []]
|
||||
if not cl:
|
||||
empty += 1
|
||||
print(f" {st['id']:8} prim={(st.get('primary') or {}).get('word'):12} n={len(cl)} {cl}")
|
||||
print("empty", empty)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user