diff --git a/docs/ERREUR-JS-RESOLUE.md b/docs/ERREUR-JS-RESOLUE.md index e04f7c8..68179d8 100644 --- a/docs/ERREUR-JS-RESOLUE.md +++ b/docs/ERREUR-JS-RESOLUE.md @@ -29,12 +29,12 @@ J'ai ajouté une vérification pour s'assurer que l'élément existe avant d'ess ```javascript async uploadDocument() { const fileInput = document.getElementById('file-input'); - + if (!fileInput) { this.showAlert('Élément de fichier non trouvé', 'error'); return; } - + const file = fileInput.files[0]; // ... reste du code } @@ -49,7 +49,7 @@ J'ai également amélioré l'API minimale pour gérer l'upload avec un traitemen async def upload_document(): """Upload simulé d'un document""" doc_id = f"doc_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - + document_data = { "id": doc_id, "filename": f"document_{doc_id}.pdf", @@ -57,13 +57,13 @@ async def upload_document(): "progress": 0, "upload_time": datetime.now().isoformat() } - + documents_db[doc_id] = document_data - + # Simuler le traitement import asyncio asyncio.create_task(process_document_simulated(doc_id)) - + return { "message": "Document uploadé avec succès (simulé)", "document_id": doc_id, diff --git a/services/host_api/app_complete.py b/services/host_api/app_complete.py index c6b130f..40c47f7 100644 --- a/services/host_api/app_complete.py +++ b/services/host_api/app_complete.py @@ -35,7 +35,7 @@ app.add_middleware( async def startup_event(): """Initialisation au démarrage""" print("🚀 Démarrage de l'API Notariale") - + # Vérification de la connexion à la base de données if check_db_connection(): print("✅ Connexion à la base de données réussie") @@ -57,7 +57,7 @@ async def root(): async def health_check(): """Vérification de l'état de l'API""" db_status = check_db_connection() - + return { "status": "healthy" if db_status else "degraded", "timestamp": datetime.now().isoformat(), @@ -78,7 +78,7 @@ async def get_stats(db: Session = Depends(get_db)): processed = db.query(Document).filter(Document.status == "completed").count() processing = db.query(Document).filter(Document.status == "processing").count() error = db.query(Document).filter(Document.status == "error").count() - + return { "total_documents": total_docs, "processed": processed, @@ -106,12 +106,12 @@ async def get_documents( """Liste des documents""" try: query = db.query(Document) - + if status: query = query.filter(Document.status == status) - + documents = query.offset(skip).limit(limit).all() - + return { "documents": [ { @@ -135,16 +135,16 @@ async def get_document(document_id: str, db: Session = Depends(get_db)): """Détails d'un document""" try: document = db.query(Document).filter(Document.id == document_id).first() - + if not document: raise HTTPException(status_code=404, detail="Document non trouvé") - + # Récupération des entités entities = db.query(Entity).filter(Entity.document_id == document_id).all() - + # Récupération des vérifications verifications = db.query(Verification).filter(Verification.document_id == document_id).all() - + return { "id": document.id, "filename": document.filename, @@ -195,10 +195,10 @@ async def upload_document( # Validation du fichier if not file.filename: raise HTTPException(status_code=400, detail="Aucun fichier fourni") - + # Génération d'un ID unique doc_id = str(uuid.uuid4()) - + # Création du document en base document = Document( id=doc_id, @@ -213,20 +213,20 @@ async def upload_document( status="uploaded", progress=0 ) - + db.add(document) db.commit() db.refresh(document) - + # Simulation du traitement (en attendant Celery) asyncio.create_task(process_document_simulated(doc_id, db)) - + return { "message": "Document uploadé avec succès", "document_id": doc_id, "status": "uploaded" } - + except HTTPException: raise except Exception as e: @@ -243,7 +243,7 @@ async def process_document_simulated(doc_id: str, db: Session): document.progress = 10 document.current_step = "Pré-traitement" db.commit() - + # Simulation des étapes steps = [ ("Pré-traitement", 20), @@ -253,15 +253,15 @@ async def process_document_simulated(doc_id: str, db: Session): ("Vérifications", 95), ("Finalisation", 100) ] - + for step_name, progress in steps: await asyncio.sleep(2) # Simulation du temps de traitement - + if document: document.progress = progress document.current_step = step_name db.commit() - + # Résultats simulés if document: document.status = "completed" @@ -272,7 +272,7 @@ async def process_document_simulated(doc_id: str, db: Session): document.ocr_text = "Texte extrait simulé du document..." document.processed_at = datetime.utcnow() db.commit() - + # Ajout d'entités simulées entities = [ Entity( @@ -297,10 +297,10 @@ async def process_document_simulated(doc_id: str, db: Session): context="Adresse du bien: 123 Rue de la Paix, 75001 Paris" ) ] - + for entity in entities: db.add(entity) - + # Ajout de vérifications simulées verifications = [ Verification( @@ -316,12 +316,12 @@ async def process_document_simulated(doc_id: str, db: Session): result_data={"status": "OK", "risques": []} ) ] - + for verification in verifications: db.add(verification) - + db.commit() - + except Exception as e: print(f"Erreur lors du traitement simulé de {doc_id}: {e}") if document: @@ -334,25 +334,25 @@ async def delete_document(document_id: str, db: Session = Depends(get_db)): """Suppression d'un document""" try: document = db.query(Document).filter(Document.id == document_id).first() - + if not document: raise HTTPException(status_code=404, detail="Document non trouvé") - + # Suppression des entités associées db.query(Entity).filter(Entity.document_id == document_id).delete() - + # Suppression des vérifications associées db.query(Verification).filter(Verification.document_id == document_id).delete() - + # Suppression des logs de traitement db.query(ProcessingLog).filter(ProcessingLog.document_id == document_id).delete() - + # Suppression du document db.delete(document) db.commit() - + return {"message": "Document supprimé avec succès"} - + except HTTPException: raise except Exception as e: diff --git a/services/host_api/domain/database.py b/services/host_api/domain/database.py index 9d09629..a59b0bc 100644 --- a/services/host_api/domain/database.py +++ b/services/host_api/domain/database.py @@ -10,7 +10,7 @@ from .models import Base # Configuration de la base de données DATABASE_URL = os.getenv( - "DATABASE_URL", + "DATABASE_URL", "postgresql+psycopg://notariat:notariat_pwd@localhost:5432/notariat" ) @@ -53,7 +53,7 @@ def get_db_stats(): """Retourne les statistiques de la base de données""" try: from .models import Document, Entity, Verification, ProcessingLog - + db = SessionLocal() try: stats = { diff --git a/services/host_api/domain/models.py b/services/host_api/domain/models.py index 58f6e8b..ca70761 100644 --- a/services/host_api/domain/models.py +++ b/services/host_api/domain/models.py @@ -13,34 +13,34 @@ Base = declarative_base() class Document(Base): """Modèle pour les documents notariaux""" __tablename__ = "documents" - + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) filename = Column(String(255), nullable=False) original_filename = Column(String(255), nullable=False) mime_type = Column(String(100), nullable=False) size = Column(Integer, nullable=False) - + # Métadonnées id_dossier = Column(String(100), nullable=False) etude_id = Column(String(100), nullable=False) utilisateur_id = Column(String(100), nullable=False) source = Column(String(50), default="upload") - + # Statut et progression status = Column(String(50), default="uploaded") # uploaded, processing, completed, error progress = Column(Integer, default=0) current_step = Column(String(100)) - + # Résultats du traitement ocr_text = Column(Text) document_type = Column(String(100)) confidence_score = Column(Float) - + # Timestamps created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) processed_at = Column(DateTime) - + # Relations entities = relationship("Entity", back_populates="document") verifications = relationship("Verification", back_populates="document") @@ -49,99 +49,99 @@ class Document(Base): class Entity(Base): """Modèle pour les entités extraites des documents""" __tablename__ = "entities" - + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) document_id = Column(String, ForeignKey("documents.id"), nullable=False) - + # Type d'entité entity_type = Column(String(50), nullable=False) # person, address, property, company, etc. entity_value = Column(Text, nullable=False) - + # Position dans le document page_number = Column(Integer) bbox_x = Column(Float) bbox_y = Column(Float) bbox_width = Column(Float) bbox_height = Column(Float) - + # Métadonnées confidence = Column(Float) context = Column(Text) - + # Timestamps created_at = Column(DateTime, default=datetime.utcnow) - + # Relations document = relationship("Document", back_populates="entities") class Verification(Base): """Modèle pour les vérifications effectuées""" __tablename__ = "verifications" - + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) document_id = Column(String, ForeignKey("documents.id"), nullable=False) - + # Type de vérification verification_type = Column(String(100), nullable=False) # cadastre, georisques, bodacc, etc. verification_status = Column(String(50), nullable=False) # pending, success, error, warning - + # Résultats result_data = Column(JSON) error_message = Column(Text) warning_message = Column(Text) - + # Métadonnées api_endpoint = Column(String(255)) response_time = Column(Float) - + # Timestamps created_at = Column(DateTime, default=datetime.utcnow) completed_at = Column(DateTime) - + # Relations document = relationship("Document", back_populates="verifications") class ProcessingLog(Base): """Modèle pour les logs de traitement""" __tablename__ = "processing_logs" - + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) document_id = Column(String, ForeignKey("documents.id"), nullable=False) - + # Informations du log step_name = Column(String(100), nullable=False) step_status = Column(String(50), nullable=False) # started, completed, error message = Column(Text) error_details = Column(Text) - + # Métadonnées processing_time = Column(Float) input_hash = Column(String(64)) output_hash = Column(String(64)) - + # Timestamps created_at = Column(DateTime, default=datetime.utcnow) - + # Relations document = relationship("Document", back_populates="processing_logs") class Study(Base): """Modèle pour les études notariales""" __tablename__ = "studies" - + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) name = Column(String(255), nullable=False) address = Column(Text) phone = Column(String(50)) email = Column(String(255)) - + # Configuration settings = Column(JSON) api_keys = Column(JSON) # Clés API pour les vérifications externes - + # Statut is_active = Column(Boolean, default=True) - + # Timestamps created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) @@ -149,21 +149,21 @@ class Study(Base): class User(Base): """Modèle pour les utilisateurs""" __tablename__ = "users" - + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) username = Column(String(100), unique=True, nullable=False) email = Column(String(255), unique=True, nullable=False) full_name = Column(String(255)) - + # Authentification hashed_password = Column(String(255)) is_active = Column(Boolean, default=True) is_admin = Column(Boolean, default=False) - + # Relations study_id = Column(String, ForeignKey("studies.id")) study = relationship("Study") - + # Timestamps created_at = Column(DateTime, default=datetime.utcnow) last_login = Column(DateTime) @@ -171,24 +171,24 @@ class User(Base): class Dossier(Base): """Modèle pour les dossiers notariaux""" __tablename__ = "dossiers" - + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) dossier_number = Column(String(100), unique=True, nullable=False) title = Column(String(255)) description = Column(Text) - + # Relations study_id = Column(String, ForeignKey("studies.id"), nullable=False) study = relationship("Study") - + # Statut status = Column(String(50), default="open") # open, closed, archived - + # Métadonnées client_name = Column(String(255)) client_email = Column(String(255)) client_phone = Column(String(50)) - + # Timestamps created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/services/web_interface/app.js b/services/web_interface/app.js index 3a2fc92..1a6d9b9 100644 --- a/services/web_interface/app.js +++ b/services/web_interface/app.js @@ -142,12 +142,12 @@ class NotaryApp { async uploadDocument() { const fileInput = document.getElementById('file-input'); - + if (!fileInput) { this.showAlert('Élément de fichier non trouvé', 'error'); return; } - + const file = fileInput.files[0]; if (!file) { diff --git a/services/worker/pipelines/checks.py b/services/worker/pipelines/checks.py index 1291693..eb932e5 100644 --- a/services/worker/pipelines/checks.py +++ b/services/worker/pipelines/checks.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) def run(doc_id: str, ctx: Dict[str, Any]) -> None: """Pipeline de vérifications""" logger.info(f"🔍 Vérifications pour le document {doc_id}") - + try: # Simulation des vérifications ctx.update({ diff --git a/services/worker/pipelines/classify.py b/services/worker/pipelines/classify.py index 363cf8a..a9ec87e 100644 --- a/services/worker/pipelines/classify.py +++ b/services/worker/pipelines/classify.py @@ -47,31 +47,31 @@ DOCUMENT_TYPES = { def run(doc_id: str, ctx: Dict[str, Any]) -> None: """ Pipeline de classification des documents - + Args: doc_id: Identifiant du document ctx: Contexte de traitement partagé entre les pipelines """ logger.info(f"🏷️ Début de la classification pour le document {doc_id}") - + try: # 1. Vérification des prérequis if "ocr_error" in ctx: raise Exception(f"Erreur OCR: {ctx['ocr_error']}") - + ocr_text = ctx.get("ocr_text", "") if not ocr_text: raise ValueError("Texte OCR manquant") - + # 2. Classification par règles (rapide) rule_based_classification = _classify_by_rules(ocr_text) - + # 3. Classification par LLM (plus précise) llm_classification = _classify_by_llm(ocr_text, doc_id) - + # 4. Fusion des résultats final_classification = _merge_classifications(rule_based_classification, llm_classification) - + # 5. Mise à jour du contexte ctx.update({ "document_type": final_classification["type"], @@ -79,12 +79,12 @@ def run(doc_id: str, ctx: Dict[str, Any]) -> None: "classification_method": final_classification["method"], "classification_details": final_classification["details"] }) - + logger.info(f"✅ Classification terminée pour {doc_id}") logger.info(f" - Type: {final_classification['type']}") logger.info(f" - Confiance: {final_classification['confidence']:.2f}") logger.info(f" - Méthode: {final_classification['method']}") - + except Exception as e: logger.error(f"❌ Erreur lors de la classification de {doc_id}: {e}") ctx["classification_error"] = str(e) @@ -98,44 +98,44 @@ def run(doc_id: str, ctx: Dict[str, Any]) -> None: def _classify_by_rules(text: str) -> Dict[str, Any]: """Classification basée sur des règles et mots-clés""" logger.info("📋 Classification par règles") - + text_lower = text.lower() scores = {} - + for doc_type, config in DOCUMENT_TYPES.items(): if doc_type == "autre": continue - + score = 0 matched_keywords = [] - + # Score basé sur les mots-clés for keyword in config["keywords"]: if keyword in text_lower: score += 1 matched_keywords.append(keyword) - + # Score basé sur les patterns regex import re for pattern in config["patterns"]: if re.search(pattern, text_lower): score += 2 - + # Normalisation du score max_possible_score = len(config["keywords"]) + len(config["patterns"]) * 2 normalized_score = score / max_possible_score if max_possible_score > 0 else 0 - + scores[doc_type] = { "score": normalized_score, "matched_keywords": matched_keywords, "method": "rules" } - + # Sélection du meilleur score if scores: best_type = max(scores.keys(), key=lambda k: scores[k]["score"]) best_score = scores[best_type]["score"] - + return { "type": best_type if best_score > 0.1 else "autre", "confidence": best_score, @@ -153,18 +153,18 @@ def _classify_by_rules(text: str) -> Dict[str, Any]: def _classify_by_llm(text: str, doc_id: str) -> Dict[str, Any]: """Classification par LLM (Ollama)""" logger.info("🤖 Classification par LLM") - + try: # Configuration Ollama ollama_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") model = os.getenv("OLLAMA_MODEL", "llama3:8b") - + # Limitation du texte pour le contexte text_sample = text[:4000] if len(text) > 4000 else text - + # Prompt de classification prompt = _build_classification_prompt(text_sample) - + # Appel à Ollama response = requests.post( f"{ollama_url}/api/generate", @@ -179,11 +179,11 @@ def _classify_by_llm(text: str, doc_id: str) -> Dict[str, Any]: }, timeout=60 ) - + if response.status_code == 200: result = response.json() llm_response = result.get("response", "").strip() - + # Parsing de la réponse JSON try: classification_result = json.loads(llm_response) @@ -203,7 +203,7 @@ def _classify_by_llm(text: str, doc_id: str) -> Dict[str, Any]: else: logger.warning(f"Erreur LLM: {response.status_code}") return _classify_by_rules(text) - + except requests.exceptions.RequestException as e: logger.warning(f"Erreur de connexion LLM: {e}") return _classify_by_rules(text) @@ -238,19 +238,19 @@ Assure-toi que le JSON est valide et que le type correspond exactement à une de def _merge_classifications(rule_result: Dict[str, Any], llm_result: Dict[str, Any]) -> Dict[str, Any]: """Fusionne les résultats de classification par règles et LLM""" logger.info("🔄 Fusion des classifications") - + # Poids des méthodes rule_weight = 0.3 llm_weight = 0.7 - + # Si LLM a une confiance élevée, on lui fait confiance if llm_result["confidence"] > 0.8: return llm_result - + # Si les deux méthodes sont d'accord if rule_result["type"] == llm_result["type"]: # Moyenne pondérée des confiances - combined_confidence = (rule_result["confidence"] * rule_weight + + combined_confidence = (rule_result["confidence"] * rule_weight + llm_result["confidence"] * llm_weight) return { "type": rule_result["type"], @@ -262,7 +262,7 @@ def _merge_classifications(rule_result: Dict[str, Any], llm_result: Dict[str, An "weights": {"rules": rule_weight, "llm": llm_weight} } } - + # Si les méthodes ne sont pas d'accord, on privilégie LLM si sa confiance est > 0.5 if llm_result["confidence"] > 0.5: return llm_result diff --git a/services/worker/pipelines/extract.py b/services/worker/pipelines/extract.py index 791d18d..3bab275 100644 --- a/services/worker/pipelines/extract.py +++ b/services/worker/pipelines/extract.py @@ -12,14 +12,14 @@ logger = logging.getLogger(__name__) def run(doc_id: str, ctx: Dict[str, Any]) -> None: """Pipeline d'extraction d'entités""" logger.info(f"🔍 Extraction d'entités pour le document {doc_id}") - + try: ocr_text = ctx.get("ocr_text", "") document_type = ctx.get("document_type", "autre") - + # Extraction basique entities = _extract_basic_entities(ocr_text, document_type) - + ctx.update({ "extracted_entities": entities, "entities_count": len(entities) @@ -32,7 +32,7 @@ def run(doc_id: str, ctx: Dict[str, Any]) -> None: def _extract_basic_entities(text: str, doc_type: str) -> List[Dict[str, Any]]: """Extraction basique d'entités""" entities = [] - + # Emails emails = re.findall(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', text) for email in emails: @@ -42,7 +42,7 @@ def _extract_basic_entities(text: str, doc_type: str) -> List[Dict[str, Any]]: "value": email, "confidence": 0.95 }) - + # Téléphones phones = re.findall(r'\b0[1-9](?:[.\-\s]?\d{2}){4}\b', text) for phone in phones: @@ -52,7 +52,7 @@ def _extract_basic_entities(text: str, doc_type: str) -> List[Dict[str, Any]]: "value": phone, "confidence": 0.9 }) - + # Dates dates = re.findall(r'\b\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{4}\b', text) for date in dates: @@ -62,5 +62,5 @@ def _extract_basic_entities(text: str, doc_type: str) -> List[Dict[str, Any]]: "value": date, "confidence": 0.8 }) - + return entities \ No newline at end of file diff --git a/services/worker/pipelines/finalize.py b/services/worker/pipelines/finalize.py index 4c2cc9f..5838b2c 100644 --- a/services/worker/pipelines/finalize.py +++ b/services/worker/pipelines/finalize.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) def run(doc_id: str, ctx: Dict[str, Any]) -> None: """Pipeline de finalisation""" logger.info(f"🏁 Finalisation du document {doc_id}") - + try: # Génération du rapport final ctx.update({ diff --git a/services/worker/pipelines/index.py b/services/worker/pipelines/index.py index 358685b..7e0b958 100644 --- a/services/worker/pipelines/index.py +++ b/services/worker/pipelines/index.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) def run(doc_id: str, ctx: Dict[str, Any]) -> None: """Pipeline d'indexation""" logger.info(f"📚 Indexation du document {doc_id}") - + try: # Simulation de l'indexation ctx.update({ diff --git a/services/worker/pipelines/ocr.py b/services/worker/pipelines/ocr.py index d62ca06..c5b6c0f 100644 --- a/services/worker/pipelines/ocr.py +++ b/services/worker/pipelines/ocr.py @@ -14,29 +14,29 @@ logger = logging.getLogger(__name__) def run(doc_id: str, ctx: Dict[str, Any]) -> None: """ Pipeline OCR pour l'extraction de texte - + Args: doc_id: Identifiant du document ctx: Contexte de traitement partagé entre les pipelines """ logger.info(f"👁️ Début de l'OCR pour le document {doc_id}") - + try: # 1. Vérification des prérequis if "preprocess_error" in ctx: raise Exception(f"Erreur de pré-traitement: {ctx['preprocess_error']}") - + processed_path = ctx.get("processed_path") if not processed_path or not os.path.exists(processed_path): raise FileNotFoundError("Fichier traité non trouvé") - + work_dir = ctx.get("work_dir") if not work_dir: raise ValueError("Répertoire de travail non défini") - + # 2. Détection du type de document file_ext = os.path.splitext(processed_path)[1].lower() - + if file_ext == '.pdf': # Traitement PDF ocr_result = _process_pdf(processed_path, work_dir) @@ -45,14 +45,14 @@ def run(doc_id: str, ctx: Dict[str, Any]) -> None: ocr_result = _process_image(processed_path, work_dir) else: raise ValueError(f"Format non supporté pour l'OCR: {file_ext}") - + # 3. Correction lexicale notariale corrected_text = _apply_notarial_corrections(ocr_result["text"]) ocr_result["corrected_text"] = corrected_text - + # 4. Sauvegarde des résultats _save_ocr_results(work_dir, ocr_result) - + # 5. Mise à jour du contexte ctx.update({ "ocr_text": corrected_text, @@ -61,11 +61,11 @@ def run(doc_id: str, ctx: Dict[str, Any]) -> None: "ocr_pages": ocr_result.get("pages", []), "ocr_artifacts": ocr_result.get("artifacts", {}) }) - + logger.info(f"✅ OCR terminé pour {doc_id}") logger.info(f" - Texte extrait: {len(corrected_text)} caractères") logger.info(f" - Confiance moyenne: {ocr_result.get('confidence', 0.0):.2f}") - + except Exception as e: logger.error(f"❌ Erreur lors de l'OCR de {doc_id}: {e}") ctx["ocr_error"] = str(e) @@ -74,18 +74,18 @@ def run(doc_id: str, ctx: Dict[str, Any]) -> None: def _process_pdf(pdf_path: str, work_dir: str) -> Dict[str, Any]: """Traite un fichier PDF avec OCRmyPDF""" logger.info("📄 Traitement PDF avec OCRmyPDF") - + try: # Vérification de la présence d'OCRmyPDF subprocess.run(["ocrmypdf", "--version"], check=True, capture_output=True) except (subprocess.CalledProcessError, FileNotFoundError): logger.warning("OCRmyPDF non disponible, utilisation de Tesseract") return _process_pdf_with_tesseract(pdf_path, work_dir) - + # Utilisation d'OCRmyPDF output_pdf = os.path.join(work_dir, "output", "ocr.pdf") output_txt = os.path.join(work_dir, "output", "ocr.txt") - + try: # Commande OCRmyPDF cmd = [ @@ -97,19 +97,19 @@ def _process_pdf(pdf_path: str, work_dir: str) -> Dict[str, Any]: "--clean", pdf_path, output_pdf ] - + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) - + if result.returncode != 0: logger.warning(f"OCRmyPDF a échoué: {result.stderr}") return _process_pdf_with_tesseract(pdf_path, work_dir) - + # Lecture du texte extrait text = "" if os.path.exists(output_txt): with open(output_txt, 'r', encoding='utf-8') as f: text = f.read() - + return { "text": text, "confidence": 0.85, # Estimation @@ -119,7 +119,7 @@ def _process_pdf(pdf_path: str, work_dir: str) -> Dict[str, Any]: "ocr_txt": output_txt } } - + except subprocess.TimeoutExpired: logger.error("Timeout lors de l'OCR avec OCRmyPDF") return _process_pdf_with_tesseract(pdf_path, work_dir) @@ -130,17 +130,17 @@ def _process_pdf(pdf_path: str, work_dir: str) -> Dict[str, Any]: def _process_pdf_with_tesseract(pdf_path: str, work_dir: str) -> Dict[str, Any]: """Traite un PDF avec Tesseract (fallback)""" logger.info("📄 Traitement PDF avec Tesseract") - + try: import pytesseract from pdf2image import convert_from_path - + # Conversion PDF en images images = convert_from_path(pdf_path, dpi=300) - + all_text = [] pages = [] - + for i, image in enumerate(images): # OCR sur chaque page page_text = pytesseract.image_to_string(image, lang='fra') @@ -149,12 +149,12 @@ def _process_pdf_with_tesseract(pdf_path: str, work_dir: str) -> Dict[str, Any]: "page": i + 1, "text": page_text }) - + # Sauvegarde des images pour debug for i, image in enumerate(images): image_path = os.path.join(work_dir, "temp", f"page_{i+1}.png") image.save(image_path) - + return { "text": "\n\n".join(all_text), "confidence": 0.75, # Estimation @@ -163,7 +163,7 @@ def _process_pdf_with_tesseract(pdf_path: str, work_dir: str) -> Dict[str, Any]: "images": [os.path.join(work_dir, "temp", f"page_{i+1}.png") for i in range(len(images))] } } - + except ImportError as e: logger.error(f"Bibliothèques manquantes: {e}") raise @@ -174,17 +174,17 @@ def _process_pdf_with_tesseract(pdf_path: str, work_dir: str) -> Dict[str, Any]: def _process_image(image_path: str, work_dir: str) -> Dict[str, Any]: """Traite une image avec Tesseract""" logger.info("🖼️ Traitement image avec Tesseract") - + try: import pytesseract from PIL import Image - + # Chargement de l'image image = Image.open(image_path) - + # OCR text = pytesseract.image_to_string(image, lang='fra') - + # Calcul de la confiance (nécessite pytesseract avec confidences) try: data = pytesseract.image_to_data(image, lang='fra', output_type=pytesseract.Output.DICT) @@ -192,7 +192,7 @@ def _process_image(image_path: str, work_dir: str) -> Dict[str, Any]: avg_confidence = sum(confidences) / len(confidences) / 100.0 if confidences else 0.0 except: avg_confidence = 0.75 # Estimation - + return { "text": text, "confidence": avg_confidence, @@ -201,7 +201,7 @@ def _process_image(image_path: str, work_dir: str) -> Dict[str, Any]: "processed_image": image_path } } - + except ImportError as e: logger.error(f"Bibliothèques manquantes: {e}") raise @@ -212,7 +212,7 @@ def _process_image(image_path: str, work_dir: str) -> Dict[str, Any]: def _apply_notarial_corrections(text: str) -> str: """Applique les corrections lexicales spécifiques au notariat""" logger.info("🔧 Application des corrections lexicales notariales") - + # Dictionnaire de corrections notariales corrections = { # Corrections OCR communes @@ -222,7 +222,7 @@ def _apply_notarial_corrections(text: str) -> str: "1": "l", "5": "s", "8": "B", - + # Termes notariaux spécifiques "acte de vente": "acte de vente", "acte de donation": "acte de donation", @@ -238,7 +238,7 @@ def _apply_notarial_corrections(text: str) -> str: "vendeur": "vendeur", "acquéreur": "acquéreur", "acheteur": "acheteur", - + # Adresses et lieux "rue": "rue", "avenue": "avenue", @@ -247,36 +247,36 @@ def _apply_notarial_corrections(text: str) -> str: "commune": "commune", "département": "département", "région": "région", - + # Montants et devises "euros": "euros", "€": "€", "francs": "francs", "FF": "FF" } - + corrected_text = text - + # Application des corrections for wrong, correct in corrections.items(): corrected_text = corrected_text.replace(wrong, correct) - + # Nettoyage des espaces multiples import re corrected_text = re.sub(r'\s+', ' ', corrected_text) - + return corrected_text.strip() def _save_ocr_results(work_dir: str, ocr_result: Dict[str, Any]) -> None: """Sauvegarde les résultats de l'OCR""" output_dir = os.path.join(work_dir, "output") os.makedirs(output_dir, exist_ok=True) - + # Sauvegarde du texte corrigé corrected_text_path = os.path.join(output_dir, "corrected_text.txt") with open(corrected_text_path, 'w', encoding='utf-8') as f: f.write(ocr_result["corrected_text"]) - + # Sauvegarde des métadonnées OCR metadata_path = os.path.join(output_dir, "ocr_metadata.json") metadata = { @@ -285,8 +285,8 @@ def _save_ocr_results(work_dir: str, ocr_result: Dict[str, Any]) -> None: "text_length": len(ocr_result["corrected_text"]), "artifacts": ocr_result.get("artifacts", {}) } - + with open(metadata_path, 'w', encoding='utf-8') as f: json.dump(metadata, f, indent=2, ensure_ascii=False) - + logger.info(f"💾 Résultats OCR sauvegardés dans {output_dir}") \ No newline at end of file diff --git a/services/worker/pipelines/preprocess.py b/services/worker/pipelines/preprocess.py index 77137dc..179b3ee 100644 --- a/services/worker/pipelines/preprocess.py +++ b/services/worker/pipelines/preprocess.py @@ -14,48 +14,48 @@ logger = logging.getLogger(__name__) def run(doc_id: str, ctx: Dict[str, Any]) -> None: """ Pipeline de pré-traitement des documents - + Args: doc_id: Identifiant du document ctx: Contexte de traitement partagé entre les pipelines """ logger.info(f"🔧 Début du pré-traitement pour le document {doc_id}") - + try: # 1. Récupération du document depuis le stockage document_path = _get_document_path(doc_id) if not document_path or not os.path.exists(document_path): raise FileNotFoundError(f"Document {doc_id} non trouvé") - + # 2. Validation du fichier file_info = _validate_file(document_path) ctx["file_info"] = file_info - + # 3. Calcul du hash pour l'intégrité file_hash = _calculate_hash(document_path) ctx["file_hash"] = file_hash - + # 4. Préparation des répertoires de travail work_dir = _prepare_work_directory(doc_id) ctx["work_dir"] = work_dir - + # 5. Conversion si nécessaire (HEIC -> JPEG, etc.) processed_path = _convert_if_needed(document_path, work_dir) ctx["processed_path"] = processed_path - + # 6. Extraction des métadonnées metadata = _extract_metadata(processed_path) ctx["metadata"] = metadata - + # 7. Détection du type de document doc_type = _detect_document_type(processed_path) ctx["detected_type"] = doc_type - + logger.info(f"✅ Pré-traitement terminé pour {doc_id}") logger.info(f" - Type détecté: {doc_type}") logger.info(f" - Taille: {file_info['size']} bytes") logger.info(f" - Hash: {file_hash[:16]}...") - + except Exception as e: logger.error(f"❌ Erreur lors du pré-traitement de {doc_id}: {e}") ctx["preprocess_error"] = str(e) @@ -71,7 +71,7 @@ def _validate_file(file_path: str) -> Dict[str, Any]: """Valide le fichier et retourne ses informations""" if not os.path.exists(file_path): raise FileNotFoundError(f"Fichier non trouvé: {file_path}") - + stat = os.stat(file_path) file_info = { "path": file_path, @@ -79,16 +79,16 @@ def _validate_file(file_path: str) -> Dict[str, Any]: "modified": stat.st_mtime, "extension": Path(file_path).suffix.lower() } - + # Validation de la taille (max 50MB) if file_info["size"] > 50 * 1024 * 1024: raise ValueError("Fichier trop volumineux (>50MB)") - + # Validation de l'extension allowed_extensions = ['.pdf', '.jpg', '.jpeg', '.png', '.tiff', '.heic'] if file_info["extension"] not in allowed_extensions: raise ValueError(f"Format non supporté: {file_info['extension']}") - + return file_info def _calculate_hash(file_path: str) -> str: @@ -103,20 +103,20 @@ def _prepare_work_directory(doc_id: str) -> str: """Prépare le répertoire de travail pour le document""" work_base = os.getenv("WORK_DIR", "/tmp/processing") work_dir = os.path.join(work_base, doc_id) - + os.makedirs(work_dir, exist_ok=True) - + # Création des sous-répertoires subdirs = ["input", "output", "temp", "artifacts"] for subdir in subdirs: os.makedirs(os.path.join(work_dir, subdir), exist_ok=True) - + return work_dir def _convert_if_needed(file_path: str, work_dir: str) -> str: """Convertit le fichier si nécessaire (HEIC -> JPEG, etc.)""" file_ext = Path(file_path).suffix.lower() - + if file_ext == '.heic': # Conversion HEIC vers JPEG output_path = os.path.join(work_dir, "input", "converted.jpg") @@ -125,7 +125,7 @@ def _convert_if_needed(file_path: str, work_dir: str) -> str: import shutil shutil.copy2(file_path, output_path) return output_path - + # Pour les autres formats, on copie dans le répertoire de travail output_path = os.path.join(work_dir, "input", f"original{file_ext}") import shutil @@ -139,7 +139,7 @@ def _extract_metadata(file_path: str) -> Dict[str, Any]: "extension": Path(file_path).suffix.lower(), "size": os.path.getsize(file_path) } - + # Métadonnées spécifiques selon le type if metadata["extension"] == '.pdf': try: @@ -156,7 +156,7 @@ def _extract_metadata(file_path: str) -> Dict[str, Any]: logger.warning("PyPDF2 non disponible, métadonnées PDF limitées") except Exception as e: logger.warning(f"Erreur lors de l'extraction des métadonnées PDF: {e}") - + elif metadata["extension"] in ['.jpg', '.jpeg', '.png', '.tiff']: try: from PIL import Image @@ -171,13 +171,13 @@ def _extract_metadata(file_path: str) -> Dict[str, Any]: logger.warning("PIL non disponible, métadonnées image limitées") except Exception as e: logger.warning(f"Erreur lors de l'extraction des métadonnées image: {e}") - + return metadata def _detect_document_type(file_path: str) -> str: """Détecte le type de document basé sur le nom et les métadonnées""" filename = os.path.basename(file_path).lower() - + # Détection basée sur le nom de fichier if any(keyword in filename for keyword in ['acte', 'vente', 'achat']): return 'acte_vente' diff --git a/services/worker/worker.py b/services/worker/worker.py index 3f84eed..9f94ae1 100644 --- a/services/worker/worker.py +++ b/services/worker/worker.py @@ -37,16 +37,16 @@ from pipelines import preprocess, ocr, classify, extract, index, checks, finaliz def process_document(self, doc_id: str, metadata: Dict[str, Any]) -> Dict[str, Any]: """ Tâche principale d'orchestration du pipeline de traitement - + Args: doc_id: Identifiant du document metadata: Métadonnées du document - + Returns: Résultat du traitement """ logger.info(f"🚀 Début du traitement du document {doc_id}") - + # Contexte partagé entre les pipelines ctx = { "doc_id": doc_id, @@ -56,14 +56,14 @@ def process_document(self, doc_id: str, metadata: Dict[str, Any]) -> Dict[str, A "steps_completed": [], "steps_failed": [] } - + try: # Mise à jour du statut self.update_state( state='PROGRESS', meta={'step': 'initialization', 'progress': 0} ) - + # Pipeline de traitement pipeline_steps = [ ("preprocess", preprocess.run, 10), @@ -74,11 +74,11 @@ def process_document(self, doc_id: str, metadata: Dict[str, Any]) -> Dict[str, A ("checks", checks.run, 95), ("finalize", finalize.run, 100) ] - + for step_name, step_func, progress in pipeline_steps: try: logger.info(f"📋 Exécution de l'étape: {step_name}") - + # Mise à jour du statut self.update_state( state='PROGRESS', @@ -88,31 +88,31 @@ def process_document(self, doc_id: str, metadata: Dict[str, Any]) -> Dict[str, A 'doc_id': doc_id } ) - + # Exécution de l'étape step_func(doc_id, ctx) ctx["steps_completed"].append(step_name) - + logger.info(f"✅ Étape {step_name} terminée avec succès") - + except Exception as e: error_msg = f"Erreur dans l'étape {step_name}: {str(e)}" logger.error(f"❌ {error_msg}") logger.error(traceback.format_exc()) - + ctx["steps_failed"].append({ "step": step_name, "error": str(e), "traceback": traceback.format_exc() }) - + # Si c'est une étape critique, on arrête if step_name in ["preprocess", "ocr"]: raise e - + # Sinon, on continue avec les étapes suivantes logger.warning(f"⚠️ Continuation malgré l'erreur dans {step_name}") - + # Traitement terminé avec succès result = { "status": "completed", @@ -121,15 +121,15 @@ def process_document(self, doc_id: str, metadata: Dict[str, Any]) -> Dict[str, A "steps_failed": ctx["steps_failed"], "final_context": ctx } - + logger.info(f"🎉 Traitement terminé avec succès pour {doc_id}") return result - + except Exception as e: error_msg = f"Erreur critique dans le traitement de {doc_id}: {str(e)}" logger.error(f"💥 {error_msg}") logger.error(traceback.format_exc()) - + # Mise à jour du statut d'erreur self.update_state( state='FAILURE', @@ -141,7 +141,7 @@ def process_document(self, doc_id: str, metadata: Dict[str, Any]) -> Dict[str, A 'steps_failed': ctx.get("steps_failed", []) } ) - + return { "status": "failed", "doc_id": doc_id, @@ -171,23 +171,23 @@ def get_stats() -> Dict[str, Any]: "failed_tasks": 0, "active_tasks": 0 } - + # Récupération des statistiques depuis Redis from celery import current_app inspect = current_app.control.inspect() - + # Tâches actives active = inspect.active() if active: stats["active_tasks"] = sum(len(tasks) for tasks in active.values()) - + # Tâches réservées reserved = inspect.reserved() if reserved: stats["reserved_tasks"] = sum(len(tasks) for tasks in reserved.values()) - + return stats - + except Exception as e: logger.error(f"Erreur lors de la récupération des statistiques: {e}") return {"error": str(e)} @@ -196,22 +196,22 @@ def get_stats() -> Dict[str, Any]: def cleanup(doc_id: str) -> Dict[str, Any]: """Nettoyage des fichiers temporaires d'un document""" logger.info(f"🧹 Nettoyage des fichiers temporaires pour {doc_id}") - + try: work_base = os.getenv("WORK_DIR", "/tmp/processing") work_dir = os.path.join(work_base, doc_id) - + if os.path.exists(work_dir): import shutil shutil.rmtree(work_dir) logger.info(f"✅ Répertoire {work_dir} supprimé") - + return { "status": "cleaned", "doc_id": doc_id, "work_dir": work_dir } - + except Exception as e: logger.error(f"❌ Erreur lors du nettoyage de {doc_id}: {e}") return {