mirc detection

This commit is contained in:
2025-07-09 06:40:36 +02:00
parent a5e044d747
commit 386b34526b
7 changed files with 1918 additions and 128 deletions

View File

@@ -39,12 +39,20 @@ class Settings(BaseSettings):
UPLOAD_FOLDER: str = os.getenv("UPLOAD_FOLDER", "/app/data/uploads") UPLOAD_FOLDER: str = os.getenv("UPLOAD_FOLDER", "/app/data/uploads")
RESULT_FOLDER: str = os.getenv("RESULT_FOLDER", "/app/data/results") RESULT_FOLDER: str = os.getenv("RESULT_FOLDER", "/app/data/results")
TEMP_FOLDER: str = os.getenv("TEMP_FOLDER", "/app/data/tmp") TEMP_FOLDER: str = os.getenv("TEMP_FOLDER", "/app/data/tmp")
CONVERTED_FOLDER: str = os.getenv("CONVERTED_FOLDER", "/app/data/converted")
MAX_CONTENT_LENGTH: int = int(os.getenv("MAX_CONTENT_LENGTH", "16777216")) # 16MB MAX_CONTENT_LENGTH: int = int(os.getenv("MAX_CONTENT_LENGTH", "16777216")) # 16MB
ALLOWED_EXTENSIONS: set = {"png", "jpg", "jpeg", "gif", "tiff", "pdf"} ALLOWED_EXTENSIONS: set = {"png", "jpg", "jpeg", "gif", "tiff", "pdf"}
# Formats modernes (nécessitant conversion)
MODERN_EXTENSIONS: set = {"heic", "heif", "webp", "dng", "cr2", "arw", "nef", "raw"}
# Configuration de conversion
CONVERSION_QUALITY: int = int(os.getenv("CONVERSION_QUALITY", "95")) # Qualité JPEG/PNG
DEFAULT_OUTPUT_FORMAT: str = os.getenv("DEFAULT_OUTPUT_FORMAT", "jpg")
# Configuration du traitement d'image # Configuration du traitement d'image
DEFAULT_OCR_LANGUAGE: str = os.getenv("DEFAULT_OCR_LANGUAGE", "eng") DEFAULT_OCR_LANGUAGE: str = os.getenv("DEFAULT_OCR_LANGUAGE", "fra")
ALTERNATIVE_OCR_LANGUAGE: str = os.getenv("ALTERNATIVE_OCR_LANGUAGE", "fra") ALTERNATIVE_OCR_LANGUAGE: str = os.getenv("ALTERNATIVE_OCR_LANGUAGE", "eng")
TESSERACT_DATA_PATH: str = os.getenv("TESSERACT_DATA_PATH", "/usr/share/tesseract-ocr/4.00/tessdata") TESSERACT_DATA_PATH: str = os.getenv("TESSERACT_DATA_PATH", "/usr/share/tesseract-ocr/4.00/tessdata")
# Délais et timeouts # Délais et timeouts
@@ -66,4 +74,5 @@ settings = Settings()
# Créer les dossiers nécessaires s'ils n'existent pas # Créer les dossiers nécessaires s'ils n'existent pas
os.makedirs(settings.UPLOAD_FOLDER, exist_ok=True) os.makedirs(settings.UPLOAD_FOLDER, exist_ok=True)
os.makedirs(settings.RESULT_FOLDER, exist_ok=True) os.makedirs(settings.RESULT_FOLDER, exist_ok=True)
os.makedirs(settings.TEMP_FOLDER, exist_ok=True) os.makedirs(settings.TEMP_FOLDER, exist_ok=True)
os.makedirs(settings.CONVERTED_FOLDER, exist_ok=True)

238
api/app/conversion.py Normal file
View File

