File size: 14,185 Bytes
03977cf
 
5fb4696
9db766f
c45f0d6
 
60af588
 
 
 
 
 
 
 
 
 
 
d43ba60
9db766f
674469e
 
60af588
674469e
2da4544
c45f0d6
 
9db766f
 
c45f0d6
 
 
 
 
 
 
8ddb255
c45f0d6
8ddb255
c45f0d6
 
 
9db766f
c45f0d6
 
 
 
 
 
 
8ddb255
c45f0d6
8ddb255
d43ba60
c45f0d6
83c5f9d
 
60af588
7f4de42
c45f0d6
 
25481b5
60af588
7f4de42
8ddb255
c45f0d6
8ddb255
d43ba60
8ddb255
83c5f9d
8ddb255
 
 
 
 
 
 
d43ba60
9db766f
 
674469e
8ddb255
674469e
9db766f
 
 
70a2026
dc6c29a
9db766f
 
 
 
f28285b
 
 
 
 
 
 
 
0d13811
 
 
 
 
 
 
 
0e19c41
7f4de42
0e19c41
 
7f4de42
0e19c41
 
 
 
 
 
 
9db766f
3bd4a19
9db766f
c45f0d6
 
8ddb255
c45f0d6
8ddb255
c45f0d6
 
8ddb255
c45f0d6
8ddb255
c45f0d6
 
674469e
c45f0d6
d43ba60
c45f0d6
674469e
d43ba60
8ddb255
674469e
8ddb255
9db766f
d43ba60
c45f0d6
674469e
d43ba60
8ddb255
674469e
8ddb255
 
 
 
 
 
 
 
 
9db766f
c45f0d6
8ddb255
c45f0d6
 
 
7f4de42
0e19c41
8ddb255
0e19c41
 
c45f0d6
 
2da4544
c45f0d6
8ddb255
c45f0d6
 
2da4544
d43ba60
9db766f
 
 
 
c45f0d6
9db766f
 
c45f0d6
9db766f
 
d43ba60
9db766f
 
 
 
 
 
 
 
d43ba60
c45f0d6
56dc677
2da4544
 
8ddb255
c45f0d6
8ddb255
56dc677
8ddb255
56dc677
c45f0d6
674469e
2da4544
 
8ddb255
c45f0d6
8ddb255
674469e
8ddb255
674469e
8ddb255
1d46e48
 
 
8ddb255
1d46e48
8ddb255
1d46e48
8ddb255
1d46e48
1315b27
8791d59
1315b27
83c5f9d
8ddb255
5eb5350
 
 
 
8791d59
5eb5350
 
 
1315b27
83c5f9d
1315b27
83c5f9d
1315b27
83c5f9d
 
 
1315b27
8ddb255
5eb5350
 
 
8791d59
f171734
1315b27
f171734
 
 
 
 
 
 
 
 
1315b27
83c5f9d
1315b27
8791d59
d43ba60
c45f0d6
2da4544
c45f0d6
 
 
8ddb255
c45f0d6
 
 
 
7f4de42
0e19c41
8ddb255
 
0e19c41
8ddb255
 
 
0e19c41
 
 
 
 
 
 
c45f0d6
 
 
674469e
c45f0d6
5fb4696
1315b27
9db766f
d25effa
c45f0d6
 
 
8ddb255
c2e9c8f
8ddb255
 
 
0e19c41
 
 
8ddb255
 
 
 
 
0e19c41
 
 
 
 
 
 
 
8ddb255
c45f0d6
 
8ddb255
 
5fb4696
9db766f
d43ba60
9db766f
c45f0d6
8ddb255
 
c45f0d6
9db766f
 
 
 
 
 
 
c45f0d6
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
import sys
from pathlib import Path
import logging
from contextlib import asynccontextmanager
import atexit
import shutil
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse

# Setup logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
# --- Config ---
from config import (
    API_TITLE, API_DESCRIPTION, API_VERSION,
    HUGGINGFACE_API_KEY, HUGGINGFACE_STANCE_MODEL_ID, HUGGINGFACE_LABEL_MODEL_ID,
    HUGGINGFACE_GENERATE_MODEL_ID,
    HOST, PORT, RELOAD,
    CORS_ORIGINS, CORS_METHODS, CORS_HEADERS, CORS_CREDENTIALS,
    PRELOAD_MODELS_ON_STARTUP, LOAD_STANCE_MODEL, LOAD_KPA_MODEL,
    GROQ_API_KEY, GROQ_STT_MODEL, GROQ_TTS_MODEL, GROQ_CHAT_MODEL
)

