Files
999/scripts/passport_manual_enricher.py
T

393 lines
20 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()