@@ -0,0 +1,238 @@
"""
Module de conversion pour les formats d'images modernes
Supporte les formats courants des appareils Android et iOS
"""
import os
import subprocess
from pathlib import Path
import tempfile
import shutil
import logging
from typing import List, Optional, Tuple
from .config import settings
# Configuration du logging
logger = logging.getLogger("cheque_scanner_api.conversion")
# Formats modernes pris en charge
MODERN_FORMATS = {
# Formats iPhone
"heic": "HEIC (High Efficiency Image Format) - iPhone",
"heif": "HEIF (High Efficiency Image Format) - iPhone",
# Formats Android haute qualité
"webp": "WebP - Android",
"dng": "DNG (Digital Negative) - Android RAW",
# Formats communs haute résolution
"cr2": "Canon RAW Format",
"arw": "Sony RAW Format",
"nef": "Nikon RAW Format",
"raw": "Format RAW générique",
}
# Mapping des extensions aux formats de sortie recommandés
FORMAT_MAPPING = {
"heic": "jpg",
"heif": "jpg",
"webp": "png",
"dng": "jpg",
"cr2": "jpg",
"arw": "jpg",
"nef": "jpg",
"raw": "jpg",
}
def is_modern_format(filename: str) -> bool:
"""
Vérifie si le fichier est dans un format moderne qui nécessite une conversion
Args:
filename (str): Nom du fichier à vérifier
Returns:
bool: True si le fichier est dans un format moderne, False sinon
"""
ext = Path(filename).suffix.lower().lstrip(".")
return ext in MODERN_FORMATS
def get_supported_input_formats() -> List[str]:
"""
Renvoie la liste des formats d'entrée pris en charge
Returns:
List[str]: Liste des extensions de fichiers pris en charge
"""
return list(MODERN_FORMATS.keys()) + list(settings.ALLOWED_EXTENSIONS)
def convert_heic_to_jpg(input_path: str, output_path: str) -> bool:
"""
Convertit un fichier HEIC en JPG en utilisant pillow-heif
Args:
input_path (str): Chemin du fichier HEIC
output_path (str): Chemin de sortie pour le fichier JPG
Returns:
bool: True si la conversion a réussi, False sinon
"""
try:
import pillow_heif
from PIL import Image
# Enregistrer le décodeur HEIF
pillow_heif.register_heif_opener()
# Ouvrir et convertir l'image
img = Image.open(input_path)
img.convert("RGB").save(output_path, format="JPEG", quality=95)
return True
except ImportError:
logger.warning("pillow-heif n'est pas installé, utilisation de la méthode alternative")
return _convert_with_imagemagick(input_path, output_path)
except Exception as e:
logger.error(f"Erreur lors de la conversion HEIC: {str(e)}")
return False
def convert_webp_to_png(input_path: str, output_path: str) -> bool:
"""
Convertit un fichier WebP en PNG
Args:
input_path (str): Chemin du fichier WebP
output_path (str): Chemin de sortie pour le fichier PNG
Returns:
bool: True si la conversion a réussi, False sinon
"""
try:
from PIL import Image
img = Image.open(input_path)
img.save(output_path, format="PNG")
return True
except Exception as e:
logger.error(f"Erreur lors de la conversion WebP: {str(e)}")
return _convert_with_imagemagick(input_path, output_path)
def _convert_with_imagemagick(input_path: str, output_path: str) -> bool:
"""
Méthode alternative utilisant ImageMagick pour la conversion
Args:
input_path (str): Chemin du fichier d'entrée
output_path (str): Chemin du fichier de sortie
Returns:
bool: True si la conversion a réussi, False sinon
"""
try:
# Vérifier si ImageMagick est installé
subprocess.run(["which", "convert"], check=True, capture_output=True)
# Conversion avec ImageMagick
result = subprocess.run(
["convert", input_path, output_path],
check=True,
capture_output=True
)
return result.returncode == 0
except subprocess.CalledProcessError as e:
logger.error(f"Erreur ImageMagick: {e.stderr.decode() if e.stderr else str(e)}")
return False
except Exception as e:
logger.error(f"Erreur lors de la conversion avec ImageMagick: {str(e)}")
return False
def convert_raw_to_jpg(input_path: str, output_path: str) -> bool:
"""
Convertit un fichier RAW (DNG, CR2, ARW, NEF, etc.) en JPG
Args:
input_path (str): Chemin du fichier RAW
output_path (str): Chemin de sortie pour le fichier JPG
Returns:
bool: True si la conversion a réussi, False sinon
"""
try:
# Essayer d'abord avec rawpy
import rawpy
import imageio
with rawpy.imread(input_path) as raw:
rgb = raw.postprocess(use_camera_wb=True, half_size=False, no_auto_bright=False)
imageio.imsave(output_path, rgb)
return True
except ImportError:
logger.warning("rawpy n'est pas installé, utilisation de la méthode alternative")
return _convert_with_imagemagick(input_path, output_path)
except Exception as e:
logger.error(f"Erreur lors de la conversion RAW: {str(e)}")
return _convert_with_imagemagick(input_path, output_path)
def convert_file(input_path: str, output_dir: str = None) -> Tuple[bool, Optional[str], Optional[str]]:
"""
Convertit un fichier d'un format moderne vers un format standard
Args:
input_path (str): Chemin du fichier à convertir
output_dir (str, optional): Répertoire de sortie pour le fichier converti
Si non spécifié, utilise le même répertoire que le fichier d'entrée
Returns:
Tuple[bool, Optional[str], Optional[str]]:
- Succès de la conversion (bool)
- Chemin du fichier converti (str ou None si échec)
- Message d'erreur (str ou None si succès)
"""
input_path = Path(input_path)
# Vérifier si le fichier existe
if not input_path.exists():
return False, None, f"Le fichier {input_path} n'existe pas"
# Obtenir l'extension du fichier
ext = input_path.suffix.lower().lstrip(".")
# Si l'extension est déjà dans les formats autorisés, pas besoin de conversion
if ext in settings.ALLOWED_EXTENSIONS:
return True, str(input_path), None
# Vérifier si le format est pris en charge
if ext not in MODERN_FORMATS:
return False, None, f"Format non pris en charge: {ext}"
# Déterminer le format de sortie
output_ext = FORMAT_MAPPING.get(ext, "jpg")
# Déterminer le chemin de sortie
if output_dir:
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"{input_path.stem}.{output_ext}"
else:
output_path = input_path.with_suffix(f".{output_ext}")
# Effectuer la conversion en fonction du format
success = False
if ext in ["heic", "heif"]:
success = convert_heic_to_jpg(str(input_path), str(output_path))
elif ext == "webp":
success = convert_webp_to_png(str(input_path), str(output_path))
elif ext in ["dng", "cr2", "arw", "nef", "raw"]:
success = convert_raw_to_jpg(str(input_path), str(output_path))
else:
# Fallback sur ImageMagick pour les autres formats
success = _convert_with_imagemagick(str(input_path), str(output_path))
if success:
return True, str(output_path), None
else:
return False, None, f"Échec de la conversion du fichier {input_path}"