# --- Fonction de nettoyage ---
def cleanup_temp_files():
    """Nettoyer les fichiers temporaires audio au démarrage"""
    temp_dir = Path("temp_audio")
    if temp_dir.exists():
        try:
            shutil.rmtree(temp_dir)
            logger.info("✓ Fichiers temporaires audio nettoyés")
        except Exception as e:
            logger.warning(f"⚠ Impossible de nettoyer le répertoire temporaire: {e}")

# Appeler au démarrage
cleanup_temp_files()

# Configurer le nettoyage à la fermeture
@atexit.register
def cleanup_on_exit():
    temp_dir = Path("temp_audio")
    if temp_dir.exists():
        try:
            shutil.rmtree(temp_dir)
            logger.info("Nettoyage final des fichiers temporaires")
        except:
            logger.warning("Échec du nettoyage final")

# --- Import des singletons de services ---
stance_model_manager = None
kpa_model_manager = None
generate_model_manager = None
topic_similarity_service = None
try:
    from services.stance_model_manager import stance_model_manager
    from services.label_model_manager import kpa_model_manager 
    from services.generate_model_manager import generate_model_manager 
    from services.topic_similarity_service import topic_similarity_service
    logger.info("✓ Gestionnaires de modèles importés")
except ImportError as e:
    logger.warning(f"⚠ Impossible d'importer les gestionnaires de modèles: {e}")

# --- Vérification MCP ---
MCP_ENABLED = False
try:
    from services.mcp_service import init_mcp_server
    MCP_ENABLED = True
    logger.info("✓ Modules MCP détectés")
except ImportError as e:
    logger.warning(f"⚠ MCP non disponible: {e}")

# --- Lifespan / startup API ---
@asynccontextmanager
async def lifespan(app: FastAPI):
    logger.info("="*60)
    logger.info("🚀 DÉMARRAGE API - Chargement des modèles et vérification des APIs...")
    logger.info("="*60)
    
    # Load stance detection model
    try:
        logger.info(f"Loading stance model from Hugging Face: {HUGGINGFACE_STANCE_MODEL_ID}")
        stance_model_manager.load_model(HUGGINGFACE_STANCE_MODEL_ID, HUGGINGFACE_API_KEY)
    except Exception as e:
        logger.error(f"✗ Failed to load stance model: {str(e)}")
        logger.error("⚠️  Stance detection endpoints will not work!")
    
    # Load KPA (label) model
    try:
        logger.info(f"Loading KPA model from Hugging Face: {HUGGINGFACE_LABEL_MODEL_ID}")
        kpa_model_manager.load_model(HUGGINGFACE_LABEL_MODEL_ID, HUGGINGFACE_API_KEY)
    except Exception as e:
        logger.error(f"✗ Failed to load KPA model: {str(e)}")
        logger.error("⚠️  KPA/Label prediction endpoints will not work!")
    
    # Load Generation model
    try:
        logger.info(f"Loading Generation model from Hugging Face: {HUGGINGFACE_GENERATE_MODEL_ID}")
        generate_model_manager.load_model(HUGGINGFACE_GENERATE_MODEL_ID, HUGGINGFACE_API_KEY)
    except Exception as e:
        logger.error(f"✗ Failed to load Generation model: {str(e)}")
        logger.error("⚠️  Generation endpoints will not work!")
    
    # Initialize Topic Extraction service (uses Groq LLM)
    if topic_similarity_service and GROQ_API_KEY:
        try:
            logger.info("Initializing Topic Extraction service (Groq LLM)...")
            topic_similarity_service.initialize()
            logger.info("✓ Topic Extraction service initialized")
        except Exception as e:
            logger.error(f"✗ Failed to initialize Topic Extraction service: {str(e)}")
            logger.error("⚠️  Topic extraction endpoints will not work!")
    elif not GROQ_API_KEY:
        logger.warning("⚠ GROQ_API_KEY not configured. Topic extraction service will not be available.")
    
    logger.info("✓ API startup complete")
    logger.info("https://nlp-debater-project-fastapi-backend-models.hf.space/docs")
    
    # Vérifier les clés API
    if not GROQ_API_KEY:
        logger.warning("⚠ GROQ_API_KEY non configurée. Fonctions STT/TTS désactivées.")
    else:
        logger.info("✓ GROQ_API_KEY configurée")
        
    if not HUGGINGFACE_API_KEY:
        logger.warning("⚠ HUGGINGFACE_API_KEY non configurée. Modèles locaux désactivés.")
    else:
        logger.info("✓ HUGGINGFACE_API_KEY configurée")

    # Précharger les modèles Hugging Face si configuré
    if PRELOAD_MODELS_ON_STARTUP:
        
        # Charger stance model
        if LOAD_STANCE_MODEL and stance_model_manager and HUGGINGFACE_STANCE_MODEL_ID:
            try:
                stance_model_manager.load_model(HUGGINGFACE_STANCE_MODEL_ID, HUGGINGFACE_API_KEY)
                logger.info("✓ Modèle de détection de stance chargé")
            except Exception as e:
                logger.error(f"✗ Échec chargement modèle stance: {e}")

        # Charger KPA model
        if LOAD_KPA_MODEL and kpa_model_manager and HUGGINGFACE_LABEL_MODEL_ID:
            try:
                kpa_model_manager.load_model(HUGGINGFACE_LABEL_MODEL_ID, HUGGINGFACE_API_KEY)
                logger.info("✓ Modèle KPA chargé")
            except Exception as e:
                logger.error(f"✗ Échec chargement modèle KPA: {e}")

    # Initialiser MCP si disponible
    if MCP_ENABLED:
        try:
            init_mcp_server(app)
            logger.info("✓ Serveur MCP initialisé")
        except Exception as e:
            logger.error(f"✗ Échec initialisation MCP: {e}")

    logger.info("="*60)
    logger.info("✓ Démarrage terminé. API prête à recevoir des requêtes.")
    logger.info(f"  STT Model: {GROQ_STT_MODEL}")
    logger.info(f"  TTS Model: {GROQ_TTS_MODEL}")
    logger.info(f"  Chat Model: {GROQ_CHAT_MODEL}")
    logger.info(f"  Topic Extraction: {'Initialized' if (topic_similarity_service and topic_similarity_service.initialized) else 'Not initialized'}")
    logger.info(f"  Voice Chat: {'Available' if GROQ_API_KEY else 'Disabled (no GROQ_API_KEY)'}")
    logger.info(f"  MCP: {'Activé' if MCP_ENABLED else 'Désactivé'}")
    if MCP_ENABLED:
        logger.info(f"    - Tools: detect_stance, match_keypoint_argument, transcribe_audio, generate_speech, generate_argument, extract_topic, voice_chat, health_check")
    logger.info("="*60)
    
    yield
    
    logger.info("🛑 Arrêt de l'API...")
    # Nettoyage final
    cleanup_on_exit()

