393 lines
20 KiB
Python
Executable File
393 lines
20 KiB
Python
Executable File
#!/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()
|