Files
Effi/effi-stt/main.py
T

196 lines
6.3 KiB
Python

# 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
import uvicorn
from faster_whisper import WhisperModel
import tempfile
import os
from typing import Optional
import logging
import threading
import time
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.1.0"
)
MODEL_SIZE = os.getenv("MODEL_SIZE", "distil-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")
model = None
model_loaded = False
model_lock = threading.Lock()
last_request_time = time.time()
unload_timer = None
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", "8"))
)
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():
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(
file: UploadFile = File(..., description="Fichier audio à transcrire"),
model_param: Optional[str] = Form(None, alias="model"),
language: Optional[str] = Form(LANGUAGE, description="Code langue (fr, en, auto)"),
prompt: Optional[str] = Form(None, description="Prompt optionnel"),
response_format: Optional[str] = Form("json", description="Format de réponse")
):
try:
check_and_load_model()
reset_unload_timer()
logger.info(f"📝 Transcription: lang={language}, file={file.filename}")
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: {tmp_path} ({len(content)} bytes)")
try:
with model_lock:
segments, info = model.transcribe(
tmp_path,
language="fr",
task="transcribe",
beam_size=5,
best_of=5,
condition_on_previous_text=True,
initial_prompt="Transcription en français. L'utilisateur parle français."
)
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})")
if response_format == "text":
return full_text
else:
return JSONResponse(content={"text": full_text})
finally:
os.unlink(tmp_path)
except Exception as 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():
return {
"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__":
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")
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")