Files
999/scripts/master_fix_all_parser_anomalies.py

512 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)