|
|
@@ -5,8 +5,10 @@ import time
|
|
|
import logging
|
|
|
import os
|
|
|
import sys
|
|
|
+import asyncio
|
|
|
+from threading import Lock
|
|
|
|
|
|
-from fastapi import FastAPI, UploadFile, File, WebSocket, WebSocketDisconnect
|
|
|
+from fastapi import FastAPI, UploadFile, File, WebSocket, WebSocketDisconnect, HTTPException
|
|
|
from faster_whisper import WhisperModel
|
|
|
|
|
|
def env_bool(name: str, default: bool = False) -> bool:
|
|
|
@@ -37,30 +39,39 @@ app = FastAPI()
|
|
|
# Whisper model (shared)
|
|
|
# =========================
|
|
|
model = WhisperModel(MODEL_SIZE, device="cuda", compute_type="float32")
|
|
|
+model_lock = Lock()
|
|
|
+
|
|
|
+def transcribe_audio(audio):
|
|
|
+ # Consommer le générateur sous le verrou, hors de la boucle asyncio.
|
|
|
+ with model_lock:
|
|
|
+ segments, info = model.transcribe(
|
|
|
+ audio, language="fr", vad_filter=VAD_ENABLED,
|
|
|
+ beam_size=1, condition_on_previous_text=False,
|
|
|
+ )
|
|
|
+ return " ".join(seg.text for seg in segments).strip(), info.language
|
|
|
+
|
|
|
+@app.get("/healthz")
|
|
|
+def healthz():
|
|
|
+ return {"status": "ready", "model": MODEL_SIZE}
|
|
|
|
|
|
# =========================
|
|
|
# REST API (Swagger / tests)
|
|
|
# =========================
|
|
|
@app.post("/transcribe")
|
|
|
-async def transcribe(file: UploadFile = File(...)):
|
|
|
+def transcribe(file: UploadFile = File(...)):
|
|
|
+ # Discord envoie au maximum 20 secondes de WAV stéréo 48 kHz.
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
|
|
- tmp.write(await file.read())
|
|
|
tmp_path = tmp.name
|
|
|
-
|
|
|
- segments, info = model.transcribe(
|
|
|
- tmp_path,
|
|
|
- language="fr",
|
|
|
- vad_filter=True
|
|
|
- )
|
|
|
-
|
|
|
- os.remove(tmp_path)
|
|
|
-
|
|
|
- text = " ".join(seg.text for seg in segments)
|
|
|
-
|
|
|
- return {
|
|
|
- "text": text,
|
|
|
- "language": info.language
|
|
|
- }
|
|
|
+ try:
|
|
|
+ data = file.file.read(8 * 1024 * 1024 + 1)
|
|
|
+ if len(data) > 8 * 1024 * 1024:
|
|
|
+ raise HTTPException(413, "Audio trop volumineux")
|
|
|
+ tmp.write(data)
|
|
|
+ tmp.flush()
|
|
|
+ text, language = transcribe_audio(tmp_path)
|
|
|
+ return {"text": text, "language": language}
|
|
|
+ finally:
|
|
|
+ os.remove(tmp_path)
|
|
|
|
|
|
# =========================
|
|
|
# WebSocket STT (streaming)
|
|
|
@@ -92,13 +103,7 @@ async def ws_transcribe(ws: WebSocket):
|
|
|
duration = len(pcm) / SAMPLE_RATE
|
|
|
logger.info(f"Transcription chunk {duration:.2f}s")
|
|
|
|
|
|
- segments, _ = model.transcribe(
|
|
|
- pcm,
|
|
|
- language="fr",
|
|
|
- vad_filter=VAD_ENABLED
|
|
|
- )
|
|
|
-
|
|
|
- text = " ".join(seg.text for seg in segments).strip()
|
|
|
+ text, _ = await asyncio.to_thread(transcribe_audio, pcm)
|
|
|
|
|
|
logger.info(f"Texte: '{text}'")
|
|
|
|
|
|
@@ -112,4 +117,4 @@ async def ws_transcribe(ws: WebSocket):
|
|
|
logger.info("WebSocket déconnecté")
|
|
|
except Exception:
|
|
|
logger.exception("Erreur WebSocket")
|
|
|
- await ws.close()
|
|
|
+ await ws.close()
|