Spaces:
Sleeping
Sleeping
| """ | |
| MARSA Moderation API - FastAPI | |
| Pour déploiement sur Hugging Face Spaces | |
| """ | |
| from fastapi import FastAPI, File, UploadFile, HTTPException, Form | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from typing import Optional, List | |
| import os | |
| import tempfile | |
| import traceback | |
| # Importer tes modules de modération | |
| try: | |
| from text_moderation_system import HybridTextModerator, KeywordBasedModerator | |
| from Image_moderation import ImageModerator | |
| from classification_moderation_system import CategoryClassificationModel | |
| print("✅ Modules de modération importés avec succès") | |
| except Exception as e: | |
| print(f"❌ Erreur d'import: {e}") | |
| traceback.print_exc() | |
| # ============================================================================ | |
| # CONFIGURATION DE L'API | |
| # ============================================================================ | |
| app = FastAPI( | |
| title="MARSA Moderation API", | |
| description="API de modération intelligente pour les annonces MARSA", | |
| version="1.0.0", | |
| docs_url="/docs", # Swagger UI | |
| redoc_url="/redoc" # ReDoc | |
| ) | |
| # CORS - Permet les appels depuis n'importe où (pour l'instant) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # En prod: remplacer par ton domaine Django | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Variables globales pour les modèles | |
| text_moderator = None | |
| image_moderator = None | |
| category_classifier = None | |
| # ============================================================================ | |
| # CHARGEMENT DES MODÈLES AU DÉMARRAGE | |
| # ============================================================================ | |
| async def load_models(): | |
| """ | |
| Charge tous les modèles au démarrage de l'API | |
| """ | |
| global text_moderator, image_moderator, category_classifier | |
| print("=" * 70) | |
| print("🚀 DÉMARRAGE DE L'API MARSA MODERATION") | |
| print("=" * 70) | |
| try: | |
| # 1. Modérateur de texte (léger et rapide) | |
| print("\n🔧 Chargement du modérateur de texte...") | |
| text_moderator = KeywordBasedModerator() # Version simple et rapide | |
| print("✅ Modérateur de texte chargé (Keywords)") | |
| # Option avancée (si tu veux utiliser les modèles ML) | |
| # text_moderator = HybridTextModerator(use_ml_model=True) | |
| except Exception as e: | |
| print(f"❌ Erreur modérateur texte: {e}") | |
| text_moderator = None | |
| try: | |
| # 2. Modérateur d'images | |
| print("\n🔧 Chargement du modérateur d'images...") | |
| image_moderator = ImageModerator( | |
| nsfw_threshold=0.7, | |
| weapon_threshold=0.5, | |
| yolo_model="yolov8n.pt" # Modèle le plus léger | |
| ) | |
| print("✅ Modérateur d'images chargé") | |
| except Exception as e: | |
| print(f"❌ Erreur modérateur images: {e}") | |
| print(" L'API fonctionnera sans modération d'images") | |
| image_moderator = None | |
| try: | |
| # 3. Classificateur (optionnel si tu as le modèle entraîné) | |
| model_path = "./models/category_classification" | |
| if os.path.exists(model_path): | |
| print("\n🔧 Chargement du classificateur de catégories...") | |
| category_classifier = CategoryClassificationModel() | |
| category_classifier.load(model_path) | |
| print("✅ Classificateur de catégories chargé") | |
| else: | |
| print("\n⚠️ Modèle de classification non trouvé") | |
| print(" Classification désactivée (pas grave pour commencer)") | |
| category_classifier = None | |
| except Exception as e: | |
| print(f"❌ Erreur classificateur: {e}") | |
| category_classifier = None | |
| print("\n" + "=" * 70) | |
| print("🎉 API PRÊTE À RECEVOIR DES REQUÊTES!") | |
| print("=" * 70) | |
| print("\nModèles disponibles:") | |
| print(f" - Texte: {'✅' if text_moderator else '❌'}") | |
| print(f" - Images: {'✅' if image_moderator else '❌'}") | |
| print(f" - Classification: {'✅' if category_classifier else '❌'}") | |
| print() | |
| # ============================================================================ | |
| # MODÈLES PYDANTIC (Schémas de requête/réponse) | |
| # ============================================================================ | |
| class TextModerationRequest(BaseModel): | |
| title: str | |
| description: str | |
| class TextModerationResponse(BaseModel): | |
| approved: bool | |
| reason: str | |
| category: str # 'safe', 'weapon', 'nsfw', 'spam' | |
| detected_keywords: List[str] | |
| confidence: float | |
| class ImageModerationResponse(BaseModel): | |
| approved: bool | |
| rejection_reason: Optional[str] | |
| nsfw_detected: bool | |
| weapon_detected: bool | |
| confidence: float | |
| class CategoryPrediction(BaseModel): | |
| category: str | |
| confidence: float | |
| class CategoryClassificationResponse(BaseModel): | |
| category: str | |
| confidence: float | |
| top_predictions: List[dict] | |
| # ============================================================================ | |
| # ENDPOINTS | |
| # ============================================================================ | |
| async def root(): | |
| """ | |
| Page d'accueil - Informations sur l'API | |
| """ | |
| return { | |
| "message": "🎉 Bienvenue sur l'API de Modération MARSA", | |
| "version": "1.0.0", | |
| "status": "running", | |
| "documentation": "/docs", | |
| "endpoints": { | |
| "health": "GET /health", | |
| "moderate_text": "POST /moderate/text", | |
| "moderate_image": "POST /moderate/image", | |
| "classify_category": "POST /classify/category", | |
| "moderate_complete": "POST /moderate/complete" | |
| }, | |
| "github": "https://github.com/ton-repo", | |
| "contact": "[email protected]" | |
| } | |
| async def health_check(): | |
| """ | |
| Vérifie l'état de santé de l'API et des modèles | |
| """ | |
| return { | |
| "status": "healthy", | |
| "version": "1.0.0", | |
| "models": { | |
| "text_moderator": text_moderator is not None, | |
| "image_moderator": image_moderator is not None, | |
| "category_classifier": category_classifier is not None | |
| }, | |
| "ready": text_moderator is not None # Au minimum le texte doit marcher | |
| } | |
| # ---------------------------------------------------------------------------- | |
| # MODÉRATION DE TEXTE | |
| # ---------------------------------------------------------------------------- | |
| async def moderate_text(request: TextModerationRequest): | |
| """ | |
| Modère le contenu textuel d'une annonce | |
| **Détecte:** | |
| - Contenu NSFW (xxx, porn, escort, etc.) | |
| - Armes (pistolet, fusil, arme, etc.) | |
| - Spam (argent facile, cliquez ici, etc.) | |
| **Exemple de requête:** | |
| ```json | |
| { | |
| "title": "iPhone 13 Pro", | |
| "description": "Téléphone en excellent état, très peu utilisé" | |
| } | |
| ``` | |
| **Réponse:** | |
| ```json | |
| { | |
| "approved": true, | |
| "reason": "Texte approuvé", | |
| "category": "safe", | |
| "detected_keywords": [], | |
| "confidence": 0.0 | |
| } | |
| ``` | |
| """ | |
| if text_moderator is None: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Modérateur de texte non disponible. Veuillez contacter le support." | |
| ) | |
| try: | |
| # Appeler ton modérateur | |
| result = text_moderator.predict(request.title, request.description) | |
| return TextModerationResponse( | |
| approved=result['approved'], | |
| reason=result['reason'], | |
| category=result['category'], | |
| detected_keywords=result.get('detected_keywords', []), | |
| confidence=result.get('confidence', 0.0) | |
| ) | |
| except Exception as e: | |
| print(f"❌ Erreur modération texte: {e}") | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Erreur lors de la modération: {str(e)}" | |
| ) | |
| # ---------------------------------------------------------------------------- | |
| # MODÉRATION D'IMAGE | |
| # ---------------------------------------------------------------------------- | |
| async def moderate_image(image: UploadFile = File(...)): | |
| """ | |
| Modère l'image d'une annonce | |
| **Détecte:** | |
| - Contenu NSFW (nudité, contenu adulte) | |
| - Armes (pistolets, couteaux, etc.) | |
| **Formats acceptés:** JPG, PNG, JPEG, WEBP | |
| **Exemple d'utilisation:** | |
| ```python | |
| files = {"image": open("photo.jpg", "rb")} | |
| response = requests.post(url + "/moderate/image", files=files) | |
| ``` | |
| """ | |
| if image_moderator is None: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Modérateur d'images non disponible. La modération d'images est désactivée." | |
| ) | |
| # Vérifier le type de fichier | |
| if not image.content_type or not image.content_type.startswith("image/"): | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"Le fichier doit être une image (reçu: {image.content_type})" | |
| ) | |
| tmp_path = None | |
| try: | |
| # Sauvegarder temporairement l'image | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp_file: | |
| content = await image.read() | |
| tmp_file.write(content) | |
| tmp_path = tmp_file.name | |
| # Modérer l'image | |
| result = image_moderator.moderate_image(tmp_path) | |
| # Nettoyer | |
| os.unlink(tmp_path) | |
| return ImageModerationResponse( | |
| approved=result['approved'], | |
| rejection_reason=result.get('rejection_reason'), | |
| nsfw_detected=result.get('nsfw_result', {}).get('is_nsfw', False), | |
| weapon_detected=result.get('weapon_result', {}).get('weapons_detected', False), | |
| confidence=max( | |
| result.get('nsfw_result', {}).get('confidence', 0.0), | |
| result.get('weapon_result', {}).get('confidence', 0.0) | |
| ) | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| print(f"❌ Erreur modération image: {e}") | |
| traceback.print_exc() | |
| # Nettoyer en cas d'erreur | |
| if tmp_path and os.path.exists(tmp_path): | |
| try: | |
| os.unlink(tmp_path) | |
| except: | |
| pass | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Erreur lors de la modération: {str(e)}" | |
| ) | |
| # ---------------------------------------------------------------------------- | |
| # CLASSIFICATION DE CATÉGORIE | |
| # ---------------------------------------------------------------------------- | |
| async def classify_category(request: TextModerationRequest): | |
| """ | |
| Classifie automatiquement une annonce par catégorie | |
| **Catégories possibles:** | |
| - Électronique | |
| - Véhicules | |
| - Immobilier | |
| - Mode & Beauté | |
| - Maison & Jardin | |
| - Sports & Loisirs | |
| - etc. | |
| **Exemple:** | |
| ```json | |
| { | |
| "title": "iPhone 13 Pro 128GB", | |
| "description": "Téléphone Apple en excellent état" | |
| } | |
| ``` | |
| **Réponse:** | |
| ```json | |
| { | |
| "category": "Électronique", | |
| "confidence": 0.95, | |
| "top_predictions": [ | |
| {"category": "Électronique", "confidence": 0.95}, | |
| {"category": "Téléphones", "confidence": 0.03}, | |
| {"category": "Informatique", "confidence": 0.02} | |
| ] | |
| } | |
| ``` | |
| """ | |
| if category_classifier is None: | |
| # Retourner une catégorie par défaut | |
| return CategoryClassificationResponse( | |
| category="Non classé", | |
| confidence=0.0, | |
| top_predictions=[{"category": "Non classé", "confidence": 0.0}] | |
| ) | |
| try: | |
| result = category_classifier.predict( | |
| title=request.title, | |
| description=request.description, | |
| top_k=3 | |
| ) | |
| # Formater les prédictions | |
| top_preds = [ | |
| {"category": cat, "confidence": conf} | |
| for cat, conf in result['top_predictions'] | |
| ] | |
| return CategoryClassificationResponse( | |
| category=result['category'], | |
| confidence=result['confidence'], | |
| top_predictions=top_preds | |
| ) | |
| except Exception as e: | |
| print(f"❌ Erreur classification: {e}") | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Erreur lors de la classification: {str(e)}" | |
| ) | |
| # ---------------------------------------------------------------------------- | |
| # MODÉRATION COMPLÈTE (Texte + Image) | |
| # ---------------------------------------------------------------------------- | |
| async def moderate_complete( | |
| title: str = Form(...), | |
| description: str = Form(...), | |
| image: Optional[UploadFile] = File(None) | |
| ): | |
| """ | |
| Modération complète d'une annonce (texte + image optionnelle) | |
| **Process:** | |
| 1. Modère le texte | |
| 2. Modère l'image (si fournie) | |
| 3. Classifie la catégorie | |
| 4. Retourne une décision finale | |
| **Exemple d'utilisation:** | |
| ```python | |
| data = { | |
| "title": "iPhone 13 Pro", | |
| "description": "Téléphone en excellent état" | |
| } | |
| files = {"image": open("photo.jpg", "rb")} if has_image else {} | |
| response = requests.post(url + "/moderate/complete", data=data, files=files) | |
| ``` | |
| """ | |
| results = { | |
| "approved": False, | |
| "text_approved": False, | |
| "image_approved": True, # Par défaut si pas d'image | |
| "suggested_category": None, | |
| "rejection_reasons": [], | |
| "confidence": 0.0 | |
| } | |
| try: | |
| # 1. Modération du TEXTE | |
| text_result = await moderate_text( | |
| TextModerationRequest(title=title, description=description) | |
| ) | |
| results["text_approved"] = text_result.approved | |
| if not text_result.approved: | |
| results["rejection_reasons"].append(f"Texte: {text_result.reason}") | |
| # 2. Modération de l'IMAGE (si fournie) | |
| if image and image_moderator: | |
| image_result = await moderate_image(image) | |
| results["image_approved"] = image_result.approved | |
| if not image_result.approved: | |
| results["rejection_reasons"].append(f"Image: {image_result.rejection_reason}") | |
| # 3. Classification (si disponible) | |
| if category_classifier: | |
| try: | |
| category_result = await classify_category( | |
| TextModerationRequest(title=title, description=description) | |
| ) | |
| results["suggested_category"] = category_result.category | |
| except: | |
| results["suggested_category"] = "Non classé" | |
| # 4. Décision FINALE | |
| results["approved"] = results["text_approved"] and results["image_approved"] | |
| results["confidence"] = text_result.confidence | |
| return results | |
| except Exception as e: | |
| print(f"❌ Erreur modération complète: {e}") | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Erreur: {str(e)}" | |
| ) | |
| # ============================================================================ | |
| # ENDPOINT DE DEBUG (À désactiver en production) | |
| # ============================================================================ | |
| async def get_keywords(): | |
| """ | |
| Retourne les mots-clés utilisés par le modérateur | |
| (Utile pour debugging) | |
| """ | |
| if not isinstance(text_moderator, KeywordBasedModerator): | |
| return {"error": "Keywords moderator not loaded"} | |
| return { | |
| "weapon_keywords": text_moderator.weapon_keywords[:10], # Premiers 10 | |
| "nsfw_keywords": text_moderator.nsfw_keywords[:10], | |
| "spam_keywords": text_moderator.spam_keywords[:10] | |
| } | |
| # ============================================================================ | |
| # GESTION DES ERREURS | |
| # ============================================================================ | |
| async def global_exception_handler(request, exc): | |
| """ | |
| Gère toutes les erreurs non capturées | |
| """ | |
| print(f"❌ ERREUR NON GÉRÉE: {exc}") | |
| traceback.print_exc() | |
| return { | |
| "error": "Internal server error", | |
| "detail": str(exc), | |
| "type": type(exc).__name__ | |
| } | |
| # ============================================================================ | |
| # POUR TESTER EN LOCAL | |
| # ============================================================================ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| print("🚀 Lancement de l'API en mode développement...") | |
| print("📍 http://localhost:8000") | |
| print("📚 Documentation: http://localhost:8000/docs") | |
| uvicorn.run(app, host="0.0.0.0", port=8000, reload=True) | |