First commit
This commit is contained in:
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