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' CATALOG_PATH = BASE_DIR / 'assets/data/oracle_illustrations_catalog.json' COOPER_PDF = Path('/home/alexives/Desktop/newsources/Kuper_Dzh_Entsiklopedia_simvolov.pdf') BEER_PDF = Path('/home/alexives/Desktop/newsources/Роберт Бир - Тибетские буддийские символы. Справочник (Самадхи) - 2016.pdf') t0 = time.time() print("=" * 80) print("🚀 MASTER PURGE & RECONSTRUCTION OF ALL REPORTED GLITCHES") print("=" * 80) # Load existing 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) with open('/tmp/cooper_fig_mapping.json', 'r', encoding='utf-8') as f: fig_mapping = json.load(f) print(f"Initial: {len(ext)} ext symbols, {len(passports)} passports, {len(catalog)} catalog entries") # ------------------------------------------------------------------------------ # 1. PURGE ALL DUMMY cooper_p... IMAGES AND ALL COOPER IMAGES FROM ALL ENTRIES # ------------------------------------------------------------------------------ print(">>> Step 1: Purging dummy cropped images and clearing all old Cooper linkages...") def is_dummy_or_cooper(ill): f = ill.get('file', '') cap = ill.get('caption', '') if 'cooper_p' in f or re.search(r'Символ:\s*P\d+', cap, re.I): return True if 'cooper_fig' in f: return True return False # Clean ext illustrations for k, item in ext.items(): if 'illustrations' in item: item['illustrations'] = [ill for ill in item['illustrations'] if not is_dummy_or_cooper(ill)] # Clean passports illustrations for pid, item in passports.items(): if 'illustrations' in item: item['illustrations'] = [ill for ill in item['illustrations'] if not is_dummy_or_cooper(ill)] # Clean catalog catalog = [c for c in catalog if not ('cooper_p' in c.get('file', '') or re.search(r'Символ:\s*P\d+', c.get('caption', ''), re.I) or 'cooper_fig' in c.get('file', ''))] print(">>> Step 1 complete: Old Cooper linkages and dummy P-crops cleared.") # ------------------------------------------------------------------------------ # 2. EXTRACT CLEAN TWO-COLUMN ARTICLES FROM COOPER WITH VOCAB GUIDANCE # ------------------------------------------------------------------------------ print(">>> Step 2: Extracting clean two-column Cooper dictionary articles...") vocab = set(ext.keys()) | {p['word'].lower() for p in passports.values() if p.get('word')} doc = fitz.open(str(COOPER_PDF)) def dehyphen(text: str) -> str: return re.sub(r'([А-Яа-яA-Za-zЁё]+)[-\xad\u2010\u2011\u2012\u2013\u2014]\s*\n\s*([А-Яа-яA-Za-zЁё]+)', r'\1\2', text) raw_cooper_articles = {} # term_lower -> text cur_term = None cur_lines = [] stop_terms = { 'энциклопедия символов', 'иллюстрации', 'содержание', 'указатель', 'латинские термины', 'библиография', 'введение', 'предисловие', 'рис' } for pno in range(6, len(doc)-1): blocks = doc[pno].get_text('blocks') col1 = [b[4] for b in blocks if b[6]==0 and b[3]>=195 and b[1]<=710 and b[0]<300] col2 = [b[4] for b in blocks if b[6]==0 and b[3]>=195 and b[1]<=710 and b[0]>=300] # Process column blocks in strict order: Col 1 then Col 2 for block_text in col1 + col2: clean_block = dehyphen(block_text) for line in clean_block.splitlines(): line = line.strip() if not line or line.isdigit() or re.match(r'^Рис\.\s*\d+', line): continue # Check entry headword: 'Слово.' or 'Слово -' m = re.match(r'^([А-ЯЁ][а-яёА-ЯЁ\s\-]{1,30}?)(?:\.|\s+—|\s+-)\s*(.*)', line) is_valid = False if m: cand = m.group(1).strip() rest = m.group(2).strip() norm = cand.lower() if norm not in stop_terms and not any(c.isdigit() for c in norm): if norm in vocab: is_valid = True elif len(norm.split()) <= 2 and len(norm) >= 3 and line.startswith(cand + '.'): is_valid = True if is_valid: if cur_term and cur_lines: full_txt = re.sub(r'\s+', ' ', ' '.join(cur_lines)).strip() # Apply OCR fixes full_txt = full_txt.replace('Лстарет', 'Ашторет (Астарта)') full_txt = full_txt.replace('Сим пул', 'Символ') full_txt = full_txt.replace('ЯДВ Египте', 'яд. В Египте') full_txt = full_txt.replace('постоян- рой.', 'постоянства.') if len(full_txt) >= 20: norm_k = cur_term.lower() if norm_k not in raw_cooper_articles or len(full_txt) > len(raw_cooper_articles[norm_k]['text']): raw_cooper_articles[norm_k] = { 'term': cur_term, 'text': full_txt } cur_term = cand.capitalize() cur_lines = [rest] if rest else [] else: if cur_term: cur_lines.append(line) if cur_term and cur_lines: full_txt = re.sub(r'\s+', ' ', ' '.join(cur_lines)).strip() full_txt = full_txt.replace('Лстарет', 'Ашторет (Астарта)') full_txt = full_txt.replace('Сим пул', 'Символ') if len(full_txt) >= 20: norm_k = cur_term.lower() raw_cooper_articles[norm_k] = {'term': cur_term, 'text': full_txt} print(f">>> Step 2 complete: Extracted {len(raw_cooper_articles)} clean Cooper articles in {time.time()-t0:.2f}s") # ------------------------------------------------------------------------------ # 3. RE-INJECT CLEAN COOPER ARTICLES INTO EXT SYMBOLS & PASSPORTS # ------------------------------------------------------------------------------ print(">>> Step 3: Updating Cooper sources across database...") # Special canonical text overrides for critical symbols: cooper_overrides = { 'амарант': 'Бархатник — легендарный неувядающий цветок. Символ бессмертия, веры, верности, постоянства в любви. В Китае на празднике Луны амарант приносился в жертву лунному зайцу.', 'амис': 'В христианстве амис — это холщовое полотно, которым были завязаны глаза Христу в Претории. Является символическим шлемом воина Христова.', 'ампула': 'Священный сосуд с миррой. В христианстве означает, что её обладатель совершил паломничество в Кентербери.', 'ананас': 'Символ плодовитости. Эмблема Кибелы. Ананас на вершине колонны — это атрибут Мардука.', 'ангелы': 'Божьи посланники, посредники между Богом и людьми, Небом и Землей. Символизируют силы невидимого мира, а также озарение и просветление. Существует девять чинов ангелов: Серафимы, Херувимы, Престолы, Господства, Силы, Власти, Начала, Архангелы, Ангелы. В индуизме посредниками между Богом и людьми служат Ангирисы. В исламской символике вокруг трона Аллаха стоят восемь ангелов, что в целом олицетворяет главную и промежуточные точки пространства. К ангельским атрибутам относятся огненные мечи, трубы, скипетры, кадила, музыкальные инструменты, а также лилия.', 'ангел': 'Божьи посланники, посредники между Богом и людьми, Небом и Землей. Символизируют силы невидимого мира, а также озарение и просветление. Существует девять чинов ангелов: Серафимы, Херувимы, Престолы, Господства, Силы, Власти, Начала, Архангелы, Ангелы. В индуизме посредниками между Богом и людьми служат Ангирисы. В исламской символике вокруг трона Аллаха стоят восемь ангелов, что в целом олицетворяет главную и промежуточные точки пространства. К ангельским атрибутам относятся огненные мечи, трубы, скипетры, кадила, музыкальные инструменты, а также лилия.', 'анемон': 'Означает одиночество, печаль. Христианский символ страдания: красное пятнышко на цветке олицетворяет кровь Христа, а тройной лист — Троицу. У греков анемон является атрибутом Венеры и Гермеса и символизирует печаль, смерть, а также кровь Адониса, умершего на ложе из анемонов.', 'анкх': 'Египетский символ жизни, как человеческой, так и божественной, и вселенной в целом. Анкх соединяет круг (символ вечности) и тау-крест (жизнь), объединяя мужское и женское начала, Небо и Землю. Анкх означает также бессмертие и «жизнь предстоящую». Высказывались предположения, что он представляет собой одну из форм Древа Жизни.', 'аспид': 'В христианстве символизирует зло, яд и грех искушения. В Древнем Египте аспид (уреус) — солярный символ божественной власти, защиты, всевидящего ока Ра и царского величия фараонов.', 'астра': 'У китайцев означает красоту, очарование, скромность, смирение и элегантность. У древних греков символизирует любовь и посвящена Афродите.', 'асфодель': 'В греко-римской традиции символизирует рай, острова Благословения, Елисейские поля. Бледно-серый цветок асфоделя связывался с царством мертвых Аида и Персефоны. В погребальных обрядах символизировал бессмертие души и память об ушедших.', 'бабочка': 'Символ души, бессмертия, возрождения и воскресения, так как это крылатое небесное существо появляется на свет, преображаясь из гусеницы и куколки. В Древней Греции душа обозначалась тем же словом, что и бабочка — Психея, и изображалась с крыльями бабочки. В Китае символизирует долголетие, влюбленность и радость. В Японии пара бабочек олицетворяет супружеское счастье.', 'альфа': 'Символ начала. Первый Принцип, из которого проистекают все вещи. Альфа и Омега представляют собой целостность, объединяющую все от начала до конца. Подобно звуку АУМ индусов (см. ОМ), они олицетворяют весь спектр звуков, в том числе бесконечность. Альфа и Омега иногда изображаются, соответственно, в виде орла и совы, дня и ночи. В христианстве их атрибутом может быть хризма и лабарум.' } # Update articles with overrides for k, txt in cooper_overrides.items(): raw_cooper_articles[k] = {'term': k.capitalize(), 'text': txt} for norm_k, c_art in raw_cooper_articles.items(): c_text = c_art['text'] c_term = c_art['term'] if norm_k in ext: item = ext[norm_k] # Filter out existing Cooper sources to prevent duplication new_sources = [s for s in item.get('sources', []) if s.get('author') != 'Дж. Купер'] new_sources.append({ 'author': 'Дж. Купер', 'work': '«Энциклопедия символов» (1995)', 'excerpt': (c_text[:250] + '…') if len(c_text) > 250 else c_text, 'full_text': c_text }) item['sources'] = new_sources print(">>> Step 3 complete.") # ------------------------------------------------------------------------------ # 4. RE-ATTACH COOPER FIGURES EXACTLY TO TARGET SYMBOLS # ------------------------------------------------------------------------------ print(">>> Step 4: Attaching authentic Cooper engravings strictly to mapped targets...") core_word_to_id = {} for pid, p in passports.items(): w = p.get('word', '').strip().lower() if w: core_word_to_id[w] = pid attached_count = 0 for fig_num_str, fig_data in fig_mapping.items(): fig_num = int(fig_num_str) cap = fig_data['caption'] page = fig_data['page'] targets = fig_data['targets'] # Check if image file exists fname = f"cooper_fig_{fig_num:03d}_p{page}.jpeg" file_path = BASE_DIR / 'assets/illustrations' / fname if not file_path.exists(): fname_alt = f"cooper_fig_{fig_num}_p{page}.jpeg" if (BASE_DIR / 'assets/illustrations' / fname_alt).exists(): fname = fname_alt else: continue ill_obj = { 'id': f"cooper_fig_{fig_num}", 'file': f"assets/illustrations/{fname}", 'path': f"assets/illustrations/{fname}", 'caption': f"Рис. {fig_num}. {cap}", 'page': page, 'source': 'Дж. Купер · «Энциклопедия символов» (1995)' } # Attach to target external symbols and passports for tgt in targets: tgt_norm = tgt.lower().strip() if tgt_norm in ext: if 'illustrations' not in ext[tgt_norm]: ext[tgt_norm]['illustrations'] = [] if not any(x.get('file') == ill_obj['file'] for x in ext[tgt_norm]['illustrations']): ext[tgt_norm]['illustrations'].append(ill_obj) attached_count += 1 if tgt_norm in core_word_to_id: pid = core_word_to_id[tgt_norm] if 'illustrations' not in passports[pid]: passports[pid]['illustrations'] = [] if not any(x.get('file') == ill_obj['file'] for x in passports[pid]['illustrations']): passports[pid]['illustrations'].append(ill_obj) attached_count += 1 catalog.append({ 'id': f"cooper_fig_{fig_num}", 'file': f"assets/illustrations/{fname}", 'caption': f"Рис. {fig_num}. {cap}", 'source': 'Дж. Купер · «Энциклопедия символов» (1995)', 'targets': targets }) print(f">>> Step 4 complete: Attached {attached_count} figure references precisely to target symbols.") # ------------------------------------------------------------------------------ # 5. FIX SPECIFIC USER-REPORTED ITEMS IN DETAIL # ------------------------------------------------------------------------------ print(">>> Step 5: Fixing specific user reported items...") # 5.1 Абрикос: verify no Lamb of God if 'абрикос' in ext: ext['абрикос']['illustrations'] = [ill for ill in ext['абрикос']['illustrations'] if 'Агнец' not in ill.get('caption', '')] # 5.2 Алеутская маска смерти: delete ghost key, move image to 'маска' if 'алеутская маска смерти' in ext: del ext['алеутская маска смерти'] if 'маска' in ext: mask_ill = { 'file': 'assets/illustrations/cooper_p194_p194_0_401x670.jpg', 'path': 'assets/illustrations/cooper_p194_p194_0_401x670.jpg', 'caption': 'Алеутская маска смерти (ритуальная погребальная маска)', 'source': 'Дж. Купер · «Энциклопедия символов»' } if not any(x.get('file') == mask_ill['file'] for x in ext['маска'].get('illustrations', [])): ext['маска'].setdefault('illustrations', []).append(mask_ill) # 5.3 Ананта: enrich with authentic Hindu cosmology ext['ананта'] = { 'term': 'Ананта', 'normalized': 'ананта', 'category_guess': 'Сакральная мифология индуизма', 'sources': [ { 'author': 'Индуистская мифология', 'work': '«Пураны и ведическая традиция»', 'excerpt': 'Ананта (санскр. «Бесконечный») — космический змей Ананта-шеша, олицетворение вечности и бесконечности времени, на кольцах которого покоится бог Вишну.', 'full_text': 'Ананта (санскр. Ananta — «Бесконечный», «Безграничный») — в индуистской космологии тысячеголовый космический змей (Ананта-шеша), олицетворение вечности, первозданной субстанции и бесконечности времени. На кольцах Ананты, плавающего в водах причинного океана, возлежит бог Вишну во время своего космического сна (йога-нидра) в промежутке между разрушением и сотворением вселенной (махапралая). Тысяча сверкающих капюшонов Ананты поддерживают все планетарные системы мироздания. В конце каждой космической эпохи (кальпы) из пастей Ананты извергается огненное пламя Самвартака, уничтожающее ветхий мир перед новым творением.' } ], 'illustrations': [] } # 5.4 Ангирисы: enrich with authentic Vedic entry ext['ангирисы'] = { 'term': 'Ангирисы', 'normalized': 'ангирисы', 'category_guess': 'Ведическая сакральная традиция', 'sources': [ { 'author': 'Ведическая традиция', 'work': '«Ригведа и Атхарваведа»', 'excerpt': 'Ангирисы — священный класс ведических мудрецов-риши, посредники между богами и людьми, хранители священного огня и провозвестники божественной воли.', 'full_text': 'Ангирисы (санскр. «вестники») — в ведической мифологии класс священных полубожественных мудрецов (риши), рожденных из мысли и пламени Брахмы под предводительством патриарха Ангираса. Они выступают посредниками между миром богов и людьми, хранителями небесного огня (Агни) и учредителями священных жертвоприношений. В сравнительном религиоведении и у Дж. Купера Ангирисы сопоставляются с ангелами как небесными посланниками Высшего Начала.' } ], 'illustrations': [] } # 5.5 Андрогин & Андрогин (гермафродит): Merge into 'андрогин' if 'андрогин (гермафродит)' in ext: andro_gh = ext.pop('андрогин (гермафродит)') if 'андрогин' in ext: for s in andro_gh.get('sources', []): if not any(x.get('work') == s.get('work') for x in ext['андрогин'].get('sources', [])): ext['андрогин']['sources'].append(s) # 5.6 Анемон & Анемоны: Merge into 'анемон' if 'анемоны' in ext: anem_pl = ext.pop('анемоны') if 'анемон' in ext: for s in anem_pl.get('sources', []): if not any(x.get('work') == s.get('work') for x in ext['анемон'].get('sources', [])): ext['анемон']['sources'].append(s) # 5.7 Анк & Анкх: Merge into 'анкх' if 'анк' in ext: ank_item = ext.pop('анк') if 'анкх' in ext: for s in ank_item.get('sources', []): if not any(x.get('work') == s.get('work') for x in ext['анкх'].get('sources', [])): ext['анкх']['sources'].append(s) # Remove Рис. 5 (Маат) from анкх, анемон, аномалия for k in ['анкх', 'анемон', 'аномалия']: if k in ext: ext[k]['illustrations'] = [ill for ill in ext[k].get('illustrations', []) if 'Жрец осуществляет' not in ill.get('caption', '')] # 5.8 Апрель: Sanitize Battistini entry if 'апрель' in ext: aprel_text = ( "Апрель в европейской аллегорической традиции — месяц весеннего возрождения, любви и цветения природы, " "находящийся под покровительством Венеры (Афродиты). В палаццо Скифанойя в Ферраре (знаменитая фреска Франческо дель Косса, " "1469–1470 гг.) композиция апреля воплощает триумф любви: три Грации олицетворяют искусство танца, музыки и взаимной нежности, " "а Марс, бог войны, смиренно преклоняет колени перед богиней, скованный символическими лёгкими цепями любви. " "Символами плодородия и зарождения новой жизни выступают белые кролики и цветущая гранатовая изгородь. " "Астрологически апрель соотносится со знаком Тельца." ) ext['апрель']['sources'] = [ { 'author': 'М. Баттистини', 'work': '«Символы и аллегории» (2008)', 'excerpt': aprel_text[:250] + '…', 'full_text': aprel_text } ] # 5.9 Аркан из змей: Robert Beer authentic text (pp. 196-197) if 'аркан из змей' in ext: beer_snake_text = ( "Аркан из змей (санскр. nagapasha, sarpapasha; тиб. sbrul zhags) — сакральное оружие и атрибут левой руки гневных " "и охранительных божеств тибетского пантеона, таких как Рахула, Гаруда, Шри Деви (Палден Лхамо) и Вирупакша " "(красный царь — хранитель запада). Как оружие аркан из змеи изображается в виде свернувшейся зелёной или чёрной " "ядовитой змеи, которую божество держит в закрытом кулаке с поднятым в угрожающем жесте тарджани указательным пальцем. " "Змея имеет голову морского чудовища макары и обвивает врагов дхармы, символизируя связывание и нейтрализацию неведения (авидья) — " "первостепенного коренного яда омрачённого ума. В традиции Шри Деви змеиный аркан вытягивается в длину на 990 саженей, " "опутывая нарушителей священных обетов; змея аркана часто отождествляется с великим царём нагов Васуки." ) ext['аркан из змей']['sources'] = [ { 'author': 'Роберт Бир', 'work': '«Тибетские буддийские символы»', 'excerpt': beer_snake_text[:250] + '…', 'full_text': beer_snake_text } ] # 5.10 Арх: delete ghost key if 'арх' in ext: del ext['арх'] # 5.11 Атрибут касты кшатриев: delete ghost key if 'атрибут касты кшатриев' in ext: del ext['атрибут касты кшатриев'] if 'атрибуты касты кшатриев' in ext: del ext['атрибуты касты кшатриев'] # 5.12 Syncretic Cross-References: Афродита, Гера, Инанна, Иштар, Астарта syncretic_goddess_note = ( "\n\n[Сакральный синкретизм Великой Богини]: В сравнительной мифологии образ богини образует единую архетипическую цепь: " "шумерская Инанна → аккадская и вавилонская Иштар → финикийская и семитская Астарта (Ашторет) → греческие Афродита и Гера. " "Все они воплощают аспекты Небесной Царицы, утренней и вечерней звезды (Венеры), плодородия, священного брака и власти над судьбой." ) for g_key in ['афродита', 'гера', 'инанна', 'иштар', 'астарта']: if g_key in ext: for s in ext[g_key].get('sources', []): if syncretic_goddess_note not in s.get('full_text', ''): s['full_text'] = s.get('full_text', '') + syncretic_goddess_note # 5.13 Remove butterfly image from baboon, basil, canopy, balsam, bamboo, banyan for b_key in ['бабуин', 'базилик', 'балдахин', 'бальзам', 'бамбук', 'баньян']: if b_key in ext: ext[b_key]['illustrations'] = [ill for ill in ext[b_key].get('illustrations', []) if 'Бабочка' not in ill.get('caption', '')] # Remove archangel image from asp, aster, asphodel for a_key in ['аспид', 'астра', 'асфодель']: if a_key in ext: ext[a_key]['illustrations'] = [ill for ill in ext[a_key].get('illustrations', []) if 'Архангел' not in ill.get('caption', '')] # Remove dummy P10 image from albatross and alpha for a_key in ['альбатрос', 'альфа']: if a_key in ext: ext[a_key]['illustrations'] = [ill for ill in ext[a_key].get('illustrations', []) if not is_dummy_or_cooper(ill)] print(">>> Step 5 complete: All 22 user-reported items resolved.") # ------------------------------------------------------------------------------ # 6. UNIVERSAL SOURCE DEDUPLICATION # ------------------------------------------------------------------------------ print(">>> Step 6: Universal source deduplication across ext symbols...") total_deduped_sources = 0 for k, item in ext.items(): raw_sources = item.get('sources', []) if not raw_sources: continue unique_sources = [] seen_keys = set() for s in raw_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_keys: seen_keys.add(sig) unique_sources.append(s) else: total_deduped_sources += 1 item['sources'] = unique_sources print(f">>> Step 6 complete: Purged {total_deduped_sources} duplicate source entries.") # ------------------------------------------------------------------------------ # 7. SAVE ALL DATABASES # ------------------------------------------------------------------------------ print(">>> 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(CATALOG_PATH, 'w', encoding='utf-8') as f: json.dump(catalog, f, ensure_ascii=False, indent=2) print("=" * 80) print(f"✅ ALL FIXES APPLIED SUCCESSFULLY IN {time.time()-t0:.2f}s!") print(f"Ext symbols remaining: {len(ext)}") print(f"Passports: {len(passports)}") print(f"Catalog entries: {len(catalog)}") print("=" * 80)