#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ scripts/sanitize_battistini_corpus.py Complete re-parsing and sanitization of M. Battistini's «Символы и аллегории» (2008), and deep sanitization of all databases (passports, staging, external symbols, illustration catalog). Fixes: - OCR artifacts (drop caps, Latin homoglyphs, column/caption bleeding, mangled names). - Broken hyphenation (soft wraps) across the entire corpus. - Purges fake/fragmented symbols and installs 71 canonical symbols + 12 calendar months. """ import json import re import glob from pathlib import Path import fitz BASE_DIR = Path(__file__).resolve().parent.parent DATA_DIR = BASE_DIR / "assets" / "data" PDF_PATH = Path("/home/alexives/Desktop/newsources/Battistini - SIMVOLY I ALLEGORII (Enziklopedija iskusstva) - 2008, pdf.pdf") # 71 Canonical symbols in Battistini's index (PDF page 314) # (Name, Start PDF Page, End PDF Page) CANONICAL_SYMBOLS = [ ("Уроборус", 7, 9), ("Океан", 10, 11), ("Случай", 12, 12), ("Эон", 13, 13), ("Время", 14, 18), ("Христос-Хронократор", 19, 23), ("Зодиак", 24, 25), ("Времена года", 26, 27), ("Весна", 28, 29), ("Лето", 30, 32), ("Осень", 33, 35), ("Зима", 36, 39), ("Месяцы", 40, 50), ("Аврора", 51, 53), ("Полдень", 54, 55), ("Сумерки", 56, 57), ("Ночь", 58, 62), ("Оры", 63, 65), ("Жизнь", 66, 69), ("Смерть", 70, 76), ("Возрасты человечества", 77, 79), ("Возрасты человека", 80, 83), ("Андрогин", 84, 85), ("Микрокосм", 86, 91), ("Христос", 92, 97), ("Дева Мария", 98, 101), ("Троица", 102, 104), ("Ева", 105, 107), ("Мать", 108, 112), ("Яйцо", 113, 117), ("Зеркало", 118, 123), ("Крест", 124, 126), ("Ореол", 127, 128), ("Ангел", 129, 134), ("Сатана", 135, 138), ("Чудовище", 139, 143), ("Полицефал", 144, 145), ("Тетраморф", 146, 147), ("Зооморф", 148, 152), ("Хаос", 153, 155), ("Космос", 156, 160), ("Небо", 161, 165), ("Солнце", 166, 167), ("Луна", 168, 169), ("Земля", 170, 174), ("Стихии", 175, 180), ("Потусторонний мир", 181, 183), ("Рай", 184, 185), ("Чистилище", 186, 188), ("Лимб", 189, 190), ("Ад", 191, 192), ("Странствия", 193, 199), ("Сон", 200, 205), ("Лестница", 206, 208), ("Гора", 209, 210), ("Лес", 211, 213), ("Дерево", 214, 217), ("Сад", 218, 222), ("Источник", 223, 225), ("Лабиринт", 226, 228), ("Город", 229, 233), ("Башня", 234, 237), ("Пороки", 238, 251), ("Добродетели", 252, 264), ("Фортуна", 265, 270), ("Пять чувств", 271, 276), ("Темпераменты", 277, 286), ("Любовь", 287, 298), ("Искусства", 299, 303), ("Науки", 304, 308), ("Суета сует (Vanitas)", 309, 314), ] # 12 Individual months MONTHS_SPEC = [ ("Январь", 41, 41), ("Февраль", 42, 42), ("Март", 43, 43), ("Апрель", 44, 44), ("Май", 45, 45), ("Июнь", 46, 46), ("Июль", 47, 47), ("Август", 47, 47), ("Сентябрь", 48, 48), ("Октябрь", 48, 48), ("Ноябрь", 49, 49), ("Декабрь", 50, 50), ] def clean_russian_ocr(text: str) -> str: """Comprehensive text sanitizer for OCR and hyphenation artifacts.""" if not text: return "" # Unhyphenate words broken across line wraps: 'сло- \n во' -> 'слово' text = re.sub(r'([а-яёА-ЯЁa-zA-Z]+)-\s*\n\s*([а-яёА-ЯЁa-zA-Z]+)', r'\1\2', text) # Unhyphenate words broken within line: 'сло- во' -> 'слово' text = re.sub(r'([а-яёА-ЯЁa-zA-Z]+)-\s+([а-яёА-ЯЁa-zA-Z]+)', r'\1\2', text) # Specific OCR homoglyphs and corruptions text = re.sub(r'\b11\s*редставляют', 'Представляют', text) text = re.sub(r'\b11редставляют', 'Представляют', text) text = re.sub(r'\bcivUate\b', 'civitate', text) text = re.sub(r'\bvirtite\b', 'virtute', text) text = re.sub(r'\bdei-v\b', 'девы', text, flags=re.I) text = re.sub(r'\bдеывы\b', 'девы', text, flags=re.I) text = re.sub(r'\bдеыва\b', 'дева', text, flags=re.I) text = re.sub(r'\bдеыву\b', 'деву', text, flags=re.I) text = re.sub(r'\bобродетели\b', 'добродетели', text, flags=re.I) text = re.sub(r'\bобродетель\b', 'добродетель', text, flags=re.I) text = re.sub(r'\bаимб\b', 'лимб', text, flags=re.I) text = re.sub(r'\bАимб\b', 'Лимб', text) text = re.sub(r'\bТор од\b', 'Город', text) text = re.sub(r'\bгор\s+од\b', 'город', text, flags=re.I) text = re.sub(r'\bГор\s+од\b', 'Город', text) text = re.sub(r'\bекбрь\b', 'декабрь', text, flags=re.I) text = re.sub(r'Месяцы\s*•?\s*екбрь', 'Месяцы • Декабрь', text, flags=re.I) text = re.sub(r'Месяцы\s*•?\s*нь', 'Месяцы • Июнь', text, flags=re.I) text = re.sub(r'христо\s*с-?\s*хронократор', 'Христос-Хронократор', text, flags=re.I) text = re.sub(r'христохронократор', 'Христос-Хронократор', text, flags=re.I) text = re.sub(r'ХристосХронократор', 'Христос-Хронократор', text) text = re.sub(r'христос-хронократор', 'Христос-Хронократор', text, flags=re.I) text = re.sub(r'\bcon\b', 'сон', text, flags=re.I) text = re.sub(r'\bCon\b', 'Сон', text) text = re.sub(r'пьер\s*д[\'’`]?аль?и\s*образ(?:\s*мира)?', "Пьер д'Айи («Образ мира»)", text, flags=re.I) text = re.sub(r'пьердали', "Пьер д'Айи", text, flags=re.I) text = re.sub(r'\bпердаль\b', "Пьер д'Айи", text, flags=re.I) # Double spaces cleanup text = re.sub(r'[ \t]+', ' ', text) return text.strip() def extract_clean_battistini_symbol(doc, name: str, start_p: int, end_p: int): """Extracts pristine, structured entry for a Battistini symbol across its page range.""" page_texts = [] artworks = [] for p in range(start_p, end_p + 1): raw = doc[p].get_text() cleaned = clean_russian_ocr(raw) page_texts.append(cleaned) # Check for artwork attributions on illustration pages # Format usually: "• Имя Фамилия. Название картины. Год. Музей" m_art = re.findall(r'([А-ЯЁ][а-яёA-Za-z\s]+(?:\([^\)]+\))?\.\s+[А-ЯЁ«"][^.\n]+(?:\.[^.\n]+)?\.\s*(?:Около\s*)?\d{3,4}(?:[—–-]\d{3,4})?\.\s*[^.\n]+)', cleaned) for art in m_art: art_str = ' '.join(art.split()).strip() if len(art_str) > 20 and not any(skip in art_str for skip in ['Происхождение', 'Характеристики', 'Изображается']): if art_str not in artworks: artworks.append(art_str) first_page_text = page_texts[0] # Extract metadata fields if present etymology = "" m_etym = re.search(r'Происхождение слова\s+([^.\n]+(?:\.[^.\n]+)*)', first_page_text) if m_etym: etymology = ' '.join(m_etym.group(1).split()) characteristics = "" m_char = re.search(r'Характеристики\s+([^.\n]+(?:\.[^.\n]+){1,3})', first_page_text) if m_char: characteristics = ' '.join(m_char.group(1).split()) iconography = "" m_icon = re.search(r'(Изображается[^\n]+(?:\.[^\n]+)?)', first_page_text) if m_icon: iconography = ' '.join(m_icon.group(1).split()) # Extract deep synthesis essay # Synthesis is usually the main continuous text block on the first page synthesis_lines = [] for para in first_page_text.split('\n\n'): para = ' '.join(para.split()).strip() if len(para) > 120: if not any(header in para for header in ['Происхождение слова', 'Родственные божества', 'Религиозно-философские']): synthesis_lines.append(para) synthesis = ' '.join(synthesis_lines) if synthesis_lines else first_page_text[:400] synthesis = ' '.join(synthesis.split()) # Build rich, elegant excerpt (ideal for passport quotes & summaries) excerpt_parts = [] if characteristics and len(characteristics) > 20: excerpt_parts.append(characteristics) if iconography and len(iconography) > 20: excerpt_parts.append(iconography) if synthesis and len(synthesis) > 50: # Add first 1-2 sentences of synthesis s_sentences = [s.strip() for s in synthesis.split('.') if s.strip()] if s_sentences: excerpt_parts.append('. '.join(s_sentences[:2]) + '.') full_excerpt = ' '.join(excerpt_parts) if excerpt_parts else synthesis[:350] # Clean up excerpt formatting full_excerpt = ' '.join(full_excerpt.split()) if len(full_excerpt) > 450: # trim at sentence boundary cut = full_excerpt[:450].rfind('.') if cut > 200: full_excerpt = full_excerpt[:cut+1] # Full formatted text full_text = f"СИМВОЛ И АЛЛЕГОРИЯ: {name.upper()}\n(М. Баттистини · «Символы и аллегории в искусстве», 2008)\n\n" if etymology: full_text += f"• Происхождение слова: {etymology}\n" if characteristics: full_text += f"• Характеристики: {characteristics}\n" if iconography: full_text += f"• Иконография: {iconography}\n" full_text += f"\nСИНТЕЗ И ЗНАЧЕНИЕ В ИСКУССТВЕ:\n{synthesis}\n\n" if artworks: full_text += "КЛЮЧЕВЫЕ ПРОИЗВЕДЕНИЯ В МИРОВОМ ИСКУССТВЕ:\n" for art in artworks[:6]: full_text += f" — {art}\n" return { "term": name, "normalized": name.lower(), "pages": (start_p, end_p), "excerpt": full_excerpt, "full_text": full_text.strip(), "artworks": artworks } def main(): print("=" * 70) print("🧹 САНИТАРИЗАЦИЯ И РЕПАРСИНГ КОРПУСА М. БАТТИСТИНИ") print("=" * 70) if not PDF_PATH.exists(): print(f"❌ Файл PDF не найден: {PDF_PATH}") return doc = fitz.open(str(PDF_PATH)) print(f"📖 PDF открыт: {len(doc)} страниц.") # 1. Парсим все 71 канонических символов print("\n1. Извлечение 71 канонических символов Баттистини...") clean_canon = {} for name, sp, ep in CANONICAL_SYMBOLS: clean_canon[name.lower()] = extract_clean_battistini_symbol(doc, name, sp, ep) # 2. Парсим 12 календарных месяцев print("2. Извлечение 12 календарных месяцев из Баттистини...") for name, sp, ep in MONTHS_SPEC: raw_m = clean_russian_ocr(doc[sp].get_text()) m_lines = [l.strip() for l in raw_m.splitlines() if l.strip()] m_txt = ' '.join(m_lines[:8]) clean_canon[name.lower()] = { "term": name, "normalized": name.lower(), "pages": (sp, ep), "excerpt": f"Месяц {name} в традиции европейской живописи и аллегории: {m_txt}", "full_text": f"КАЛЕНДАРНЫЙ СИМВОЛ: {name.upper()}\n(М. Баттистини · «Символы и аллегории», раздел «Месяцы»)\n\n{raw_m}", "artworks": [] } print(f"✓ Подготовлено {len(clean_canon)} чистых статей Баттистини.") # 3. Привязка иллюстраций к каноническим символам print("\n3. Сопоставление иллюстраций с каноническими символами...") ill_files = glob.glob(str(BASE_DIR / "assets/illustrations/bat_*.jpeg")) + glob.glob(str(BASE_DIR / "assets/illustrations/bat_*.png")) print(f" Всего файлов иллюстраций Баттистини: {len(ill_files)}") symbol_illustrations = {} for img_path_str in ill_files: rel_path = f"assets/illustrations/{Path(img_path_str).name}" m_p = re.search(r'bat_(\d+)_', Path(img_path_str).name) if m_p: pdf_p = int(m_p.group(1)) # Находим канонический символ, покрывающий эту страницу target_sym = None # Сначала проверяем индивидуальные месяцы for m_name, m_sp, m_ep in MONTHS_SPEC: if m_sp == pdf_p: target_sym = m_name.lower() break if not target_sym: for c_name, c_sp, c_ep in CANONICAL_SYMBOLS: if c_sp <= pdf_p <= c_ep: target_sym = c_name.lower() break if target_sym and target_sym in clean_canon: symbol_illustrations.setdefault(target_sym, []).append({ "id": Path(img_path_str).stem, "file": rel_path, "symbol": clean_canon[target_sym]["term"], "caption": f"Аллегория «{clean_canon[target_sym]['term']}» (М. Баттистини, стр. {pdf_p})", "page": pdf_p, "source": "М. Баттистини · «Символы и аллегории»", "normalized": target_sym, "is_core_1000": False, "core_id": None }) print(f" Привязаны иллюстрации к {len(symbol_illustrations)} символам.") # 4. Обновление assets/data/oracle_external_symbols.json print("\n4. Очистка и обновление oracle_external_symbols.json...") ext_path = DATA_DIR / "oracle_external_symbols.json" with open(ext_path, 'r', encoding='utf-8') as f: ext_db = json.load(f) # Удаляем ВСЕ 66 старых записей Баттистини (включая все глюки) old_bat_keys = [] for k, v in list(ext_db.items()): has_bat = any("баттистини" in s.get("author", "").lower() for s in v.get("sources", [])) if has_bat: old_bat_keys.append(k) del ext_db[k] print(f" Удалено старых записей Баттистини (включая фантомные 'con', 'аимб', 'гор од' и др.): {len(old_bat_keys)}") # Вставляем чистый канон now_ts = "2026-10-08T19:50:00.000000" for norm_k, item in clean_canon.items(): ext_db[norm_k] = { "term": item["term"], "normalized": norm_k, "category_guess": "Аллегории и символы искусства", "sources": [ { "author": "М. Баттистини", "work": "«Символы и аллегории» (2008)", "excerpt": item["excerpt"], "full_text": item["full_text"], } ], "illustrations": symbol_illustrations.get(norm_k, []), "created_at": now_ts, "updated_at": now_ts, } # Санитаризуем все оставшиеся внешние символы от дефисных разрывов cleaned_ext_count = 0 for k, v in ext_db.items(): v["term"] = clean_russian_ocr(v.get("term", "")) for s in v.get("sources", []): if "excerpt" in s: s["excerpt"] = clean_russian_ocr(s["excerpt"]) if "full_text" in s: s["full_text"] = clean_russian_ocr(s["full_text"]) cleaned_ext_count += 1 with open(ext_path, 'w', encoding='utf-8') as f: json.dump(ext_db, f, ensure_ascii=False, indent=2) print(f" ✓ oracle_external_symbols.json сохранен ({len(ext_db)} символов).") # 5. Обновление assets/data/oracle_passports.json и oracle_passports_staging.json print("\n5. Обновление oracle_passports.json и oracle_passports_staging.json...") for db_filename in ["oracle_passports.json", "oracle_passports_staging.json"]: db_path = DATA_DIR / db_filename with open(db_path, 'r', encoding='utf-8') as f: passports = json.load(f) updated_bat_in_passports = 0 total_fields_sanitized = 0 for p_id, entry in passports.items(): w = entry.get("word", "").lower() fields = entry.get("fields", {}) # 1. Если для этого слова есть каноническая статья Баттистини — заменяем цитаты Баттистини на чистые matched_canon = clean_canon.get(w) # Дополнительные сопоставления (например, 'стихи' -> 'стихии') if not matched_canon: if w == "стихи": matched_canon = clean_canon.get("стихии") elif w == "месяц": matched_canon = clean_canon.get("месяцы") for fk, fv in list(fields.items()): if isinstance(fv, str): if "баттистини" in fv.lower(): if matched_canon: # Ставим чистый, глубокий академический синтез fields[fk] = f"[М. Баттистини · «Символы и аллегории»]: «{matched_canon['excerpt']}»" updated_bat_in_passports += 1 else: # Санитаризуем существующую цитату fields[fk] = clean_russian_ocr(fv) else: # Санитаризуем любые другие поля от опечаток и переносов слов fields[fk] = clean_russian_ocr(fv) total_fields_sanitized += 1 # Санитаризуем метаданные карточки if "description" in entry and isinstance(entry["description"], str): entry["description"] = clean_russian_ocr(entry["description"]) if "definition" in entry and isinstance(entry["definition"], str): entry["definition"] = clean_russian_ocr(entry["definition"]) # Привязываем чистые иллюстрации Баттистини к карточке ядра if matched_canon and matched_canon["normalized"] in symbol_illustrations: target_ills = entry.setdefault("illustrations", []) for ill in symbol_illustrations[matched_canon["normalized"]]: ill_copy = dict(ill) ill_copy["is_core_1000"] = True ill_copy["core_id"] = p_id if not any(x.get("file") == ill_copy["file"] for x in target_ills): target_ills.append(ill_copy) with open(db_path, 'w', encoding='utf-8') as f: json.dump(passports, f, ensure_ascii=False, indent=2) print(f" ✓ {db_filename}: обновлено {updated_bat_in_passports} цитат Баттистини, санитаризовано {total_fields_sanitized} полей.") # 6. Обновление каталога иллюстраций oracle_illustrations_catalog.json print("\n6. Обновление каталога иллюстраций oracle_illustrations_catalog.json...") cat_path = DATA_DIR / "oracle_illustrations_catalog.json" if cat_path.exists(): with open(cat_path, 'r', encoding='utf-8') as f: catalog = json.load(f) # Очищаем все привязки к фантомным символам Баттистини cleaned_cat = [] for ill in catalog: if "bat_" in ill.get("id", "") or "bat_" in ill.get("file", ""): m_p = re.search(r'bat_(\d+)_', ill.get("file", "")) if m_p: pdf_p = int(m_p.group(1)) target_sym = None for m_name, m_sp, m_ep in MONTHS_SPEC: if m_sp == pdf_p: target_sym = m_name.lower() break if not target_sym: for c_name, c_sp, c_ep in CANONICAL_SYMBOLS: if c_sp <= pdf_p <= c_ep: target_sym = c_name.lower() break if target_sym and target_sym in clean_canon: ill["symbol"] = clean_canon[target_sym]["term"] ill["normalized"] = target_sym ill["caption"] = f"Аллегория «{clean_canon[target_sym]['term']}» (М. Баттистини)" cleaned_cat.append(ill) else: # Другие иллюстрации просто санитаризуем ill["symbol"] = clean_russian_ocr(ill.get("symbol", "")) ill["caption"] = clean_russian_ocr(ill.get("caption", "")) cleaned_cat.append(ill) with open(cat_path, 'w', encoding='utf-8') as f: json.dump(cleaned_cat, f, ensure_ascii=False, indent=2) print(f" ✓ Каталог иллюстраций обновлен: {len(cleaned_cat)} элементов.") print("\n" + "=" * 70) print("✅ ВСЕ ДАННЫЕ УСПЕШНО ОЧИЩЕНЫ И ВОССТАНОВЛЕНЫ ПО БАТТИСТИНИ!") print(" (НИКАКИЕ СБОРКИ НЕ ЗАПУСКАЛИСЬ)") print("=" * 70) if __name__ == "__main__": main()