# --- FastAPI app ---
app = FastAPI(
    title=API_TITLE,
    description=API_DESCRIPTION,
    version=API_VERSION,
    lifespan=lifespan,
    docs_url="/docs",
    redoc_url="/redoc",
    openapi_url="/openapi.json"
)

# --- CORS ---
app.add_middleware(
    CORSMiddleware,
    allow_origins=CORS_ORIGINS,
    allow_credentials=CORS_CREDENTIALS,
    allow_methods=CORS_METHODS,
    allow_headers=CORS_HEADERS,
)

# --- Routes ---
# STT Routes
try:
    from routes.stt_routes import router as stt_router
    app.include_router(stt_router, prefix="/api/v1/stt", tags=["Speech To Text"])
    logger.info("✓ Route STT chargée (Groq Whisper)")
except ImportError as e:
    logger.warning(f"⚠ Route STT non trouvée: {e}")
except Exception as e:
    logger.warning(f"⚠ Échec chargement route STT: {e}")

# TTS Routes
try:
    from routes.tts_routes import router as tts_router
    app.include_router(tts_router, prefix="/api/v1/tts", tags=["Text To Speech"])
    logger.info("✓ Route TTS chargée (Groq PlayAI TTS)")
except ImportError as e:
    logger.warning(f"⚠ Route TTS non trouvée: {e}")
except Exception as e:
    logger.warning(f"⚠ Échec chargement route TTS: {e}")

# Voice Chat Routes
try:
    from routes.voice_chat_routes import router as voice_chat_router
    app.include_router(voice_chat_router, tags=["Voice Chat"])
    logger.info("✓ Route Voice Chat chargée")
except ImportError as e:
    logger.warning(f"⚠ Route Voice Chat non trouvée: {e}")
except Exception as e:
    logger.warning(f"⚠ Échec chargement route Voice Chat: {e}")

# Main API Routes (KPA, Stance, etc.)
try:
    from routes import api_router
    app.include_router(api_router, prefix="/api/v1")
    logger.info("✓ Routes API principales chargées")
    # Log all routes for debugging
    for route in api_router.routes:
        if hasattr(route, 'path'):
            logger.info(f"  - Route: {route.path} (methods: {getattr(route, 'methods', 'N/A')})")
except ImportError as e:
    logger.error(f"⚠ Routes API principales non trouvées: {e}")
    import traceback
    logger.error(traceback.format_exc())
    # Fallback
    try:
        from routes.label import router as kpa_router
        app.include_router(kpa_router, prefix="/api/v1/kpa", tags=["KPA"])
        from routes.stance import router as stance_router
        app.include_router(stance_router, prefix="/api/v1/stance", tags=["Stance Detection"])
        logger.info("✓ Routes KPA et Stance chargées en fallback")
    except ImportError:
        logger.warning("⚠ Fallback pour KPA/Stance échoué")