View File

@@ -6,12 +6,14 @@ import os
import time import time
import uuid import uuid
import shutil import shutil
import json
from typing import List, Optional from typing import List, Optional
from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Header, BackgroundTasks, Query, Request from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Header, BackgroundTasks, Query, Request, Path
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path as FilePath
import redis import redis
from rq import Queue from rq import Queue
from rq.job import Job from rq.job import Job
@@ -20,9 +22,11 @@ import logging
from .config import settings from .config import settings
from .schemas import ( from .schemas import (
UploadResponse, JobStatusResponse, JobResult, UploadResponse, JobStatusResponse, JobResult,
ExtractionResult, HealthCheck, ErrorResponse, JobStatus ExtractionResult, HealthCheck, ErrorResponse, JobStatus,
ConversionResponse, ConversionStatus
) )
from .conversion import is_modern_format, convert_file, get_supported_input_formats
from .dependencies import verify_api_key, get_redis_connection from .dependencies import verify_api_key, get_redis_connection
from .tasks import process_cheque_image from .tasks import process_cheque_image
@@ -52,8 +56,9 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# Configuration des fichiers statiques pour accéder aux images de résultats # Configuration des fichiers statiques pour accéder aux images de résultats et fichiers convertis
app.mount("/static", StaticFiles(directory=settings.RESULT_FOLDER), name="static") app.mount("/static", StaticFiles(directory=settings.RESULT_FOLDER), name="static")
app.mount("/static/converted", StaticFiles(directory=settings.CONVERTED_FOLDER), name="converted_files")
# Variable pour stocker le temps de démarrage # Variable pour stocker le temps de démarrage
start_time = time.time() start_time = time.time()
@@ -183,6 +188,255 @@ async def upload_image(
) )
@app.post(
f"{settings.API_PREFIX}/convert",
response_model=ConversionResponse,
tags=["Conversion"],
summary="Convertit un fichier d'un format moderne vers un format standard"
)
async def convert_image(
file: UploadFile = File(...),
target_format: Optional[str] = Query(None, description="Format cible (jpg, png)"),
api_key: str = Depends(verify_api_key)
):
"""
Convertit une image d'un format moderne (HEIC, WebP, RAW, etc.) vers un format standard (JPG, PNG)
Cette API prend en charge les formats suivants:
- Formats iPhone: HEIC, HEIF
- Formats Android haute qualité: WebP, DNG
- Formats RAW: CR2, ARW, NEF, RAW
La conversion est effectuée côté serveur et le fichier converti est renvoyé.
"""
# Vérifier si le format d'entrée nécessite une conversion
filename = file.filename
file_ext = FilePath(filename).suffix.lower().lstrip(".")
# Si le format est déjà pris en charge, renvoyer une erreur
if file_ext in settings.ALLOWED_EXTENSIONS and not target_format:
return ConversionResponse(
original_filename=filename,
original_format=file_ext,
status=ConversionStatus.FAILED,
message="Le fichier est déjà dans un format pris en charge, aucune conversion nécessaire"
)
# Si le format n'est pas pris en charge du tout
if file_ext not in settings.ALLOWED_EXTENSIONS and file_ext not in settings.MODERN_EXTENSIONS:
return ConversionResponse(
original_filename=filename,
original_format=file_ext,
status=ConversionStatus.FAILED,
message=f"Format non pris en charge: {file_ext}. Formats supportés: {', '.join(get_supported_input_formats())}"
)
# Sauvegarder le fichier temporairement
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
temp_filename = f"{timestamp}_{FilePath(filename).stem}.{file_ext}"
temp_path = FilePath(settings.TEMP_FOLDER) / temp_filename
try:
with open(temp_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du fichier temporaire: {str(e)}")
return ConversionResponse(
original_filename=filename,
original_format=file_ext,
status=ConversionStatus.FAILED,
message=f"Erreur lors de la sauvegarde du fichier: {str(e)}"
)
# Déterminer le format de sortie si spécifié
output_format = target_format if target_format else None
# Effectuer la conversion
success, converted_path, error_message = convert_file(
str(temp_path),
settings.CONVERTED_FOLDER
)
# Nettoyer le fichier temporaire
if FilePath(temp_path).exists():
FilePath(temp_path).unlink()
if not success:
return ConversionResponse(
original_filename=filename,
original_format=file_ext,
status=ConversionStatus.FAILED,
message=error_message if error_message else "Échec de la conversion pour une raison inconnue"
)
# Préparer le chemin relatif pour l'URL
converted_filename = FilePath(converted_path).name
converted_format = FilePath(converted_path).suffix.lower().lstrip(".")
# Construire l'URL pour accéder au fichier converti
converted_url = f"/static/converted/{converted_filename}"
return ConversionResponse(
original_filename=filename,
original_format=file_ext,
converted_filename=converted_filename,
converted_format=converted_format,
converted_path=converted_url,
status=ConversionStatus.SUCCESS,
message=f"Conversion réussie de {file_ext} vers {converted_format}"
)
@app.post(
f"{settings.API_PREFIX}/convert-and-process",
response_model=UploadResponse,
tags=["Conversion"],
status_code=202,
summary="Convertit un fichier et lance son traitement"
)
async def convert_and_process_image(
background_tasks: BackgroundTasks,
file: UploadFile = File(...),
priority: bool = Query(False, description="Traiter avec une priorité élevée"),
api_key: str = Depends(verify_api_key),
redis_conn: redis.Redis = Depends(get_redis_connection)
):
"""
Convertit une image d'un format moderne vers un format standard, puis lance son traitement d'extraction
Cette API combine la conversion et le traitement en une seule étape pour les fichiers provenant
d'appareils modernes (iPhone, Android haut de gamme).
"""
# Vérifier le format du fichier
filename = file.filename
file_ext = FilePath(filename).suffix.lower().lstrip(".")
# Créer un identifiant unique pour la tâche
job_id = str(uuid.uuid4())
# Si le format est déjà pris en charge, procéder directement au traitement
if file_ext in settings.ALLOWED_EXTENSIONS:
# Créer le chemin de fichier
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
new_filename = f"{timestamp}_{job_id}.{file_ext}"
file_path = os.path.join(settings.UPLOAD_FOLDER, new_filename)
# Sauvegarder le fichier
try:
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du fichier: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Erreur lors de la sauvegarde de l'image: {str(e)}"
)
# Si le format nécessite une conversion
elif file_ext in settings.MODERN_EXTENSIONS:
# Sauvegarder le fichier temporairement
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
temp_filename = f"{timestamp}_{job_id}.{file_ext}"
temp_path = FilePath(settings.TEMP_FOLDER) / temp_filename
try:
with open(temp_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du fichier temporaire: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Erreur lors de la sauvegarde de l'image: {str(e)}"
)
# Effectuer la conversion
success, converted_path, error_message = convert_file(
str(temp_path),
settings.UPLOAD_FOLDER
)
# Nettoyer le fichier temporaire
if FilePath(temp_path).exists():
FilePath(temp_path).unlink()
if not success:
raise HTTPException(
status_code=500,
detail=f"Erreur lors de la conversion: {error_message if error_message else 'Raison inconnue'}"
)
# Utiliser le fichier converti
file_path = converted_path
else:
# Format non pris en charge
raise HTTPException(
status_code=400,
detail=f"Format de fichier non pris en charge. Formats acceptés: " +
f"{', '.join(settings.ALLOWED_EXTENSIONS)} et {', '.join(settings.MODERN_EXTENSIONS)}"
)
# Déterminer la file d'attente en fonction de la priorité
queue_name = settings.HIGH_PRIORITY_QUEUE_NAME if priority else settings.QUEUE_NAME
queue = Queue(queue_name, connection=redis_conn)
# Créer une tâche RQ
try:
# Utiliser directement le nom complet du module dans le worker
queue_job = queue.enqueue(
'tasks.process_cheque_image',
job_id,
file_path,
result_ttl=settings.RESULT_TTL,
timeout=settings.JOB_TIMEOUT
)
# Stocker les métadonnées dans Redis
redis_conn.hset(f"job:{job_id}", mapping={
"status": JobStatus.PENDING.value,
"created_at": datetime.now().isoformat(),
"file_path": file_path,
"filename": filename,
"priority": str(priority).lower(),
"converted": "true" if file_ext in settings.MODERN_EXTENSIONS else "false"
})
logger.info(f"Tâche créée: {job_id} - Fichier: {filename} (converti: {file_ext in settings.MODERN_EXTENSIONS})")
return UploadResponse(
job_id=job_id,
status=JobStatus.PENDING,
message=f"Image {'convertie et ' if file_ext in settings.MODERN_EXTENSIONS else ''}en file d'attente pour traitement (file {'prioritaire' if priority else 'standard'})"
)
except Exception as e:
logger.error(f"Erreur lors de la création de la tâche: {str(e)}")
# Supprimer le fichier en cas d'erreur
if os.path.exists(file_path):
os.remove(file_path)
raise HTTPException(
status_code=500,
detail=f"Erreur lors de la création de la tâche: {str(e)}"
)
@app.get(
f"{settings.API_PREFIX}/supported-formats",
response_model=List[str],
tags=["Conversion"],
summary="Liste tous les formats de fichiers pris en charge"
)
async def list_supported_formats(
api_key: str = Depends(verify_api_key)
):
"""
Renvoie la liste de tous les formats de fichiers pris en charge par l'API
Cela inclut à la fois les formats standards et les formats modernes qui nécessitent une conversion.
"""
return get_supported_input_formats()
@app.get( @app.get(
f"{settings.API_PREFIX}/status/{{job_id}}", f"{settings.API_PREFIX}/status/{{job_id}}",
response_model=JobStatusResponse, response_model=JobStatusResponse,
@@ -279,7 +533,7 @@ async def get_job_result(
# Charger les résultats depuis Redis # Charger les résultats depuis Redis
result_data = job_data.get("result") result_data = job_data.get("result")
if result_data: if result_data:
result_dict = eval(result_data) # Attention: eval n'est pas sécurisé en production result_dict = json.loads(result_data) # Utiliser json.loads au lieu de eval
result = ExtractionResult(**result_dict) result = ExtractionResult(**result_dict)
texte_brut = job_data.get("texte_brut") texte_brut = job_data.get("texte_brut")

View File

@@ -8,6 +8,23 @@ from enum import Enum
from datetime import datetime from datetime import datetime
class ConversionStatus(str, Enum):
"""Statuts possibles pour une conversion de fichier"""
SUCCESS = "success"
FAILED = "failed"
class ConversionResponse(BaseModel):
"""Réponse à une demande de conversion de fichier"""
original_filename: str = Field(..., description="Nom du fichier original")
original_format: str = Field(..., description="Format du fichier original")
converted_filename: Optional[str] = Field(None, description="Nom du fichier converti")
converted_format: Optional[str] = Field(None, description="Format du fichier converti")
converted_path: Optional[str] = Field(None, description="Chemin vers le fichier converti")
status: ConversionStatus = Field(..., description="Statut de la conversion")
message: str = Field(..., description="Message d'information ou d'erreur")
class JobStatus(str, Enum): class JobStatus(str, Enum):
"""Statuts possibles pour une tâche d'extraction""" """Statuts possibles pour une tâche d'extraction"""
PENDING = "pending" PENDING = "pending"
@@ -37,12 +54,24 @@ class JobStatusResponse(BaseModel):
class ExtractionResult(BaseModel): class ExtractionResult(BaseModel):
"""Résultat de l'extraction d'informations d'un chèque""" """Résultat de l'extraction d'informations d'un chèque"""
montant: Optional[str] = Field(None, description="Montant du chèque") # Champs standards
montant: Optional[str] = Field(None, description="Montant du chèque en chiffres")
date: Optional[str] = Field(None, description="Date du chèque") date: Optional[str] = Field(None, description="Date du chèque")
beneficiaire: Optional[str] = Field(None, description="Bénéficiaire du chèque") beneficiaire: Optional[str] = Field(None, description="Bénéficiaire du chèque")
numero_cheque: Optional[str] = Field(None, description="Numéro du chèque") numero_cheque: Optional[str] = Field(None, description="Numéro du chèque")
qualite_extraction: Optional[str] = Field(None, description="Qualité de l'extraction (échec, faible, moyenne, bonne)") qualite_extraction: Optional[str] = Field(None, description="Qualité de l'extraction (échec, faible, moyenne, bonne)")
image_zones: Optional[str] = Field(None, description="Chemin vers l'image avec les zones identifiées") image_zones: Optional[str] = Field(None, description="Chemin vers l'image avec les zones identifiées")
# Champs MICR (CMC-7)
code_banque: Optional[str] = Field(None, description="Code banque (MICR CMC-7)")
code_guichet: Optional[str] = Field(None, description="Code guichet (MICR CMC-7)")
numero_compte: Optional[str] = Field(None, description="Numéro de compte (MICR CMC-7)")
cle_rib: Optional[str] = Field(None, description="Clé RIB (MICR CMC-7)")
sequence_micr: Optional[str] = Field(None, description="Séquence MICR complète")
# Champs avancés
montant_lettres: Optional[str] = Field(None, description="Montant du chèque en lettres")
coherence_montants: Optional[bool] = Field(None, description="Cohérence entre montant en chiffres et en lettres")
class JobResult(BaseModel): class JobResult(BaseModel):

View File

@@ -23,7 +23,7 @@ services:
networks: networks:
- cheque-scanner-network - cheque-scanner-network
worker: worker1:
build: ./worker build: ./worker
depends_on: depends_on:
- redis - redis
@@ -38,11 +38,35 @@ services:
- TEMP_FOLDER=/app/data/tmp - TEMP_FOLDER=/app/data/tmp
- QUEUE_NAME=cheque_processing - QUEUE_NAME=cheque_processing
- HIGH_PRIORITY_QUEUE_NAME=cheque_processing_high - HIGH_PRIORITY_QUEUE_NAME=cheque_processing_high
- TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/4.00/tessdata - TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/5/tessdata
- DEFAULT_OCR_LANGUAGE=eng - DEFAULT_OCR_LANGUAGE=fra
- ALTERNATIVE_OCR_LANGUAGE=fra - ALTERNATIVE_OCR_LANGUAGE=eng
deploy: - WORKER_NAME=worker-1
replicas: ${WORKER_REPLICAS:-2} command: ["python", "worker.py", "--queues", "cheque_processing", "--name", "worker-1"]
restart: unless-stopped
networks:
- cheque-scanner-network
worker2:
build: ./worker
depends_on:
- redis
volumes:
- shared_data:/app/data
- ./shared:/app/shared
- ./api/app:/app/api_app
environment:
- REDIS_URL=redis://redis:6379/0
- UPLOAD_FOLDER=/app/data/uploads
- RESULT_FOLDER=/app/data/results
- TEMP_FOLDER=/app/data/tmp
- QUEUE_NAME=cheque_processing
- HIGH_PRIORITY_QUEUE_NAME=cheque_processing_high
- TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/5/tessdata
- DEFAULT_OCR_LANGUAGE=fra
- ALTERNATIVE_OCR_LANGUAGE=eng
- WORKER_NAME=worker-2
command: ["python", "worker.py", "--queues", "cheque_processing", "--name", "worker-2"]
restart: unless-stopped restart: unless-stopped
networks: networks:
- cheque-scanner-network - cheque-scanner-network
@@ -62,9 +86,9 @@ services:
- RESULT_FOLDER=/app/data/results - RESULT_FOLDER=/app/data/results
- TEMP_FOLDER=/app/data/tmp - TEMP_FOLDER=/app/data/tmp
- QUEUE_NAME=cheque_processing_high - QUEUE_NAME=cheque_processing_high
- TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/4.00/tessdata - TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/5/tessdata
- DEFAULT_OCR_LANGUAGE=eng - DEFAULT_OCR_LANGUAGE=fra
- ALTERNATIVE_OCR_LANGUAGE=fra - ALTERNATIVE_OCR_LANGUAGE=eng
- WORKER_NAME=priority-worker - WORKER_NAME=priority-worker
entrypoint: ["/priority_entrypoint.sh"] entrypoint: ["/priority_entrypoint.sh"]
command: ["python", "worker.py", "--queues", "cheque_processing_high", "--name", "priority-worker"] command: ["python", "worker.py", "--queues", "cheque_processing_high", "--name", "priority-worker"]

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ import traceback
import redis import redis
from datetime import datetime from datetime import datetime
from rq import get_current_job from rq import get_current_job
from json import JSONEncoder
# Ajouter le module d'extraction au path # Ajouter le module d'extraction au path
sys.path.append('/app/shared') sys.path.append('/app/shared')
@@ -56,7 +57,8 @@ def update_job_status(job_id, status, message=None, progress=None, result=None,
update_data["progress"] = str(progress) update_data["progress"] = str(progress)
if result: if result:
update_data["result"] = str(result) # Utiliser json.dumps pour une sérialisation correcte
update_data["result"] = json.dumps(result)
if texte_brut: if texte_brut:
update_data["texte_brut"] = texte_brut update_data["texte_brut"] = texte_brut
@@ -82,13 +84,14 @@ def update_job_status(job_id, status, message=None, progress=None, result=None,
return False return False
def process_cheque_image(file_path, job_id): def process_cheque_image(job_id, file_path, **kwargs):
""" """
Traite une image de chèque pour en extraire les informations Traite une image de chèque pour en extraire les informations
Args: Args:
file_path (str): Chemin vers l'image à traiter
job_id (str): Identifiant de la tâche job_id (str): Identifiant de la tâche
file_path (str): Chemin vers l'image à traiter
**kwargs: Paramètres supplémentaires (ignorés, pour compatibilité avec RQ)
Returns: Returns:
dict: Résultat de l'extraction dict: Résultat de l'extraction
@@ -135,12 +138,12 @@ def process_cheque_image(file_path, job_id):
infos, texte = extraire_infos_cheque( infos, texte = extraire_infos_cheque(
chemin_image=file_path, chemin_image=file_path,
methode="ocr", methode="hybride_avance",
language=DEFAULT_OCR_LANGUAGE, language=DEFAULT_OCR_LANGUAGE,
tessdata=tessdata_path tessdata=tessdata_path
) )
methode = "ocr" methode = "hybride_avance"
except Exception as e: except Exception as e:
logger.warning(f"Échec de la première tentative: {str(e)}") logger.warning(f"Échec de la première tentative: {str(e)}")
@@ -157,31 +160,50 @@ def process_cheque_image(file_path, job_id):
infos, texte = extraire_infos_cheque( infos, texte = extraire_infos_cheque(
chemin_image=file_path, chemin_image=file_path,
methode="ocr", methode="hybride_avance",
language=ALTERNATIVE_OCR_LANGUAGE, language=ALTERNATIVE_OCR_LANGUAGE,
tessdata=tessdata_path tessdata=tessdata_path
) )
methode = "ocr" methode = "hybride_avance"
except Exception as e2: except Exception as e2:
logger.warning(f"Échec de la deuxième tentative: {str(e2)}") logger.warning(f"Échec de la deuxième tentative: {str(e2)}")
# Troisième tentative avec la méthode CV # Troisième tentative avec la méthode hybride (combinant MICR et extraction du montant en lettres)
logger.info("Tentative d'extraction avec la méthode CV (sans OCR)") logger.info("Tentative d'extraction avec la méthode hybride (MICR + montant en lettres)")
update_job_status( update_job_status(
job_id=job_id, job_id=job_id,
status="processing", status="processing",
message="Extraction par détection de zones (sans OCR)", message="Extraction MICR et montant en lettres",
progress=70 progress=70
) )
infos, texte = extraire_infos_cheque( infos, texte = extraire_infos_cheque(
chemin_image=file_path, chemin_image=file_path,
methode="cv" methode="hybride",
language=DEFAULT_OCR_LANGUAGE,
tessdata=tessdata_path
) )
methode = "cv" methode = "hybride"
# Si échec, dernière tentative avec la méthode MICR seule
if not infos.get("montant") and not infos.get("code_banque"):
logger.info("Tentative d'extraction avec la méthode MICR uniquement")
update_job_status(
job_id=job_id,
status="processing",
message="Extraction MICR uniquement",
progress=80
)
infos, texte = extraire_infos_cheque(
chemin_image=file_path,
methode="micr"
)
methode = "micr"
# Mise à jour finale # Mise à jour finale
update_job_status( update_job_status(