feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets

This commit is contained in:
2026-10-09 15:26:58 +05:00
parent 7f91366201
commit 0b12e4583b
244 changed files with 611461 additions and 1617 deletions
+259
View File
@@ -0,0 +1,259 @@
#!/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()