mirc detection
This commit is contained in:
@@ -39,12 +39,20 @@ class Settings(BaseSettings):
|
||||
UPLOAD_FOLDER: str = os.getenv("UPLOAD_FOLDER", "/app/data/uploads")
|
||||
RESULT_FOLDER: str = os.getenv("RESULT_FOLDER", "/app/data/results")
|
||||
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
|
||||
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
|
||||
DEFAULT_OCR_LANGUAGE: str = os.getenv("DEFAULT_OCR_LANGUAGE", "eng")
|
||||
ALTERNATIVE_OCR_LANGUAGE: str = os.getenv("ALTERNATIVE_OCR_LANGUAGE", "fra")
|
||||
DEFAULT_OCR_LANGUAGE: str = os.getenv("DEFAULT_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")
|
||||
|
||||
# Délais et timeouts
|
||||
@@ -66,4 +74,5 @@ settings = Settings()
|
||||
# Créer les dossiers nécessaires s'ils n'existent pas
|
||||
os.makedirs(settings.UPLOAD_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
238
api/app/conversion.py
Normal 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}"
|
||||
266
api/app/main.py
266
api/app/main.py
@@ -6,12 +6,14 @@ import os
|
||||
import time
|
||||
import uuid
|
||||
import shutil
|
||||
import json
|
||||
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.responses import JSONResponse
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path as FilePath
|
||||
import redis
|
||||
from rq import Queue
|
||||
from rq.job import Job
|
||||
@@ -20,9 +22,11 @@ import logging
|
||||
|
||||
from .config import settings
|
||||
from .schemas import (
|
||||
UploadResponse, JobStatusResponse, JobResult,
|
||||
ExtractionResult, HealthCheck, ErrorResponse, JobStatus
|
||||
UploadResponse, JobStatusResponse, JobResult,
|
||||
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 .tasks import process_cheque_image
|
||||
|
||||
@@ -52,8 +56,9 @@ app.add_middleware(
|
||||
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/converted", StaticFiles(directory=settings.CONVERTED_FOLDER), name="converted_files")
|
||||
|
||||
# Variable pour stocker le temps de démarrage
|
||||
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(
|
||||
f"{settings.API_PREFIX}/status/{{job_id}}",
|
||||
response_model=JobStatusResponse,
|
||||
@@ -279,7 +533,7 @@ async def get_job_result(
|
||||
# Charger les résultats depuis Redis
|
||||
result_data = job_data.get("result")
|
||||
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)
|
||||
|
||||
texte_brut = job_data.get("texte_brut")
|
||||
|
||||
@@ -8,6 +8,23 @@ from enum import Enum
|
||||
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):
|
||||
"""Statuts possibles pour une tâche d'extraction"""
|
||||
PENDING = "pending"
|
||||
@@ -37,12 +54,24 @@ class JobStatusResponse(BaseModel):
|
||||
|
||||
class ExtractionResult(BaseModel):
|
||||
"""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")
|
||||
beneficiaire: Optional[str] = Field(None, description="Bénéficiaire 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)")
|
||||
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):
|
||||
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
networks:
|
||||
- cheque-scanner-network
|
||||
|
||||
worker:
|
||||
worker1:
|
||||
build: ./worker
|
||||
depends_on:
|
||||
- redis
|
||||
@@ -38,11 +38,35 @@ services:
|
||||
- TEMP_FOLDER=/app/data/tmp
|
||||
- QUEUE_NAME=cheque_processing
|
||||
- HIGH_PRIORITY_QUEUE_NAME=cheque_processing_high
|
||||
- TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/4.00/tessdata
|
||||
- DEFAULT_OCR_LANGUAGE=eng
|
||||
- ALTERNATIVE_OCR_LANGUAGE=fra
|
||||
deploy:
|
||||
replicas: ${WORKER_REPLICAS:-2}
|
||||
- TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/5/tessdata
|
||||
- DEFAULT_OCR_LANGUAGE=fra
|
||||
- ALTERNATIVE_OCR_LANGUAGE=eng
|
||||
- WORKER_NAME=worker-1
|
||||
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
|
||||
networks:
|
||||
- cheque-scanner-network
|
||||
@@ -62,9 +86,9 @@ services:
|
||||
- RESULT_FOLDER=/app/data/results
|
||||
- TEMP_FOLDER=/app/data/tmp
|
||||
- QUEUE_NAME=cheque_processing_high
|
||||
- TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/4.00/tessdata
|
||||
- DEFAULT_OCR_LANGUAGE=eng
|
||||
- ALTERNATIVE_OCR_LANGUAGE=fra
|
||||
- TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/5/tessdata
|
||||
- DEFAULT_OCR_LANGUAGE=fra
|
||||
- ALTERNATIVE_OCR_LANGUAGE=eng
|
||||
- WORKER_NAME=priority-worker
|
||||
entrypoint: ["/priority_entrypoint.sh"]
|
||||
command: ["python", "worker.py", "--queues", "cheque_processing_high", "--name", "priority-worker"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import traceback
|
||||
import redis
|
||||
from datetime import datetime
|
||||
from rq import get_current_job
|
||||
from json import JSONEncoder
|
||||
|
||||
# Ajouter le module d'extraction au path
|
||||
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)
|
||||
|
||||
if result:
|
||||
update_data["result"] = str(result)
|
||||
# Utiliser json.dumps pour une sérialisation correcte
|
||||
update_data["result"] = json.dumps(result)
|
||||
|
||||
if 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
|
||||
|
||||
|
||||
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
|
||||
|
||||
Args:
|
||||
file_path (str): Chemin vers l'image à traiter
|
||||
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:
|
||||
dict: Résultat de l'extraction
|
||||
@@ -135,12 +138,12 @@ def process_cheque_image(file_path, job_id):
|
||||
|
||||
infos, texte = extraire_infos_cheque(
|
||||
chemin_image=file_path,
|
||||
methode="ocr",
|
||||
methode="hybride_avance",
|
||||
language=DEFAULT_OCR_LANGUAGE,
|
||||
tessdata=tessdata_path
|
||||
)
|
||||
|
||||
methode = "ocr"
|
||||
methode = "hybride_avance"
|
||||
|
||||
except Exception as 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(
|
||||
chemin_image=file_path,
|
||||
methode="ocr",
|
||||
methode="hybride_avance",
|
||||
language=ALTERNATIVE_OCR_LANGUAGE,
|
||||
tessdata=tessdata_path
|
||||
)
|
||||
|
||||
methode = "ocr"
|
||||
methode = "hybride_avance"
|
||||
|
||||
except Exception as e2:
|
||||
logger.warning(f"Échec de la deuxième tentative: {str(e2)}")
|
||||
|
||||
# Troisième tentative avec la méthode CV
|
||||
logger.info("Tentative d'extraction avec la méthode CV (sans OCR)")
|
||||
# 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 hybride (MICR + montant en lettres)")
|
||||
update_job_status(
|
||||
job_id=job_id,
|
||||
status="processing",
|
||||
message="Extraction par détection de zones (sans OCR)",
|
||||
message="Extraction MICR et montant en lettres",
|
||||
progress=70
|
||||
)
|
||||
|
||||
infos, texte = extraire_infos_cheque(
|
||||
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
|
||||
update_job_status(
|
||||
|
||||
Reference in New Issue
Block a user