File size: 10,721 Bytes
03977cf
 
5fb4696
674469e
c45f0d6
 
5fb4696
d43ba60
 
 
 
 
674469e
 
2da4544
674469e
5fb4696
03977cf
d43ba60
03977cf
2da4544
5fb4696
d43ba60
9db766f
674469e
 
 
2da4544
c45f0d6
 
9db766f
 
c45f0d6
 
 
 
 
 
 
8ddb255
c45f0d6
8ddb255
c45f0d6
 
 
 
 
 
 
 
 
 
 
8ddb255
c45f0d6
8ddb255
d43ba60
c45f0d6
83c5f9d
 
c45f0d6
 
83c5f9d
8ddb255
c45f0d6
8ddb255
d43ba60
8ddb255
83c5f9d
 
8ddb255
 
 
 
 
 
 
 
d43ba60
9db766f
 
674469e
8ddb255
674469e
c45f0d6
 
 
8ddb255
c45f0d6
8ddb255
c45f0d6
 
8ddb255
c45f0d6
8ddb255
c45f0d6
 
674469e
c45f0d6
d43ba60
c45f0d6
674469e
d43ba60
8ddb255
674469e
8ddb255
2da4544
d43ba60
c45f0d6
674469e
d43ba60
8ddb255
674469e
8ddb255
 
 
 
 
 
 
 
 
9db766f
c45f0d6
8ddb255
c45f0d6
 
 
8ddb255
c45f0d6
 
2da4544
c45f0d6
8ddb255
c45f0d6
 
2da4544
d43ba60
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
83c5f9d
 
8791d59
83c5f9d
 
8ddb255
8791d59
8ddb255
83c5f9d
 
 
 
 
 
 
 
 
8ddb255
 
8791d59
8ddb255
83c5f9d
 
8ddb255
83c5f9d
 
8791d59
d43ba60
c45f0d6
2da4544
c45f0d6
 
 
 
8ddb255
c45f0d6
 
 
 
8ddb255
 
 
 
 
 
c45f0d6
 
 
674469e
c45f0d6
5fb4696
c45f0d6
8ddb255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5fb4696
8ddb255
c45f0d6
8ddb255
c45f0d6
8ddb255
 
 
 
c45f0d6
 
 
 
 
 
8ddb255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c45f0d6
 
8ddb255
 
5fb4696
9db766f
d43ba60
9db766f
c45f0d6
8ddb255
 
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
import sys
from pathlib import Path
import logging
from contextlib import asynccontextmanager
import atexit
import shutil

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn

# --- Logging ---
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

# --- Ajouter app dir au PATH ---
app_dir = Path(__file__).parent
sys.path.insert(0, str(app_dir))

# --- Config ---
from config import (
    API_TITLE, API_DESCRIPTION, API_VERSION,
    HUGGINGFACE_API_KEY, HUGGINGFACE_STANCE_MODEL_ID, HUGGINGFACE_LABEL_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
try:
    from services.stance_model_manager import stance_model_manager
    from services.label_model_manager import kpa_model_manager  # Corrigé : import depuis kpa_model_manager.py
    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
mcp_router = None
try:
    from services.mcp_service import init_mcp_server
    from routes.mcp_routes import router as mcp_router
    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)
    
    # 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"  MCP: {'Activé' if MCP_ENABLED else 'Désactivé'}")
    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.) - Assumant un api_router qui inclut kpa et stance
api_router = None
try:
    from routes import api_router  # Ou from routes.api_router import router as api_router si c'est un fichier dédié
    app.include_router(api_router, prefix="/api/v1")
    logger.info("✓ Routes API principales chargées")
except ImportError as e:
    logger.warning(f"⚠ Routes API principales non trouvées: {e}")
    # Fallback : Inclure directement les sub-routers si api_router n'existe pas
    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  # Assumant le nom du fichier
        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é - vérifiez vos fichiers routes/")
except Exception as e:
    logger.warning(f"⚠ Échec chargement routes API principales: {e}")

# MCP Routes
if MCP_ENABLED and mcp_router:
    app.include_router(mcp_router, prefix="/api/v1/mcp", tags=["MCP"])
    logger.info("✓ Routes MCP chargées")
else:
    logger.warning("⚠ Routes MCP non chargées (MCP désactivé ou router manquant)")

# --- Basic routes ---
@app.get("/health", tags=["Health"])
async def health():
    """Health check endpoint"""
    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",
            "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",
            "mcp": "enabled" if MCP_ENABLED else "disabled"
        },
        "endpoints": {
            "mcp": "/api/v1/mcp" if MCP_ENABLED else "disabled"
        }
    }
    return health_status

@app.get("/", tags=["Root"])
async def root():
    """Root endpoint with API information"""
    endpoints = {
        "docs": "/docs",
        "redoc": "/redoc",
        "health": "/health",
        "stt": "/api/v1/stt/",
        "tts": "/api/v1/tts/",
        "voice_chat": "/voice-chat/voice",
    }
    
    if MCP_ENABLED:
        endpoints["mcp"] = {
            "health": "/api/v1/mcp/health",
            "tools": "/api/v1/mcp/tools",
            "resources": "/api/v1/mcp/resources",
            "call_tool": "/api/v1/mcp/tools/call"
        }
    
    return {
        "message": "NLP Debater API avec support vocal Groq et MCP",
        "version": API_VERSION,
        "endpoints": endpoints,
        "models": {
            "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",
            "mcp": "enabled" if MCP_ENABLED else "disabled"
        }
    }

# --- Error handlers ---
@app.exception_handler(404)
async def not_found_handler(request, exc):
    endpoints = {
        "GET /": "Informations API",
        "GET /health": "Health check",
        "POST /api/v1/stt/": "Speech to text",
        "POST /api/v1/tts/": "Text to speech",
        "POST /voice-chat/voice": "Voice chat"
    }
    
    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"
        })
    
    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"
    )