app.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import tempfile
  2. import os
  3. import numpy as np
  4. import time
  5. import logging
  6. import os
  7. import sys
  8. import asyncio
  9. from threading import Lock
  10. from fastapi import FastAPI, UploadFile, File, WebSocket, WebSocketDisconnect, HTTPException
  11. from faster_whisper import WhisperModel
  12. def env_bool(name: str, default: bool = False) -> bool:
  13. return os.getenv(name, str(default)).lower() in ("1", "true", "yes", "on")
  14. LOG_LEVEL = os.getenv("WHISPER_LOG_LEVEL", "INFO").upper()
  15. VAD_ENABLED = env_bool("WHISPER_VAD", True)
  16. MODEL_SIZE = str(os.getenv("MODEL_SIZE", "small").lower())
  17. COMPUTE_TYPE = os.getenv("WHISPER_COMPUTE_TYPE", "float32").strip().lower()
  18. MIN_AUDIO_SEC = float(os.getenv("MIN_AUDIO_SEC", "2.0"))
  19. MAX_AUDIO_SEC = float(os.getenv("MAX_AUDIO_SEC", "10.0"))
  20. logging.basicConfig(
  21. level=getattr(logging, LOG_LEVEL, logging.INFO),
  22. format="%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
  23. stream=sys.stdout,
  24. force=True
  25. )
  26. logger = logging.getLogger("whisper-ws")
  27. logger.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
  28. logger.info(f"Log level = {LOG_LEVEL}")
  29. logger.info(f"VAD enabled = {VAD_ENABLED}")
  30. logger.info("Model = %s, compute type = %s", MODEL_SIZE, COMPUTE_TYPE)
  31. app = FastAPI()
  32. # =========================
  33. # Whisper model (shared)
  34. # =========================
  35. model = WhisperModel(MODEL_SIZE, device="cuda", compute_type=COMPUTE_TYPE)
  36. # Vérifier les kernels CUDA avant de déclarer le service prêt.
  37. list(model.transcribe(np.zeros(16000, dtype=np.float32), language="fr", beam_size=1)[0])
  38. model_lock = Lock()
  39. def transcribe_audio(audio):
  40. # Consommer le générateur sous le verrou, hors de la boucle asyncio.
  41. with model_lock:
  42. segments, info = model.transcribe(
  43. audio, language="fr", vad_filter=VAD_ENABLED,
  44. beam_size=1, condition_on_previous_text=False,
  45. )
  46. return " ".join(seg.text for seg in segments).strip(), info.language
  47. @app.get("/healthz")
  48. def healthz():
  49. return {"status": "ready", "model": MODEL_SIZE, "compute_type": COMPUTE_TYPE}
  50. # =========================
  51. # REST API (Swagger / tests)
  52. # =========================
  53. @app.post("/transcribe")
  54. def transcribe(file: UploadFile = File(...)):
  55. # Discord envoie au maximum 20 secondes de WAV stéréo 48 kHz.
  56. with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
  57. tmp_path = tmp.name
  58. try:
  59. data = file.file.read(8 * 1024 * 1024 + 1)
  60. if len(data) > 8 * 1024 * 1024:
  61. raise HTTPException(413, "Audio trop volumineux")
  62. tmp.write(data)
  63. tmp.flush()
  64. text, language = transcribe_audio(tmp_path)
  65. return {"text": text, "language": language}
  66. finally:
  67. os.remove(tmp_path)
  68. # =========================
  69. # WebSocket STT (streaming)
  70. # =========================
  71. SAMPLE_RATE = 16000
  72. @app.websocket("/ws/transcribe")
  73. async def ws_transcribe(ws: WebSocket):
  74. await ws.accept()
  75. logger.info("WebSocket connecté !")
  76. try:
  77. while True:
  78. data = await ws.receive_bytes()
  79. logger.debug(f"Chunk reçu: {len(data)} bytes")
  80. pcm = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
  81. energy = float(np.abs(pcm).mean())
  82. logger.debug(f"Énergie audio = {energy:.6f}")
  83. # silence gate léger
  84. if np.abs(pcm).mean() < 0.00005:
  85. logger.debug("Chunk silencieux ignoré")
  86. continue
  87. duration = len(pcm) / SAMPLE_RATE
  88. logger.info(f"Transcription chunk {duration:.2f}s")
  89. text, _ = await asyncio.to_thread(transcribe_audio, pcm)
  90. logger.info(f"Texte: '{text}'")
  91. if text:
  92. await ws.send_json({
  93. "type": "final",
  94. "text": text
  95. })
  96. except WebSocketDisconnect:
  97. logger.info("WebSocket déconnecté")
  98. except Exception:
  99. logger.exception("Erreur WebSocket")
  100. await ws.close()