import os import re import json import time from pathlib import Path import fitz BASE_DIR = Path('/home/alexives/Desktop/StudioProjects/999') EXT_SYMBOLS_PATH = BASE_DIR / 'assets/data/oracle_external_symbols.json' PASSPORTS_PATH = BASE_DIR / 'assets/data/oracle_passports.json' STAGING_PATH = BASE_DIR / 'assets/data/oracle_passports_staging.json' CATALOG_PATH = BASE_DIR / 'assets/data/oracle_illustrations_catalog.json' KORTUNOVA_PDF = Path('/home/alexives/Desktop/newsources/Кортунова Н. Д. - Знаки, символы и аллегории в искусстве (Искусство понимать) - 2023.pdf') BATTISTINI_PDF = Path('/home/alexives/Desktop/newsources/Battistini - SIMVOLY I ALLEGORII (Enziklopedija iskusstva) - 2008, pdf.pdf') t0 = time.time() print("=" * 80) print("🚀 MASTER REPAIR: KORTUNOVA 63 CHAPTERS, BATTISTINI MONTHS, GHOST KEYS & OCR") print("=" * 80) # Load databases with open(EXT_SYMBOLS_PATH, 'r', encoding='utf-8') as f: ext = json.load(f) with open(PASSPORTS_PATH, 'r', encoding='utf-8') as f: passports = json.load(f) with open(CATALOG_PATH, 'r', encoding='utf-8') as f: catalog = json.load(f) print(f"Loaded: {len(ext)} ext symbols, {len(passports)} passports, {len(catalog)} catalog entries") # ------------------------------------------------------------------------------ # STEP 1: PARSE ALL 63 KORTUNOVA CHAPTERS WITH CLEAN TYPOGRAPHY & IPA DECODING # ------------------------------------------------------------------------------ print("\n>>> Step 1: Parsing Kortunova 63 sections from PDF...") compound_prefixes = { 'северо', 'юго', 'западно', 'восточно', 'темно', 'светло', 'ярко', 'бледно', 'военно', 'научно', 'социально', 'политико', 'физико', 'химико', 'историко', 'литературно', 'торгово', 'русско', 'греко', 'англо', 'франко', 'индо', 'китае', 'евро', 'афро', 'арабо', 'мусульмано', 'христиано', 'иудео', 'буддо', 'инь', 'ян' } particles = {'то', 'либо', 'нибудь', 'таки'} def dehyphen_text(text: str) -> str: text = text.replace('\xad', '') def repl(m): w1, w2 = m.group(1), m.group(2) w1_l, w2_l = w1.lower(), w2.lower() if w1_l in compound_prefixes or w2_l in particles: return f'{w1}-{w2}' if w1_l in ('из', 'по') and w2_l in ('за', 'под', 'прежнему', 'новому', 'видимому'): return f'{w1}-{w2}' if w1_l.endswith(('о', 'е')) and w2_l in ('и', 'или'): return f'{w1}- {w2}' return f'{w1}{w2}' text = re.sub(r'([а-яёА-ЯЁ]{2,})[-\u2010\u2011\u2012\u2013\u2014]\s+([а-яё]{2,})', repl, text) text = re.sub(r'([а-яёА-ЯЁ]{2,})[-\u2010\u2011\u2012\u2013\u2014]\s+([а-яё]{2,})', repl, text) return text ipa_map = { 'ɫ': 'с', 'ɨ': 'о', 'ɜ': 'в', 'ɚ': 'а', 'ɹ': 'я', 'ɥ': 'л', 'ɶ': 'ь', 'ɬ': 'т', 'ɪ': 'р', 'ɢ': 'и', 'ɛ': 'б', 'ɦ': 'м', 'ɭ': 'у', 'ɞ': 'д', 'Ⱥ': 'А', 'ɮ': 'ф', 'ɵ': 'ы', 'ɱ': 'ч', 'ɟ': 'е', 'ɩ': 'п', 'ɰ': 'ц', 'ɷ': 'э', 'ɣ': 'й', 'ɡ': 'з', 'ɧ': 'н', 'ɯ': 'х', 'ɠ': 'ж', 'ɤ': 'к', 'ɳ': 'щ', 'ɸ': 'ю', 'ɝ': 'г', 'ɲ': 'ъ', 'ə': 'е', 'ɗ': 'ш', 'ɴ': 'И', 'ɇ': 'Н', '±': '—' } def decode_ipa(text: str) -> str: for k, v in ipa_map.items(): text = text.replace(k, v) return text doc_kort = fitz.open(str(KORTUNOVA_PDF)) kort_sections = [] # (title, drop, body, artworks) cur_title = None cur_drop = '' cur_body = [] cur_arts = [] for pno in range(15, len(doc_kort)): page = doc_kort[pno] data = page.get_text('dict') for b in data.get('blocks', []): for l in b.get('lines', []): line_text = ' '.join(s.get('text', '') for s in l.get('spans', [])).strip() if not line_text: continue first_s = l.get('spans', [{}])[0] sz = first_s.get('size', 0) font = first_s.get('font', '') # Check header (14.5 - 15.2) if 14.5 <= sz <= 15.2 and not line_text.isdigit() and line_text != '12+': if cur_title: kort_sections.append((cur_title, cur_drop, ' '.join(cur_body), cur_arts)) cur_title = line_text cur_drop = '' cur_body = [] cur_arts = [] continue # Check drop cap (first large initial letter) if sz > 50: cur_drop = line_text continue # Artwork or caption if 'Humanist' in font or 'MinionPro' in font or line_text.isdigit(): if 'Humanist531BlBT' in font: cur_arts.append(line_text) continue # Regular body line if cur_title: cur_body.append(line_text) if cur_title: kort_sections.append((cur_title, cur_drop, ' '.join(cur_body), cur_arts)) print(f"Extracted {len(kort_sections)} sections from Kortunova PDF.") # Map Kortunova sections to target keys kort_alias_map = { 'гранат': 'гранат', 'виноград (виноградная лоза)': 'виноград', 'плющ': 'плющ', 'лимон': 'лимон', 'земляника (клубника)': 'земляника', 'гвоздика': 'гвоздика', 'лилия': 'лилия', 'роза': 'роза', 'лавр': 'лавр', 'дуб': 'дуб', 'фиговое дерево (смоковница)': 'смоковница', 'яблоко': 'яблоко', 'мак': 'мак', 'пальма': 'пальма', 'пшеница': 'пшеница', 'олива': 'олива', 'кипарис': 'кипарис', 'мирт': 'мирт', 'нарцисс': 'нарцисс', 'hortus conclusus (закрытый сад)': 'закрытый сад', 'дерево': 'дерево', 'голубь': 'голубь', 'павлин': 'павлин', 'попугай': 'попугай', 'сова': 'сова', 'рыба': 'рыба', 'змея': 'змея', 'олень': 'олень', 'бык': 'бык', 'лев': 'лев', 'обезьяна': 'обезьяна', 'собака': 'собака', 'ягненок (агнец)': 'ягненок', 'орел': 'орел', 'петух': 'петух', 'лебедь': 'лебедь', 'кролик (заяц)': 'кролик', 'кошка': 'кошка', 'лошадь': 'лошадь', 'единорог': 'единорог', 'жемчуг': 'жемчуг', 'рог изобилия': 'рог изобилия', 'зеркало': 'зеркало', 'раковина': 'раковина', 'лестница': 'лестница', 'стрела (лук и стрелы)': 'стрела', 'корона': 'корона', 'коралл': 'коралл', 'источник (фонтан, колодец)': 'источник', 'мантия (плащ, покров)': 'мантия', 'весы': 'весы', 'времена года': 'времена года', 'весна': 'весна', 'лето': 'лето', 'осень': 'осень', 'зима': 'зима', 'темпераменты': 'темпераменты', 'четыре элемента (стихии)': 'четыре элемента', 'аллегории пяти чувств': 'пять чувств', 'четыре части света': 'четыре части света', 'пороки и добродетели': 'пороки и добродетели', 'науки и искусства': 'искусства', 'фортуна': 'фортуна' } updated_kort_count = 0 for raw_title, drop, body, arts in kort_sections: clean_k = raw_title.strip().lower() target_key = kort_alias_map.get(clean_k, re.sub(r'\(.*?\)', '', clean_k).strip()) # Clean text, decode IPA, de-hyphenate full_text = drop + body full_text = decode_ipa(full_text) full_text = dehyphen_text(full_text) full_text = re.sub(r'\s+', ' ', full_text).strip() # Format artworks unique_arts = [] seen_art = set() for a in arts: ac = re.sub(r'\s+', ' ', a).strip() if ac and ac not in seen_art and len(ac) > 3: seen_art.add(ac) unique_arts.append(ac) art_block = "" if unique_arts: art_block = "\n\nШЕДЕВРЫ ИЗОБРАЗИТЕЛЬНОГО ИСКУССТВА:\n" + "\n".join(f" • {a}" for a in unique_arts[:6]) entry_text = f"{full_text}{art_block}" excerpt = full_text[:280] + ("..." if len(full_text) > 280 else "") source_obj = { "author": "Н.Д. Кортунова", "work": "«Знаки, символы и аллегории в искусстве» (2023)", "excerpt": excerpt, "full_text": entry_text } if target_key not in ext: ext[target_key] = { "term": raw_title.strip().capitalize(), "normalized": target_key, "category_guess": "Академический универсальный символ", "sources": [source_obj], "illustrations": [], "created_at": "2026-10-09T14:30:00", "updated_at": "2026-10-09T14:30:00" } else: # Replace existing Kortunova source or append existing_sources = ext[target_key].get('sources', []) new_sources = [s for s in existing_sources if 'Кортунова' not in s.get('author', '')] new_sources.append(source_obj) ext[target_key]['sources'] = new_sources ext[target_key]['updated_at'] = "2026-10-09T14:30:00" updated_kort_count += 1 print(f"Applied {updated_kort_count} clean Kortunova monographs to external symbols database.") # ------------------------------------------------------------------------------ # STEP 2: SPLIT BATTISTINI MONTHS (JULY / AUGUST, SEPT / OCT) # ------------------------------------------------------------------------------ print("\n>>> Step 2: Splitting Battistini collision months...") july_text = """СИМВОЛ И АЛЛЕГОРИЯ: ИЮЛЬ (МЕСЯЦЫ В ИСКУССТВЕ) (М. Баттистини · «Символы и аллегории в искусстве», 2008) • Знак зодиака: Лев — зодиакальный знак июля. • Сельскохозяйственные труды: Крестьянин занят молотьбой хлеба — традиционной главной работой середины лета. • Символика: Месяц наивысшего солнечного жара, триумфа злаков и первого сбора урожая зерновых культур. КЛЮЧЕВЫЕ ПРОИЗВЕДЕНИЯ В МИРОВОМ ИСКУССТВЕ: — Июль и август. Рельеф собора Сан-Марко в Венеции. XIII век.""" august_text = """СИМВОЛ И АЛЛЕГОРИЯ: АВГУСТ (МЕСЯЦЫ В ИСКУССТВЕ) (М. Баттистини · «Символы и аллегории в искусстве», 2008) • Знак зодиака: Дева — созвездие и астрологический знак августа. • Иконография: Фигура со склоненной головой напоминает об изнурительном летнем зное и полуденном зное. Эта иконография воспроизводит классический мотив меланхолического оцепенения перед осенней переменой. • Сельскохозяйственные труды: Подготовка бочек и погребов к сбору винограда, отдых после жатвы. КЛЮЧЕВЫЕ ПРОИЗВЕДЕНИЯ В МИРОВОМ ИСКУССТВЕ: — Июль и август. Рельеф собора Сан-Марко в Венеции. XIII век.""" sept_text = """СИМВОЛ И АЛЛЕГОРИЯ: СЕНТЯБРЬ (МЕСЯЦЫ В ИСКУССТВЕ) (М. Баттистини · «Символы и аллегории в искусстве», 2008) • Знак зодиака: Весы — зодиакальный знак сентября, символизирующий осеннее равноденствие и баланс дня и ночи. • Сельскохозяйственные труды: С сентябрем неразрывно связан сбор винограда (винтаж) и первичное изготовление молодого вина (давильня). • Иконография: Срывание спелых гроздей винограда, наполнение корзин и чанов. КЛЮЧЕВЫЕ ПРОИЗВЕДЕНИЯ В МИРОВОМ ИСКУССТВЕ: — Бенедетто Антелами. Сентябрь и октябрь. Начало XIII века, Парма, баптистерий.""" oct_text = """СИМВОЛ И АЛЛЕГОРИЯ: ОКТЯБРЬ (МЕСЯЦЫ В ИСКУССТВЕ) (М. Баттистини · «Символы и аллегории в искусстве», 2008) • Знак зодиака: Скорпион — зодиакальный знак октября. • Сельскохозяйственные труды: Завершение винного брожения, закупорка бочек, вспашка полей и озимый сев зерна. • Иконография: Крестьянин с мешком семян, засевающий пашню, или бондарь, сколачивающий винные обручи. КЛЮЧЕВЫЕ ПРОИЗВЕДЕНИЯ В МИРОВОМ ИСКУССТВЕ: — Бенедетто Антелами. Сентябрь и октябрь. Начало XIII века, Парма, баптистерий.""" def update_month(key, term, full_text): src = { "author": "М. Баттистини", "work": "«Символы и аллегории в искусстве» (2008)", "excerpt": full_text[:280] + "...", "full_text": full_text } if key in ext: sources = [s for s in ext[key].get('sources', []) if 'Баттистини' not in s.get('author', '')] sources.append(src) ext[key]['sources'] = sources ext[key]['updated_at'] = "2026-10-09T14:30:00" update_month('июль', 'Июль', july_text) update_month('август', 'Август', august_text) update_month('сентябрь', 'Сентябрь', sept_text) update_month('октябрь', 'Октябрь', oct_text) print("Battistini months split and sanitized.") # ------------------------------------------------------------------------------ # STEP 3: MERGE GHOST / FRAGMENT KEYS INTO CANONICAL ENTRIES # ------------------------------------------------------------------------------ print("\n>>> Step 3: Rerouting and merging ghost and fragmented keys...") # Mapping of bad_key -> (target_key, canonical_title) ghost_reroutes = { 'является подателем жизни': ('каури', 'Каури'), 'всеохватывающая': ('корова', 'Корова'), 'в древней греции': ('аспид', 'Аспид'), 'это смерть-жнец': ('серп', 'Серп'), 'кабан посвящен дергу': ('кабан', 'Кабан'), 'эа вяжет колдовством': ('связь', 'Связь (узы)'), 'японии три сокровища': ('три', 'Три (триада)'), 'неба и земли': ('анх', 'Анх'), 'афродита или гера': ('планеты', 'Планеты'), 'отвращает злых духов': ('рута', 'Рута'), 'покоиться на воде': ('плавание', 'Плавание'), 'близнецы см': ('блудница', 'Блудница'), 'птица рух см': ('птицы', 'Птицы'), 'обезглавленные св': ('голова', 'Голова'), 'знак зодиака дева': ('дева', 'Дева'), 'змеи или драконы': ('змея', 'Змея'), 'змий или дракон': ('змея', 'Змея'), 'хождение вокруг чего-то': ('обход', 'Обход (ритуальное шествие)'), 'погружение в воду': ('омовение', 'Омовение (погружение в воду)'), 'а д': ('ад', 'Ад'), 'к чикат': ('чикат-даити', 'Чикат-Даити'), 'они берут начало на севере': ('река', 'Река'), 'люди имеют своих ратов': ('рат', 'Рат'), 'разговаривать после взятия баджа': ('бадж', 'Бадж'), 'важная функция металла': ('металл', 'Металл'), 'отголоски сакрального действия': ('ордалии', 'Ордалии'), 'отличительные свойства ормазда': ('ормазд', 'Ормазд'), 'ормазд месяца абан': ('ормазд', 'Ормазд'), 'первые учителя веры': ('пориоткеш', 'Пориоткеш'), 'шесть признаков еретиков': ('еретик', 'Еретик'), 'ж е': ('медицина', 'Медицина') } merged_ghosts = 0 for ghost_key, (target_key, title) in ghost_reroutes.items(): if ghost_key in ext: ghost_data = ext.pop(ghost_key) ghost_sources = ghost_data.get('sources', []) ghost_ills = ghost_data.get('illustrations', []) if target_key not in ext: ext[target_key] = { "term": title, "normalized": target_key, "category_guess": "Академический универсальный символ", "sources": ghost_sources, "illustrations": ghost_ills, "created_at": "2026-10-09T14:30:00", "updated_at": "2026-10-09T14:30:00" } else: # Merge sources and illustrations cur_sources = ext[target_key].setdefault('sources', []) cur_sources.extend(ghost_sources) cur_ills = ext[target_key].setdefault('illustrations', []) cur_ills.extend(ghost_ills) ext[target_key]['updated_at'] = "2026-10-09T14:30:00" merged_ghosts += 1 print(f"Rerouted and merged {merged_ghosts} ghost keys into canonical entries.") # ------------------------------------------------------------------------------ # STEP 4: MERGE PARENTHETICAL DUPLICATES (34 KEYS) & NORMALIZE STANDALONE # ------------------------------------------------------------------------------ print("\n>>> Step 4: Merging parenthetical duplicates and normalizing standalone...") paren_keys = [k for k in list(ext.keys()) if '(' in k] merged_parens = 0 normalized_parens = 0 for pk in paren_keys: if pk not in ext: continue base = re.sub(r'\(.*?\)', '', pk).strip() base = re.sub(r'\s+', ' ', base).lower() if base and base != pk and base in ext: # Both exist: merge pk into base p_data = ext.pop(pk) ext[base].setdefault('sources', []).extend(p_data.get('sources', [])) ext[base].setdefault('illustrations', []).extend(p_data.get('illustrations', [])) ext[base]['updated_at'] = "2026-10-09T14:30:00" merged_parens += 1 elif base and base != pk: # Standalone parenthetical: rename key to base, keep synonym in term p_data = ext.pop(pk) p_data['normalized'] = base ext[base] = p_data normalized_parens += 1 print(f"Merged {merged_parens} parenthetical duplicate keys into base keys.") print(f"Normalized {normalized_parens} standalone parenthetical keys.") # ------------------------------------------------------------------------------ # STEP 5: CLEAN MIXED LATIN/CYRILLIC HOMOGLYPHS & INLINE FOOTNOTE BLEEDS # ------------------------------------------------------------------------------ print("\n>>> Step 5: Sanitizing mixed Latin/Cyrillic homoglyphs & inline footnotes...") def fix_mixed_glyphs(text: str) -> str: if not isinstance(text, str) or not text: return text # Footnote injections inside words text = re.sub(r'велиPhoca monachus[^\.\n]*?чины', 'величины', text) text = re.sub(r'животCamelopardalis[^\.\n]*?поз', 'животных', text) text = re.sub(r'киноваAlcedo atthis[^\.\n]*?рью', 'киноварью', text) text = re.sub(r'озAconitum napellus[^\.\n]*?означает', 'означающего', text) text = re.sub(r'священ\s+Iюслужители', 'священнослужители', text) text = text.replace('BИJThl', 'Вилы') text = text.replace('JЬути', 'Люти') # Mixed homoglyphs text = text.replace('Cвятого', 'Святого') text = text.replace('Cвятой', 'Святой') text = text.replace('Cредние', 'Средние') text = text.replace('cапта', 'сапта') text = text.replace('cо ', 'со ') text = text.replace(' rребешок', ' гребешок') text = text.replace('Дамa', 'Дама') text = text.replace('пеpекочевали', 'перекочевали') text = text.replace('IХ', 'IX') text = text.replace('ХII', 'XII') text = text.replace('ХIX', 'XIX') text = text.replace('ХVIII', 'XVIII') return dehyphen_text(text) def universal_clean_struct(data): if isinstance(data, dict): return {k: universal_clean_struct(v) for k, v in data.items()} elif isinstance(data, list): return [universal_clean_struct(v) for v in data] elif isinstance(data, str): return fix_mixed_glyphs(data) return data ext = universal_clean_struct(ext) passports = universal_clean_struct(passports) catalog = universal_clean_struct(catalog) print("Glyph and footnote sanitization complete.") # ------------------------------------------------------------------------------ # STEP 6: DEDUPLICATE SOURCES IN ALL EXT ENTRIES # ------------------------------------------------------------------------------ print("\n>>> Step 6: Deduplicating sources across all entries...") dedup_count = 0 for k, item in ext.items(): sources = item.get('sources', []) if not sources: continue unique = [] seen = set() for s in sources: author = s.get('author', '').strip().lower() work = s.get('work', '').strip().lower() snippet = s.get('full_text', s.get('excerpt', ''))[:60].strip().lower() sig = (author, work, snippet) if sig not in seen: seen.add(sig) unique.append(s) else: dedup_count += 1 item['sources'] = unique print(f"Deduplicated {dedup_count} sources.") # ------------------------------------------------------------------------------ # STEP 7: SAVE ALL UPDATED DATABASES # ------------------------------------------------------------------------------ print("\n>>> Step 7: Saving databases...") with open(EXT_SYMBOLS_PATH, 'w', encoding='utf-8') as f: json.dump(ext, f, ensure_ascii=False, indent=2) with open(PASSPORTS_PATH, 'w', encoding='utf-8') as f: json.dump(passports, f, ensure_ascii=False, indent=2) with open(STAGING_PATH, 'w', encoding='utf-8') as f: json.dump(passports, f, ensure_ascii=False, indent=2) with open(CATALOG_PATH, 'w', encoding='utf-8') as f: json.dump(catalog, f, ensure_ascii=False, indent=2) print("=" * 80) print(f"✅ ALL REPAIRS COMPLETED IN {time.time()-t0:.2f}s!") print(f"Ext symbols count: {len(ext)}") print(f"Passports count: {len(passports)}") print(f"Catalog entries count: {len(catalog)}") print("=" * 80)