First commit

This commit is contained in:
2025-07-09 02:12:06 +02:00
parent 1a7946495c
commit a5e044d747
23 changed files with 1967 additions and 0 deletions

34
api/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
FROM python:3.10-slim
WORKDIR /app
# Installation des dépendances système
RUN apt-get update && apt-get install -y \
gcc \
python3-dev \
tesseract-ocr \
tesseract-ocr-fra \
tesseract-ocr-eng \
libgl1-mesa-glx \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
# Copie et installation des dépendances Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copie du code de l'application
COPY ./app /app/app
COPY ./app/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# Variables d'environnement
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Exposition du port
EXPOSE 8000
# Commande d'exécution
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

3
api/app/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Package pour l'API Cheque Scanner
"""

69
api/app/config.py Normal file
View File

@@ -0,0 +1,69 @@
"""
Configuration de l'API Cheque Scanner
"""
import os
from pydantic_settings import BaseSettings
from dotenv import load_dotenv
# Charger les variables d'environnement depuis le fichier .env
load_dotenv()
class Settings(BaseSettings):
"""Configuration de l'application"""
# Informations de base
APP_NAME: str = "Cheque Scanner API"
API_VERSION: str = "v1"
API_PREFIX: str = f"/api/{API_VERSION}"
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
# Sécurité
API_KEY: str = os.getenv("API_KEY", "your-secret-api-key")
# Configuration du serveur
HOST: str = os.getenv("HOST", "0.0.0.0")
PORT: int = int(os.getenv("PORT", "8000"))
# Configuration Redis
REDIS_HOST: str = os.getenv("REDIS_HOST", "redis")
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
REDIS_URL: str = os.getenv("REDIS_URL", f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}")
# Configuration des files d'attente
QUEUE_NAME: str = os.getenv("QUEUE_NAME", "cheque_processing")
HIGH_PRIORITY_QUEUE_NAME: str = os.getenv("HIGH_PRIORITY_QUEUE_NAME", "cheque_processing_high")
# Configuration du stockage
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")
MAX_CONTENT_LENGTH: int = int(os.getenv("MAX_CONTENT_LENGTH", "16777216")) # 16MB
ALLOWED_EXTENSIONS: set = {"png", "jpg", "jpeg", "gif", "tiff", "pdf"}
# 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")
TESSERACT_DATA_PATH: str = os.getenv("TESSERACT_DATA_PATH", "/usr/share/tesseract-ocr/4.00/tessdata")
# Délais et timeouts
JOB_TIMEOUT: int = int(os.getenv("JOB_TIMEOUT", "300")) # 5 minutes
RESULT_TTL: int = int(os.getenv("RESULT_TTL", "86400")) # 24 heures
# Limites
MAX_WORKERS: int = int(os.getenv("MAX_WORKERS", "3"))
RATE_LIMIT: int = int(os.getenv("RATE_LIMIT", "100")) # requêtes par heure
class Config:
env_file = ".env"
case_sensitive = True
# Instance de configuration globale
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)

45
api/app/dependencies.py Normal file
View File

@@ -0,0 +1,45 @@
"""
Dépendances pour l'API FastAPI
"""
import redis
from fastapi import Depends, HTTPException, Header
from typing import Optional
from .config import settings
def get_redis_connection():
"""
Crée et retourne une connexion à Redis
"""
try:
conn = redis.Redis.from_url(settings.REDIS_URL)
yield conn
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Erreur de connexion à Redis: {str(e)}"
)
finally:
# Fermer la connexion
pass # La connexion est fermée automatiquement
async def verify_api_key(x_api_key: Optional[str] = Header(None)):
"""
Vérifie que la clé API fournie est valide
"""
if not x_api_key:
raise HTTPException(
status_code=401,
detail="Clé API manquante"
)
if x_api_key != settings.API_KEY:
raise HTTPException(
status_code=403,
detail="Clé API invalide"
)
return x_api_key

11
api/app/entrypoint.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -e
# Attendre que Redis soit disponible
echo "Démarrage de l'API..."
# Ajouter un petit délai pour s'assurer que Redis a eu le temps de démarrer
sleep 5
echo "Redis devrait être disponible maintenant!"
# Exécuter la commande spécifiée
exec "$@"

