feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets

This commit is contained in:
2026-10-09 15:26:58 +05:00
parent 7f91366201
commit 0b12e4583b
244 changed files with 611461 additions and 1617 deletions
+456
View File
@@ -0,0 +1,456 @@
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)