First commit
This commit is contained in:
27
.env
Normal file
27
.env
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Configuration de l'API Cheque Scanner
|
||||||
|
API_KEY=your-secret-api-key-change-me
|
||||||
|
DEBUG=false
|
||||||
|
|
||||||
|
# Configuration Redis
|
||||||
|
REDIS_URL=redis://redis:6379/0
|
||||||
|
|
||||||
|
# Configuration des workers
|
||||||
|
WORKER_REPLICAS=2
|
||||||
|
|
||||||
|
# Configuration OCR
|
||||||
|
DEFAULT_OCR_LANGUAGE=fra
|
||||||
|
ALTERNATIVE_OCR_LANGUAGE=eng
|
||||||
|
|
||||||
|
# Configuration des files d'attente
|
||||||
|
QUEUE_NAME=cheque_processing
|
||||||
|
HIGH_PRIORITY_QUEUE_NAME=cheque_processing_high
|
||||||
|
|
||||||
|
# Chemins de stockage
|
||||||
|
UPLOAD_FOLDER=/app/data/uploads
|
||||||
|
RESULT_FOLDER=/app/data/results
|
||||||
|
TEMP_FOLDER=/app/data/tmp
|
||||||
|
|
||||||
|
# Limites et timeouts
|
||||||
|
MAX_CONTENT_LENGTH=16777216 # 16MB
|
||||||
|
JOB_TIMEOUT=300 # 5 minutes
|
||||||
|
RESULT_TTL=86400 # 24 heures
|
||||||
34
api/Dockerfile
Normal file
34
api/Dockerfile
Normal 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
3
api/app/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Package pour l'API Cheque Scanner
|
||||||
|
"""
|
||||||
69
api/app/config.py
Normal file
69
api/app/config.py
Normal 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
45
api/app/dependencies.py
Normal 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
11
api/app/entrypoint.sh
Normal 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
321
api/app/main.py
Normal 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
77
api/app/schemas.py
Normal 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
228
api/app/tasks.py
Normal 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
14
api/requirements.txt
Normal 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
|
||||||
95
docker-compose.yml
Normal file
95
docker-compose.yml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build: ./api
|
||||||
|
container_name: cheque-scanner-api
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
volumes:
|
||||||
|
- shared_data:/app/data
|
||||||
|
- ./shared:/app/shared
|
||||||
|
environment:
|
||||||
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- UPLOAD_FOLDER=/app/data/uploads
|
||||||
|
- RESULT_FOLDER=/app/data/results
|
||||||
|
- TEMP_FOLDER=/app/data/tmp
|
||||||
|
- DEBUG=false
|
||||||
|
- API_KEY=${API_KEY:-your-secret-api-key}
|
||||||
|
- MAX_WORKERS=3
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- cheque-scanner-network
|
||||||
|
|
||||||
|
worker:
|
||||||
|
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/4.00/tessdata
|
||||||
|
- DEFAULT_OCR_LANGUAGE=eng
|
||||||
|
- ALTERNATIVE_OCR_LANGUAGE=fra
|
||||||
|
deploy:
|
||||||
|
replicas: ${WORKER_REPLICAS:-2}
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- cheque-scanner-network
|
||||||
|
|
||||||
|
# Service supplémentaire pour traiter les requêtes prioritaires
|
||||||
|
priority-worker:
|
||||||
|
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
|
||||||
|
- TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/4.00/tessdata
|
||||||
|
- DEFAULT_OCR_LANGUAGE=eng
|
||||||
|
- ALTERNATIVE_OCR_LANGUAGE=fra
|
||||||
|
- WORKER_NAME=priority-worker
|
||||||
|
entrypoint: ["/priority_entrypoint.sh"]
|
||||||
|
command: ["python", "worker.py", "--queues", "cheque_processing_high", "--name", "priority-worker"]
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- cheque-scanner-network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
container_name: cheque-scanner-redis
|
||||||
|
ports:
|
||||||
|
- "6380:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- cheque-scanner-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
shared_data:
|
||||||
|
name: cheque-scanner-shared-data
|
||||||
|
redis_data:
|
||||||
|
name: cheque-scanner-redis-data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
cheque-scanner-network:
|
||||||
|
name: cheque-scanner-network
|
||||||
212
examples/php-integration.php
Normal file
212
examples/php-integration.php
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Exemple d'intégration PHP avec l'API Cheque Scanner
|
||||||
|
*
|
||||||
|
* Ce script démontre comment :
|
||||||
|
* 1. Envoyer une image de chèque à l'API
|
||||||
|
* 2. Vérifier périodiquement l'état du traitement
|
||||||
|
* 3. Récupérer les résultats une fois le traitement terminé
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration de l'API
|
||||||
|
$api_url = 'http://localhost:8000/api/v1'; // URL de l'API
|
||||||
|
$api_key = 'your-secret-api-key-change-me'; // Clé API (doit correspondre à celle définie dans .env)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie une image de chèque à l'API et récupère l'ID de la tâche
|
||||||
|
*
|
||||||
|
* @param string $imagePath Chemin local vers l'image à envoyer
|
||||||
|
* @param bool $priority Définir à true pour un traitement prioritaire
|
||||||
|
* @return string L'identifiant de la tâche créée
|
||||||
|
* @throws Exception En cas d'erreur
|
||||||
|
*/
|
||||||
|
function uploadChequeImage($imagePath, $priority = false) {
|
||||||
|
global $api_url, $api_key;
|
||||||
|
|
||||||
|
// Vérifier que le fichier existe
|
||||||
|
if (!file_exists($imagePath)) {
|
||||||
|
throw new Exception("Le fichier n'existe pas: " . $imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Préparer la requête
|
||||||
|
$curl = curl_init();
|
||||||
|
$postFields = [
|
||||||
|
'file' => new CURLFile($imagePath),
|
||||||
|
'priority' => $priority ? 'true' : 'false'
|
||||||
|
];
|
||||||
|
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_URL => $api_url . '/upload',
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $postFields,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'X-API-Key: ' . $api_key
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Exécuter la requête
|
||||||
|
$response = curl_exec($curl);
|
||||||
|
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
// Vérifier la réponse
|
||||||
|
if ($status != 202) {
|
||||||
|
throw new Exception("Erreur lors de l'upload: " . $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Décoder la réponse JSON
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
|
||||||
|
if (!isset($result['job_id'])) {
|
||||||
|
throw new Exception("Réponse invalide de l'API: " . $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result['job_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'état de traitement d'une tâche
|
||||||
|
*
|
||||||
|
* @param string $jobId Identifiant de la tâche
|
||||||
|
* @return array Informations sur l'état de la tâche
|
||||||
|
* @throws Exception En cas d'erreur
|
||||||
|
*/
|
||||||
|
function checkJobStatus($jobId) {
|
||||||
|
global $api_url, $api_key;
|
||||||
|
|
||||||
|
$curl = curl_init();
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_URL => $api_url . '/status/' . $jobId,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'X-API-Key: ' . $api_key
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($curl);
|
||||||
|
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
if ($status != 200) {
|
||||||
|
throw new Exception("Erreur lors de la vérification du statut: " . $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode($response, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les résultats d'une tâche terminée
|
||||||
|
*
|
||||||
|
* @param string $jobId Identifiant de la tâche
|
||||||
|
* @return array Résultats de l'extraction
|
||||||
|
* @throws Exception En cas d'erreur
|
||||||
|
*/
|
||||||
|
function getJobResult($jobId) {
|
||||||
|
global $api_url, $api_key;
|
||||||
|
|
||||||
|
$curl = curl_init();
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_URL => $api_url . '/result/' . $jobId,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'X-API-Key: ' . $api_key
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($curl);
|
||||||
|
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
if ($status != 200) {
|
||||||
|
throw new Exception("Erreur lors de la récupération des résultats: " . $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode($response, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attend que le traitement d'une tâche soit terminé
|
||||||
|
*
|
||||||
|
* @param string $jobId Identifiant de la tâche
|
||||||
|
* @param int $maxWaitTime Temps d'attente maximum en secondes
|
||||||
|
* @param int $pollInterval Intervalle de vérification en secondes
|
||||||
|
* @return array État final de la tâche
|
||||||
|
* @throws Exception Si le temps d'attente est dépassé ou en cas d'erreur
|
||||||
|
*/
|
||||||
|
function waitForJobCompletion($jobId, $maxWaitTime = 300, $pollInterval = 2) {
|
||||||
|
$startTime = time();
|
||||||
|
$status = ['status' => 'pending'];
|
||||||
|
|
||||||
|
while (($status['status'] == 'pending' || $status['status'] == 'processing') &&
|
||||||
|
(time() - $startTime < $maxWaitTime)) {
|
||||||
|
|
||||||
|
// Attendre avant de vérifier à nouveau
|
||||||
|
sleep($pollInterval);
|
||||||
|
|
||||||
|
// Vérifier l'état
|
||||||
|
$status = checkJobStatus($jobId);
|
||||||
|
|
||||||
|
// Afficher la progression
|
||||||
|
if (isset($status['progress'])) {
|
||||||
|
echo "Progression: " . $status['progress'] . "% - " . $status['message'] . "\n";
|
||||||
|
} else {
|
||||||
|
echo "État: " . $status['status'] . " - " . $status['message'] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status['status'] != 'completed' && $status['status'] != 'failed') {
|
||||||
|
throw new Exception("Délai d'attente dépassé pour la tâche " . $jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exemple d'utilisation
|
||||||
|
try {
|
||||||
|
// Chemin de l'image à traiter
|
||||||
|
$imagePath = '/home/alex/Téléchargements/pymupdf/test_qheque-PXL_2025.jpg';
|
||||||
|
|
||||||
|
echo "=== EXTRACTION D'INFORMATIONS DE CHÈQUE ===\n";
|
||||||
|
|
||||||
|
// 1. Envoyer l'image
|
||||||
|
echo "Envoi de l'image: " . $imagePath . "\n";
|
||||||
|
$jobId = uploadChequeImage($imagePath, true); // true pour un traitement prioritaire
|
||||||
|
echo "Image envoyée, ID de la tâche: " . $jobId . "\n\n";
|
||||||
|
|
||||||
|
// 2. Attendre la fin du traitement
|
||||||
|
echo "Attente du traitement...\n";
|
||||||
|
$finalStatus = waitForJobCompletion($jobId);
|
||||||
|
|
||||||
|
// 3. Si le traitement est terminé avec succès, récupérer les résultats
|
||||||
|
if ($finalStatus['status'] == 'completed') {
|
||||||
|
echo "\nTraitement terminé avec succès!\n";
|
||||||
|
|
||||||
|
// Récupérer les résultats détaillés
|
||||||
|
$results = getJobResult($jobId);
|
||||||
|
|
||||||
|
echo "\n=== RÉSULTATS DE L'EXTRACTION ===\n";
|
||||||
|
if (isset($results['result'])) {
|
||||||
|
echo "Montant: " . ($results['result']['montant'] ?? 'Non détecté') . "\n";
|
||||||
|
echo "Date: " . ($results['result']['date'] ?? 'Non détectée') . "\n";
|
||||||
|
echo "Bénéficiaire: " . ($results['result']['beneficiaire'] ?? 'Non détecté') . "\n";
|
||||||
|
echo "Numéro de chèque: " . ($results['result']['numero_cheque'] ?? 'Non détecté') . "\n";
|
||||||
|
echo "Qualité de l'extraction: " . ($results['result']['qualite_extraction'] ?? 'Inconnue') . "\n";
|
||||||
|
|
||||||
|
if (isset($results['result']['image_zones'])) {
|
||||||
|
echo "Image avec zones identifiées: " . $results['result']['image_zones'] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nMéthode utilisée: " . $results['methode'] . "\n";
|
||||||
|
|
||||||
|
if (isset($results['texte_brut']) && !empty($results['texte_brut'])) {
|
||||||
|
echo "\n=== TEXTE BRUT EXTRAIT ===\n";
|
||||||
|
echo $results['texte_brut'] . "\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "\nÉchec du traitement: " . ($results['erreur'] ?? 'Erreur inconnue') . "\n";
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "ERREUR: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
25
shared/extraction/__init__.py
Normal file
25
shared/extraction/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Package pour l'extraction d'informations à partir d'images de chèques.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .scanner import (
|
||||||
|
extraire_infos_cheque,
|
||||||
|
extraire_par_ocr,
|
||||||
|
extraire_par_cv,
|
||||||
|
extraire_montant,
|
||||||
|
extraire_date,
|
||||||
|
extraire_beneficiaire,
|
||||||
|
extraire_numero_cheque,
|
||||||
|
get_tessdata_path
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'extraire_infos_cheque',
|
||||||
|
'extraire_par_ocr',
|
||||||
|
'extraire_par_cv',
|
||||||
|
'extraire_montant',
|
||||||
|
'extraire_date',
|
||||||
|
'extraire_beneficiaire',
|
||||||
|
'extraire_numero_cheque',
|
||||||
|
'get_tessdata_path'
|
||||||
|
]
|
||||||
294
shared/extraction/scanner.py
Normal file
294
shared/extraction/scanner.py
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
"""
|
||||||
|
Module pour l'extraction d'informations de chèques à partir d'images.
|
||||||
|
Ce module fournit les fonctionnalités de base pour extraire des informations
|
||||||
|
telles que le montant, la date, le bénéficiaire et le numéro de chèque.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import fitz # PyMuPDF
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import tempfile
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Tuple, Optional, Any, Union
|
||||||
|
|
||||||
|
# Configuration du logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("cheque_scanner")
|
||||||
|
|
||||||
|
|
||||||
|
def extraire_infos_cheque(
|
||||||
|
chemin_image: str,
|
||||||
|
methode: str = "ocr",
|
||||||
|
language: str = "eng",
|
||||||
|
tessdata: Optional[str] = None
|
||||||
|
) -> Tuple[Dict[str, Any], str]:
|
||||||
|
"""
|
||||||
|
Extrait les informations d'un chèque à partir d'une image
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chemin_image: Chemin vers l'image du chèque
|
||||||
|
methode: "ocr" (utilise PyMuPDF+Tesseract) ou "cv" (utilise OpenCV sans OCR)
|
||||||
|
language: Code de langue pour OCR (eng par défaut, utiliser fra pour français si disponible)
|
||||||
|
tessdata: Chemin vers le dossier tessdata (optionnel)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (infos, texte) où infos est un dictionnaire et texte le texte brut extrait
|
||||||
|
"""
|
||||||
|
if not os.path.exists(chemin_image):
|
||||||
|
raise FileNotFoundError(f"L'image {chemin_image} n'existe pas")
|
||||||
|
|
||||||
|
logger.info(f"Traitement de l'image: {chemin_image}")
|
||||||
|
|
||||||
|
if methode == "ocr":
|
||||||
|
try:
|
||||||
|
logger.info("Utilisation de la méthode OCR")
|
||||||
|
return extraire_par_ocr(chemin_image, language, tessdata)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur avec OCR: {e}")
|
||||||
|
logger.info("Tentative d'extraction par traitement d'image...")
|
||||||
|
return extraire_par_cv(chemin_image)
|
||||||
|
else:
|
||||||
|
logger.info("Utilisation de la méthode de traitement d'image sans OCR")
|
||||||
|
return extraire_par_cv(chemin_image)
|
||||||
|
|
||||||
|
|
||||||
|
def extraire_par_ocr(
|
||||||
|
chemin_image: str,
|
||||||
|
language: str = "eng",
|
||||||
|
tessdata: Optional[str] = None
|
||||||
|
) -> Tuple[Dict[str, Any], str]:
|
||||||
|
"""Extraction par OCR avec PyMuPDF et Tesseract"""
|
||||||
|
|
||||||
|
# 1. Charger l'image avec PyMuPDF
|
||||||
|
pixmap = fitz.Pixmap(chemin_image)
|
||||||
|
|
||||||
|
# 2. S'assurer que l'image est au format approprié (RGB sans transparence)
|
||||||
|
if pixmap.alpha:
|
||||||
|
pixmap = fitz.Pixmap(fitz.csRGB, pixmap) # Convertir si nécessaire
|
||||||
|
|
||||||
|
# 3. Améliorer la qualité de l'image pour l'OCR
|
||||||
|
# Convertir en format OpenCV pour le prétraitement
|
||||||
|
img = np.frombuffer(pixmap.samples, dtype=np.uint8).reshape(pixmap.h, pixmap.w, 3)
|
||||||
|
|
||||||
|
# Prétraitement pour améliorer la détection de texte
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
||||||
|
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
|
||||||
|
_, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||||
|
|
||||||
|
# Sauvegarder l'image prétraitée temporairement
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file:
|
||||||
|
tmp_path = tmp_file.name
|
||||||
|
cv2.imwrite(tmp_path, thresh)
|
||||||
|
|
||||||
|
# Charger l'image prétraitée
|
||||||
|
pixmap_processed = fitz.Pixmap(tmp_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 4. Effectuer l'OCR et générer un PDF avec une couche de texte
|
||||||
|
if tessdata:
|
||||||
|
# Si un chemin vers tessdata est fourni
|
||||||
|
logger.debug(f"Utilisation du dossier tessdata personnalisé: {tessdata}")
|
||||||
|
pdf_ocr = pixmap_processed.pdfocr_tobytes(language=language, tessdata=tessdata)
|
||||||
|
else:
|
||||||
|
# Essai avec la configuration par défaut
|
||||||
|
logger.debug(f"Utilisation du dossier tessdata par défaut")
|
||||||
|
pdf_ocr = pixmap_processed.pdfocr_tobytes(language=language)
|
||||||
|
|
||||||
|
# 5. Charger le PDF OCR et extraire le texte
|
||||||
|
doc = fitz.open("pdf", pdf_ocr)
|
||||||
|
page = doc[0]
|
||||||
|
texte = page.get_text()
|
||||||
|
|
||||||
|
# 6. Analyser le texte pour trouver les informations pertinentes
|
||||||
|
infos = {
|
||||||
|
"montant": extraire_montant(texte),
|
||||||
|
"date": extraire_date(texte),
|
||||||
|
"beneficiaire": extraire_beneficiaire(texte),
|
||||||
|
"numero_cheque": extraire_numero_cheque(texte)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ajouter des métadonnées sur la qualité de l'extraction
|
||||||
|
infos["qualite_extraction"] = evaluer_qualite_extraction(infos)
|
||||||
|
|
||||||
|
doc.close()
|
||||||
|
|
||||||
|
# Nettoyage du fichier temporaire
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
|
return infos, texte
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Nettoyage en cas d'erreur
|
||||||
|
if os.path.exists(tmp_path):
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
logger.error(f"Erreur pendant l'OCR: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def extraire_par_cv(chemin_image: str) -> Tuple[Dict[str, Any], str]:
|
||||||
|
"""Extraction par traitement d'image avec OpenCV sans OCR"""
|
||||||
|
|
||||||
|
# Charger l'image
|
||||||
|
img = cv2.imread(chemin_image)
|
||||||
|
|
||||||
|
if img is None:
|
||||||
|
raise ValueError(f"Impossible de charger l'image: {chemin_image}")
|
||||||
|
|
||||||
|
# Prétraitement
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||||
|
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
|
||||||
|
thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
cv2.THRESH_BINARY_INV, 11, 2)
|
||||||
|
|
||||||
|
# Trouver les contours (rectangles potentiels des zones importantes)
|
||||||
|
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
# Trier les contours par taille (du plus grand au plus petit)
|
||||||
|
contours = sorted(contours, key=cv2.contourArea, reverse=True)
|
||||||
|
|
||||||
|
# Estimation des zones d'intérêt basée sur la position relative
|
||||||
|
# (ceci est approximatif et dépend du format exact du chèque)
|
||||||
|
height, width = img.shape[:2]
|
||||||
|
|
||||||
|
# Zone approximative pour le montant (généralement en bas à droite)
|
||||||
|
zone_montant = (int(width*0.6), int(height*0.35), int(width*0.95), int(height*0.55))
|
||||||
|
|
||||||
|
# Zone approximative pour la date (généralement en haut à droite)
|
||||||
|
zone_date = (int(width*0.7), int(height*0.1), int(width*0.95), int(height*0.25))
|
||||||
|
|
||||||
|
# Zone approximative pour le bénéficiaire (généralement au milieu)
|
||||||
|
zone_beneficiaire = (int(width*0.2), int(height*0.25), int(width*0.8), int(height*0.4))
|
||||||
|
|
||||||
|
# Pour la démonstration, créer une image avec les zones identifiées
|
||||||
|
img_zones = img.copy()
|
||||||
|
cv2.rectangle(img_zones, (zone_montant[0], zone_montant[1]),
|
||||||
|
(zone_montant[2], zone_montant[3]), (0, 255, 0), 2)
|
||||||
|
cv2.rectangle(img_zones, (zone_date[0], zone_date[1]),
|
||||||
|
(zone_date[2], zone_date[3]), (255, 0, 0), 2)
|
||||||
|
cv2.rectangle(img_zones, (zone_beneficiaire[0], zone_beneficiaire[1]),
|
||||||
|
(zone_beneficiaire[2], zone_beneficiaire[3]), (0, 0, 255), 2)
|
||||||
|
|
||||||
|
# Générer un nom de fichier unique pour l'image avec les zones identifiées
|
||||||
|
image_result_path = f"zones_identifiees_{os.path.basename(chemin_image)}"
|
||||||
|
cv2.imwrite(image_result_path, img_zones)
|
||||||
|
logger.info(f"Image avec zones identifiées enregistrée sous: {image_result_path}")
|
||||||
|
|
||||||
|
# Sans OCR, nous ne pouvons pas extraire le texte directement,
|
||||||
|
# mais nous pouvons indiquer les zones où se trouvent les informations
|
||||||
|
infos = {
|
||||||
|
"montant": f"Zone identifiée, voir {image_result_path} (vert)",
|
||||||
|
"date": f"Zone identifiée, voir {image_result_path} (bleu)",
|
||||||
|
"beneficiaire": f"Zone identifiée, voir {image_result_path} (rouge)",
|
||||||
|
"numero_cheque": "Non détecté sans OCR",
|
||||||
|
"image_zones": image_result_path
|
||||||
|
}
|
||||||
|
|
||||||
|
return infos, f"Texte non disponible sans OCR - visualisation des zones générée dans {image_result_path}"
|
||||||
|
|
||||||
|
|
||||||
|
# Fonctions d'extraction pour des champs spécifiques
|
||||||
|
def extraire_montant(texte: str) -> Optional[str]:
|
||||||
|
"""Extrait le montant à partir du texte OCR"""
|
||||||
|
|
||||||
|
# Recherche de motifs comme "€ 123,45" ou "123,45 €" ou "123 €"
|
||||||
|
motifs = [
|
||||||
|
r'(\d+[.,]\d{2})\s*€', # 123,45 €
|
||||||
|
r'€\s*(\d+[.,]\d{2})', # € 123,45
|
||||||
|
r'(\d+)\s*€' # 123 €
|
||||||
|
]
|
||||||
|
|
||||||
|
for motif in motifs:
|
||||||
|
matches = re.findall(motif, texte)
|
||||||
|
if matches:
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
logger.warning("Aucun montant détecté dans le texte")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extraire_date(texte: str) -> Optional[str]:
|
||||||
|
"""Extrait la date à partir du texte OCR"""
|
||||||
|
|
||||||
|
# Recherche de dates au format JJ/MM/AAAA ou JJ-MM-AAAA
|
||||||
|
match = re.search(r'(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})', texte)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
logger.warning("Aucune date détectée dans le texte")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extraire_beneficiaire(texte: str) -> Optional[str]:
|
||||||
|
"""Extrait le bénéficiaire à partir du texte OCR"""
|
||||||
|
|
||||||
|
# Cette fonction est plus complexe car elle dépend du format du chèque
|
||||||
|
# Recherche souvent après "Payez contre ce chèque à" ou similaire
|
||||||
|
match = re.search(r'(?:Payez|Payé)(?:\s+contre\s+ce\s+chèque)?\s+(?:à|au?)\s+([A-Z\s]+)',
|
||||||
|
texte, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
|
||||||
|
logger.warning("Aucun bénéficiaire détecté dans le texte")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extraire_numero_cheque(texte: str) -> Optional[str]:
|
||||||
|
"""Extrait le numéro de chèque à partir du texte OCR"""
|
||||||
|
|
||||||
|
# Recherche d'un numéro de chèque (généralement 7 chiffres)
|
||||||
|
match = re.search(r'N°\s*(\d{7})', texte)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
# Autre format possible
|
||||||
|
match = re.search(r'(?:chèque|cheque)\s*(?:n[o°]?)?\s*(\d{7})', texte, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
logger.warning("Aucun numéro de chèque détecté dans le texte")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def evaluer_qualite_extraction(infos: Dict[str, Any]) -> str:
|
||||||
|
"""Évalue la qualité de l'extraction basée sur les champs extraits"""
|
||||||
|
|
||||||
|
# Compter le nombre de champs extraits avec succès
|
||||||
|
champs_extraits = sum(1 for v in infos.values() if v is not None)
|
||||||
|
total_champs = 4 # montant, date, beneficiaire, numero_cheque
|
||||||
|
|
||||||
|
if champs_extraits == 0:
|
||||||
|
return "échec"
|
||||||
|
elif champs_extraits < 2:
|
||||||
|
return "faible"
|
||||||
|
elif champs_extraits < 4:
|
||||||
|
return "moyenne"
|
||||||
|
else:
|
||||||
|
return "bonne"
|
||||||
|
|
||||||
|
|
||||||
|
def get_tessdata_path() -> Optional[str]:
|
||||||
|
"""Trouve le chemin vers le dossier tessdata"""
|
||||||
|
|
||||||
|
# Suggestions de chemins pour tessdata
|
||||||
|
suggestions_tessdata = [
|
||||||
|
"/usr/share/tesseract-ocr/4.00/tessdata",
|
||||||
|
"/usr/share/tesseract-ocr/tessdata",
|
||||||
|
"/usr/local/share/tesseract-ocr/tessdata",
|
||||||
|
"/usr/local/share/tessdata",
|
||||||
|
os.path.expanduser("~/tessdata")
|
||||||
|
]
|
||||||
|
|
||||||
|
# Chercher si un des chemins existe
|
||||||
|
for path in suggestions_tessdata:
|
||||||
|
if os.path.exists(path):
|
||||||
|
logger.info(f"Dossier tessdata trouvé à: {path}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
logger.warning("Aucun dossier tessdata trouvé dans les emplacements standards")
|
||||||
|
return None
|
||||||
33
worker/Dockerfile
Normal file
33
worker/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installation des dépendances système et Tesseract OCR
|
||||||
|
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 .
|
||||||
|
COPY ./app/entrypoint.sh /entrypoint.sh
|
||||||
|
COPY ./app/priority_entrypoint.sh /priority_entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh /priority_entrypoint.sh
|
||||||
|
|
||||||
|
# Variables d'environnement
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV TESSERACT_DATA_PATH=/usr/share/tesseract-ocr/4.00/tessdata
|
||||||
|
|
||||||
|
# Commande d'exécution
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["python", "worker.py", "--queues", "cheque_processing"]
|
||||||
14
worker/app/__init__.py
Normal file
14
worker/app/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Module d'initialisation du worker
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configuration du logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("cheque_scanner_worker_module")
|
||||||
|
|
||||||
|
logger.info("Module worker initialisé")
|
||||||
14
worker/app/app/__init__.py
Normal file
14
worker/app/app/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Module proxy pour l'API
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configuration du logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("app.proxy")
|
||||||
|
|
||||||
|
logger.info("Module proxy app initialisé")
|
||||||
37
worker/app/app/tasks/__init__.py
Normal file
37
worker/app/app/tasks/__init__.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""
|
||||||
|
Module proxy pour importer process_cheque_image depuis le module tasks parent
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configuration du logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("app.tasks.proxy")
|
||||||
|
|
||||||
|
# Importer la fonction process_cheque_image depuis le module tasks parent
|
||||||
|
logger.info("Initialisation du module proxy app.tasks")
|
||||||
|
|
||||||
|
# Ajouter le répertoire parent au path pour pouvoir importer tasks
|
||||||
|
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Importer depuis le module tasks parent
|
||||||
|
from tasks import process_cheque_image
|
||||||
|
logger.info("Fonction process_cheque_image importée avec succès")
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(f"Erreur lors de l'importation de process_cheque_image: {str(e)}")
|
||||||
|
# Créer une fonction de secours qui enregistre l'erreur
|
||||||
|
def process_cheque_image(job_id, file_path):
|
||||||
|
logger.error(f"Appel à la fonction de secours process_cheque_image: {job_id}, {file_path}")
|
||||||
|
raise RuntimeError(f"Impossible d'importer la véritable fonction process_cheque_image: {str(e)}")
|
||||||
|
|
||||||
|
# Exporter la fonction pour qu'elle soit accessible via app.tasks.process_cheque_image
|
||||||
|
__all__ = ['process_cheque_image']
|
||||||
|
|
||||||
|
logger.info("Module proxy app.tasks initialisé")
|
||||||
16
worker/app/entrypoint.sh
Normal file
16
worker/app/entrypoint.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Suppression de la vérification Redis qui nécessite redis-cli
|
||||||
|
echo "Démarrage du worker..."
|
||||||
|
|
||||||
|
# Créer les dossiers nécessaires s'ils n'existent pas
|
||||||
|
mkdir -p /app/data/uploads
|
||||||
|
mkdir -p /app/data/results
|
||||||
|
mkdir -p /app/data/tmp
|
||||||
|
|
||||||
|
# Définir les permissions
|
||||||
|
chmod -R 755 /app/data
|
||||||
|
|
||||||
|
# Exécuter la commande spécifiée
|
||||||
|
exec "$@"
|
||||||
16
worker/app/priority_entrypoint.sh
Executable file
16
worker/app/priority_entrypoint.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Suppression de la vérification Redis qui nécessite redis-cli
|
||||||
|
echo "Démarrage du worker prioritaire..."
|
||||||
|
|
||||||
|
# Créer les dossiers nécessaires s'ils n'existent pas
|
||||||
|
mkdir -p /app/data/uploads
|
||||||
|
mkdir -p /app/data/results
|
||||||
|
mkdir -p /app/data/tmp
|
||||||
|
|
||||||
|
# Définir les permissions
|
||||||
|
chmod -R 755 /app/data
|
||||||
|
|
||||||
|
# Exécuter la commande spécifiée
|
||||||
|
exec "$@"
|
||||||
232
worker/app/tasks.py
Normal file
232
worker/app/tasks.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
"""
|
||||||
|
Tâches de traitement pour le worker Cheque Scanner
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
import redis
|
||||||
|
from datetime import datetime
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configuration du logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("cheque_scanner_tasks")
|
||||||
|
|
||||||
|
# Variables d'environnement
|
||||||
|
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
|
||||||
|
DEFAULT_OCR_LANGUAGE = os.getenv("DEFAULT_OCR_LANGUAGE", "eng")
|
||||||
|
ALTERNATIVE_OCR_LANGUAGE = os.getenv("ALTERNATIVE_OCR_LANGUAGE", "fra")
|
||||||
|
TESSERACT_DATA_PATH = os.getenv("TESSERACT_DATA_PATH", "/usr/share/tesseract-ocr/4.00/tessdata")
|
||||||
|
|
||||||
|
|
||||||
|
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(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(file_path, job_id):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
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 = 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: {DEFAULT_OCR_LANGUAGE}")
|
||||||
|
update_job_status(
|
||||||
|
job_id=job_id,
|
||||||
|
status="processing",
|
||||||
|
message=f"Extraction avec langue {DEFAULT_OCR_LANGUAGE}",
|
||||||
|
progress=50
|
||||||
|
)
|
||||||
|
|
||||||
|
infos, texte = extraire_infos_cheque(
|
||||||
|
chemin_image=file_path,
|
||||||
|
methode="ocr",
|
||||||
|
language=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: {ALTERNATIVE_OCR_LANGUAGE}")
|
||||||
|
update_job_status(
|
||||||
|
job_id=job_id,
|
||||||
|
status="processing",
|
||||||
|
message=f"Extraction avec langue {ALTERNATIVE_OCR_LANGUAGE}",
|
||||||
|
progress=60
|
||||||
|
)
|
||||||
|
|
||||||
|
infos, texte = extraire_infos_cheque(
|
||||||
|
chemin_image=file_path,
|
||||||
|
methode="ocr",
|
||||||
|
language=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)
|
||||||
|
}
|
||||||
142
worker/app/worker.py
Normal file
142
worker/app/worker.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
"""
|
||||||
|
Worker principal pour le traitement des images de chèques
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import redis
|
||||||
|
import logging
|
||||||
|
import signal
|
||||||
|
import argparse
|
||||||
|
import importlib
|
||||||
|
from rq import Connection, Worker, Queue
|
||||||
|
from rq.job import Job
|
||||||
|
from rq.worker import SimpleWorker
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Configuration du logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("cheque_scanner_worker")
|
||||||
|
|
||||||
|
# Ajouter les modules au path
|
||||||
|
sys.path.append('/app/shared')
|
||||||
|
sys.path.append('/app/api_app')
|
||||||
|
sys.path.append('/app')
|
||||||
|
|
||||||
|
# Importer notre module tasks local
|
||||||
|
try:
|
||||||
|
from tasks import process_cheque_image
|
||||||
|
logger.info("Fonction process_cheque_image importée avec succès")
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(f"Erreur lors de l'importation directe de process_cheque_image: {str(e)}")
|
||||||
|
|
||||||
|
# Charger les variables d'environnement
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Classe Worker personnalisée qui remplace l'importation de fonction
|
||||||
|
class CustomWorker(SimpleWorker):
|
||||||
|
def perform_job(self, job, queue):
|
||||||
|
"""
|
||||||
|
Remplace l'importation de app.tasks.process_cheque_image par notre fonction locale
|
||||||
|
"""
|
||||||
|
# Si la tâche est app.tasks.process_cheque_image, remplacer par notre fonction locale
|
||||||
|
if job.func_name == 'app.tasks.process_cheque_image' or job.func_name == 'process_cheque_image' or job.func_name == 'tasks.process_cheque_image':
|
||||||
|
job.func_name = 'tasks.process_cheque_image'
|
||||||
|
job._func = process_cheque_image
|
||||||
|
logger.info(f"Fonction remplacée pour la tâche {job.id}")
|
||||||
|
|
||||||
|
return super().perform_job(job, queue)
|
||||||
|
|
||||||
|
# Variables d'environnement
|
||||||
|
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
|
||||||
|
QUEUE_NAME = os.getenv("QUEUE_NAME", "cheque_processing")
|
||||||
|
HIGH_PRIORITY_QUEUE_NAME = os.getenv("HIGH_PRIORITY_QUEUE_NAME", "cheque_processing_high")
|
||||||
|
WORKER_NAME = os.getenv("WORKER_NAME", f"worker-{os.getpid()}")
|
||||||
|
|
||||||
|
# Créer les dossiers nécessaires
|
||||||
|
UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER", "/app/data/uploads")
|
||||||
|
RESULT_FOLDER = os.getenv("RESULT_FOLDER", "/app/data/results")
|
||||||
|
TEMP_FOLDER = os.getenv("TEMP_FOLDER", "/app/data/tmp")
|
||||||
|
|
||||||
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||||
|
os.makedirs(RESULT_FOLDER, exist_ok=True)
|
||||||
|
os.makedirs(TEMP_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
"""
|
||||||
|
Gestionnaire de signal pour arrêter proprement le worker
|
||||||
|
"""
|
||||||
|
logger.info(f"Signal {signum} reçu, arrêt du worker...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def start_worker(queues, worker_name=None):
|
||||||
|
"""
|
||||||
|
Démarre un worker pour traiter les tâches des files d'attente spécifiées
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queues (list): Liste des noms de files d'attente à traiter
|
||||||
|
worker_name (str, optional): Nom du worker. Si None, un nom sera généré
|
||||||
|
"""
|
||||||
|
if not worker_name:
|
||||||
|
worker_name = WORKER_NAME
|
||||||
|
|
||||||
|
logger.info(f"Démarrage du worker '{worker_name}' sur les files: {', '.join(queues)}")
|
||||||
|
|
||||||
|
# Connexion à Redis
|
||||||
|
redis_conn = redis.Redis.from_url(REDIS_URL)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Vérifier la connexion à Redis
|
||||||
|
redis_conn.ping()
|
||||||
|
logger.info("Connexion à Redis établie")
|
||||||
|
except redis.exceptions.ConnectionError:
|
||||||
|
logger.error(f"Impossible de se connecter à Redis: {REDIS_URL}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Configurer le gestionnaire de signal
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
|
# Vérifier la connexion à Redis
|
||||||
|
logger.info("Vérification de la connexion à Redis")
|
||||||
|
|
||||||
|
# Démarrer le worker avec notre classe personnalisée
|
||||||
|
with Connection(redis_conn):
|
||||||
|
# Créer les queues
|
||||||
|
worker_queues = [Queue(name, connection=redis_conn) for name in queues]
|
||||||
|
|
||||||
|
# Créer le worker personnalisé
|
||||||
|
worker = CustomWorker(
|
||||||
|
worker_queues,
|
||||||
|
name=worker_name
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Worker '{worker_name}' prêt à traiter les tâches")
|
||||||
|
worker.work(with_scheduler=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Worker pour le traitement des images de chèques")
|
||||||
|
parser.add_argument(
|
||||||
|
"--queues",
|
||||||
|
nargs="+",
|
||||||
|
default=[HIGH_PRIORITY_QUEUE_NAME, QUEUE_NAME],
|
||||||
|
help="Liste des files d'attente à traiter (par ordre de priorité)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--name",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Nom du worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Démarrer le worker
|
||||||
|
start_worker(args.queues, args.name)
|
||||||
8
worker/requirements.txt
Normal file
8
worker/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
rq==1.15.1
|
||||||
|
redis==4.6.0
|
||||||
|
PyMuPDF==1.22.5
|
||||||
|
opencv-python==4.8.0.74
|
||||||
|
numpy==1.24.3
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
pytesseract==0.3.10
|
||||||
|
Pillow==10.0.0
|
||||||
Reference in New Issue
Block a user