mirc detection

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

View File

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