feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets
This commit is contained in:
Executable
+259
@@ -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()
|
||||
Reference in New Issue
Block a user