#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ extract_and_link_illustrations.py ================================= Извлекает все 75 гравюр/иллюстраций из словаря Х.Э. Керлота: 1. Сохраняет изображения в папку assets/illustrations/ 2. Привязывает их к словарным статьям и подписям 3. Обогащает ТЕСТОВУЮ базу (assets/data/oracle_passports_staging.json) для слов из 1000 4. Привязывает к базе внешних символов (assets/data/oracle_external_symbols.json) 5. Создает единый реестр иллюстраций assets/data/oracle_illustrations_catalog.json """ import os import sys import json import re import subprocess from pathlib import Path from typing import Dict, Any, List, Optional, Tuple PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROJECT_ROOT)) import fitz DICT_PDF = PROJECT_ROOT / "dictionaries" / "Кирло Х. - Словарь символов. 1000 статей о важнейших понятиях религии, литературы, архитектуры, истории - 2010.pdf" ILLUSTRATIONS_DIR = PROJECT_ROOT / "assets" / "illustrations" CATALOG_PATH = PROJECT_ROOT / "assets" / "data" / "oracle_illustrations_catalog.json" STAGING_DB_PATH = PROJECT_ROOT / "assets" / "data" / "oracle_passports_staging.json" EXTERNAL_DB_PATH = PROJECT_ROOT / "assets" / "data" / "oracle_external_symbols.json" ENRICHER_EXTERNAL_PATH = PROJECT_ROOT / "oracle_enricher" / "data" / "oracle_external_symbols.json" def clean_spaces(text: str) -> str: return re.sub(r'\s+', ' ', text).strip() def transliterate_to_slug(text: str) -> str: """Транслитерация в безопасное имя файла.""" ru_to_en = { 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'shch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya' } slug = [] for c in text.lower(): if c in ru_to_en: slug.append(ru_to_en[c]) elif c.isalnum(): slug.append(c) elif c in [' ', '-', '_']: slug.append('_') res = re.sub(r'_+', '_', ''.join(slug)).strip('_') return res[:30] if res else 'symbol' def get_page_text(p_num: int) -> str: """Извлекает текст страницы PDF через pdftotext (1-indexed).""" try: cmd = ["pdftotext", "-f", str(p_num), "-l", str(p_num), str(DICT_PDF), "-"] out = subprocess.check_output(cmd) return out.decode("utf-8", errors="ignore").encode("latin1", errors="ignore").decode("cp1251", errors="ignore") except Exception: return "" def main(): print("=" * 75) print("🎨 ИЗВЛЕЧЕНИЕ И ПРИВЯЗКА ИЛЛЮСТРАЦИЙ / ГРАВЮР СИМВОЛОВ") print(f"📖 Источник: {DICT_PDF.name}") print(f"📁 Папка сохранения: {ILLUSTRATIONS_DIR}") print("=" * 75) if not DICT_PDF.exists(): print(f"[ERROR] Файл словаря не найден: {DICT_PDF}") return ILLUSTRATIONS_DIR.mkdir(parents=True, exist_ok=True) doc = fitz.open(str(DICT_PDF)) print(f"Всего страниц в PDF: {len(doc)}") # 1. Загрузка баз данных with open(STAGING_DB_PATH, "r", encoding="utf-8") as f: staging_db = json.load(f) with open(EXTERNAL_DB_PATH, "r", encoding="utf-8") as f: external_db = json.load(f) # Индекс канонических слов 1000 знаков core_index = {} for idx_str, entry in staging_db.items(): w = entry.get("word", "").lower().strip() if w: core_index[w] = idx_str # 2. Карта заголовков по страницам print("\nСканирование структуры статей по страницам...") page_to_head = {} current_head = "ВВЕДЕНИЕ" page_texts = {} for p in range(50, len(doc)): p_num = p + 1 txt = get_page_text(p_num) page_texts[p_num] = txt lines = [l.strip() for l in txt.splitlines() if l.strip()] for l in lines: if l.isupper() and 2 <= len(l) <= 35 and re.match(r'^[А-ЯЁ\s\-]+$', l): if l not in ['СЛОВАРЬ СИМВОЛОВ', 'СОДЕРЖАНИЕ', 'АЛФАВИТНЫЙ УКАЗАТЕЛЬ']: current_head = l page_to_head[p_num] = current_head # Специфические уточнения для страниц, где иллюстрация относится к предыдущей статье page_overrides = { 57: ("АЛХИМИЯ", "Атенор, печь алхимиков (реконструкция по старой гравюре)"), 87: ("ВЕСЫ", "Зодиакальный знак Весы"), 111: ("ГЕРАЛЬДИЧЕСКИЕ СИМВОЛЫ", "Геральдический орел"), 113: ("ГЕРОЙ", "Арданари Ишвара (андрогинный аспект Шивы)"), 128: ("ГРАФИКА", "Единство — происхождение и центр бесконечности"), 129: ("ГРАФИКА", "Центр бесконечности, излучение или первопричина"), 140: ("ДЕВА", "Знак зодиака Дева"), 198: ("КАДУЦЕЙ", "Древнее шумерское изображение кадуцея"), 234: ("КРУГ", "Китайский знак ян-инь, окруженный восемью триграммами"), 241: ("ЛАБИРИНТ", "Лабиринт — часы, олицетворяющие единство времени и космоса (старинная гравюра)"), 402: ("СОЛНЦЕ", "Солнце и луна, активный и пассивный принципы (гравюра из «De Intellectu», 1510)"), 479: ("ЧЕРЕПАХА", "Восточное космологическое изображение черепахи"), 502: ("ЯКОРЬ", "Якорь спасения, ранний христианский символ"), } # 3. Извлечение иллюстраций catalog = [] extracted_count = 0 staging_linked_count = 0 external_linked_count = 0 print("\nИзвлечение графических иллюстраций...") for p in range(50, len(doc)): p_num = p + 1 imgs = doc[p].get_images() if not imgs: continue txt = page_texts.get(p_num, "") lines = [l.strip() for l in txt.splitlines() if l.strip()] for idx, img_info in enumerate(imgs): xref = img_info[0] base_img = doc.extract_image(xref) w, h = base_img["width"], base_img["height"] ext = base_img["ext"] # Фильтр мелких разделителей и пустых артефактов if w < 120 or h < 120: continue # Определяем заголовок и подпись if p_num in page_overrides: head_symbol, caption = page_overrides[p_num] else: head_symbol = page_to_head.get(p_num, "СИМВОЛ") # Ищем подпись: короткая изолированная строка caption = "" for l in lines: if not l.isupper() and not l.endswith("-") and 8 <= len(l) <= 90 and not l.isdigit(): if any(w in l.lower() for w in ["знак", "символ", "гравюр", "изображен", "схема", "рисунок", "бог", "образ"]): caption = l break if not caption: caption = f"Иллюстрация к понятию «{head_symbol.title()}»" # Генерируем красивое имя файла slug = transliterate_to_slug(head_symbol) fname = f"cirlot_{p_num:03d}_{slug}_{idx}.{ext}" file_path = ILLUSTRATIONS_DIR / fname rel_asset_path = f"assets/illustrations/{fname}" with open(file_path, "wb") as f_out: f_out.write(base_img["image"]) extracted_count += 1 # Создаем объект метаданных иллюстрации ill_item = { "id": f"cirlot_{p_num}_{idx}", "file": rel_asset_path, "symbol": head_symbol, "normalized": head_symbol.lower(), "caption": caption, "source": "Х.Э. Керлот · «Словарь символов» (2010)", "page": p_num, "width": w, "height": h, } norm = head_symbol.lower() # Проверяем привязку к 1000 знаков (Staging) matched_core_id = None if norm in core_index: matched_core_id = core_index[norm] elif norm.endswith("ы") and norm[:-1] in core_index: matched_core_id = core_index[norm[:-1]] elif norm.endswith("и") and norm[:-1] in core_index: matched_core_id = core_index[norm[:-1]] if matched_core_id: ill_item["is_core_1000"] = True ill_item["core_id"] = matched_core_id target_entry = staging_db[matched_core_id] target_ills = target_entry.setdefault("illustrations", []) if not any(x.get("file") == rel_asset_path for x in target_ills): target_ills.append(ill_item) staging_linked_count += 1 else: ill_item["is_core_1000"] = False ill_item["core_id"] = None # Привязываем к базе внешних символов if norm in external_db: ext_ills = external_db[norm].setdefault("illustrations", []) if not any(x.get("file") == rel_asset_path for x in ext_ills): ext_ills.append(ill_item) external_linked_count += 1 catalog.append(ill_item) # 4. Сохранение каталога и обновленных баз print(f"\n💾 Сохранение общего каталога: {CATALOG_PATH}...") CATALOG_PATH.parent.mkdir(parents=True, exist_ok=True) with open(CATALOG_PATH, "w", encoding="utf-8") as f: json.dump(catalog, f, ensure_ascii=False, indent=2) print(f"💾 Обновление тестовой базы (staging): {STAGING_DB_PATH}...") with open(STAGING_DB_PATH, "w", encoding="utf-8") as f: json.dump(staging_db, f, ensure_ascii=False, indent=2) print(f"💾 Обновление базы внешних символов: {EXTERNAL_DB_PATH}...") with open(EXTERNAL_DB_PATH, "w", encoding="utf-8") as f: json.dump(external_db, f, ensure_ascii=False, indent=2) with open(ENRICHER_EXTERNAL_PATH, "w", encoding="utf-8") as f: json.dump(external_db, f, ensure_ascii=False, indent=2) print("\n" + "=" * 75) print("📊 ИТОГОВЫЙ ОТЧЕТ ПО ИЛЛЮСТРАЦИЯМ:") print(f"• Всего извлечено гравюр/изображений: {extracted_count}") print(f"• Привязано к знакам канона 1000 в staging: {staging_linked_count}") print(f"• Привязано к внешним символам: {external_linked_count}") print(f"• Папка иллюстраций: {ILLUSTRATIONS_DIR} ({len(list(ILLUSTRATIONS_DIR.iterdir()))} файлов)") print(f"• Каталог иллюстраций: {CATALOG_PATH}") print("=" * 75) if __name__ == "__main__": main()