feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
Executable
+392
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Ручной интерактивный обогатитель паспортов Оракула 999.
|
||||
Позволяет:
|
||||
- Просматривать статус заполненности любого знака из 1000 по 78 полям
|
||||
- Генерировать точный промпт для любого из 78 пунктов (или всего раздела)
|
||||
- Вводить ответ на конкретное поле
|
||||
- Вставлять готовую карточку (или кусок) из буфера обмена с автопарсингом
|
||||
- Экспортировать все 78 промптов для символа в файл
|
||||
- Отображать канонический вид карточки по шаблону
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, os.path.abspath("oracle_enricher"))
|
||||
from enricher.passport_schema import (
|
||||
PROMPTS_78,
|
||||
PROMPT_BY_INDEX,
|
||||
PROMPT_BY_KEY,
|
||||
get_field_prompt,
|
||||
get_all_78_prompts,
|
||||
render_passport_markdown,
|
||||
count_filled_fields,
|
||||
empty_passport_record,
|
||||
)
|
||||
|
||||
PASSPORTS_PATH = "assets/data/oracle_passports.json"
|
||||
PASSPORTS_BACKUP_PATH = "oracle_enricher/data/oracle_passports.json"
|
||||
|
||||
|
||||
def load_db():
|
||||
if not os.path.exists(PASSPORTS_PATH):
|
||||
print(f"База данных {PASSPORTS_PATH} не найдена. Запустите сначала scripts/init_passport_database.py")
|
||||
sys.exit(1)
|
||||
with open(PASSPORTS_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_db(db):
|
||||
with open(PASSPORTS_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(db, f, ensure_ascii=False, indent=2)
|
||||
os.makedirs(os.path.dirname(PASSPORTS_BACKUP_PATH), exist_ok=True)
|
||||
with open(PASSPORTS_BACKUP_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(db, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def find_word_entry(db, query):
|
||||
query = str(query).strip().lower()
|
||||
if query.isdigit():
|
||||
qid = str(int(query))
|
||||
if qid in db:
|
||||
return db[qid]
|
||||
for wid, entry in db.items():
|
||||
if entry.get("word", "").lower() == query:
|
||||
return entry
|
||||
return None
|
||||
|
||||
|
||||
def show_word_status(entry):
|
||||
wid = entry.get("id", 0)
|
||||
word = entry.get("word", "")
|
||||
emoji = entry.get("emoji", "")
|
||||
filled, total = count_filled_fields(entry)
|
||||
pct = (filled / total) * 100
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(f"ПАСПОРТ ЗНАКА #{str(wid).zfill(3)} «{word.upper()}» {emoji}")
|
||||
print(f"Заполнено: {filled} / {total} полей ({pct:.1f}%)")
|
||||
print("=" * 70)
|
||||
|
||||
sections = {
|
||||
"I": ("Заголовочный паспорт (Метаданные)", [1, 2, 3, 4]),
|
||||
"II": ("Кросс-мантическая сеть (16 систем + выжимка)", list(range(5, 22))),
|
||||
"III": ("Мультилингвальный и графический паспорт", list(range(22, 39))),
|
||||
"IV": ("Трехполярная матрица состояний (0 / +1 / -1)", [39, 40, 41]),
|
||||
"V": ("Культурологическая матрица (16 культур + синтез)", list(range(42, 75))),
|
||||
"VI": ("Мантический & Обучающий синтез", [75, 76, 77, 78]),
|
||||
}
|
||||
|
||||
fields = entry.get("fields", {})
|
||||
|
||||
for s_code, (s_name, idx_list) in sections.items():
|
||||
s_filled = sum(1 for i in idx_list if fields.get(PROMPT_BY_INDEX[i].key, "").strip())
|
||||
print(f"\n[{s_code}] {s_name} — {s_filled}/{len(idx_list)}:")
|
||||
for idx in idx_list:
|
||||
p_def = PROMPT_BY_INDEX[idx]
|
||||
val = fields.get(p_def.key, "").strip()
|
||||
status_sym = "✓" if val else "✗"
|
||||
val_snippet = (val[:50] + "...") if len(val) > 50 else val
|
||||
val_display = f" → {val_snippet}" if val else " [ПУСТО]"
|
||||
print(f" {status_sym} #{idx:02d} {p_def.title:<36} {val_display}")
|
||||
|
||||
|
||||
def export_prompts_to_file(entry, out_path=None):
|
||||
wid = entry.get("id", 0)
|
||||
word = entry.get("word", "")
|
||||
if not out_path:
|
||||
out_dir = "out/prompts"
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
out_path = f"{out_dir}/passport_{str(wid).zfill(3)}_{word}_prompts.txt"
|
||||
|
||||
prompts = get_all_78_prompts(entry)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"=== 78 АТОМАРНЫХ ПРОМПТОВ ДЛЯ ПАСПОРТА ЗНАКА #{str(wid).zfill(3)} «{word.upper()}» ===\n\n")
|
||||
for idx, key, title, prompt_text in prompts:
|
||||
f.write(f"--- [ПРОМПТ #{idx:02d}] {title} ({key}) ---\n")
|
||||
f.write(f"{prompt_text}\n\n")
|
||||
|
||||
print(f"✓ Все 78 промптов успешно сохранены в файл: {out_path}")
|
||||
return out_path
|
||||
|
||||
|
||||
def parse_and_apply_markdown(entry, md_text):
|
||||
"""Парсит текст паспорта по шаблону и обновляет соответствующие поля."""
|
||||
fields = entry.setdefault("fields", {})
|
||||
updated_keys = []
|
||||
|
||||
# Регулярные выражения для сопоставления ключевых строк шаблона
|
||||
patterns = [
|
||||
("index_and_name", r"Индекс и имя:\s*(.+)"),
|
||||
("grammatical_class", r"Грамматический класс:\s*(.+)"),
|
||||
("functional_category", r"Функциональная категория:\s*(.+)"),
|
||||
("network_status", r"Сетевой статус:\s*(.+)"),
|
||||
("mantic_runes", r"Старший Футарк \(Руны\):\s*(.+)"),
|
||||
("mantic_ogham", r"Кельтский Огам:\s*(.+)"),
|
||||
("mantic_tarot", r"Старшие Арканы Таро:\s*(.+)"),
|
||||
("mantic_lenormand", r"Малая колода Ленорман:\s*(.+)"),
|
||||
("mantic_kabbalah", r"Каббала / Древо Жизни:\s*(.+)"),
|
||||
("mantic_geomancy", r"Арабская Геомантия \('Илм ар-Рамл\):\s*(.+)"),
|
||||
("mantic_iching", r"И Цзин \(Книга Перемен\):\s*(.+)"),
|
||||
("mantic_alchemy", r"Алхимия \(Magnum Opus\):\s*(.+)"),
|
||||
("mantic_zodiac_west", r"Классический Зодиак:\s*(.+)"),
|
||||
("mantic_zodiac_chinese", r"Китайский Зодиак:\s*(.+)"),
|
||||
("mantic_septener", r"Септенер:\s*(.+)"),
|
||||
("mantic_dodecade", r"Додекада Месяцев:\s*(.+)"),
|
||||
("mantic_numerology", r"Нумерология:\s*(.+)"),
|
||||
("mantic_cosmology", r"Космология Трёх Миров:\s*(.+)"),
|
||||
("mantic_tzolkin", r"20 Навалей Цолькин \(Майя\):\s*(.+)"),
|
||||
("mantic_nakshatras", r"27 Накшатр Джйотиш \(Индия\):\s*(.+)"),
|
||||
("mantic_station_laws", r"Действующие законы станций \(выжимка\):\s*\n*(.*?)(?=\n---|\nIII|\n\Z)"),
|
||||
("etymology_roots", r"Этимология и праформы:\s*(.+)"),
|
||||
("script_sumer_akkad", r"Шумер / Аккад:\s*(.+)"),
|
||||
("script_egypt", r"Древний Египет:\s*(.+)"),
|
||||
("script_hebrew", r"Иудейская традиция:\s*(.+)"),
|
||||
("script_antiquity", r"Античность:\s*(.+)"),
|
||||
("script_celtic", r"Кельты:\s*(.+)"),
|
||||
("script_runic_germanic", r"Скандинавы / Германцы:\s*(.+)"),
|
||||
("script_slavic", r"Славяне:\s*(.+)"),
|
||||
("script_india_sanskrit", r"Ведическая Индия:\s*(.+)"),
|
||||
("script_persia", r"Персия:\s*(.+)"),
|
||||
("script_china", r"Китай:\s*(.+)"),
|
||||
("script_japan", r"Япония:\s*(.+)"),
|
||||
("script_islamic", r"Исламский мир:\s*(.+)"),
|
||||
("script_western_europe", r"Западная Европа:\s*(.+)"),
|
||||
("script_mesoamerica", r"Мезоамерика:\s*(.+)"),
|
||||
("script_native_american", r"Коренные народы Сев\. Америки:\s*(.+)"),
|
||||
("script_polynesia", r"Полинезия:\s*(.+)"),
|
||||
("state_neutral", r"0 Нейтральный статус[^:]*:\s*(.+)"),
|
||||
("state_creative", r"\[\+1\] Созидательный вектор[^:]*:\s*(.+)"),
|
||||
("state_destructive", r"\[-1\] Деструктивный вектор[^:]*:\s*(.+)"),
|
||||
("semantic_core", r"Смысловое ядро:\s*(.+)"),
|
||||
("grammar_interpretation", r"Интерпретация по грамматическому типу:\s*(.+)"),
|
||||
("will_vector", r"Вектор Воли[^:]*:\s*(.+)"),
|
||||
("shadow_warning", r"Предостережение / Тень[^:]*:\s*(.+)"),
|
||||
]
|
||||
|
||||
for key, regex in patterns:
|
||||
m = re.search(regex, md_text, re.MULTILINE | re.DOTALL)
|
||||
if m:
|
||||
val = m.group(1).strip()
|
||||
if val and not val.startswith("[Имя станции") and not val.startswith("[существительное"):
|
||||
fields[key] = val
|
||||
updated_keys.append(key)
|
||||
|
||||
# Культуры срез свет / тень
|
||||
culture_patterns = [
|
||||
("culture_sumer_light", "culture_sumer_shadow", r"Шумер и Месопотамия:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_egypt_light", "culture_egypt_shadow", r"Древний Египет:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_kabbalah_light", "culture_kabbalah_shadow", r"Иудейская традиция и Каббала:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_antiquity_light", "culture_antiquity_shadow", r"Античность \(Греция / Рим\):\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_celtic_light", "culture_celtic_shadow", r"Кельтская традиция:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_nordic_light", "culture_nordic_shadow", r"Скандинавско-германская традиция:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_slavic_light", "culture_slavic_shadow", r"Славянская традиция:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_india_light", "culture_india_shadow", r"Ведическая Индия и Тантра:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_persia_light", "culture_persia_shadow", r"Персия и Зороастризм:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_china_light", "culture_china_shadow", r"Китай \(Даосизм, И-цзин, У-син\):\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_japan_light", "culture_japan_shadow", r"Япония \(Синтоизм, Сингон\):\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_islam_light", "culture_islam_shadow", r"Исламская традиция и суфизм:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_hermetic_light", "culture_hermetic_shadow", r"Западноевропейская традиция \(Алхимия / Герметизм\):\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_mesoamerica_light", "culture_mesoamerica_shadow", r"Мезоамерика и Анды \(Майя, Ацтеки, Инки\):\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_native_american_light", "culture_native_american_shadow", r"Традиции коренных народов Северной Америки:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
("culture_polynesia_light", "culture_polynesia_shadow", r"Полинезия и Океания:\s*\[\+\]\s*(.*?)\s*/\s*\[-\]\s*(.+)"),
|
||||
]
|
||||
|
||||
for light_k, shadow_k, regex in culture_patterns:
|
||||
m = re.search(regex, md_text, re.MULTILINE)
|
||||
if m:
|
||||
l_val = m.group(1).strip()
|
||||
s_val = m.group(2).strip()
|
||||
if l_val and not l_val.startswith("[Созидательный"):
|
||||
fields[light_k] = l_val
|
||||
updated_keys.append(light_k)
|
||||
if s_val and not s_val.startswith("[Деструктивный"):
|
||||
fields[shadow_k] = s_val
|
||||
updated_keys.append(shadow_k)
|
||||
|
||||
m_syn = re.search(r"Б\. 17-й Интегральный мета-синтез[^\n]*\n(.*?)(?=\n---|\nVI|\n\Z)", md_text, re.DOTALL)
|
||||
if m_syn:
|
||||
syn_val = m_syn.group(1).strip()
|
||||
if syn_val:
|
||||
fields["meta_synthesis"] = syn_val
|
||||
updated_keys.append("meta_synthesis")
|
||||
|
||||
filled, total = count_filled_fields(entry)
|
||||
entry["filled_count"] = filled
|
||||
entry["is_complete"] = (filled == total)
|
||||
entry["updated_at"] = datetime.now().isoformat()
|
||||
return updated_keys
|
||||
|
||||
|
||||
def interactive_loop(db, current_entry):
|
||||
while True:
|
||||
show_word_status(current_entry)
|
||||
print("\nКОМАНДЫ РУЧНОГО ОБОГАЩЕНИЯ:")
|
||||
print(" [1..78] - Показать промпт для поля #N и ввести ответ")
|
||||
print(" prompt [1..78] - Только показать промпт для поля #N (для копирования)")
|
||||
print(" export - Экспортировать все 78 промптов этого символа в файл")
|
||||
print(" paste - Вставить блок или готовый паспорт целиком (многострочный ввод)")
|
||||
print(" view - Показать готовый паспорт в каноническом формате")
|
||||
print(" word <имя/ID> - Переключиться на другой символ")
|
||||
print(" next - Перейти к следующему наименее заполненному символу")
|
||||
print(" save - Сохранить базу данных")
|
||||
print(" exit - Выход")
|
||||
|
||||
try:
|
||||
cmd = input("\n[Обогатитель] > ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print("\nВыход.")
|
||||
break
|
||||
|
||||
if not cmd:
|
||||
continue
|
||||
|
||||
if cmd.lower() in ["exit", "q", "quit"]:
|
||||
save_db(db)
|
||||
print("База сохранена. До свидания!")
|
||||
break
|
||||
|
||||
elif cmd.lower() == "save":
|
||||
save_db(db)
|
||||
print("✓ База данных сохранена.")
|
||||
|
||||
elif cmd.lower() == "view":
|
||||
print("\n" + render_passport_markdown(current_entry))
|
||||
|
||||
elif cmd.lower() == "export":
|
||||
export_prompts_to_file(current_entry)
|
||||
|
||||
elif cmd.lower().startswith("word "):
|
||||
q = cmd[5:].strip()
|
||||
found = find_word_entry(db, q)
|
||||
if found:
|
||||
current_entry = found
|
||||
else:
|
||||
print(f"Слово «{q}» не найдено в базе.")
|
||||
|
||||
elif cmd.lower() == "next":
|
||||
# Ищем символ с минимальным количеством заполненных полей
|
||||
least = min(db.values(), key=lambda e: count_filled_fields(e)[0])
|
||||
current_entry = least
|
||||
print(f"Переключено на #{str(current_entry['id']).zfill(3)} «{current_entry['word']}»")
|
||||
|
||||
elif cmd.lower().startswith("prompt "):
|
||||
arg = cmd[7:].strip()
|
||||
if arg.isdigit() and 1 <= int(arg) <= 78:
|
||||
idx = int(arg)
|
||||
p_text = get_field_prompt(idx, current_entry)
|
||||
p_def = PROMPT_BY_INDEX[idx]
|
||||
print(f"\n" + "─" * 65)
|
||||
print(f"ПРОМПТ ДЛЯ ПОЛЯ #{idx:02d}: {p_def.title} ({p_def.key})")
|
||||
print("─" * 65)
|
||||
print(p_text)
|
||||
print("─" * 65)
|
||||
else:
|
||||
print("Укажите номер поля от 1 до 78, например: prompt 10")
|
||||
|
||||
elif cmd.isdigit() and 1 <= int(cmd) <= 78:
|
||||
idx = int(cmd)
|
||||
p_def = PROMPT_BY_INDEX[idx]
|
||||
p_text = get_field_prompt(idx, current_entry)
|
||||
print(f"\n" + "─" * 65)
|
||||
print(f"ПОЛЕ #{idx:02d}: {p_def.title} [{p_def.section_title}]")
|
||||
print("─" * 65)
|
||||
print("ПРОМПТ ДЛЯ ИИ:")
|
||||
print(p_text)
|
||||
print("─" * 65)
|
||||
curr_val = current_entry.get("fields", {}).get(p_def.key, "")
|
||||
if curr_val:
|
||||
print(f"Текущее значение: {curr_val}")
|
||||
print("\nВведите значение для этого поля (или нажмите Enter чтобы оставить без изменений):")
|
||||
try:
|
||||
new_val = input("Значение > ").strip()
|
||||
if new_val:
|
||||
current_entry["fields"][p_def.key] = new_val
|
||||
filled, total = count_filled_fields(current_entry)
|
||||
current_entry["filled_count"] = filled
|
||||
current_entry["is_complete"] = (filled == total)
|
||||
current_entry["updated_at"] = datetime.now().isoformat()
|
||||
save_db(db)
|
||||
print(f"✓ Поле #{idx:02d} ({p_def.key}) обновлено и сохранено.")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
||||
|
||||
elif cmd.lower() == "paste":
|
||||
print("\nВставьте текст (заполненный блок или весь паспорт).")
|
||||
print("Для завершения ввода отправьте строку 'EOF' на отдельной строке:\n")
|
||||
lines = []
|
||||
while True:
|
||||
try:
|
||||
line = input()
|
||||
if line.strip() == "EOF":
|
||||
break
|
||||
lines.append(line)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
break
|
||||
paste_text = "\n".join(lines)
|
||||
if paste_text.strip():
|
||||
updated = parse_and_apply_markdown(current_entry, paste_text)
|
||||
save_db(db)
|
||||
print(f"\n✓ Успешно распарсено и обновлено {len(updated)} полей: {', '.join(updated[:8])}...")
|
||||
else:
|
||||
print("Пустой ввод.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Ручной обогатитель паспортов Оракула 999 (78 полей)")
|
||||
parser.add_argument("--word", "-w", type=str, help="Слово знака для обогащения (напр. 'огонь')")
|
||||
parser.add_argument("--id", "-i", type=int, help="ID знака (0..999)")
|
||||
parser.add_argument("--export", action="store_true", help="Экспортировать все 78 промптов знака в файл")
|
||||
parser.add_argument("--view", action="store_true", help="Показать готовый паспорт в каноническом формате")
|
||||
parser.add_argument("--stats", action="store_true", help="Показать общую статистику заполненности базы")
|
||||
|
||||
args = parser.parse_args()
|
||||
db = load_db()
|
||||
|
||||
if args.stats:
|
||||
total_p = len(db)
|
||||
total_f = sum(count_filled_fields(e)[0] for e in db.values())
|
||||
max_f = total_p * 78
|
||||
complete_p = sum(1 for e in db.values() if count_filled_fields(e)[0] == 78)
|
||||
print("=" * 65)
|
||||
print("📊 СТАТИСТИКА БАЗЫ 1000 ПАСПОРТОВ ОРАКУЛА 999")
|
||||
print("=" * 65)
|
||||
print(f"Всего символов: {total_p}")
|
||||
print(f"Заполнено полей: {total_f} / {max_f} ({(total_f/max_f)*100:.1f}%)")
|
||||
print(f"Среднее полей на карточку: {total_f/total_p:.1f} из 78")
|
||||
print(f"Полностью завершенных паспортов (78/78): {complete_p}")
|
||||
print("=" * 65)
|
||||
return
|
||||
|
||||
entry = None
|
||||
if args.word:
|
||||
entry = find_word_entry(db, args.word)
|
||||
elif args.id is not None:
|
||||
entry = find_word_entry(db, args.id)
|
||||
else:
|
||||
# По умолчанию берем слово 0 (огонь)
|
||||
entry = db.get("0") or list(db.values())[0]
|
||||
|
||||
if not entry:
|
||||
print("Указанный символ не найден.")
|
||||
sys.exit(1)
|
||||
|
||||
if args.view:
|
||||
print(render_passport_markdown(entry))
|
||||
return
|
||||
|
||||
if args.export:
|
||||
export_prompts_to_file(entry)
|
||||
return
|
||||
|
||||
interactive_loop(db, entry)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user