File size: 17,178 Bytes
054a1ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
"""
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
# ============================================================================

@app.on_event("startup")
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
# ============================================================================

@app.get("/")
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]"
    }

@app.get("/health")
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
# ----------------------------------------------------------------------------

@app.post("/moderate/text", response_model=TextModerationResponse)
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
# ----------------------------------------------------------------------------

@app.post("/moderate/image", response_model=ImageModerationResponse)
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
# ----------------------------------------------------------------------------

@app.post("/classify/category", response_model=CategoryClassificationResponse)
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)
# ----------------------------------------------------------------------------

@app.post("/moderate/complete")
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)
# ============================================================================

@app.get("/debug/keywords")
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
# ============================================================================

@app.exception_handler(Exception)
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)