app.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. from fastapi import FastAPI, UploadFile, File, WebSocket, WebSocketDisconnect
  2. from faster_whisper import WhisperModel
  3. import tempfile
  4. import os
  5. import numpy as np
  6. import time
  7. import logging
  8. import os
  9. LOG_LEVEL = os.getenv("WHISPER_LOG_LEVEL", "INFO").upper()
  10. VAD_ENABLED = env_bool("WHISPER_VAD", True)
  11. logging.basicConfig(
  12. level=getattr(logging, LOG_LEVEL, logging.INFO),
  13. format="%(asctime)s [%(levelname)s] %(message)s"
  14. )
  15. logger = logging.getLogger("whisper-ws")
  16. logger.info(f"Log level = {LOG_LEVEL}")
  17. logger.info(f"VAD enabled = {VAD_ENABLED}")
  18. def env_bool(name: str, default: bool = False) -> bool:
  19. return os.getenv(name, str(default)).lower() in ("1", "true", "yes", "on")
  20. app = FastAPI()
  21. # =========================
  22. # Whisper model (shared)
  23. # =========================
  24. model = WhisperModel(
  25. "small",
  26. device="cuda",
  27. compute_type="float32" # GTX 1050 Ti compatible
  28. )
  29. # =========================
  30. # REST API (Swagger / tests)
  31. # =========================
  32. @app.post("/transcribe")
  33. async def transcribe(file: UploadFile = File(...)):
  34. with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
  35. tmp.write(await file.read())
  36. tmp_path = tmp.name
  37. segments, info = model.transcribe(
  38. tmp_path,
  39. language="fr",
  40. vad_filter=True
  41. )
  42. os.remove(tmp_path)
  43. text = " ".join(seg.text for seg in segments)
  44. return {
  45. "text": text,
  46. "language": info.language
  47. }
  48. # =========================
  49. # WebSocket STT (streaming)
  50. # =========================
  51. SAMPLE_RATE = 16000
  52. MIN_AUDIO_SEC = 0.5
  53. MAX_AUDIO_SEC = 10.0
  54. @app.websocket("/ws/transcribe")
  55. async def ws_transcribe(ws: WebSocket):
  56. await ws.accept()
  57. logger.info("WebSocket connecté !")
  58. audio_buffer = []
  59. start_time = time.time()
  60. try:
  61. while True:
  62. data = await ws.receive_bytes()
  63. logger.debug(f"Audio reçu: {len(data)} bytes")
  64. # PCM int16 → float32
  65. pcm = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
  66. audio_buffer.append(pcm)
  67. audio_len = sum(len(chunk) for chunk in audio_buffer) / SAMPLE_RATE
  68. logger.debug(f"Buffer audio: {audio_len:.2f}s")
  69. # Sécurité : reset si trop long
  70. if audio_len > MAX_AUDIO_SEC:
  71. logger.warning("Buffer trop long, reset")
  72. audio_buffer.clear()
  73. start_time = time.time()
  74. continue
  75. # Transcription dès qu'on a assez d'audio
  76. if audio_len >= MIN_AUDIO_SEC:
  77. logger.info(f"Transcription ({audio_len:.2f}s)")
  78. audio = np.concatenate(audio_buffer)
  79. audio_buffer.clear()
  80. segments, _ = model.transcribe(
  81. audio,
  82. language="fr",
  83. vad_filter=VAD_ENABLED
  84. )
  85. text = " ".join(seg.text for seg in segments).strip()
  86. logger.info(f"Texte: '{text}'")
  87. if text:
  88. await ws.send_json({
  89. "type": "final",
  90. "text": text
  91. })
  92. logger.info("Résultat envoyé")
  93. except WebSocketDisconnect:
  94. logger.info("WebSocket déconnecté")
  95. except Exception as e:
  96. logger.exception("Erreur de WebSocket")
  97. await ws.send_json({"error": str(e)})
  98. await ws.close()