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