#!/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()