feat(memory): Ajout déchargement automatique mémoire sur FolzoCorp
- effi-stt: Déchargement Whisper après 300s inactivité (INACTIVITY_TIMEOUT) * Nouveaux endpoints: /unload, /reload, /stats * Modelo rechargé à la première requête - ComfyUI: Script monitor qui décharge VRAM après inactivité * Vérifie usage VRAM toutes les 60s * Exécute workflow UnloadAllModelsNode après 300s inactivité - Kreuzberg: Manager qui redémarre le container après seuil mémoire + inactivité * Restart si > 2GB pendant > 10min inactif - Documentation: docs/MEMOIRE-GESTION.md
This commit is contained in:
+119
-63
@@ -1,5 +1,6 @@
|
||||
# 🎙️ Effi-STT - Speech-to-Text pour Effi
|
||||
# Effi-STT - Speech-to-Text pour Effi
|
||||
# API OpenAI-compatible avec faster-whisper
|
||||
# Inclut déchargement automatique après inactivité
|
||||
|
||||
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -9,39 +10,100 @@ import tempfile
|
||||
import os
|
||||
from typing import Optional
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
title="Effi-STT",
|
||||
description="Speech-to-Text API compatible OpenAI pour Effi",
|
||||
version="1.0.0"
|
||||
version="1.1.0"
|
||||
)
|
||||
|
||||
# Configuration
|
||||
MODEL_SIZE = os.getenv("MODEL_SIZE", "large-v3")
|
||||
DEVICE = os.getenv("DEVICE", "cpu")
|
||||
COMPUTE_TYPE = os.getenv("COMPUTE_TYPE", "int8")
|
||||
LANGUAGE = os.getenv("LANGUAGE", "fr")
|
||||
INACTIVITY_TIMEOUT = int(os.getenv("INACTIVITY_TIMEOUT", "300"))
|
||||
PORT = int(os.getenv("PORT", "8200"))
|
||||
HOST = os.getenv("HOST", "0.0.0.0")
|
||||
|
||||
logger.info(f"🎙️ Chargement du modèle Whisper: {MODEL_SIZE}")
|
||||
logger.info(f" Device: {DEVICE}, Compute type: {COMPUTE_TYPE}")
|
||||
model = None
|
||||
model_loaded = False
|
||||
model_lock = threading.Lock()
|
||||
last_request_time = time.time()
|
||||
unload_timer = None
|
||||
|
||||
# Chargement du modèle au démarrage
|
||||
model = WhisperModel(
|
||||
MODEL_SIZE,
|
||||
device=DEVICE,
|
||||
compute_type=COMPUTE_TYPE,
|
||||
cpu_threads=int(os.getenv("CPU_THREADS", "4"))
|
||||
)
|
||||
logger.info("✅ Modèle chargé avec succès!")
|
||||
def load_model():
|
||||
global model, model_loaded
|
||||
with model_lock:
|
||||
if not model_loaded:
|
||||
logger.info(f"Chargement du modèle Whisper: {MODEL_SIZE}")
|
||||
logger.info(f" Device: {DEVICE}, Compute type: {COMPUTE_TYPE}")
|
||||
model = WhisperModel(
|
||||
MODEL_SIZE,
|
||||
device=DEVICE,
|
||||
compute_type=COMPUTE_TYPE,
|
||||
cpu_threads=int(os.getenv("CPU_THREADS", "4"))
|
||||
)
|
||||
model_loaded = True
|
||||
logger.info("✅ Modèle chargé avec succès!")
|
||||
|
||||
def unload_model():
|
||||
global model, model_loaded
|
||||
with model_lock:
|
||||
if model_loaded:
|
||||
logger.info("🕐 Inactivité détectée - Déchargement du modèle Whisper...")
|
||||
del model
|
||||
model = None
|
||||
model_loaded = False
|
||||
import gc
|
||||
gc.collect()
|
||||
logger.info("✅ Modèle déchargé, mémoire libérée")
|
||||
|
||||
def reset_unload_timer():
|
||||
global last_request_time, unload_timer
|
||||
last_request_time = time.time()
|
||||
if unload_timer is not None:
|
||||
unload_timer.cancel()
|
||||
if model_loaded:
|
||||
timer = threading.Timer(INACTIVITY_TIMEOUT, unload_model)
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
unload_timer = timer
|
||||
logger.debug(f"⏱️ Timer déchargement programmé dans {INACTIVITY_TIMEOUT}s")
|
||||
|
||||
def check_and_load_model():
|
||||
global model_loaded
|
||||
if not model_loaded:
|
||||
logger.info("📥 Chargement du modèle Whisper à la demande...")
|
||||
load_model()
|
||||
reset_unload_timer()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
logger.info(f"🚀 Démarrage effi-stt v1.1.0 sur http://{HOST}:{PORT}")
|
||||
logger.info(f" Timeout d'inactivité: {INACTIVITY_TIMEOUT}s")
|
||||
load_model()
|
||||
reset_unload_timer()
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
if unload_timer:
|
||||
unload_timer.cancel()
|
||||
unload_model()
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Endpoint de vérification de santé"""
|
||||
return {"status": "healthy", "model": MODEL_SIZE}
|
||||
check_and_load_model()
|
||||
return {
|
||||
"status": "healthy",
|
||||
"model": MODEL_SIZE,
|
||||
"model_loaded": model_loaded,
|
||||
"inactivity_timeout": INACTIVITY_TIMEOUT
|
||||
}
|
||||
|
||||
@app.post("/v1/audio/transcriptions")
|
||||
async def transcribe(
|
||||
@@ -51,88 +113,82 @@ async def transcribe(
|
||||
prompt: Optional[str] = Form(None, description="Prompt optionnel"),
|
||||
response_format: Optional[str] = Form("json", description="Format de réponse")
|
||||
):
|
||||
"""
|
||||
Endpoint de transcription compatible API OpenAI
|
||||
"""
|
||||
try:
|
||||
logger.info(f"📝 Transcription demandée: lang={language}, file={file.filename}")
|
||||
check_and_load_model()
|
||||
reset_unload_timer()
|
||||
logger.info(f"📝 Transcription: lang={language}, file={file.filename}")
|
||||
|
||||
# Sauvegarder le fichier temporairement
|
||||
suffix = os.path.splitext(file.filename)[1] or ".mp3"
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
||||
content = await file.read()
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
logger.info(f" Fichier sauvegardé: {tmp_path} ({len(content)} bytes)")
|
||||
logger.info(f" Fichier: {tmp_path} ({len(content)} bytes)")
|
||||
|
||||
try:
|
||||
# Transcription avec faster-whisper
|
||||
segments, info = model.transcribe(
|
||||
tmp_path,
|
||||
language=language if language != "auto" else None,
|
||||
task="transcribe",
|
||||
beam_size=5,
|
||||
best_of=5,
|
||||
condition_on_previous_text=True
|
||||
)
|
||||
with model_lock:
|
||||
segments, info = model.transcribe(
|
||||
tmp_path,
|
||||
language=language if language != "auto" else None,
|
||||
task="transcribe",
|
||||
beam_size=5,
|
||||
best_of=5,
|
||||
condition_on_previous_text=True
|
||||
)
|
||||
|
||||
# Concaténer tous les segments
|
||||
text_parts = []
|
||||
for segment in segments:
|
||||
text_parts.append(segment.text)
|
||||
|
||||
full_text = " ".join(text_parts).strip()
|
||||
logger.info(f"✅ Transcription: '{full_text[:50]}...'")
|
||||
logger.info(f" Langue: {info.language} ({info.language_probability:.2f})")
|
||||
|
||||
logger.info(f"✅ Transcription réussie: '{full_text[:50]}...'")
|
||||
logger.info(f" Langue détectée: {info.language}, Probabilité: {info.language_probability:.2f}")
|
||||
|
||||
# Format OpenAI standard
|
||||
if response_format == "text":
|
||||
return full_text
|
||||
elif response_format == "srt":
|
||||
# TODO: Implémenter format SRT
|
||||
return full_text
|
||||
elif response_format == "vtt":
|
||||
# TODO: Implémenter format VTT
|
||||
return full_text
|
||||
else:
|
||||
# Format JSON (défaut)
|
||||
return JSONResponse(content={"text": full_text})
|
||||
|
||||
finally:
|
||||
# Nettoyage
|
||||
os.unlink(tmp_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Erreur transcription: {str(e)}")
|
||||
logger.error(f"❌ Erreur: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/unload")
|
||||
async def unload():
|
||||
unload_model()
|
||||
return {"status": "unloaded"}
|
||||
|
||||
@app.post("/reload")
|
||||
async def reload():
|
||||
load_model()
|
||||
reset_unload_timer()
|
||||
return {"status": "loaded"}
|
||||
|
||||
@app.get("/v1/models")
|
||||
async def list_models():
|
||||
"""Liste les modèles disponibles (compatibilité OpenAI)"""
|
||||
return {
|
||||
"data": [
|
||||
{
|
||||
"id": "whisper-1",
|
||||
"object": "model",
|
||||
"created": 1677610602,
|
||||
"owned_by": "openai",
|
||||
"permission": [],
|
||||
"root": "whisper-1",
|
||||
"parent": None
|
||||
}
|
||||
],
|
||||
"data": [{"id": "whisper-1", "object": "model", "created": 1677610602, "owned_by": "openai", "permission": [], "root": "whisper-1", "parent": None}],
|
||||
"object": "list"
|
||||
}
|
||||
|
||||
@app.get("/stats")
|
||||
async def stats():
|
||||
return {
|
||||
"model_loaded": model_loaded,
|
||||
"last_request_age_seconds": int(time.time() - last_request_time),
|
||||
"inactivity_timeout": INACTIVITY_TIMEOUT
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.getenv("PORT", "8200"))
|
||||
host = os.getenv("HOST", "0.0.0.0")
|
||||
|
||||
logger.info(f"🚀 Démarrage du serveur sur http://{host}:{port}")
|
||||
logger.info(f"🚀 Serveur sur http://{HOST}:{PORT}")
|
||||
logger.info(f" Endpoints:")
|
||||
logger.info(f" - GET /health")
|
||||
logger.info(f" - POST /v1/audio/transcriptions")
|
||||
logger.info(f" - GET /v1/models")
|
||||
|
||||
uvicorn.run(app, host=host, port=port, log_level="info")
|
||||
logger.info(f" - POST /unload (déchargement manuel)")
|
||||
logger.info(f" - POST /reload (rechargement manuel)")
|
||||
logger.info(f" - GET /stats")
|
||||
uvicorn.run(app, host=HOST, port=PORT, log_level="info")
|
||||
|
||||
Reference in New Issue
Block a user