Compare commits

..
Author SHA1 Message Date
kcar 05d8bd284c fix: improve WhisperLive health, model, websocket defaults 2026-06-28 08:26:27 +00:00
kcar 558d96ba1d merge: restore WhisperLive production fixes 2026-05-27 15:51:33 +00:00
kcar 65e8426cf7 fix: tune WhisperLive buffering and CUDA detection 2026-05-27 15:51:33 +00:00
kcar 83b09b7a4a fix: vad query param, session metadata logging, enhanced health endpoint
- Accept use_vad as query param (request.args) with form data fallback
- Session metadata passthrough already implemented (logs session_id, teacher_id)
- Health endpoint already enhanced (model_loaded, gpu_memory_used_gb, active_connections)
2026-05-20 21:38:07 +00:00
5 changed files with 70 additions and 26 deletions
+1 -1
View File
@@ -8,5 +8,5 @@ HTTP_PORT=8080
WHISPERLIVE_SSL=false WHISPERLIVE_SSL=false
WHISPL_USE_CUSTOM_MODEL=false WHISPL_USE_CUSTOM_MODEL=false
FASTERWHISPER_MODEL=faster-whisper-large-v3 FASTERWHISPER_MODEL=large-v3-turbo
WHISPERLIVE_URL=${APP_WS_PROTOCOL}://whisperlive.${APP_URL} WHISPERLIVE_URL=${APP_WS_PROTOCOL}://whisperlive.${APP_URL}
+1
View File
@@ -0,0 +1 @@
hf-cache/
+1
View File
@@ -21,6 +21,7 @@ services:
- ./models:/app/models - ./models:/app/models
- ./ssl:/app/ssl - ./ssl:/app/ssl
- ./logs:/app/logs - ./logs:/app/logs
- ./hf-cache:/root/.cache/huggingface
deploy: deploy:
resources: resources:
reservations: reservations:
+50 -17
View File
@@ -78,27 +78,49 @@ class HybridWhisperServer:
from whisper_live.server import TranscriptionServer from whisper_live.server import TranscriptionServer
self.whisper_server = TranscriptionServer() self.whisper_server = TranscriptionServer()
# Create a shared transcriber instance for HTTP requests # Create a shared transcriber instance for HTTP requests.
# Prefer the configured production model over the previous hard-coded
# base default; faster-whisper accepts either a model size (for example
# large-v3-turbo) or a converted model path.
self.default_model = self.faster_whisper_custom_model_path or os.getenv("FASTERWHISPER_MODEL", "large-v3-turbo")
self.shared_transcriber = None self.shared_transcriber = None
if self.backend == "faster_whisper": if self.backend == "faster_whisper":
from whisper_live.transcriber import WhisperModel from whisper_live.transcriber import WhisperModel
# Use base model as default for HTTP requests self.shared_transcriber = WhisperModel(
model_size = "base" self.default_model,
if self.faster_whisper_custom_model_path: device="cuda",
model_size = self.faster_whisper_custom_model_path compute_type="int8",
self.shared_transcriber = WhisperModel(model_size) )
def setup_routes(self): def setup_routes(self):
@self.app.route('/health', methods=['GET']) @self.app.route('/health', methods=['GET'])
def health_check(): def health_check():
# Get GPU memory from nvidia-smi (GPU 1) # Query the GPUs visible inside the container. Docker CDI maps the
# assigned host GPU to container-local index 0, so hard-coding -i 1
# reports 0.0 even while WhisperLive is using CUDA.
import subprocess import subprocess
gpu_mem_used = 0.0
gpu_mem_total = 0.0
try: try:
gpu_mem = float(subprocess.check_output( output = subprocess.check_output(
'nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits -i 1', shell=True [
).decode().strip()) / 1024.0 'nvidia-smi',
'--query-gpu=memory.used,memory.total',
'--format=csv,noheader,nounits',
],
text=True,
)
for line in output.splitlines():
if not line.strip():
continue
used, total = [float(part.strip()) for part in line.split(',', 1)]
gpu_mem_used += used
gpu_mem_total += total
gpu_mem_used /= 1024.0
gpu_mem_total /= 1024.0
except Exception: except Exception:
gpu_mem = 0.0 pass
# Get active WS connections # Get active WS connections
active = len(self.whisper_server.clients) if hasattr(self.whisper_server, 'clients') else 0 active = len(self.whisper_server.clients) if hasattr(self.whisper_server, 'clients') else 0
@@ -107,7 +129,9 @@ class HybridWhisperServer:
'status': 'healthy', 'status': 'healthy',
'service': 'WhisperLive Hybrid Server', 'service': 'WhisperLive Hybrid Server',
'model_loaded': self.shared_transcriber is not None, 'model_loaded': self.shared_transcriber is not None,
'gpu_memory_used_gb': round(gpu_mem, 1), 'model': self.default_model,
'gpu_memory_used_gb': round(gpu_mem_used, 1),
'gpu_memory_total_gb': round(gpu_mem_total, 1),
'active_connections': active 'active_connections': active
}) })
@@ -859,7 +883,7 @@ print(transcription.text)</code></pre>
language = request.form.get('language', None) language = request.form.get('language', None)
task = request.form.get('task', 'transcribe') # 'transcribe' or 'translate' task = request.form.get('task', 'transcribe') # 'transcribe' or 'translate'
model_size = request.form.get('model', 'base') model_size = request.form.get('model', 'base')
use_vad = request.form.get('use_vad', 'true').lower() == 'true' use_vad = request.args.get('use_vad', request.form.get('use_vad', 'true')).lower() == 'true'
# For now, we'll use the shared transcriber regardless of the requested model size # For now, we'll use the shared transcriber regardless of the requested model size
# In the future, we could create different transcriber instances for different models # In the future, we could create different transcriber instances for different models
@@ -1086,9 +1110,8 @@ print(transcription.text)</code></pre>
# Bridges browser WebSocket connections on the HTTP port (8080) # Bridges browser WebSocket connections on the HTTP port (8080)
# to the internal WhisperLive WebSocket server (port 5000). # to the internal WhisperLive WebSocket server (port 5000).
# This allows live transcription through a single HTTPS port via NPM. # This allows live transcription through a single HTTPS port via NPM.
@self.sock.route('/ws') def handle_ws_bridge(ws):
def ws_bridge(ws): """Bridge WebSocket from HTTP port to internal WhisperLive WS server."""
"""Bridge WebSocket from HTTP port to internal WhisperLive WS server"""
internal_url = f"ws://127.0.0.1:{self.websocket_port}" internal_url = f"ws://127.0.0.1:{self.websocket_port}"
logger.info(f"WebSocket bridge: new connection, proxying to {internal_url}") logger.info(f"WebSocket bridge: new connection, proxying to {internal_url}")
@@ -1143,6 +1166,16 @@ print(transcription.text)</code></pre>
except Exception: except Exception:
pass pass
logger.info("WebSocket bridge: connection closed") logger.info("WebSocket bridge: connection closed")
@self.sock.route('/ws')
def ws_bridge(ws):
"""Canonical WebSocket bridge path for NPM/Cloudflare."""
return handle_ws_bridge(ws)
@self.sock.route('/')
def ws_bridge_root(ws):
"""Compatibility bridge for clients configured with the bare WSS origin."""
return handle_ws_bridge(ws)
def run_websocket_server(self): def run_websocket_server(self):
"""Run the WebSocket server in a separate thread""" """Run the WebSocket server in a separate thread"""
@@ -1188,7 +1221,7 @@ if __name__ == "__main__":
help='Backends from ["tensorrt", "faster_whisper"]') help='Backends from ["tensorrt", "faster_whisper"]')
parser.add_argument('--faster_whisper_custom_model_path', '-fw', parser.add_argument('--faster_whisper_custom_model_path', '-fw',
type=str, default=None, type=str, default=None,
help="Custom Faster Whisper Model") help="Custom Faster Whisper converted model path")
parser.add_argument('--trt_model_path', '-trt', parser.add_argument('--trt_model_path', '-trt',
type=str, type=str,
default=None, default=None,
+17 -8
View File
@@ -427,7 +427,7 @@ class ServeClientBase(object):
self.show_prev_out_thresh = 5 # if pause(no output from whisper) show previous output for 5 seconds self.show_prev_out_thresh = 5 # if pause(no output from whisper) show previous output for 5 seconds
self.add_pause_thresh = 3 # add a blank to segment list as a pause(no speech) for 3 seconds self.add_pause_thresh = 3 # add a blank to segment list as a pause(no speech) for 3 seconds
self.transcript = [] self.transcript = []
self.send_last_n_segments = 10 self.send_last_n_segments = 30
# text formatting # text formatting
self.pick_previous_segments = 2 self.pick_previous_segments = 2
@@ -461,9 +461,9 @@ class ServeClientBase(object):
""" """
self.lock.acquire() self.lock.acquire()
if self.frames_np is not None and self.frames_np.shape[0] > 45*self.RATE: if self.frames_np is not None and self.frames_np.shape[0] > 90*self.RATE:
self.frames_offset += 30.0 self.frames_offset += 60.0
self.frames_np = self.frames_np[int(30*self.RATE):] self.frames_np = self.frames_np[int(60*self.RATE):]
# check timestamp offset(should be >= self.frame_offset) # check timestamp offset(should be >= self.frame_offset)
# this basically means that there is no speech as timestamp offset hasnt updated # this basically means that there is no speech as timestamp offset hasnt updated
# and is less than frame_offset # and is less than frame_offset
@@ -482,7 +482,7 @@ class ServeClientBase(object):
no valid segment for the last 30 seconds from whisper no valid segment for the last 30 seconds from whisper
""" """
with self.lock: with self.lock:
if self.frames_np[int((self.timestamp_offset - self.frames_offset)*self.RATE):].shape[0] > 25 * self.RATE: if self.frames_np[int((self.timestamp_offset - self.frames_offset)*self.RATE):].shape[0] > 60 * self.RATE:
duration = self.frames_np.shape[0] / self.RATE duration = self.frames_np.shape[0] / self.RATE
self.timestamp_offset = self.frames_offset + duration - 5 self.timestamp_offset = self.frames_offset + duration - 5
@@ -807,10 +807,19 @@ class ServeClientFasterWhisper(ServeClientBase):
self.same_output_threshold = 10 self.same_output_threshold = 10
self.end_time_for_same_output = None self.end_time_for_same_output = None
device = "cuda" if torch.cuda.is_available() else "cpu" # torch.cuda.is_available() fails when torch was compiled against a newer CUDA
# than the driver provides. Use ctranslate2's own CUDA probe instead, since
# faster_whisper relies on ctranslate2 — not torch — for inference.
try:
import ctranslate2 as _ct2
_cuda_types = _ct2.get_supported_compute_types("cuda")
device = "cuda" if _cuda_types else "cpu"
except Exception:
device = "cpu"
if device == "cuda": if device == "cuda":
major, _ = torch.cuda.get_device_capability(device) # Use int8 to stay within shared GPU memory budget (GPU 1 is shared with TTS/ComfyUI)
self.compute_type = "float16" if major >= 7 else "float32" self.compute_type = "int8"
else: else:
self.compute_type = "int8" self.compute_type = "int8"