321
api/app/main.py Normal file
View File

@@ -0,0 +1,321 @@
"""
API principale pour le service d'extraction d'informations de chèques
"""
import os
import time
import uuid
import shutil
from typing import List, Optional
from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Header, BackgroundTasks, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from datetime import datetime, timedelta
import redis
from rq import Queue
from rq.job import Job
from rq.registry import StartedJobRegistry, FinishedJobRegistry, FailedJobRegistry
import logging
from .config import settings
from .schemas import (
UploadResponse, JobStatusResponse, JobResult,
ExtractionResult, HealthCheck, ErrorResponse, JobStatus
)
from .dependencies import verify_api_key, get_redis_connection
from .tasks import process_cheque_image
# Configuration du logging
logging.basicConfig(
level=logging.INFO if not settings.DEBUG else logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("cheque_scanner_api")
# Création de l'application FastAPI
app = FastAPI(
title=settings.APP_NAME,
description="API pour l'extraction d'informations de chèques à partir d'images",
version=settings.API_VERSION,
docs_url=f"{settings.API_PREFIX}/docs",
redoc_url=f"{settings.API_PREFIX}/redoc",
openapi_url=f"{settings.API_PREFIX}/openapi.json"
)
# Middleware CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # À ajuster en production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Configuration des fichiers statiques pour accéder aux images de résultats
app.mount("/static", StaticFiles(directory=settings.RESULT_FOLDER), name="static")
# Variable pour stocker le temps de démarrage
start_time = time.time()
@app.get(f"{settings.API_PREFIX}/health", response_model=HealthCheck, tags=["Système"])
async def health_check(redis_conn: redis.Redis = Depends(get_redis_connection)):
"""
Vérifie l'état de santé de l'API et des services associés
"""
# Vérifier la connexion Redis
try:
redis_conn.ping()
redis_status = "ok"
except Exception as e:
logger.error(f"Erreur Redis: {str(e)}")
redis_status = f"error: {str(e)}"
# Obtenir le nombre de workers actifs
registry = StartedJobRegistry(settings.QUEUE_NAME, connection=redis_conn)
worker_count = len(registry.get_job_ids())
# Obtenir la taille de la file d'attente
queue = Queue(settings.QUEUE_NAME, connection=redis_conn)
queue_size = len(queue.get_job_ids())
# Calculer le temps de fonctionnement
uptime_seconds = time.time() - start_time
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime = f"{int(days)}d {int(hours)}h {int(minutes)}m {int(seconds)}s"
return HealthCheck(
status="ok",
version=settings.API_VERSION,
redis_status=redis_status,
worker_count=worker_count,
queue_size=queue_size,
uptime=uptime
)
@app.post(
f"{settings.API_PREFIX}/upload",
response_model=UploadResponse,
tags=["Extraction"],
status_code=202
)
async def upload_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)
):
"""
Télécharge une image de chèque et lance son traitement
"""
# Vérifier l'extension du fichier
file_ext = os.path.splitext(file.filename)[1].lower().lstrip(".")
if file_ext not in settings.ALLOWED_EXTENSIONS:
raise HTTPException(
status_code=400,
detail=f"Format de fichier non pris en charge. Formats acceptés: {', '.join(settings.ALLOWED_EXTENSIONS)}"
)
# Créer un identifiant unique pour la tâche
job_id = str(uuid.uuid4())
# Créer le chemin de fichier
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{timestamp}_{job_id}.{file_ext}"
file_path = os.path.join(settings.UPLOAD_FOLDER, 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)}"
)
# 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": file.filename,
"priority": str(priority).lower()
})
logger.info(f"Tâche créée: {job_id} - Fichier: {file.filename}")
return UploadResponse(
job_id=job_id,
status=JobStatus.PENDING,
message=f"Image 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}/status/{{job_id}}",
response_model=JobStatusResponse,
tags=["Extraction"]
)
async def get_job_status(
job_id: str,
api_key: str = Depends(verify_api_key),
redis_conn: redis.Redis = Depends(get_redis_connection)
):
"""
Vérifie l'état d'une tâche d'extraction
"""
# Vérifier si la tâche existe
if not redis_conn.exists(f"job:{job_id}"):
raise HTTPException(
status_code=404,
detail=f"Tâche non trouvée: {job_id}"
)
# Récupérer les métadonnées de la tâche
job_data = {k.decode(): v.decode() for k, v in redis_conn.hgetall(f"job:{job_id}").items()}
# Récupérer l'état de la tâche RQ
queue_name = settings.HIGH_PRIORITY_QUEUE_NAME if job_data.get("priority") == "true" else settings.QUEUE_NAME
queue = Queue(queue_name, connection=redis_conn)
status = job_data.get("status", JobStatus.PENDING.value)
message = job_data.get("message", "En attente de traitement")
# Vérifier si la tâche est dans différentes files d'attente
progress = None
queue_position = None
# Si la tâche est en attente, déterminer sa position dans la file
if status == JobStatus.PENDING.value:
job_ids = queue.get_job_ids()
if job_id in job_ids:
queue_position = job_ids.index(job_id) + 1
# Si la tâche est en cours, récupérer la progression
elif status == JobStatus.PROCESSING.value:
progress = job_data.get("progress")
if progress:
progress = int(progress)
return JobStatusResponse(
job_id=job_id,
status=JobStatus(status),
message=message,
created_at=datetime.fromisoformat(job_data.get("created_at")),
updated_at=datetime.fromisoformat(job_data.get("updated_at")) if "updated_at" in job_data else None,
progress=progress,
queue_position=queue_position
)
@app.get(
f"{settings.API_PREFIX}/result/{{job_id}}",
response_model=JobResult,
tags=["Extraction"]
)
async def get_job_result(
job_id: str,
api_key: str = Depends(verify_api_key),
redis_conn: redis.Redis = Depends(get_redis_connection)
):
"""
Récupère les résultats d'une tâche d'extraction terminée
"""
# Vérifier si la tâche existe
if not redis_conn.exists(f"job:{job_id}"):
raise HTTPException(
status_code=404,
detail=f"Tâche non trouvée: {job_id}"
)
# Récupérer les métadonnées de la tâche
job_data = {k.decode(): v.decode() for k, v in redis_conn.hgetall(f"job:{job_id}").items()}
# Vérifier si la tâche est terminée
status = JobStatus(job_data.get("status", JobStatus.PENDING.value))
if status != JobStatus.COMPLETED and status != JobStatus.FAILED:
raise HTTPException(
status_code=400,
detail=f"La tâche n'est pas encore terminée. Statut actuel: {status.value}"
)
# Récupérer les résultats d'extraction si disponibles
result = None
texte_brut = None
if status == JobStatus.COMPLETED:
# 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 = ExtractionResult(**result_dict)
texte_brut = job_data.get("texte_brut")
return JobResult(
job_id=job_id,
status=status,
created_at=datetime.fromisoformat(job_data.get("created_at")),
completed_at=datetime.fromisoformat(job_data.get("completed_at")) if "completed_at" in job_data else None,
image_path=job_data.get("file_path"),
result=result,
texte_brut=texte_brut,
methode=job_data.get("methode", "inconnu"),
erreur=job_data.get("erreur") if status == JobStatus.FAILED else None
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Gestionnaire d'exceptions global"""
logger.error(f"Exception non gérée: {str(exc)}")
return JSONResponse(
status_code=500,
content=ErrorResponse(
message="Erreur interne du serveur",
error_code="INTERNAL_SERVER_ERROR",
details={"error": str(exc)}
).dict()
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host=settings.HOST,
port=settings.PORT,
reload=settings.DEBUG
)

77
api/app/schemas.py Normal file
View File

@@ -0,0 +1,77 @@
"""
Schémas Pydantic pour la validation et la sérialisation des données
"""
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, Field
from enum import Enum
from datetime import datetime
class JobStatus(str, Enum):
"""Statuts possibles pour une tâche d'extraction"""
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
class UploadResponse(BaseModel):
"""Réponse à une demande d'upload d'image"""
job_id: str = Field(..., description="Identifiant unique de la tâche")
status: JobStatus = Field(default=JobStatus.PENDING, description="Statut de la tâche")
message: str = Field(default="Image en file d'attente pour traitement", description="Message d'information")
created_at: datetime = Field(default_factory=datetime.now, description="Date de création de la tâche")
class JobStatusResponse(BaseModel):
"""Réponse à une demande de statut de tâche"""
job_id: str = Field(..., description="Identifiant unique de la tâche")
status: JobStatus = Field(..., description="Statut de la tâche")
message: str = Field(..., description="Message d'information")
created_at: datetime = Field(..., description="Date de création de la tâche")
updated_at: Optional[datetime] = Field(None, description="Date de dernière mise à jour")
progress: Optional[int] = Field(None, description="Progression en pourcentage (0-100)")
queue_position: Optional[int] = Field(None, description="Position dans la file d'attente")
class ExtractionResult(BaseModel):
"""Résultat de l'extraction d'informations d'un chèque"""
montant: Optional[str] = Field(None, description="Montant 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")
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")
class JobResult(BaseModel):
"""Résultat complet d'une tâche d'extraction"""
job_id: str = Field(..., description="Identifiant unique de la tâche")
status: JobStatus = Field(..., description="Statut de la tâche")
created_at: datetime = Field(..., description="Date de création de la tâche")
completed_at: Optional[datetime] = Field(None, description="Date de complétion de la tâche")
image_path: str = Field(..., description="Chemin de l'image originale")
result: Optional[ExtractionResult] = Field(None, description="Résultats de l'extraction")
texte_brut: Optional[str] = Field(None, description="Texte brut extrait (si disponible)")
methode: str = Field(..., description="Méthode utilisée (ocr ou cv)")
erreur: Optional[str] = Field(None, description="Message d'erreur (si échec)")
class HealthCheck(BaseModel):
"""Réponse du health check"""
status: str = Field(default="ok", description="Statut du service")
version: str = Field(..., description="Version de l'API")
timestamp: datetime = Field(default_factory=datetime.now, description="Horodatage de la vérification")
redis_status: str = Field(..., description="Statut de la connexion Redis")
worker_count: int = Field(..., description="Nombre de workers disponibles")
queue_size: int = Field(..., description="Nombre de tâches en attente")
uptime: str = Field(..., description="Temps de fonctionnement du service")
class ErrorResponse(BaseModel):
"""Réponse en cas d'erreur"""
status: str = Field(default="error", description="Statut de la réponse")
message: str = Field(..., description="Message d'erreur")
error_code: Optional[str] = Field(None, description="Code d'erreur")
details: Optional[Dict[str, Any]] = Field(None, description="Détails supplémentaires")

228
api/app/tasks.py Normal file
View File

@@ -0,0 +1,228 @@
"""
Tâches de traitement pour l'API Cheque Scanner
"""
import os
import sys
import time
import json
import logging
import traceback
from datetime import datetime
import redis
from rq import get_current_job
# Ajouter le module d'extraction au path
sys.path.append('/app/shared')
# Importer les fonctions d'extraction
from extraction import (
extraire_infos_cheque,
get_tessdata_path
)
from .config import settings
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("cheque_scanner_tasks")
def update_job_status(job_id, status, message=None, progress=None, result=None, texte_brut=None, erreur=None, methode=None):
"""
Met à jour le statut d'une tâche dans Redis
"""
try:
# Connexion à Redis
redis_conn = redis.Redis.from_url(settings.REDIS_URL)
# Préparer les données à mettre à jour
update_data = {
"status": status,
"updated_at": datetime.now().isoformat()
}
if message:
update_data["message"] = message
if progress:
update_data["progress"] = str(progress)
if result:
update_data["result"] = str(result)
if texte_brut:
update_data["texte_brut"] = texte_brut
if erreur:
update_data["erreur"] = erreur
if methode:
update_data["methode"] = methode
if status == "completed":
update_data["completed_at"] = datetime.now().isoformat()
# Mettre à jour les données dans Redis
redis_conn.hset(f"job:{job_id}", mapping=update_data)
logger.info(f"Statut de la tâche {job_id} mis à jour: {status}")
return True
except Exception as e:
logger.error(f"Erreur lors de la mise à jour du statut de la tâche {job_id}: {str(e)}")
return False
def process_cheque_image(job_id, file_path):
"""
Traite une image de chèque pour en extraire les informations
Args:
job_id (str): Identifiant de la tâche
file_path (str): Chemin vers l'image à traiter
Returns:
dict: Résultat de l'extraction
"""
job = get_current_job()
logger.info(f"Début du traitement de l'image: {file_path} (Tâche: {job_id})")
# Mettre à jour le statut
update_job_status(
job_id=job_id,
status="processing",
message="Traitement en cours",
progress=10
)
try:
# Vérifier que le fichier existe
if not os.path.exists(file_path):
raise FileNotFoundError(f"L'image {file_path} n'existe pas")
# Récupérer le chemin vers tessdata
tessdata_path = settings.TESSERACT_DATA_PATH
if not os.path.exists(tessdata_path):
# Essayer de trouver automatiquement
tessdata_path = get_tessdata_path()
# Mise à jour intermédiaire
update_job_status(
job_id=job_id,
status="processing",
message="Extraction des informations en cours",
progress=30
)
# Première tentative avec la langue par défaut
try:
logger.info(f"Tentative d'extraction avec la langue: {settings.DEFAULT_OCR_LANGUAGE}")
update_job_status(
job_id=job_id,
status="processing",
message=f"Extraction avec langue {settings.DEFAULT_OCR_LANGUAGE}",
progress=50
)
infos, texte = extraire_infos_cheque(
chemin_image=file_path,
methode="ocr",
language=settings.DEFAULT_OCR_LANGUAGE,
tessdata=tessdata_path
)
methode = "ocr"
except Exception as e:
logger.warning(f"Échec de la première tentative: {str(e)}")
# Deuxième tentative avec la langue alternative
try:
logger.info(f"Tentative d'extraction avec la langue: {settings.ALTERNATIVE_OCR_LANGUAGE}")
update_job_status(
job_id=job_id,
status="processing",
message=f"Extraction avec langue {settings.ALTERNATIVE_OCR_LANGUAGE}",
progress=60
)
infos, texte = extraire_infos_cheque(
chemin_image=file_path,
methode="ocr",
language=settings.ALTERNATIVE_OCR_LANGUAGE,
tessdata=tessdata_path
)
methode = "ocr"
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)")
update_job_status(
job_id=job_id,
status="processing",
message="Extraction par détection de zones (sans OCR)",
progress=70
)
infos, texte = extraire_infos_cheque(
chemin_image=file_path,
methode="cv"
)
methode = "cv"
# Mise à jour finale
update_job_status(
job_id=job_id,
status="processing",
message="Finalisation des résultats",
progress=90
)
# Sauvegarder les résultats dans Redis
update_job_status(
job_id=job_id,
status="completed",
message="Extraction terminée avec succès",
progress=100,
result=infos,
texte_brut=texte,
methode=methode
)
logger.info(f"Traitement terminé pour la tâche {job_id}")
return {
"job_id": job_id,
"status": "completed",
"result": infos,
"methode": methode
}
except Exception as e:
# Capturer l'erreur
error_trace = traceback.format_exc()
logger.error(f"Erreur lors du traitement de l'image: {str(e)}\n{error_trace}")
# Mettre à jour le statut avec l'erreur
update_job_status(
job_id=job_id,
status="failed",
message="Échec du traitement",
erreur=str(e)
)
# Retourner l'erreur
return {
"job_id": job_id,
"status": "failed",
"error": str(e)
}

14
api/requirements.txt Normal file
View File

@@ -0,0 +1,14 @@
fastapi==0.100.0
uvicorn==0.23.2
python-multipart==0.0.6
rq==1.15.1
redis==4.6.0
pydantic==2.3.0
pydantic-settings==2.0.3
python-dotenv==1.0.0
httpx==0.24.1
PyMuPDF==1.22.5
opencv-python==4.8.0.74
numpy==1.24.3
pytesseract==0.3.10
Pillow==10.0.0