#!/usr/bin/env python3 """ Pipeline OCR pour extraire le texte des manuels de chinois (PDF scannés) Utilise PaddleOCR pour la reconnaissance de caractères chinois """ import os import sys import argparse from pathlib import Path from typing import List, Tuple import json from datetime import datetime from dotenv import load_dotenv try: from paddleocr import PaddleOCR from pdf2image import convert_from_path from PIL import Image from openai import OpenAI except ImportError as e: print(f"Erreur d'import: {e}") print("\nInstallez les dépendances avec:") print("pip install paddlepaddle paddleocr pdf2image Pillow openai python-dotenv") print("\nSur Linux, installez aussi: sudo apt-get install poppler-utils") sys.exit(1) # Charger les variables d'environnement load_dotenv() class ChineseOCRPipeline: """Pipeline OCR pour traiter les PDFs de manuels chinois""" def __init__(self, lang: str = 'ch', use_gpu: bool = False, use_ai_correction: bool = False): """ Initialise le pipeline OCR Args: lang: Langue pour l'OCR ('ch' pour chinois, 'en' pour anglais) use_gpu: Utiliser le GPU si disponible use_ai_correction: Activer la correction IA avec GPT-4o-mini """ print(f"Initialisation de PaddleOCR (langue: {lang})...") self.ocr = PaddleOCR( use_textline_orientation=True, # Détection d'angle pour images tournées lang=lang ) print("✓ PaddleOCR initialisé") # Initialiser OpenAI si correction activée self.use_ai_correction = use_ai_correction self.openai_client = None if use_ai_correction: api_key = os.getenv('OPENAI_API_KEY') if not api_key: print("⚠️ OPENAI_API_KEY non trouvée dans .env - correction IA désactivée") self.use_ai_correction = False else: self.openai_client = OpenAI(api_key=api_key) print("✓ Correction IA (GPT-4o-mini) activée") def pdf_to_images(self, pdf_path: Path, dpi: int = 300) -> List[Image.Image]: """ Convertit un PDF en images Args: pdf_path: Chemin vers le PDF dpi: Résolution de conversion (300 recommandé pour l'OCR) Returns: Liste d'images PIL """ print(f" Conversion du PDF en images (DPI: {dpi})...") try: images = convert_from_path(str(pdf_path), dpi=dpi) print(f" ✓ {len(images)} pages converties") return images except Exception as e: print(f" ✗ Erreur lors de la conversion: {e}") return [] def extract_text_from_image(self, image: Image.Image) -> Tuple[str, List[dict]]: """ Extrait le texte d'une image avec PaddleOCR Args: image: Image PIL Returns: Tuple (texte_brut, résultats_détaillés) """ import numpy as np # Convertir PIL Image en numpy array img_array = np.array(image) # Lancer l'OCR (utilise la nouvelle API predict) result = self.ocr.predict(img_array) # Extraire le texte # Le format de retour est une liste contenant un objet OCRResult if result is None or not isinstance(result, list) or len(result) == 0: return "", [] # Prendre le premier résultat (un seul résultat par image) ocr_result = result[0] # Accéder aux attributs comme un dictionnaire rec_texts = ocr_result.get('rec_texts', []) rec_scores = ocr_result.get('rec_scores', []) rec_polys = ocr_result.get('rec_polys', []) if not rec_texts: return "", [] text_lines = [] detailed_results = [] # Itérer sur les textes, scores et polygones for i, text in enumerate(rec_texts): confidence = rec_scores[i] if i < len(rec_scores) else 0.0 bbox = rec_polys[i].tolist() if i < len(rec_polys) else [] text_lines.append(text) detailed_results.append({ 'text': text, 'confidence': float(confidence), 'bbox': bbox }) return '\n'.join(text_lines), detailed_results def correct_text_with_ai(self, text: str, page_num: int) -> str: """ Corrige le texte OCR avec GPT-4o-mini Args: text: Texte brut de l'OCR page_num: Numéro de la page Returns: Texte corrigé """ if not self.use_ai_correction or not self.openai_client: return text if not text.strip(): return text try: response = self.openai_client.chat.completions.create( model="gpt-4o-mini", messages=[ { "role": "system", "content": """Tu es un expert en chinois qui corrige les erreurs OCR dans des manuels de chinois. Ton travail: 1. Corriger les erreurs OCR évidentes (caractères manquants, mal reconnus) 2. Vérifier la cohérence pinyin/caractères chinois 3. Corriger la ponctuation chinoise si nécessaire 4. Si une phrase semble incomplète (ex: "心里有说不出的兴" au lieu de "心里有说不出的高兴"), la compléter 5. Corriger les questions qui citent le texte principal IMPORTANT: - Ne PAS modifier la structure ou l'ordre du texte - Ne PAS ajouter de contenu qui n'était pas là - Ne PAS traduire - Garder EXACTEMENT le même formatage (sauts de ligne, espaces) - Seulement corriger les erreurs évidentes""" }, { "role": "user", "content": f"Corrige les erreurs OCR dans ce texte de la page {page_num} d'un manuel de chinois:\n\n{text}" } ], temperature=0.1, max_tokens=4000 ) corrected_text = response.choices[0].message.content return corrected_text except Exception as e: print(f" ⚠️ Erreur correction IA: {e}") return text def process_pdf(self, pdf_path: Path, output_dir: Path) -> bool: """ Traite un fichier PDF et sauvegarde les résultats Args: pdf_path: Chemin vers le PDF output_dir: Dossier de sortie Returns: True si succès, False sinon """ print(f"\n{'='*60}") print(f"Traitement: {pdf_path.name}") print(f"{'='*60}") # Convertir PDF en images images = self.pdf_to_images(pdf_path) if not images: return False # Créer le dossier de sortie output_dir.mkdir(parents=True, exist_ok=True) # Nom de base pour les fichiers de sortie base_name = pdf_path.stem # Traiter chaque page all_text = [] all_text_corrected = [] all_detailed_results = [] for page_num, image in enumerate(images, start=1): print(f"\n {'─'*50}") print(f" 📄 Page {page_num}/{len(images)}") print(f" {'─'*50}") text, detailed = self.extract_text_from_image(image) # Afficher un aperçu du texte extrait if text: lines = text.split('\n') num_lines = len(lines) preview = '\n '.join(lines[:5]) # Afficher les 5 premières lignes print(f" ✓ {num_lines} ligne(s) détectée(s)") print(f" 📝 Aperçu OCR brut:") print(f" {preview}") if num_lines > 5: print(f" ... ({num_lines - 5} ligne(s) supplémentaire(s))") else: print(f" ⚠️ Aucun texte détecté sur cette page") # Correction IA si activée corrected_text = text if self.use_ai_correction and text: print(f" 🤖 Correction IA en cours...") corrected_text = self.correct_text_with_ai(text, page_num) print(f" ✓ Correction terminée") all_text.append(f"=== Page {page_num} ===\n{text}\n") all_text_corrected.append(f"=== Page {page_num} ===\n{corrected_text}\n") all_detailed_results.append({ 'page': page_num, 'text': text, 'text_corrected': corrected_text if self.use_ai_correction else None, 'details': detailed }) # Sauvegarder le texte brut txt_output = output_dir / f"{base_name}.txt" with open(txt_output, 'w', encoding='utf-8') as f: f.write('\n'.join(all_text)) print(f" ✓ Texte brut sauvegardé: {txt_output}") # Sauvegarder le texte corrigé si correction IA activée if self.use_ai_correction: txt_corrected_output = output_dir / f"{base_name}_corrected.txt" with open(txt_corrected_output, 'w', encoding='utf-8') as f: f.write('\n'.join(all_text_corrected)) print(f" ✓ Texte corrigé sauvegardé: {txt_corrected_output}") # Sauvegarder les résultats détaillés (JSON) json_output = output_dir / f"{base_name}.json" with open(json_output, 'w', encoding='utf-8') as f: json.dump({ 'source_pdf': str(pdf_path), 'processed_at': datetime.now().isoformat(), 'ai_correction_enabled': self.use_ai_correction, 'pages': all_detailed_results }, f, ensure_ascii=False, indent=2) print(f" ✓ Détails sauvegardés: {json_output}") return True def process_directory(self, base_dir: Path = Path("Raw")) -> dict: """ Traite tous les PDFs dans Raw/*/PDF/ Args: base_dir: Dossier de base (défaut: Raw) Returns: Dictionnaire avec statistiques de traitement """ stats = { 'total': 0, 'success': 0, 'failed': 0, 'files': [] } # Trouver tous les dossiers PDF pdf_dirs = list(base_dir.glob("*/PDF")) print(f"\nTrouvé {len(pdf_dirs)} dossiers de PDFs") for pdf_dir in pdf_dirs: # Dossier de sortie (même niveau que PDF) output_dir = pdf_dir.parent / "OCR" # Trouver tous les PDFs pdf_files = list(pdf_dir.glob("*.pdf")) + list(pdf_dir.glob("*.PDF")) print(f"\n{pdf_dir.relative_to(base_dir)}: {len(pdf_files)} fichiers") for pdf_file in pdf_files: stats['total'] += 1 success = self.process_pdf(pdf_file, output_dir) if success: stats['success'] += 1 stats['files'].append(str(pdf_file.relative_to(base_dir))) else: stats['failed'] += 1 return stats def main(): parser = argparse.ArgumentParser( description="Pipeline OCR pour manuels de chinois", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Exemples: # Mode auto - traiter tous les PDFs python ocr_pipeline.py --mode auto # Mode manuel - traiter un fichier spécifique python ocr_pipeline.py --mode manual --input "Raw/DevelopChinese/PDF/听力Chapter1.pdf" # Avec GPU python ocr_pipeline.py --mode auto --gpu """ ) parser.add_argument( '--mode', choices=['auto', 'manual'], default='auto', help='Mode de traitement (auto: tous les PDFs, manual: un fichier)' ) parser.add_argument( '--input', type=str, help='Chemin du fichier PDF (requis en mode manual)' ) parser.add_argument( '--output', type=str, help='Dossier de sortie personnalisé (optionnel en mode manual)' ) parser.add_argument( '--gpu', action='store_true', help='Utiliser le GPU si disponible' ) parser.add_argument( '--dpi', type=int, default=300, help='Résolution pour la conversion PDF (défaut: 300)' ) parser.add_argument( '--ai-correction', action='store_true', help='Activer la correction IA avec GPT-4o-mini (nécessite OPENAI_API_KEY dans .env)' ) args = parser.parse_args() # Validation if args.mode == 'manual' and not args.input: parser.error("--input est requis en mode manual") # Initialiser le pipeline pipeline = ChineseOCRPipeline( lang='ch', use_gpu=args.gpu, use_ai_correction=args.ai_correction ) # Mode auto if args.mode == 'auto': print("\n🚀 Mode AUTO - Traitement de tous les PDFs") stats = pipeline.process_directory() print(f"\n{'='*60}") print("RÉSUMÉ") print(f"{'='*60}") print(f"Total: {stats['total']} fichiers") print(f"Succès: {stats['success']} fichiers") print(f"Échecs: {stats['failed']} fichiers") print(f"{'='*60}") # Mode manuel else: print(f"\n🎯 Mode MANUAL - Traitement de: {args.input}") pdf_path = Path(args.input) if not pdf_path.exists(): print(f"Erreur: Le fichier {args.input} n'existe pas") sys.exit(1) # Déterminer le dossier de sortie if args.output: output_dir = Path(args.output) else: # Par défaut: Raw/*/OCR/ output_dir = pdf_path.parent.parent / "OCR" success = pipeline.process_pdf(pdf_path, output_dir) if success: print("\n✓ Traitement réussi!") else: print("\n✗ Échec du traitement") sys.exit(1) if __name__ == "__main__": main()