LAD / modules /processors.py
ozeiri's picture
Deploy LAD as a Docker Space
857664f
Raw
History Blame Contribute Delete
36.1 kB
"""Main document processing functions"""
import os
import json
import logging
from dotenv import load_dotenv
from pathlib import Path
from .image_processing import extract_images_from_pdf, prepare_input_image
from .correction import perform_ocr_with_adaptive_correction
from .extraction import extract_artifacts_from_page, extract_multilingual_names_from_page
from .validation import validate_and_complete_multilingual_names
from .data_utils import save_artifacts_to_csv
from .simple_db import get_simple_db
import re
# Load configuration using the configuration manager
try:
from .config_manager import load_configuration
load_configuration()
print("✅ Configuration loaded in processors.py")
except Exception as e:
print(f"⚠️ Error loading configuration in processors.py: {e}")
# Fallback to manual loading
project_root = Path(__file__).parent.parent
env_path = project_root / ".env"
load_dotenv(env_path, override=True)
logger = logging.getLogger(__name__)
def process_english_document(input_file, output_dir, model, start_page=1, end_page=None,
correction_threshold=0.05, ocr_prompt=None, correction_prompt=None,
artifact_prompt=None, ocr_model=None, extraction_model=None):
"""Process English document fully with OCR, adaptive correction, and artifact extraction."""
# Set up model selection
actual_ocr_model = ocr_model or model
actual_extraction_model = extraction_model or model
# Set up document-specific directories
pdf_name = os.path.splitext(os.path.basename(input_file))[0]
doc_base_dir = os.path.join(output_dir, pdf_name)
pages_dir = os.path.join(doc_base_dir, "EN", "pages")
ocr_dir = os.path.join(doc_base_dir, "EN", "ocr")
ocr_corrected_dir = os.path.join(doc_base_dir, "EN", "ocr_corrected")
ocr_corrected2_dir = os.path.join(doc_base_dir, "EN", "ocr_corrected2")
ocr_corrected3_dir = os.path.join(doc_base_dir, "EN", "ocr_corrected3")
results_dir = os.path.join(doc_base_dir, model)
# Log which models are being used
if actual_ocr_model != model:
logger.info(f"Using {actual_ocr_model} for OCR")
if actual_extraction_model != model:
logger.info(f"Using {actual_extraction_model} for artifact extraction")
# Create directories
os.makedirs(doc_base_dir, exist_ok=True)
os.makedirs(pages_dir, exist_ok=True)
os.makedirs(results_dir, exist_ok=True)
# Document name for source tracking
document_name = os.path.basename(input_file)
# Extract pages from the document
if input_file.lower().endswith('.pdf'):
logger.info(f"Processing English PDF: {input_file}")
image_paths = extract_images_from_pdf(input_file, pages_dir, start_page, end_page)
else:
logger.info(f"Processing English image: {input_file}")
image_paths = prepare_input_image(input_file, pages_dir)
# Process each page
all_artifacts = []
for image_path, page_num in image_paths:
logger.info(f"Processing English page {page_num}: {image_path}")
# Check if this page has already been processed
page_output_file = os.path.join(results_dir, f"page_{page_num}_artifacts.json")
if os.path.exists(page_output_file):
logger.info(f"Page {page_num} already processed, loading results")
with open(page_output_file, 'r', encoding='utf-8') as f:
page_artifacts = json.load(f)
all_artifacts.extend(page_artifacts)
continue
# Set up directories for this page's OCR and correction
output_dirs = {
"ocr": ocr_dir,
"corrected1": ocr_corrected_dir,
"corrected2": ocr_corrected2_dir,
"corrected3": ocr_corrected3_dir
}
try:
# Perform OCR with adaptive correction using OCR model
final_corrected_text = perform_ocr_with_adaptive_correction(
image_path=image_path,
page_num=page_num,
document_name=document_name,
model=actual_ocr_model, # Use OCR-specific model
ocr_prompt_template=ocr_prompt,
correction_prompt_template=correction_prompt,
output_dirs=output_dirs,
lang="EN",
correction_threshold=correction_threshold
)
# Extract artifacts using extraction model
artifacts = extract_artifacts_from_page(
image_path=image_path,
page_num=page_num,
document_name=document_name,
model=actual_extraction_model, # Use extraction-specific model
final_corrected_text=final_corrected_text,
artifact_prompt_template=artifact_prompt,
results_dir=results_dir
)
all_artifacts.extend(artifacts)
except Exception as e:
logger.error(f"Error processing English page {page_num}: {e}")
continue
# Save all artifacts
if all_artifacts:
all_artifacts_file = os.path.join(results_dir, "english_artifacts.json")
with open(all_artifacts_file, 'w', encoding='utf-8') as f:
json.dump(all_artifacts, f, indent=2, ensure_ascii=False)
logger.info(f"Processed English document, found {len(all_artifacts)} artifacts")
else:
logger.warning(f"No artifacts found in English document")
return all_artifacts, doc_base_dir
def extract_multilingual_names(artifacts_en, other_lang_file, output_dir, model, lang, doc_base_dir,
correction_threshold=0.05, ocr_prompt=None, correction_prompt=None,
name_extraction_prompt=None, ocr_model=None, extraction_model=None):
"""Extract artifact names in another language (Arabic or French) with adaptive OCR correction."""
# Set up model selection
actual_ocr_model = ocr_model or model
actual_extraction_model = extraction_model or model
if not artifacts_en:
logger.warning(f"No English artifacts to align with {lang}")
return []
if not other_lang_file:
logger.warning(f"No {lang} document provided")
return []
# Log which models are being used
if actual_ocr_model != model:
logger.info(f"Using {actual_ocr_model} for {lang} OCR")
if actual_extraction_model != model:
logger.info(f"Using {actual_extraction_model} for {lang} name extraction")
logger.info(f"Extracting {lang} names for {len(artifacts_en)} artifacts (threshold: {correction_threshold:.4f})")
# Set up directories
lang_pages_dir = os.path.join(doc_base_dir, lang, "pages")
lang_ocr_dir = os.path.join(doc_base_dir, lang, "ocr")
lang_ocr_corrected_dir = os.path.join(doc_base_dir, lang, "ocr_corrected")
lang_ocr_corrected2_dir = os.path.join(doc_base_dir, lang, "ocr_corrected2")
lang_ocr_corrected3_dir = os.path.join(doc_base_dir, lang, "ocr_corrected3")
results_dir = os.path.join(doc_base_dir, model)
os.makedirs(lang_pages_dir, exist_ok=True)
os.makedirs(lang_ocr_dir, exist_ok=True)
os.makedirs(lang_ocr_corrected_dir, exist_ok=True)
os.makedirs(lang_ocr_corrected2_dir, exist_ok=True)
os.makedirs(lang_ocr_corrected3_dir, exist_ok=True)
os.makedirs(results_dir, exist_ok=True)
# Group artifacts by page
artifacts_by_page = {}
current_pages = set()
for artifact in artifacts_en:
page_num = artifact.get("source_page", 0)
current_pages.add(page_num)
if page_num not in artifacts_by_page:
artifacts_by_page[page_num] = []
artifacts_by_page[page_num].append(artifact)
# Delete the global result file to force regeneration for current pages
lang_result_file = os.path.join(results_dir, f"{lang.lower()}_names.json")
if os.path.exists(lang_result_file):
logger.info(f"Deleting existing {lang} names file to force regeneration")
os.remove(lang_result_file)
# Extract pages from the document
if other_lang_file.lower().endswith('.pdf'):
logger.info(f"Processing {lang} PDF: {other_lang_file}")
if artifacts_by_page:
image_paths = extract_images_from_pdf(
other_lang_file,
lang_pages_dir,
min(artifacts_by_page.keys()),
max(artifacts_by_page.keys())
)
else:
image_paths = []
else:
logger.info(f"Processing {lang} image: {other_lang_file}")
image_paths = prepare_input_image(other_lang_file, lang_pages_dir)
# Set up output directories for OCR and correction
output_dirs = {
"ocr": lang_ocr_dir,
"corrected1": lang_ocr_corrected_dir,
"corrected2": lang_ocr_corrected2_dir,
"corrected3": lang_ocr_corrected3_dir
}
# Load existing name mappings from all pages not in current processing batch
all_name_mappings = []
# First load mappings for pages we're not currently processing
for filename in os.listdir(results_dir):
if filename.startswith("page_") and filename.endswith(f"_{lang.lower()}_names.json"):
try:
page_num = int(filename.split("_")[1])
if page_num not in current_pages: # Only load if not in current batch
with open(os.path.join(results_dir, filename), 'r', encoding='utf-8') as f:
existing_mappings = json.load(f)
if isinstance(existing_mappings, list):
all_name_mappings.extend(existing_mappings)
except (ValueError, json.JSONDecodeError):
continue
# Process current pages
for image_path, page_num in image_paths:
if page_num not in artifacts_by_page:
continue # Skip pages with no artifacts
page_artifacts = artifacts_by_page[page_num]
logger.info(f"Processing {lang} page {page_num} with {len(page_artifacts)} artifacts")
# Delete any existing page result file to force regeneration
page_output_file = os.path.join(results_dir, f"page_{page_num}_{lang.lower()}_names.json")
if os.path.exists(page_output_file):
logger.info(f"Deleting existing {lang} names for page {page_num} to force regeneration")
os.remove(page_output_file)
try:
# First ensure we have OCR text for this page
ocr_output_file = os.path.join(output_dirs["ocr"], f"page_{page_num}_ocr.txt")
if not os.path.exists(ocr_output_file):
logger.info(f"Performing OCR for {lang} page {page_num}")
# Perform OCR with adaptive correction using OCR model
perform_ocr_with_adaptive_correction(
image_path=image_path,
page_num=page_num,
document_name=os.path.basename(other_lang_file),
model=actual_ocr_model, # Use OCR-specific model
ocr_prompt_template=ocr_prompt,
correction_prompt_template=correction_prompt,
output_dirs=output_dirs,
lang=lang,
correction_threshold=correction_threshold
)
# Extract multilingual names from the page using extraction model
logger.info(f"About to extract {lang} names for {len(page_artifacts)} artifacts on page {page_num}")
name_mappings = extract_multilingual_names_from_page(
image_path=image_path,
page_num=page_num,
page_artifacts=page_artifacts,
document_name=other_lang_file,
model=actual_extraction_model, # Use extraction-specific model
lang=lang,
name_extraction_prompt=name_extraction_prompt,
ocr_prompt_template=ocr_prompt,
correction_prompt_template=correction_prompt,
output_dirs=output_dirs,
results_dir=results_dir,
correction_threshold=correction_threshold
)
logger.info(f"Extracted {len(name_mappings)} {lang} name mappings from page {page_num}")
if not name_mappings:
logger.warning(f"No {lang} name mappings found for page {page_num} - this will result in empty {lang} names")
all_name_mappings.extend(name_mappings)
except Exception as e:
logger.error(f"Error processing {lang} page {page_num}: {e}")
continue
# Save all name mappings
if all_name_mappings:
with open(lang_result_file, 'w', encoding='utf-8') as f:
json.dump(all_name_mappings, f, indent=2, ensure_ascii=False)
logger.info(f"Extracted {len(all_name_mappings)} {lang} names")
else:
logger.warning(f"No {lang} names extracted")
return all_name_mappings
def create_consolidated_database(artifacts_en, ar_name_mappings, fr_name_mappings, output_dir, doc_name,
model, validation_prompt_func, csv_fields):
"""Create a consolidated database with English metadata and multilingual names."""
logger.info("Creating consolidated multilingual database")
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Create mappings for easier lookup
ar_name_dict = {}
for mapping in ar_name_mappings:
en_name = mapping.get("English_Name", "")
ar_name = mapping.get("Arabic_Name", "")
if en_name and ar_name and ar_name != "NOT_FOUND":
ar_name_dict[en_name] = ar_name
logger.info(f"Created AR name dictionary with {len(ar_name_dict)} mappings")
fr_name_dict = {}
for mapping in fr_name_mappings:
en_name = mapping.get("English_Name", "")
fr_name = mapping.get("French_Name", "")
if en_name and fr_name and fr_name != "NOT_FOUND":
fr_name_dict[en_name] = fr_name
logger.info(f"Created FR name dictionary with {len(fr_name_dict)} mappings")
# Check for existing database
json_output_file = os.path.join(output_dir, f"{doc_name}_multilingual.json")
existing_artifacts = {}
if os.path.exists(json_output_file):
try:
with open(json_output_file, 'r', encoding='utf-8') as f:
existing_data = json.load(f)
# Create lookup by English name
for item in existing_data:
if "Name_EN" in item:
existing_artifacts[item["Name_EN"]] = item
except Exception as e:
logger.warning(f"Error loading existing database: {e}")
# Create multilingual artifacts
multilingual_artifacts = []
processed_names = set() # Track names we've processed to avoid duplicates
for artifact in artifacts_en:
en_name = artifact.get("Name", "")
if en_name in processed_names:
continue # Skip duplicates
processed_names.add(en_name)
# Create multilingual version
multilingual_artifact = {
"Name_EN": en_name,
"Name_AR": ar_name_dict.get(en_name, ""),
"Name_FR": fr_name_dict.get(en_name, ""),
"Creator": artifact.get("Creator", ""),
"Creation Date": artifact.get("Creation Date", ""),
"Materials": artifact.get("Materials", ""),
"Origin": artifact.get("Origin", ""),
"Description": artifact.get("Description", ""),
"Category": artifact.get("Category", ""),
"source_page": artifact.get("source_page", ""),
"source_document": artifact.get("source_document", "")
}
# If this artifact exists in previous database, use existing translations if available
if en_name in existing_artifacts:
existing = existing_artifacts[en_name]
if not multilingual_artifact["Name_AR"] and existing.get("Name_AR"):
multilingual_artifact["Name_AR"] = existing["Name_AR"]
if not multilingual_artifact["Name_FR"] and existing.get("Name_FR"):
multilingual_artifact["Name_FR"] = existing["Name_FR"]
# Remove from existing to track what's been processed
del existing_artifacts[en_name]
multilingual_artifacts.append(multilingual_artifact)
# Add any remaining existing artifacts that weren't in current batch
for _, artifact in existing_artifacts.items():
if artifact.get("Name_EN") not in processed_names:
multilingual_artifacts.append(artifact)
processed_names.add(artifact.get("Name_EN", ""))
# Save raw (pre-validation) as JSON for comparison
raw_json_output_file = os.path.join(output_dir, f"{doc_name}_multilingual_raw.json")
with open(raw_json_output_file, 'w', encoding='utf-8') as f:
json.dump(multilingual_artifacts, f, indent=2, ensure_ascii=False)
# Validate and complete multilingual names
logger.info(f"About to validate {len(multilingual_artifacts)} multilingual artifacts")
validated_artifacts = validate_and_complete_multilingual_names(
multilingual_artifacts, model, validation_prompt_func
)
logger.info(f"Validation complete. Got {len(validated_artifacts)} validated artifacts")
# Ensure all metadata is preserved from raw to validated artifacts
if len(validated_artifacts) == len(multilingual_artifacts):
for i, validated in enumerate(validated_artifacts):
# Copy all metadata fields except name fields, preserving original values
for key, value in multilingual_artifacts[i].items():
if key not in ["Name_EN", "Name_AR", "Name_FR", "Name_validation"]:
validated[key] = value
# Save validated version as JSON
with open(json_output_file, 'w', encoding='utf-8') as f:
json.dump(validated_artifacts, f, indent=2, ensure_ascii=False)
# Save as CSV
csv_output_file = os.path.join(output_dir, f"{doc_name}_multilingual.csv")
save_artifacts_to_csv(validated_artifacts, csv_output_file, csv_fields)
logger.info(f"Created multilingual database with {len(validated_artifacts)} artifacts")
logger.info(f"Results saved to {json_output_file} and {csv_output_file}")
return validated_artifacts
def process_specific_pages_english(input_file, output_dir, model, pages_to_process,
correction_threshold=0.05, ocr_prompt=None, correction_prompt=None,
artifact_prompt=None, ocr_model=None, extraction_model=None):
"""Process specific pages of an English document."""
actual_ocr_model = ocr_model or model
actual_extraction_model = extraction_model or model
# Set up document-specific directories
pdf_name = os.path.splitext(os.path.basename(input_file))[0]
doc_base_dir = os.path.join(output_dir, pdf_name)
pages_dir = os.path.join(doc_base_dir, "EN", "pages")
ocr_dir = os.path.join(doc_base_dir, "EN", "ocr")
ocr_corrected_dir = os.path.join(doc_base_dir, "EN", "ocr_corrected")
ocr_corrected2_dir = os.path.join(doc_base_dir, "EN", "ocr_corrected2")
ocr_corrected3_dir = os.path.join(doc_base_dir, "EN", "ocr_corrected3")
results_dir = os.path.join(doc_base_dir, model)
# Create directories
os.makedirs(doc_base_dir, exist_ok=True)
os.makedirs(pages_dir, exist_ok=True)
os.makedirs(results_dir, exist_ok=True)
document_name = os.path.basename(input_file)
# Extract pages from the document (only needed pages)
if input_file.lower().endswith('.pdf'):
start_page = min(pages_to_process)
end_page = max(pages_to_process)
image_paths = extract_images_from_pdf(input_file, pages_dir, start_page, end_page)
else:
image_paths = prepare_input_image(input_file, pages_dir)
# Process only the specified pages
all_artifacts = []
for image_path, page_num in image_paths:
if page_num not in pages_to_process:
continue # Skip pages not in our processing list
logger.info(f"Processing English page {page_num}: {image_path}")
# Set up directories for this page's OCR and correction
output_dirs = {
"ocr": ocr_dir,
"corrected1": ocr_corrected_dir,
"corrected2": ocr_corrected2_dir,
"corrected3": ocr_corrected3_dir
}
try:
# Perform OCR with adaptive correction
final_corrected_text = perform_ocr_with_adaptive_correction(
image_path=image_path,
page_num=page_num,
document_name=document_name,
model=actual_ocr_model,
ocr_prompt_template=ocr_prompt,
correction_prompt_template=correction_prompt,
output_dirs=output_dirs,
lang="EN",
correction_threshold=correction_threshold
)
# Extract artifacts
artifacts = extract_artifacts_from_page(
image_path=image_path,
page_num=page_num,
document_name=document_name,
model=actual_extraction_model,
final_corrected_text=final_corrected_text,
artifact_prompt_template=artifact_prompt,
results_dir=results_dir
)
all_artifacts.extend(artifacts)
except Exception as e:
logger.error(f"Error processing English page {page_num}: {e}")
continue
logger.info(f"Processed {len(pages_to_process)} pages, found {len(all_artifacts)} artifacts")
return all_artifacts
def extract_multilingual_names_for_page(page_artifacts, other_lang_file, page_num, lang,
ocr_model, extraction_model, correction_threshold, prompts):
"""Extract multilingual names for artifacts from a specific page."""
try:
if not page_artifacts:
return []
# Set up directories for this page's OCR and correction
pdf_name = os.path.splitext(os.path.basename(other_lang_file))[0]
doc_base_dir = os.path.join(os.path.dirname(other_lang_file), f"processing_{pdf_name}")
lang_pages_dir = os.path.join(doc_base_dir, lang, "pages")
lang_ocr_dir = os.path.join(doc_base_dir, lang, "ocr")
lang_ocr_corrected_dir = os.path.join(doc_base_dir, lang, "ocr_corrected")
lang_ocr_corrected2_dir = os.path.join(doc_base_dir, lang, "ocr_corrected2")
lang_ocr_corrected3_dir = os.path.join(doc_base_dir, lang, "ocr_corrected3")
results_dir = os.path.join(doc_base_dir, "results")
# Create directories
for dir_path in [lang_pages_dir, lang_ocr_dir, lang_ocr_corrected_dir,
lang_ocr_corrected2_dir, lang_ocr_corrected3_dir, results_dir]:
os.makedirs(dir_path, exist_ok=True)
# Extract page image if not already done
if other_lang_file.lower().endswith('.pdf'):
from .image_processing import extract_images_from_pdf
image_paths = extract_images_from_pdf(other_lang_file, lang_pages_dir, page_num, page_num)
if not image_paths:
logger.warning(f"Could not extract page {page_num} from {lang} document")
return []
image_path, _ = image_paths[0]
else:
image_path = other_lang_file
# Set up output directories
output_dirs = {
"ocr": lang_ocr_dir,
"corrected1": lang_ocr_corrected_dir,
"corrected2": lang_ocr_corrected2_dir,
"corrected3": lang_ocr_corrected3_dir
}
# Use existing extraction function
name_mappings = extract_multilingual_names_from_page(
image_path=image_path,
page_num=page_num,
page_artifacts=page_artifacts,
document_name=os.path.basename(other_lang_file),
model=extraction_model,
lang=lang,
name_extraction_prompt=prompts.get("multilingual"),
ocr_prompt_template=prompts.get("ocr"),
correction_prompt_template=prompts.get("correction"),
output_dirs=output_dirs,
results_dir=results_dir,
correction_threshold=correction_threshold
)
logger.info(f"Extracted {len(name_mappings)} {lang} names for page {page_num}")
return name_mappings
except Exception as e:
logger.error(f"Error extracting {lang} names for page {page_num}: {e}")
return []
def merge_multilingual_names_for_page(page_artifacts, ar_names, fr_names):
"""Merge English artifacts with multilingual names for a specific page."""
# Create name mappings
ar_name_dict = {}
for mapping in ar_names:
en_name = mapping.get("English_Name", "")
ar_name = mapping.get("Arabic_Name", "")
if en_name and ar_name and ar_name != "NOT_FOUND":
ar_name_dict[en_name] = ar_name
fr_name_dict = {}
for mapping in fr_names:
en_name = mapping.get("English_Name", "")
fr_name = mapping.get("French_Name", "")
if en_name and fr_name and fr_name != "NOT_FOUND":
fr_name_dict[en_name] = fr_name
# Merge with English artifacts
merged_artifacts = []
for artifact in page_artifacts:
en_name = artifact.get("Name", "")
merged_artifact = {
"Name_EN": en_name,
"Name_AR": ar_name_dict.get(en_name, ""),
"Name_FR": fr_name_dict.get(en_name, ""),
"Creator": artifact.get("Creator", ""),
"Creation Date": artifact.get("Creation Date", ""),
"Materials": artifact.get("Materials", ""),
"Origin": artifact.get("Origin", ""),
"Description": artifact.get("Description", ""),
"Category": artifact.get("Category", ""),
"source_page": artifact.get("source_page", ""),
"source_document": artifact.get("source_document", "")
}
merged_artifacts.append(merged_artifact)
return merged_artifacts
def process_multilingual_document_set(doc_group, output_dir, model, start_page=1, end_page=None,
correction_thresholds=None, prompts=None, csv_fields=None,
ocr_model=None, extraction_model=None, save_to_db=True):
"""Process a set of multilingual documents with intelligent page-level caching."""
# Extract document base name
base_name = os.path.basename(doc_group.get("EN", ""))
base_name = os.path.splitext(base_name)[0]
base_name = re.sub(r'_(?:en|ar|fr|english|arabic|french)$', '', base_name, flags=re.IGNORECASE)
logger.info(f"Processing multilingual document set: {base_name}")
# Set up models
actual_ocr_model = ocr_model or model
actual_extraction_model = extraction_model or model
# Debug: Log the model assignments
logger.info(f"🔧 Model setup - OCR: {actual_ocr_model}, Extraction: {actual_extraction_model}, Base: {model}")
# Get database client
db = get_simple_db()
# Check cache first with page-level intelligence
en_file = doc_group.get("EN")
if not en_file:
logger.error("No English document provided. English is required for this workflow.")
return
# Handle None end_page by determining actual document length
if end_page is None:
# Import here to avoid circular import
import fitz
try:
doc = fitz.open(en_file)
actual_end_page = len(doc)
doc.close()
logger.info(f"📄 Document has {actual_end_page} pages, processing from {start_page} to end")
except Exception as e:
logger.warning(f"Could not determine document length: {e}, using large number")
actual_end_page = 9999
else:
actual_end_page = end_page
logger.info(f"🔍 Checking page-level cache for pages {start_page}-{actual_end_page}")
logger.info(f"🚨 CRITICAL DEBUG: This line proves the new code is running! actual_end_page={actual_end_page}")
# Check page-level cache
cached_artifacts, missing_pages, cache_stats = db.check_page_level_cache(
doc_group, start_page, actual_end_page,
actual_ocr_model, actual_extraction_model, correction_thresholds
)
# Report cache analysis
total_pages = actual_end_page - start_page + 1
if cache_stats["cached_pages"] > 0:
logger.info(f"✅ Cache hit: {cache_stats['cached_pages']}/{total_pages} pages found in cache")
logger.info(f"📦 Retrieved {cache_stats['total_cached_artifacts']} cached artifacts")
if not missing_pages:
logger.info("🎯 All pages found in cache! No processing needed.")
# Save to local files for compatibility
doc_base_dir = os.path.join(output_dir, base_name)
results_dir = os.path.join(doc_base_dir, model)
os.makedirs(results_dir, exist_ok=True)
json_output_file = os.path.join(results_dir, f"{base_name}_multilingual.json")
csv_output_file = os.path.join(results_dir, f"{base_name}_multilingual.csv")
with open(json_output_file, 'w', encoding='utf-8') as f:
json.dump(cached_artifacts, f, indent=2, ensure_ascii=False)
save_artifacts_to_csv(cached_artifacts, csv_output_file, csv_fields)
# Save run statistics
if save_to_db:
db.save_run_statistics(
doc_group, start_page, actual_end_page, actual_ocr_model, actual_extraction_model,
correction_thresholds, len(cached_artifacts), cache_stats["cached_pages"], 0
)
return cached_artifacts
# Process missing pages only
logger.info(f"🔄 Processing {len(missing_pages)} missing pages: {missing_pages}")
# Process only missing pages for English document
new_artifacts_en = process_specific_pages_english(
input_file=en_file,
output_dir=output_dir,
model=model,
pages_to_process=missing_pages,
correction_threshold=correction_thresholds.get("EN", 0.05),
ocr_prompt=prompts.get("ocr"),
correction_prompt=prompts.get("correction"),
artifact_prompt=prompts.get("artifact"),
ocr_model=actual_ocr_model,
extraction_model=actual_extraction_model
)
if not new_artifacts_en:
logger.warning("No new artifacts found in missing pages")
return cached_artifacts
# Process multilingual names for new artifacts only
# Group new artifacts by page
new_artifacts_by_page = {}
for artifact in new_artifacts_en:
page_num = artifact.get("source_page", 1)
if page_num not in new_artifacts_by_page:
new_artifacts_by_page[page_num] = []
new_artifacts_by_page[page_num].append(artifact)
# Extract names in other languages for missing pages
all_new_artifacts = []
for page_num in missing_pages:
if page_num not in new_artifacts_by_page:
continue
page_artifacts = new_artifacts_by_page[page_num]
# Process Arabic names for this page
ar_file = doc_group.get("AR")
ar_names = []
if ar_file:
ar_names = extract_multilingual_names_for_page(
page_artifacts, ar_file, page_num, "AR",
actual_ocr_model, actual_extraction_model,
correction_thresholds.get("AR", 0.10),
prompts
)
# Process French names for this page
fr_file = doc_group.get("FR")
fr_names = []
if fr_file:
fr_names = extract_multilingual_names_for_page(
page_artifacts, fr_file, page_num, "FR",
actual_ocr_model, actual_extraction_model,
correction_thresholds.get("FR", 0.07),
prompts
)
# Merge multilingual names for this page
page_final_artifacts = merge_multilingual_names_for_page(
page_artifacts, ar_names, fr_names
)
# Apply validation if available
if prompts.get("validation"):
try:
original_artifacts = page_final_artifacts.copy()
page_final_artifacts = validate_and_complete_multilingual_names(
page_final_artifacts, actual_extraction_model, prompts.get("validation")
)
# Ensure all metadata is preserved from original to validated artifacts
if len(page_final_artifacts) == len(original_artifacts):
for i, validated in enumerate(page_final_artifacts):
# Copy all metadata fields except name fields, preserving original values
for key, value in original_artifacts[i].items():
if key not in ["Name_EN", "Name_AR", "Name_FR", "Name_validation"]:
validated[key] = value
except Exception as e:
logger.warning(f"Validation failed for page {page_num}, using unvalidated results: {e}")
# Save this page to cache
if save_to_db:
logger.info(f"💾 Saving page {page_num} to DB with OCR model: {actual_ocr_model}, Extraction model: {actual_extraction_model}")
db.save_page_artifacts(
doc_group, page_num, page_final_artifacts,
actual_ocr_model, actual_extraction_model, correction_thresholds
)
all_new_artifacts.extend(page_final_artifacts)
# Combine cached and new artifacts
final_artifacts = cached_artifacts + all_new_artifacts
# Save to local files
doc_base_dir = os.path.join(output_dir, base_name)
results_dir = os.path.join(doc_base_dir, model)
os.makedirs(results_dir, exist_ok=True)
json_output_file = os.path.join(results_dir, f"{base_name}_multilingual.json")
csv_output_file = os.path.join(results_dir, f"{base_name}_multilingual.csv")
with open(json_output_file, 'w', encoding='utf-8') as f:
json.dump(final_artifacts, f, indent=2, ensure_ascii=False)
save_artifacts_to_csv(final_artifacts, csv_output_file, csv_fields)
# Save run statistics
if save_to_db:
db.save_run_statistics(
doc_group, start_page, actual_end_page, actual_ocr_model, actual_extraction_model,
correction_thresholds, len(final_artifacts), cache_stats["cached_pages"], len(missing_pages)
)
logger.info(f"✅ Processing complete!")
logger.info(f"📊 Final results: {len(final_artifacts)} total artifacts")
logger.info(f"📈 Performance: {cache_stats['cached_pages']} pages from cache, {len(missing_pages)} pages processed")
# Calculate performance metrics
if total_pages > 0:
cache_hit_rate = (cache_stats["cached_pages"] / total_pages) * 100
processing_saved = cache_stats["cached_pages"] * 100 / total_pages
logger.info(f"🚀 Cache efficiency: {cache_hit_rate:.1f}% hit rate, saved {processing_saved:.1f}% processing time")
return final_artifacts