Explorar o código

Fiabiliser la transcription HTTP et relancer Whisper sur OpenShift

Andy Esnard hai 1 día
pai
achega
0af2b049a5
Modificáronse 4 ficheiros con 95 adicións e 32 borrados
  1. 31 26
      app.py
  2. 20 0
      openshift-deployment-patch.json
  3. 37 0
      openshift.yaml
  4. 7 6
      requirements.txt

+ 31 - 26
app.py

@@ -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()

+ 20 - 0
openshift-deployment-patch.json

@@ -0,0 +1,20 @@
+[
+  {"op":"test","path":"/spec/template/spec/containers/0/name","value":"whisper-stt-chatbot"},
+  {"op":"replace","path":"/spec/replicas","value":1},
+  {"op":"add","path":"/spec/template/spec/serviceAccountName","value":"whisper-stt-chatbot"},
+  {"op":"add","path":"/spec/template/spec/automountServiceAccountToken","value":false},
+  {"op":"add","path":"/spec/strategy","value":{"type":"Recreate"}},
+  {"op":"replace","path":"/spec/template/spec/containers/0/resources","value":{"limits":{"nvidia.com/gpu":"1"}}},
+  {"op":"replace","path":"/spec/template/spec/volumes","value":[{"name":"hf-cache","persistentVolumeClaim":{"claimName":"whisper-model-cache"}}]},
+  {"op":"replace","path":"/spec/template/spec/containers/0/env","value":[
+    {"name":"HF_HOME","value":"/tmp/hf-cache"},
+    {"name":"HUGGINGFACE_HUB_CACHE","value":"/tmp/hf-cache"},
+    {"name":"WHISPER_LOG_LEVEL","value":"INFO"},
+    {"name":"WHISPER_VAD","value":"true"},
+    {"name":"MODEL_SIZE","value":"small"}
+  ]},
+  {"op":"replace","path":"/spec/template/spec/containers/0/args","value":["app:app","--host","0.0.0.0","--port","8000","--log-level","info"]},
+  {"op":"add","path":"/spec/template/spec/containers/0/startupProbe","value":{"httpGet":{"path":"/healthz","port":8000},"periodSeconds":10,"timeoutSeconds":5,"failureThreshold":90}},
+  {"op":"add","path":"/spec/template/spec/containers/0/readinessProbe","value":{"httpGet":{"path":"/healthz","port":8000},"periodSeconds":10,"timeoutSeconds":5}},
+  {"op":"add","path":"/spec/template/spec/containers/0/livenessProbe","value":{"httpGet":{"path":"/healthz","port":8000},"periodSeconds":30,"timeoutSeconds":5,"failureThreshold":5}}
+]

+ 37 - 0
openshift.yaml

@@ -0,0 +1,37 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: whisper-stt-chatbot
+  namespace: localai
+automountServiceAccountToken: false
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  name: whisper-model-cache
+  namespace: localai
+spec:
+  accessModes: [ReadWriteOnce]
+  storageClassName: ocs-storagecluster-ceph-rbd
+  resources:
+    requests:
+      storage: 5Gi
+---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: whisper-stt-ingress
+  namespace: localai
+spec:
+  podSelector:
+    matchLabels:
+      app: whisper-stt-chatbot
+  policyTypes: [Ingress]
+  ingress:
+    - from:
+        - podSelector:
+            matchLabels:
+              app: satoko-chatbot
+      ports:
+        - protocol: TCP
+          port: 8000

+ 7 - 6
requirements.txt

@@ -1,6 +1,7 @@
-fastapi
-uvicorn[standard]
-faster-whisper
-soundfile
-numpy
-python-multipart
+fastapi==0.162.0
+uvicorn[standard]==0.54.0
+faster-whisper==1.2.1
+# CUDA 12 / cuDNN 8 de l'image, compatible avec les GPU Maxwell/Pascal.
+ctranslate2==4.4.0
+numpy<3
+python-multipart==0.0.32