Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d8bd284c | ||
|
|
558d96ba1d | ||
|
|
65e8426cf7 | ||
|
|
83b09b7a4a |
@@ -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}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
hf-cache/
|
||||||
@@ -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
@@ -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}")
|
||||||
|
|
||||||
@@ -1144,6 +1167,16 @@ print(transcription.text)</code></pre>
|
|||||||
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"""
|
||||||
logger.info(f"Starting WebSocket server on port {self.websocket_port}")
|
logger.info(f"Starting WebSocket server on port {self.websocket_port}")
|
||||||
@@ -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
@@ -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"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user