except Exception as e:
    logger.error(f"⚠ Échec chargement routes API principales: {e}")
    import traceback
    logger.error(traceback.format_exc())

# MCP Routes - Routes FastAPI pour Swagger UI + mount pour compatibilité MCP
if MCP_ENABLED:
    try:
        from routes.mcp_routes import router as mcp_router
        app.include_router(mcp_router)  # Pas de prefix car déjà dans le router
        logger.info("✓ Routes MCP FastAPI chargées (visibles dans Swagger)")
    except ImportError as e:
        logger.warning(f"⚠ Routes MCP FastAPI non trouvées: {e}")
    except Exception as e:
        logger.warning(f"⚠ Échec chargement routes MCP FastAPI: {e}")
    
    logger.info("✓ MCP monté via lifespan (endpoints auto-gérés)")
else:
    logger.warning("⚠ MCP désactivé")

# --- Basic routes ---
@app.get("/health", tags=["Health"])
async def health():
    health_status = {
        "status": "healthy",
        "service": "NLP Debater + Groq Voice",
        "version": API_VERSION,
        "features": {
            "stt": GROQ_STT_MODEL if GROQ_API_KEY else "disabled",
            "tts": GROQ_TTS_MODEL if GROQ_API_KEY else "disabled",
            "chat": GROQ_CHAT_MODEL if GROQ_API_KEY else "disabled",
            "topic_extraction": "initialized" if (topic_similarity_service and hasattr(topic_similarity_service, 'initialized') and topic_similarity_service.initialized) else "not initialized",
            "voice_chat": "available" if GROQ_API_KEY else "disabled",
            "stance_model": "loaded" if (stance_model_manager and hasattr(stance_model_manager, 'model_loaded') and stance_model_manager.model_loaded) else "not loaded",
            "kpa_model": "loaded" if (kpa_model_manager and hasattr(kpa_model_manager, 'model_loaded') and kpa_model_manager.model_loaded) else "not loaded",
            "generate_model": "loaded" if (generate_model_manager and hasattr(generate_model_manager, 'model_loaded') and generate_model_manager.model_loaded) else "not loaded",
            "mcp": "enabled" if MCP_ENABLED else "disabled"
        },
        "endpoints": {
            "mcp": "/api/v1/mcp" if MCP_ENABLED else "disabled",
            "topic_extraction": "/api/v1/topic/extract",
            "voice_chat": "/voice-chat/voice or /voice-chat/text",
            "mcp_tools": {
                "extract_topic": "/api/v1/mcp/tools/extract-topic",
                "voice_chat": "/api/v1/mcp/tools/voice-chat"
            } if MCP_ENABLED else "disabled"
        }
    }
    return health_status

@app.get("/", tags=["Root"])
async def root():
    return RedirectResponse(url="/docs")


# --- Error handlers ---
@app.exception_handler(404)
async def not_found_handler(request, exc):
    endpoints = {
        "GET /": "Redirige vers /docs (Swagger UI)",
        "GET /health": "Health check",
        "POST /api/v1/stt/": "Speech to text",
        "POST /api/v1/tts/": "Text to speech",
        "POST /voice-chat/voice": "Voice chat (audio input)",
        "POST /voice-chat/text": "Voice chat (text input)",
        "POST /api/v1/topic/extract": "Extract topic from text"
    }
    if MCP_ENABLED:
        endpoints.update({
            "GET /api/v1/mcp/health": "Health check MCP",
            "GET /api/v1/mcp/tools": "Liste outils MCP",
            "POST /api/v1/mcp/tools/call": "Appel d'outil MCP",
            "POST /api/v1/mcp/tools/extract-topic": "Extract topic (MCP tool)",
            "POST /api/v1/mcp/tools/voice-chat": "Voice chat (MCP tool)",
            "POST /api/v1/mcp/tools/detect-stance": "Detect stance (MCP tool)",
            "POST /api/v1/mcp/tools/match-keypoint": "Match keypoint (MCP tool)",
            "POST /api/v1/mcp/tools/transcribe-audio": "Transcribe audio (MCP tool)",
            "POST /api/v1/mcp/tools/generate-speech": "Generate speech (MCP tool)",
            "POST /api/v1/mcp/tools/generate-argument": "Generate argument (MCP tool)"
        })
    return {
        "error": "Not Found",
        "message": f"URL {request.url} non trouvée",
        "available_endpoints": endpoints
    }

# --- Run server ---
if __name__ == "__main__":
    logger.info("="*60)
    logger.info(f"Démarrage du serveur sur {HOST}:{PORT}")
    logger.info(f"Mode reload: {RELOAD}")
    logger.info("="*60)
    
    uvicorn.run(
        "main:app",
        host=HOST,
        port=PORT,
        reload=RELOAD,
        log_level="info"
    )