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