Spaces:
Running
Running
from fastapi import FastAPI, UploadFile, File, HTTPException | |
from fastapi.responses import HTMLResponse, Response | |
from fastapi.staticfiles import StaticFiles | |
from pathlib import Path | |
import os | |
from backend.utils import extract_text_from_file, translate_text | |
import io | |
from docx import Document | |
import asyncio | |
from typing import Optional | |
import logging | |
from datetime import datetime | |
app = FastAPI( | |
title="DocTranslator Pro", | |
description="Service avancé de traduction de documents avec gestion améliorée des erreurs", | |
version="2.0.0" | |
) | |
# Configuration améliorée | |
MAX_FILE_SIZE = 15 * 1024 * 1024 # 15 Mo (augmenté de 10 Mo) | |
MAX_TEXT_LENGTH = 75000 # 75k caractères (augmenté de 50k) | |
PROCESSING_TIMEOUT = 600 # 10 minutes (augmenté de 5 minutes) | |
SUPPORTED_FORMATS = ('.pdf', '.docx', '.xlsx', '.pptx') # Ajout de PPTX | |
LOG_FILE = "translation_logs.log" | |
# Configuration des fichiers statiques | |
app.mount("/static", StaticFiles(directory="frontend"), name="static") | |
# Configuration du logging avancé | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | |
handlers=[ | |
logging.FileHandler(LOG_FILE), | |
logging.StreamHandler() | |
] | |
) | |
logger = logging.getLogger(__name__) | |
class TranslationLogger: | |
def log_transaction(filename: str, char_count: int, status: str): | |
with open(LOG_FILE, "a") as f: | |
f.write(f"{datetime.now().isoformat()}|{filename}|{char_count}|{status}\n") | |
async def startup_event(): | |
"""Vérification améliorée du modèle au démarrage""" | |
from backend.utils import translator | |
if translator is None: | |
logger.error("❌ ERREUR CRITIQUE: Le modèle de traduction n'a pas pu être chargé!") | |
raise RuntimeError("Échec du chargement du modèle") | |
else: | |
logger.info("✅ Modèle de traduction initialisé avec succès") | |
logger.info(f"⚙️ Configuration - Taille max fichier: {MAX_FILE_SIZE/1024/1024}MB") | |
logger.info(f"⚙️ Configuration - Texte max: {MAX_TEXT_LENGTH} caractères") | |
async def serve_frontend(): | |
"""Endpoint amélioré pour servir l'interface frontend""" | |
try: | |
with open(os.path.join("frontend", "index.html"), "r", encoding="utf-8") as f: | |
return HTMLResponse(content=f.read(), status_code=200) | |
except Exception as e: | |
logger.error(f"Erreur de chargement du frontend: {str(e)}") | |
raise HTTPException( | |
status_code=500, | |
detail="Erreur de chargement de l'interface. Contactez l'administrateur." | |
) | |
async def process_file(file: UploadFile, target_lang: str) -> dict: | |
""" | |
Fonction de traitement améliorée avec gestion d'erreur granulaire | |
""" | |
# Vérification de la taille | |
if file.size > MAX_FILE_SIZE: | |
error_msg = f"Fichier trop volumineux (> {MAX_FILE_SIZE/1024/1024:.1f} Mo)" | |
logger.warning(error_msg) | |
raise HTTPException(status_code=413, detail=error_msg) | |
# Vérification du type de fichier | |
if not file.filename.lower().endswith(SUPPORTED_FORMATS): | |
error_msg = f"Format non supporté. Formats acceptés: {', '.join(SUPPORTED_FORMATS)}" | |
logger.warning(error_msg) | |
raise HTTPException(status_code=415, detail=error_msg) | |
# Lecture du contenu avec gestion d'erreur | |
try: | |
contents = await file.read() | |
except Exception as e: | |
error_msg = f"Erreur de lecture du fichier: {str(e)}" | |
logger.error(error_msg) | |
raise HTTPException(status_code=422, detail=error_msg) | |
# Extraction du texte | |
extracted_text = extract_text_from_file(contents, file.filename) | |
if extracted_text.startswith("Erreur:"): | |
error_msg = extracted_text.replace("Erreur:", "").strip() | |
logger.error(f"Erreur extraction: {error_msg}") | |
raise HTTPException(status_code=422, detail=error_msg) | |
if len(extracted_text) > MAX_TEXT_LENGTH: | |
error_msg = f"Texte trop long ({len(extracted_text)} > {MAX_TEXT_LENGTH} caractères)" | |
logger.warning(error_msg) | |
raise HTTPException(status_code=413, detail=error_msg) | |
# Traduction avec suivi | |
logger.info(f"Début traduction - {len(extracted_text)} caractères") | |
translated_text = translate_text(extracted_text, target_lang) | |
if translated_text.startswith("Erreur:"): | |
error_detail = translated_text.replace("Erreur:", "").strip() | |
TranslationLogger.log_transaction(file.filename, len(extracted_text), "failed") | |
logger.error(f"Échec traduction: {error_detail}") | |
raise HTTPException(status_code=500, detail=error_detail) | |
# Succès | |
TranslationLogger.log_transaction(file.filename, len(extracted_text), "success") | |
logger.info(f"Traduction réussie - {len(extracted_text)}→{len(translated_text)} caractères") | |
return { | |
"filename": file.filename, | |
"original_text": extracted_text, | |
"translated_text": translated_text, | |
"char_count": len(extracted_text), | |
"translation_ratio": f"{len(translated_text)/len(extracted_text):.2f}" if extracted_text else "0" | |
} | |
async def translate_endpoint( | |
file: UploadFile = File(...), | |
target_lang: str = "en" | |
): | |
""" | |
Endpoint amélioré avec gestion complète des erreurs | |
""" | |
try: | |
# Traitement avec timeout configurable | |
result = await asyncio.wait_for( | |
process_file(file, target_lang), | |
timeout=PROCESSING_TIMEOUT | |
) | |
return { | |
"status": "success", | |
"message": "Traduction terminée avec succès", | |
"processing_time": f"{PROCESSING_TIMEOUT}s max", | |
**result | |
} | |
except asyncio.TimeoutError: | |
error_msg = "Temps de traitement dépassé. Veuillez réduire la taille du document." | |
logger.error(error_msg) | |
raise HTTPException(status_code=408, detail=error_msg) | |
except HTTPException: | |
raise # Already properly formatted | |
except Exception as e: | |
error_msg = f"Erreur inattendue: {str(e)}" | |
logger.critical(error_msg) | |
raise HTTPException(status_code=500, detail=error_msg) | |
async def download_translated( | |
file: UploadFile = File(...), | |
target_lang: str = "en" | |
): | |
""" | |
Endpoint de téléchargement amélioré avec gestion d'erreur | |
""" | |
try: | |
# Traitement avec timeout | |
result = await asyncio.wait_for( | |
process_file(file, target_lang), | |
timeout=PROCESSING_TIMEOUT | |
) | |
# Génération du DOCX avec gestion d'erreur | |
try: | |
doc = Document() | |
doc.add_paragraph(result["translated_text"]) | |
file_stream = io.BytesIO() | |
doc.save(file_stream) | |
file_stream.seek(0) | |
return Response( | |
content=file_stream.getvalue(), | |
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
headers={ | |
"Content-Disposition": f"attachment; filename=TRAD_{result['filename']}", | |
"X-File-Size": str(len(file_stream.getvalue())), | |
"X-Char-Count": str(result["char_count"]), | |
"X-Translation-Ratio": result["translation_ratio"] | |
} | |
) | |
except Exception as e: | |
error_msg = f"Erreur de génération DOCX: {str(e)}" | |
logger.error(error_msg) | |
raise HTTPException(status_code=500, detail=error_msg) | |
except asyncio.TimeoutError: | |
error_msg = "Temps de génération dépassé. Document trop volumineux." | |
logger.error(error_msg) | |
raise HTTPException(status_code=408, detail=error_msg) | |
except HTTPException: | |
raise | |
except Exception as e: | |
error_msg = f"Erreur de téléchargement: {str(e)}" | |
logger.error(error_msg) | |
raise HTTPException(status_code=500, detail=error_msg) | |
async def health_check(): | |
"""Endpoint de vérification de santé amélioré""" | |
from backend.utils import translator | |
return { | |
"status": "OK" if translator else "ERROR", | |
"model_loaded": bool(translator), | |
"max_file_size": f"{MAX_FILE_SIZE/1024/1024:.1f} MB", | |
"max_text_length": f"{MAX_TEXT_LENGTH} caractères", | |
"supported_formats": SUPPORTED_FORMATS, | |
"log_file": LOG_FILE | |
} | |
async def not_found_handler(request, exc): | |
"""Gestionnaire d'erreur 404 amélioré""" | |
logger.warning(f"404 - {request.url}") | |
return HTMLResponse( | |
content=""" | |
<div style='text-align:center; padding:2rem'> | |
<h1>404 - Ressource non trouvée</h1> | |
<p>La page que vous cherchez n'existe pas ou a été déplacée</p> | |
<a href="/">Retour à l'accueil</a> | |
</div> | |
""", | |
status_code=404 | |
) |