| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- import tempfile
- import os
- import numpy as np
- import time
- import logging
- import os
- import sys
- import asyncio
- from threading import Lock
- from fastapi import FastAPI, UploadFile, File, WebSocket, WebSocketDisconnect, HTTPException
- from faster_whisper import WhisperModel
- def env_bool(name: str, default: bool = False) -> bool:
- return os.getenv(name, str(default)).lower() in ("1", "true", "yes", "on")
- LOG_LEVEL = os.getenv("WHISPER_LOG_LEVEL", "INFO").upper()
- VAD_ENABLED = env_bool("WHISPER_VAD", True)
- MODEL_SIZE = str(os.getenv("MODEL_SIZE", "small").lower())
- COMPUTE_TYPE = os.getenv("WHISPER_COMPUTE_TYPE", "float32").strip().lower()
- BEAM_SIZE = int(os.getenv("WHISPER_BEAM_SIZE", "1"))
- if BEAM_SIZE < 1:
- raise ValueError("WHISPER_BEAM_SIZE doit être supérieur ou égal à 1")
- MIN_AUDIO_SEC = float(os.getenv("MIN_AUDIO_SEC", "2.0"))
- MAX_AUDIO_SEC = float(os.getenv("MAX_AUDIO_SEC", "10.0"))
- logging.basicConfig(
- level=getattr(logging, LOG_LEVEL, logging.INFO),
- format="%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
- stream=sys.stdout,
- force=True
- )
- logger = logging.getLogger("whisper-ws")
- logger.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
- logger.info(f"Log level = {LOG_LEVEL}")
- logger.info(f"VAD enabled = {VAD_ENABLED}")
- logger.info("Model = %s, compute type = %s", MODEL_SIZE, COMPUTE_TYPE)
- logger.info("Beam size = %d", BEAM_SIZE)
- app = FastAPI()
- # =========================
- # Whisper model (shared)
- # =========================
- model = WhisperModel(MODEL_SIZE, device="cuda", compute_type=COMPUTE_TYPE)
- # Vérifier les kernels CUDA avant de déclarer le service prêt.
- list(model.transcribe(np.zeros(16000, dtype=np.float32), language="fr", beam_size=BEAM_SIZE)[0])
- 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=BEAM_SIZE, 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, "compute_type": COMPUTE_TYPE, "beam_size": BEAM_SIZE}
- # =========================
- # REST API (Swagger / tests)
- # =========================
- @app.post("/transcribe")
- 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_path = tmp.name
- 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)
- # =========================
- SAMPLE_RATE = 16000
- @app.websocket("/ws/transcribe")
- async def ws_transcribe(ws: WebSocket):
- await ws.accept()
- logger.info("WebSocket connecté !")
- try:
- while True:
- data = await ws.receive_bytes()
- logger.debug(f"Chunk reçu: {len(data)} bytes")
- pcm = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
- energy = float(np.abs(pcm).mean())
- logger.debug(f"Énergie audio = {energy:.6f}")
- # silence gate léger
- if np.abs(pcm).mean() < 0.00005:
- logger.debug("Chunk silencieux ignoré")
- continue
- duration = len(pcm) / SAMPLE_RATE
- logger.info(f"Transcription chunk {duration:.2f}s")
- text, _ = await asyncio.to_thread(transcribe_audio, pcm)
- logger.info(f"Texte: '{text}'")
- if text:
- await ws.send_json({
- "type": "final",
- "text": text
- })
- except WebSocketDisconnect:
- logger.info("WebSocket déconnecté")
- except Exception:
- logger.exception("Erreur WebSocket")
- await ws.close()
|