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:
Folzo
2026-04-12 09:26:29 +02:00
parent e501213959
commit f69dda5e20
6 changed files with 679 additions and 63 deletions
+119 -63
View File
@@ -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")