74 Commits

Author SHA1 Message Date
makaveli f5bea0a693 Bump version v0.6.3 2025-02-26 19:41:56 +05:30
Marcus Edel 5b3bef5845 Merge pull request #341 from makaveli10/fix_py312_pypi_install
Fix setup.py onnxruntime version for py312 pypi installation support.
2025-02-24 05:39:41 -05:00
makaveli10 2c761adc32 Fix setup.py onnxruntime version for py312 pypi installation support
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-02-24 10:41:32 +02:00
makaveli 379bd146fc Bump version v0.6.2 2025-02-07 17:07:13 +05:30
makaveli e93c2823b1 Merge pull request #334 from makaveli10/add_option_to_mute_audio_playback
Add option to mute audio playback for file input
2025-02-06 10:54:18 +05:30
makaveli10 87520498e9 Add option to mute audio playback for file input
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-02-05 20:23:41 +05:30
Marcus Edel 23d71fdbce Merge pull request #333 from makaveli10/add_support_py_312
Add support py 312.
2025-02-05 08:40:55 -05:00
makaveli10 ef7c32dc95 Add python 3.12 to test matrix
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-02-03 19:43:57 +05:30
makaveli10 28be23340b Upgrade onnxruntime version to 1.17.0
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-02-03 19:12:30 +05:30
makaveli ba5aa5aa38 Merge pull request #331 from makaveli10/replace_ffmpeg_with_av_lib
Replace ffmpeg with av lib for resampling, rtsp & hls streams
2025-01-22 22:23:36 +05:30
makaveli10 779baff9c3 Add pynvml missing dep for tensorrt
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-22 05:11:26 -05:00
makaveli10 5aa5826f36 Replace ffmpeg with av lib for resampling, rtsp & hls streams
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-22 05:11:04 -05:00
makaveli 893265bb3f Merge pull request #321 from makaveli10/fix_tensorrt_docker_image
Revert to 12.4.1 base image
2025-01-17 15:55:49 +05:30
makaveli10 5120afbc25 Revert to 12.4.1 base image
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-17 10:19:38 +00:00
makaveli 4baccf75a7 Bump version v0.6.1 2025-01-16 10:43:26 +05:30
makaveli b7acb8c872 Merge pull request #320 from makaveli10/fix_deprecated_package_name
Fix package name
2025-01-16 10:42:44 +05:30
makaveli10 fe7b55efe4 Fix package name
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-16 05:07:13 +00:00
makaveli c1b249ad0d Merge pull request #319 from makaveli10/upgrade_silero_vad_v5
Upgrade silero vad v5
2025-01-13 18:20:13 +05:30
makaveli10 5e4589cfe1 Upgrade silero vad v5.0
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-13 11:37:58 +00:00
makaveli10 b6b73730fb Fix: typo
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-13 11:35:22 +00:00
makaveli 953a88c7da Merge pull request #318 from makaveli10/fix_skipped_audio_chunk
Fix skipped audio chunk
2025-01-13 11:58:17 +05:30
makaveli10 182b5cbd6d Fix skipped audio chunk by recording the time of the first repition of a segment
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-08 13:58:34 +00:00
makaveli 32ba924d8c Bump version v0.6.0 2025-01-07 18:10:03 +05:30
Marcus Edel 450433b07b Merge pull request #316 from makaveli10/fix_data_incosistency
Add lock to thread shared variables updates/reads.
2025-01-06 10:00:43 -05:00
makaveli10 38bff6a901 Update requirements & versions
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-06 07:05:51 +00:00
makaveli10 c936e5f727 Add lock to thread shared variables updates/reads
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-01-06 06:34:02 +00:00
makaveli 18de63c649 Merge pull request #307 from makaveli10/fix_docker_tensorrt
Set docker tesnorrt job timeout to 60 mins
2024-12-18 17:02:16 +05:30
makaveli10 7bcd8b9520 Set docker tesnorrt job timeout to 60 mins
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-12-18 11:19:30 +00:00
Marcus Edel 19c05c8231 Merge pull request #301 from makaveli10/upgrade_tensorrt
Upgrade tensorrt_llm==0.15.0.
2024-12-03 10:04:14 -05:00
makaveli10 49e232bc4d Set segment.completed to False by default
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-12-03 20:09:41 +05:30
makaveli10 30617dfd44 Checkout git tensorrt_llm==v0.15.0 in dockerfile
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-12-03 18:27:11 +05:30
makaveli a55b99c11e Merge pull request #299 from makaveli10/fix-py38-tests
Fix requirements & tests for py38
2024-11-28 15:47:09 +05:30
makaveli10 2725f1aed9 Fix requirements & tests for py38
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-11-28 15:40:30 +05:30
Marcus Edel 53c31f3570 Merge pull request #297 from makaveli10/support_hf_models
Support loading hf models.
2024-11-27 13:54:03 -05:00
Marcus Edel e65fbcd9fc Merge pull request #298 from makaveli10/upgrade_faster_whisper
Upgrade faster_whisper==1.1.0 official release.
2024-11-27 13:53:39 -05:00
makaveli10 7f0c7a6791 Upgrade faster_whisper==1.1.0 official release
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-11-26 21:36:28 +05:30
makaveli10 2eff360b9e Support loading hf models
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-11-26 11:14:18 +05:30
makaveli10 c25a036c02 Upgrade tensorrt_llm to 0.15.0
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-11-21 06:13:00 +00:00
Marcus Edel 446fc6e835 Merge pull request #296 from makaveli10/upgrade_faster_whisper
Upgrade faster-whisper 1.1.0rc0.
2024-11-19 08:27:18 -05:00
makaveli10 a1650eaa4f Fix client tests to write srt file
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-11-19 13:31:55 +05:30
makaveli10 a6523b6b71 Minor fixes for better punctuations
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-11-19 02:13:11 -05:00
makaveli10 e275d34943 Remove pinned tiktoken version from server requirements
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-11-18 13:06:50 +05:30
makaveli10 778a9c5903 Upgrade faster-whisper 1.1.0rc0
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-11-14 10:07:37 -05:00
Marcus Edel 0e89573798 Merge pull request #292 from makaveli10/fix_srt_file_missing_segments
Fix srt file missing segments.
2024-11-05 08:41:44 -05:00
makaveli10 8d89de22d8 Update tests to incorporate the completed boolean in segments
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-11-05 18:12:09 +05:30
makaveli10 81c57ae40c Send completed bool with each segment
Completed bool represents if the segment is completely processed by the server

Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-11-05 18:11:32 +05:30
Marcus Edel 00f0ff1112 Merge pull request #284 from makaveli10/expose_client_manager_args
Expose client manager args.
2024-10-31 15:40:11 -04:00
makaveli10 8b87a0562d Fix unittest to exposed client manager args
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-10-28 17:01:06 +05:30
makaveli10 617fda2864 Update Readme
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-10-10 08:43:21 -04:00
makaveli10 0d74790c67 Expose ClientManager arguments to be passed from client
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-10-10 08:37:45 -04:00
makaveli10 1322dd3c27 Pin openai-whisper version
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-10-10 08:36:21 -04:00
Marcus Edel be71657397 Merge pull request #276 from makaveli10/fix_tensorrt_docker_deps
Upgrade tensorrt-llm==`0.10.0`.
2024-09-20 12:00:09 -04:00
makaveli10 a317597f01 Update README: fix typo
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-09-16 00:32:16 -04:00
makaveli10 aaa47cfab5 Fix requirements & upgrade tensorrt-llm
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-09-16 00:25:23 -04:00
makaveli bc070d6688 Bump version 0.5.1 2024-09-05 09:34:30 +05:30
Marcus Edel 8e7e329a39 Merge pull request #274 from makaveli10/fallback_to_fp32
Set compute_type based on device capability.
2024-09-03 09:17:31 -04:00
makaveli10 380f07394b Set compute_type based on device capability
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-09-03 00:47:38 -04:00
Marcus Edel 30f78a2cc6 Merge pull request #272 from makaveli10/fix_last_segment_init
Initialize last_segment to None.
2024-08-30 12:39:49 -04:00
makaveli10 01c6bc1ecd Initialize last_segment to None
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-08-30 07:47:03 -04:00
Marcus Edel bdaed45820 Merge pull request #262 from makaveli10/discard_no_speech_segments
Discard no speech segments.
2024-08-19 10:12:39 -04:00
makaveli10 4870e9fb9e Make text logging optional
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-08-08 06:09:44 -04:00
makaveli10 ccb183b4d8 Pin torch version to 2.3.0
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-08-08 06:05:44 -04:00
makaveli10 fac62aaccc Fix hallucinations with no_speech_thres
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-08-08 06:05:12 -04:00
makaveli aade67736a Merge pull request #257 from sondt2709/fix-ffmpeg-subprocess-deadlock
Fix deadlock issue in FFmpeg subprocess by ensuring stderr is consumed
2024-07-19 14:49:10 +05:30
Sean Dang abfe830eee Fix deadlock issue in FFmpeg subprocess by ensuring stderr is consumed 2024-07-11 23:30:44 +07:00
makaveli cb392cbb93 Merge pull request #247 from makaveli10/pin_sliero_vad_model_version
Pin silero VAD onnx model version to v4.0
2024-07-09 12:58:27 +05:30
makaveli10 42733da59a Pin numpy version to <2
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-07-02 11:49:30 +05:30
makaveli10 26c517021f Pin silero VAD onnx model version to v4.0
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-07-02 11:01:40 +05:30
makaveli cf721e8b53 Merge pull request #243 from berkaybilik/making_backend_arg_safer
Making backend arg safer
2024-07-02 10:57:58 +05:30
makaveli 5985ec82b6 Merge pull request #236 from t-nil/patch-1
Backslash missing in example
2024-06-30 20:33:07 +05:30
berkaybilik 2f1c934ea2 always use the BackendType enum to reference the backend inside the TranscriptionServer 2024-06-27 00:20:17 +01:00
berkaybilik b220ccb330 fixed reference before assignment error/warning 2024-06-27 00:11:06 +01:00
berkaybilik 5e3906fc7b use enum to validate backend validity in server.run 2024-06-26 23:59:07 +01:00
Florian Meißner a8b9275013 Update README.md 2024-06-15 12:29:36 +02:00
19 changed files with 1590 additions and 614 deletions
+5 -5
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
python-version: [3.8, 3.9, '3.10', 3.11] python-version: [3.8, 3.9, '3.10', 3.11, 3.12]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -35,7 +35,7 @@ jobs:
${{ runner.os }}-pip-${{ matrix.python-version }}- ${{ runner.os }}-pip-${{ matrix.python-version }}-
- name: Install system dependencies - name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y ffmpeg portaudio19-dev run: sudo apt-get update && sudo apt-get install -y portaudio19-dev
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |
@@ -52,7 +52,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
python-version: [3.8, 3.9, '3.10', 3.11] python-version: [3.8, 3.9, '3.10', 3.11, 3.12]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -101,7 +101,7 @@ jobs:
build-and-push-docker-tensorrt: build-and-push-docker-tensorrt:
needs: [run-tests, check-code-format] needs: [run-tests, check-code-format]
timeout-minutes: 20 timeout-minutes: 60
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
steps: steps:
@@ -180,7 +180,7 @@ jobs:
ubuntu-latest-pip-3.8- ubuntu-latest-pip-3.8-
- name: Install system dependencies - name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y ffmpeg portaudio19-dev run: sudo apt-get update && sudo apt-get install -y portaudio19-dev
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |
+19 -8
View File
@@ -12,7 +12,7 @@ to convert speech input into text output. It can be used to transcribe both live
input from microphone and pre-recorded audio files. input from microphone and pre-recorded audio files.
## Installation ## Installation
- Install PyAudio and ffmpeg - Install PyAudio
```bash ```bash
bash scripts/setup.sh bash scripts/setup.sh
``` ```
@@ -36,7 +36,7 @@ python3 run_server.py --port 9090 \
# running with custom model # running with custom model
python3 run_server.py --port 9090 \ python3 run_server.py --port 9090 \
--backend faster_whisper --backend faster_whisper \
-fw "/path/to/custom/faster/whisper/model" -fw "/path/to/custom/faster/whisper/model"
``` ```
@@ -77,6 +77,10 @@ If you don't want this, set `--no_single_model`.
- `use_vad`: Whether to use `Voice Activity Detection` on the server. - `use_vad`: Whether to use `Voice Activity Detection` on the server.
- `save_output_recording`: Set to True to save the microphone input as a `.wav` file during live transcription. This option is helpful for recording sessions for later playback or analysis. Defaults to `False`. - `save_output_recording`: Set to True to save the microphone input as a `.wav` file during live transcription. This option is helpful for recording sessions for later playback or analysis. Defaults to `False`.
- `output_recording_filename`: Specifies the `.wav` file path where the microphone input will be saved if `save_output_recording` is set to `True`. - `output_recording_filename`: Specifies the `.wav` file path where the microphone input will be saved if `save_output_recording` is set to `True`.
- `max_clients`: Specifies the maximum number of clients the server should allow. Defaults to 4.
- `max_connection_time`: Maximum connection time for each client in seconds. Defaults to 600.
- `mute_audio_playback`: Whether to mute audio playback when transcribing an audio file. Defaults to False.
```python ```python
from whisper_live.client import TranscriptionClient from whisper_live.client import TranscriptionClient
client = TranscriptionClient( client = TranscriptionClient(
@@ -84,10 +88,13 @@ client = TranscriptionClient(
9090, 9090,
lang="en", lang="en",
translate=False, translate=False,
model="small", model="small", # also support hf_model => `Systran/faster-whisper-small`
use_vad=False, use_vad=False,
save_output_recording=True, # Only used for microphone input, False by Default save_output_recording=True, # Only used for microphone input, False by Default
output_recording_filename="./output_recording.wav" # Only used for microphone input output_recording_filename="./output_recording.wav", # Only used for microphone input
max_clients=4,
max_connection_time=600,
mute_audio_playback=False, # Only used for file input, False by Default
) )
``` ```
It connects to the server running on localhost at port 9090. Using a multilingual model, language for the transcription will be automatically detected. You can also use the language option to specify the target language for the transcription, in this case, English ("en"). The translate option should be set to `True` if we want to translate from the source language to English and `False` if we want to transcribe in the source language. It connects to the server running on localhost at port 9090. Using a multilingual model, language for the transcription will be automatically detected. You can also use the language option to specify the target language for the transcription, in this case, English ("en"). The translate option should be set to `True` if we want to translate from the source language to English and `False` if we want to transcribe in the source language.
@@ -127,13 +134,17 @@ client(hls_url="http://as-hls-ww-live.akamaized.net/pool_904/live/ww/bbc_1xtra/b
```bash ```bash
docker run -p 9090:9090 --runtime=nvidia --gpus all --entrypoint /bin/bash -it ghcr.io/collabora/whisperlive-tensorrt docker run -p 9090:9090 --runtime=nvidia --gpus all --entrypoint /bin/bash -it ghcr.io/collabora/whisperlive-tensorrt
# Build tiny.en engine # Build small.en engine
bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small.en bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small.en # float16
bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small.en int8 # int8 weight only quantization
bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small.en int4 # int4 weight only quantization
# Run server with tiny.en # Run server with small.en
python3 run_server.py --port 9090 \ python3 run_server.py --port 9090 \
--backend tensorrt \ --backend tensorrt \
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_en" --trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_en_float16"
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_en_int8"
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_en_int4"
``` ```
- CPU - CPU
+6 -10
View File
@@ -1,17 +1,11 @@
# WhisperLive-TensorRT # WhisperLive-TensorRT
We have only tested the TensorRT backend in docker so, we recommend docker for a smooth TensorRT backend setup. We have only tested the TensorRT backend in docker so, we recommend docker for a smooth TensorRT backend setup.
**Note**: We use `tensorrt_llm==0.9.0` **Note**: We use `tensorrt_llm==0.15.0.dev2024111200`
## Installation ## Installation
- Install [docker](https://docs.docker.com/engine/install/) - Install [docker](https://docs.docker.com/engine/install/)
- Install [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) - Install [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
- Clone this repo.
```bash
git clone https://github.com/collabora/WhisperLive.git
cd WhisperLive
```
- Run WhisperLive TensorRT in docker - Run WhisperLive TensorRT in docker
```bash ```bash
docker run -p 9090:9090 --runtime=nvidia --gpus all --entrypoint /bin/bash -it ghcr.io/collabora/whisperlive-tensorrt:latest docker run -p 9090:9090 --runtime=nvidia --gpus all --entrypoint /bin/bash -it ghcr.io/collabora/whisperlive-tensorrt:latest
@@ -21,7 +15,9 @@ docker run -p 9090:9090 --runtime=nvidia --gpus all --entrypoint /bin/bash -it g
- We build `small.en` and `small` multilingual TensorRT engine as examples below. The script logs the path of the directory with Whisper TensorRT engine. We need that model_path to run the server. - We build `small.en` and `small` multilingual TensorRT engine as examples below. The script logs the path of the directory with Whisper TensorRT engine. We need that model_path to run the server.
```bash ```bash
# convert small.en # convert small.en
bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small.en bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small.en # float16
bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small.en int8 # int8 weight only quantization
bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small.en int4 # int4 weight only quantization
# convert small multilingual model # convert small multilingual model
bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small
@@ -32,11 +28,11 @@ bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small
# Run English only model # Run English only model
python3 run_server.py --port 9090 \ python3 run_server.py --port 9090 \
--backend tensorrt \ --backend tensorrt \
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_en" --trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_en_float16"
# Run Multilingual model # Run Multilingual model
python3 run_server.py --port 9090 \ python3 run_server.py --port 9090 \
--backend tensorrt \ --backend tensorrt \
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small" \ --trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_float16" \
--trt_multilingual --trt_multilingual
``` ```
+10 -7
View File
@@ -1,19 +1,22 @@
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 FROM nvidia/cuda:12.4.1-base-ubuntu22.04 AS base
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
python3.10 python3-pip openmpi-bin libopenmpi-dev git wget \ python3.10 python3-pip openmpi-bin libopenmpi-dev git git-lfs wget \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN pip3 install --no-cache-dir -U tensorrt_llm==0.9.0 --extra-index-url https://pypi.nvidia.com FROM base AS devel
RUN pip3 install --no-cache-dir -U tensorrt_llm==0.15.0.dev2024111200 --extra-index-url https://pypi.nvidia.com
WORKDIR /app WORKDIR /app
RUN git clone https://github.com/NVIDIA/TensorRT-LLM.git && cd TensorRT-LLM && \
RUN git clone -b v0.9.0 --depth 1 https://github.com/NVIDIA/TensorRT-LLM.git && \ git checkout c629546ce429623c8a163633095230154a6f0574 && cd ../ && \
mv TensorRT-LLM/examples ./TensorRT-LLM-examples && \ mv TensorRT-LLM/examples ./TensorRT-LLM-examples && \
rm -rf TensorRT-LLM rm -rf TensorRT-LLM
FROM devel AS release
WORKDIR /app
COPY assets/ ./assets COPY assets/ ./assets
RUN wget -nc -P assets/ https://raw.githubusercontent.com/openai/whisper/main/whisper/assets/mel_filters.npz RUN wget -nc -P assets/ https://raw.githubusercontent.com/openai/whisper/main/whisper/assets/mel_filters.npz
@@ -22,7 +25,7 @@ RUN apt update && bash setup.sh && rm setup.sh
COPY requirements/server.txt . COPY requirements/server.txt .
RUN pip install --no-cache-dir -r server.txt && rm server.txt RUN pip install --no-cache-dir -r server.txt && rm server.txt
RUN pip install pynvml==11.5.0
COPY whisper_live ./whisper_live COPY whisper_live ./whisper_live
COPY scripts/build_whisper_tensorrt.sh . COPY scripts/build_whisper_tensorrt.sh .
COPY run_server.py . COPY run_server.py .
+1 -1
View File
@@ -1,4 +1,4 @@
PyAudio PyAudio
ffmpeg-python av
scipy scipy
websocket-client websocket-client
+7 -6
View File
@@ -1,12 +1,13 @@
faster-whisper==1.0.1 faster-whisper==1.1.0
torch
websockets websockets
onnxruntime==1.16.0 onnxruntime==1.17.0
numba numba
openai-whisper
kaldialign kaldialign
soundfile soundfile
ffmpeg-python
scipy scipy
av
jiwer jiwer
evaluate evaluate
numpy<2
openai-whisper==20240930
tokenizers==0.20.3
+51 -6
View File
@@ -38,12 +38,24 @@ download_and_build_model() {
"large-v3" | "large") "large-v3" | "large")
model_url="https://openaipublic.azureedge.net/main/whisper/models/e5b1a55b89c1367dacf97e3e19bfd829a01529dbfdeefa8caeb59b3f1b81dadb/large-v3.pt" model_url="https://openaipublic.azureedge.net/main/whisper/models/e5b1a55b89c1367dacf97e3e19bfd829a01529dbfdeefa8caeb59b3f1b81dadb/large-v3.pt"
;; ;;
"large-v3-turbo" | "turbo")
model_url="https://openaipublic.azureedge.net/main/whisper/models/aff26ae408abcba5fbf8813c21e62b0941638c5f6eebfb145be0c9839262a19a/large-v3-turbo.pt"
;;
*) *)
echo "Invalid model name: $model_name" echo "Invalid model name: $model_name"
exit 1 exit 1
;; ;;
esac esac
if [ "$model_name" == "turbo" ]; then
model_name="large-v3-turbo"
fi
local inference_precision="float16"
local weight_only_precision="${2:-float16}"
local max_beam_width=4
local max_batch_size=1
echo "Downloading $model_name..." echo "Downloading $model_name..."
# wget --directory-prefix=assets "$model_url" # wget --directory-prefix=assets "$model_url"
# echo "Download completed: ${model_name}.pt" # echo "Download completed: ${model_name}.pt"
@@ -54,11 +66,43 @@ download_and_build_model() {
echo "${model_name}.pt already exists in assets directory." echo "${model_name}.pt already exists in assets directory."
fi fi
local output_dir="whisper_${model_name//./_}" local sanitized_model_name="${model_name//./_}"
local checkpoint_dir="whisper_${sanitized_model_name}_weights_${weight_only_precision}"
local output_dir="whisper_${sanitized_model_name}_${weight_only_precision}"
echo "$output_dir" echo "$output_dir"
echo "Running build script for $model_name with output directory $output_dir" echo "Converting model weights for $model_name..."
python3 build.py --output_dir "$output_dir" --use_gpt_attention_plugin --use_gemm_plugin --use_bert_attention_plugin --enable_context_fmha --model_name "$model_name" python3 convert_checkpoint.py \
echo "Whisper $model_name TensorRT engine built." $( [[ "$weight_only_precision" == "int8" || "$weight_only_precision" == "int4" ]] && echo "--use_weight_only --weight_only_precision $weight_only_precision" ) \
--output_dir "$checkpoint_dir" --model_name "$model_name"
echo "Building encoder for $model_name..."
trtllm-build \
--checkpoint_dir "${checkpoint_dir}/encoder" \
--output_dir "${output_dir}/encoder" \
--moe_plugin disable \
--enable_xqa disable \
--max_batch_size "$max_batch_size" \
--gemm_plugin disable \
--bert_attention_plugin "$inference_precision" \
--max_input_len 3000 \
--max_seq_len 3000
echo "Building decoder for $model_name..."
trtllm-build \
--checkpoint_dir "${checkpoint_dir}/decoder" \
--output_dir "${output_dir}/decoder" \
--moe_plugin disable \
--enable_xqa disable \
--max_beam_width "$max_beam_width" \
--max_batch_size "$max_batch_size" \
--max_seq_len 200 \
--max_input_len 14 \
--max_encoder_input_len 3000 \
--gemm_plugin "$inference_precision" \
--bert_attention_plugin "$inference_precision" \
--gpt_attention_plugin "$inference_precision"
echo "TensorRT LLM engine built for $model_name."
echo "=========================================" echo "========================================="
echo "Model is located at: $(pwd)/$output_dir" echo "Model is located at: $(pwd)/$output_dir"
} }
@@ -70,8 +114,9 @@ fi
tensorrt_examples_dir="$1" tensorrt_examples_dir="$1"
model_name="${2:-small.en}" model_name="${2:-small.en}"
weight_only_precision="${3:-float16}" # Default to float16 if not provided
cd $1/whisper cd $tensorrt_examples_dir/whisper
pip install --no-deps -r requirements.txt pip install --no-deps -r requirements.txt
download_and_build_model "$model_name" download_and_build_model "$model_name" "$weight_only_precision"
+1 -1
View File
@@ -1,3 +1,3 @@
#! /bin/bash #! /bin/bash
apt-get install portaudio19-dev ffmpeg wget -y apt-get install portaudio19-dev wget -y
+5 -5
View File
@@ -11,7 +11,7 @@ README = (HERE / "README.md").read_text()
# This call to setup() does all the work # This call to setup() does all the work
setup( setup(
name="whisper-live", name="whisper_live",
version=__version__, version=__version__,
description="A nearly-live implementation of OpenAI's Whisper.", description="A nearly-live implementation of OpenAI's Whisper.",
long_description=README, long_description=README,
@@ -43,18 +43,18 @@ setup(
), ),
install_requires=[ install_requires=[
"PyAudio", "PyAudio",
"faster-whisper==1.0.1", "faster-whisper==1.1.0",
"torch", "torch",
"torchaudio", "torchaudio",
"websockets", "websockets",
"onnxruntime==1.16.0", "onnxruntime==1.17.0",
"ffmpeg-python",
"scipy", "scipy",
"websocket-client", "websocket-client",
"numba", "numba",
"openai-whisper", "openai-whisper==20240930",
"kaldialign", "kaldialign",
"soundfile", "soundfile",
"tokenizers==0.20.3"
], ],
python_requires=">=3.8" python_requires=">=3.8"
) )
+7 -5
View File
@@ -48,7 +48,9 @@ class TestClientCallbacks(BaseTestCase):
"language": self.client.language, "language": self.client.language,
"task": self.client.task, "task": self.client.task,
"model": self.client.model, "model": self.client.model,
"use_vad": True "use_vad": True,
"max_clients": 4,
"max_connection_time": 600,
}) })
self.client.on_open(self.mock_ws_app) self.client.on_open(self.mock_ws_app)
self.mock_ws_app.send.assert_called_with(expected_message) self.mock_ws_app.send.assert_called_with(expected_message)
@@ -66,15 +68,15 @@ class TestClientCallbacks(BaseTestCase):
message = json.dumps({ message = json.dumps({
"uid": self.client.uid, "uid": self.client.uid,
"segments": [ "segments": [
{"start": 0, "end": 1, "text": "Test transcript"}, {"start": 0, "end": 1, "text": "Test transcript", "completed": True},
{"start": 1, "end": 2, "text": "Test transcript 2"}, {"start": 1, "end": 2, "text": "Test transcript 2", "completed": True},
{"start": 2, "end": 3, "text": "Test transcript 3"} {"start": 2, "end": 3, "text": "Test transcript 3", "completed": True}
] ]
}) })
self.client.on_message(self.mock_ws_app, message) self.client.on_message(self.mock_ws_app, message)
# Assert that the transcript was updated correctly # Assert that the transcript was updated correctly
self.assertEqual(len(self.client.transcript), 2) self.assertEqual(len(self.client.transcript), 3)
self.assertEqual(self.client.transcript[1]['text'], "Test transcript 2") self.assertEqual(self.client.transcript[1]['text'], "Test transcript 2")
def test_on_close(self): def test_on_close(self):
+12 -14
View File
@@ -5,10 +5,10 @@ import unittest
from unittest import mock from unittest import mock
import numpy as np import numpy as np
import evaluate import jiwer
from websockets.exceptions import ConnectionClosed from websockets.exceptions import ConnectionClosed
from whisper_live.server import TranscriptionServer from whisper_live.server import TranscriptionServer, BackendType, ClientManager
from whisper_live.client import Client, TranscriptionClient, TranscriptionTeeClient from whisper_live.client import Client, TranscriptionClient, TranscriptionTeeClient
from whisper.normalizers import EnglishTextNormalizer from whisper.normalizers import EnglishTextNormalizer
@@ -16,6 +16,7 @@ from whisper.normalizers import EnglishTextNormalizer
class TestTranscriptionServerInitialization(unittest.TestCase): class TestTranscriptionServerInitialization(unittest.TestCase):
def test_initialization(self): def test_initialization(self):
server = TranscriptionServer() server = TranscriptionServer()
server.client_manager = ClientManager(max_clients=4, max_connection_time=600)
self.assertEqual(server.client_manager.max_clients, 4) self.assertEqual(server.client_manager.max_clients, 4)
self.assertEqual(server.client_manager.max_connection_time, 600) self.assertEqual(server.client_manager.max_connection_time, 600)
self.assertDictEqual(server.client_manager.clients, {}) self.assertDictEqual(server.client_manager.clients, {})
@@ -25,6 +26,7 @@ class TestTranscriptionServerInitialization(unittest.TestCase):
class TestGetWaitTime(unittest.TestCase): class TestGetWaitTime(unittest.TestCase):
def setUp(self): def setUp(self):
self.server = TranscriptionServer() self.server = TranscriptionServer()
self.server.client_manager = ClientManager(max_clients=4, max_connection_time=600)
self.server.client_manager.start_times = { self.server.client_manager.start_times = {
'client1': time.time() - 120, 'client1': time.time() - 120,
'client2': time.time() - 300 'client2': time.time() - 300
@@ -49,7 +51,7 @@ class TestServerConnection(unittest.TestCase):
'task': 'transcribe', 'task': 'transcribe',
'model': 'tiny.en' 'model': 'tiny.en'
}) })
self.server.recv_audio(mock_websocket, "faster_whisper") self.server.recv_audio(mock_websocket, BackendType("faster_whisper"))
@mock.patch('websockets.WebSocketCommonProtocol') @mock.patch('websockets.WebSocketCommonProtocol')
def test_recv_audio_exception_handling(self, mock_websocket): def test_recv_audio_exception_handling(self, mock_websocket):
@@ -61,7 +63,7 @@ class TestServerConnection(unittest.TestCase):
}), np.array([1, 2, 3]).tobytes()] }), np.array([1, 2, 3]).tobytes()]
with self.assertLogs(level="ERROR"): with self.assertLogs(level="ERROR"):
self.server.recv_audio(mock_websocket, "faster_whisper") self.server.recv_audio(mock_websocket, BackendType("faster_whisper"))
self.assertNotIn(mock_websocket, self.server.client_manager.clients) self.assertNotIn(mock_websocket, self.server.client_manager.clients)
@@ -82,7 +84,6 @@ class TestServerInferenceAccuracy(unittest.TestCase):
cls.server_process.wait() cls.server_process.wait()
def setUp(self): def setUp(self):
self.metric = evaluate.load("wer")
self.normalizer = EnglishTextNormalizer() self.normalizer = EnglishTextNormalizer()
def check_prediction(self, srt_path): def check_prediction(self, srt_path):
@@ -94,11 +95,8 @@ class TestServerInferenceAccuracy(unittest.TestCase):
gt_normalized = self.normalizer(gt) gt_normalized = self.normalizer(gt)
# calculate WER # calculate WER
wer = self.metric.compute( wer_score = jiwer.wer(gt_normalized, prediction_normalized)
predictions=[prediction_normalized], self.assertLess(wer_score, 0.05)
references=[gt_normalized]
)
self.assertLess(wer, 0.05)
def test_inference(self): def test_inference(self):
client = TranscriptionClient( client = TranscriptionClient(
@@ -124,10 +122,10 @@ class TestExceptionHandling(unittest.TestCase):
@mock.patch('websockets.WebSocketCommonProtocol') @mock.patch('websockets.WebSocketCommonProtocol')
def test_connection_closed_exception(self, mock_websocket): def test_connection_closed_exception(self, mock_websocket):
mock_websocket.recv.side_effect = ConnectionClosed(1001, "testing connection closed") mock_websocket.recv.side_effect = ConnectionClosed(1001, "testing connection closed", rcvd_then_sent=mock.Mock())
with self.assertLogs(level="INFO") as log: with self.assertLogs(level="INFO") as log:
self.server.recv_audio(mock_websocket, "faster_whisper") self.server.recv_audio(mock_websocket, BackendType("faster_whisper"))
self.assertTrue(any("Connection closed by client" in message for message in log.output)) self.assertTrue(any("Connection closed by client" in message for message in log.output))
@mock.patch('websockets.WebSocketCommonProtocol') @mock.patch('websockets.WebSocketCommonProtocol')
@@ -135,7 +133,7 @@ class TestExceptionHandling(unittest.TestCase):
mock_websocket.recv.return_value = "invalid json" mock_websocket.recv.return_value = "invalid json"
with self.assertLogs(level="ERROR") as log: with self.assertLogs(level="ERROR") as log:
self.server.recv_audio(mock_websocket, "faster_whisper") self.server.recv_audio(mock_websocket, BackendType("faster_whisper"))
self.assertTrue(any("Failed to decode JSON from client" in message for message in log.output)) self.assertTrue(any("Failed to decode JSON from client" in message for message in log.output))
@mock.patch('websockets.WebSocketCommonProtocol') @mock.patch('websockets.WebSocketCommonProtocol')
@@ -143,7 +141,7 @@ class TestExceptionHandling(unittest.TestCase):
mock_websocket.recv.side_effect = RuntimeError("Unexpected error") mock_websocket.recv.side_effect = RuntimeError("Unexpected error")
with self.assertLogs(level="ERROR") as log: with self.assertLogs(level="ERROR") as log:
self.server.recv_audio(mock_websocket, "faster_whisper") self.server.recv_audio(mock_websocket, BackendType("faster_whisper"))
for message in log.output: for message in log.output:
print(message) print(message)
print() print()
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.5.0" __version__ = "0.6.3"
+116 -67
View File
@@ -2,6 +2,7 @@ import os
import shutil import shutil
import wave import wave
import logging
import numpy as np import numpy as np
import pyaudio import pyaudio
import threading import threading
@@ -9,7 +10,7 @@ import json
import websocket import websocket
import uuid import uuid
import time import time
import ffmpeg import av
import whisper_live.utils as utils import whisper_live.utils as utils
@@ -28,7 +29,10 @@ class Client:
translate=False, translate=False,
model="small", model="small",
srt_file_path="output.srt", srt_file_path="output.srt",
use_vad=True use_vad=True,
log_transcription=True,
max_clients=4,
max_connection_time=600,
): ):
""" """
Initializes a Client instance for audio recording and streaming to a server. Initializes a Client instance for audio recording and streaming to a server.
@@ -42,6 +46,12 @@ class Client:
port (int): The port number for the WebSocket server. port (int): The port number for the WebSocket server.
lang (str, optional): The selected language for transcription. Default is None. lang (str, optional): The selected language for transcription. Default is None.
translate (bool, optional): Specifies if the task is translation. Default is False. translate (bool, optional): Specifies if the task is translation. Default is False.
model (str, optional): The whisper model to use (e.g., "small", "medium", "large"). Default is "small".
srt_file_path (str, optional): The file path to save the output SRT file. Default is "output.srt".
use_vad (bool, optional): Whether to enable voice activity detection. Default is True.
log_transcription (bool, optional): Whether to log transcription output to the console. Default is True.
max_clients (int, optional): Maximum number of client connections allowed. Default is 4.
max_connection_time (int, optional): Maximum allowed connection time in seconds. Default is 600.
""" """
self.recording = False self.recording = False
self.task = "transcribe" self.task = "transcribe"
@@ -56,11 +66,13 @@ class Client:
self.use_vad = use_vad self.use_vad = use_vad
self.last_segment = None self.last_segment = None
self.last_received_segment = None self.last_received_segment = None
self.log_transcription = log_transcription
self.max_clients = max_clients
self.max_connection_time = max_connection_time
if translate: if translate:
self.task = "translate" self.task = "translate"
self.timestamp_offset = 0.0
self.audio_bytes = None self.audio_bytes = None
if host is not None and port is not None: if host is not None and port is not None:
@@ -106,9 +118,9 @@ class Client:
for i, seg in enumerate(segments): for i, seg in enumerate(segments):
if not text or text[-1] != seg["text"]: if not text or text[-1] != seg["text"]:
text.append(seg["text"]) text.append(seg["text"])
if i == len(segments) - 1: if i == len(segments) - 1 and not seg.get("completed", False):
self.last_segment = seg self.last_segment = seg
elif (self.server_backend == "faster_whisper" and elif (self.server_backend == "faster_whisper" and seg.get("completed", False) and
(not self.transcript or (not self.transcript or
float(seg['start']) >= float(self.transcript[-1]['end']))): float(seg['start']) >= float(self.transcript[-1]['end']))):
self.transcript.append(seg) self.transcript.append(seg)
@@ -117,10 +129,11 @@ class Client:
self.last_response_received = time.time() self.last_response_received = time.time()
self.last_received_segment = segments[-1]["text"] self.last_received_segment = segments[-1]["text"]
# Truncate to last 3 entries for brevity. if self.log_transcription:
text = text[-3:] # Truncate to last 3 entries for brevity.
utils.clear_screen() text = text[-3:]
utils.print_transcript(text) utils.clear_screen()
utils.print_transcript(text)
def on_message(self, ws, message): def on_message(self, ws, message):
""" """
@@ -196,7 +209,9 @@ class Client:
"language": self.language, "language": self.language,
"task": self.task, "task": self.task,
"model": self.model, "model": self.model,
"use_vad": self.use_vad "use_vad": self.use_vad,
"max_clients": self.max_clients,
"max_connection_time": self.max_connection_time,
} }
) )
) )
@@ -250,7 +265,9 @@ class Client:
""" """
if self.server_backend == "faster_whisper": if self.server_backend == "faster_whisper":
if (self.last_segment): if not self.transcript and self.last_segment is not None:
self.transcript.append(self.last_segment)
elif self.last_segment and self.transcript[-1]["text"] != self.last_segment["text"]:
self.transcript.append(self.last_segment) self.transcript.append(self.last_segment)
utils.create_srt_file(self.transcript, output_path) utils.create_srt_file(self.transcript, output_path)
@@ -274,7 +291,7 @@ class TranscriptionTeeClient:
Attributes: Attributes:
clients (list): the underlying Client instances responsible for handling WebSocket connections. clients (list): the underlying Client instances responsible for handling WebSocket connections.
""" """
def __init__(self, clients, save_output_recording=False, output_recording_filename="./output_recording.wav"): def __init__(self, clients, save_output_recording=False, output_recording_filename="./output_recording.wav", mute_audio_playback=False):
self.clients = clients self.clients = clients
if not self.clients: if not self.clients:
raise Exception("At least one client is required.") raise Exception("At least one client is required.")
@@ -285,6 +302,7 @@ class TranscriptionTeeClient:
self.record_seconds = 60000 self.record_seconds = 60000
self.save_output_recording = save_output_recording self.save_output_recording = save_output_recording
self.output_recording_filename = output_recording_filename self.output_recording_filename = output_recording_filename
self.mute_audio_playback = mute_audio_playback
self.frames = b"" self.frames = b""
self.p = pyaudio.PyAudio() self.p = pyaudio.PyAudio()
try: try:
@@ -380,6 +398,7 @@ class TranscriptionTeeClient:
output=True, output=True,
frames_per_buffer=self.chunk, frames_per_buffer=self.chunk,
) )
chunk_duration = self.chunk / float(wavfile.getframerate())
try: try:
while any(client.recording for client in self.clients): while any(client.recording for client in self.clients):
data = wavfile.readframes(self.chunk) data = wavfile.readframes(self.chunk)
@@ -388,8 +407,11 @@ class TranscriptionTeeClient:
audio_array = self.bytes_to_float_array(data) audio_array = self.bytes_to_float_array(data)
self.multicast_packet(audio_array.tobytes()) self.multicast_packet(audio_array.tobytes())
self.stream.write(data) if self.mute_audio_playback:
time.sleep(chunk_duration)
else:
self.stream.write(data)
wavfile.close() wavfile.close()
for client in self.clients: for client in self.clients:
@@ -410,72 +432,83 @@ class TranscriptionTeeClient:
def process_rtsp_stream(self, rtsp_url): def process_rtsp_stream(self, rtsp_url):
""" """
Connect to an RTSP source, process the audio stream, and send it for trascription. Connect to an RTSP source, process the audio stream, and send it for transcription.
Args: Args:
rtsp_url (str): The URL of the RTSP stream source. rtsp_url (str): The URL of the RTSP stream source.
""" """
process = self.get_rtsp_ffmpeg_process(rtsp_url) print("[INFO]: Connecting to RTSP stream...")
self.handle_ffmpeg_process(process, stream_type='RTSP') try:
container = av.open(rtsp_url, format="rtsp", options={"rtsp_transport": "tcp"})
self.process_av_stream(container, stream_type="RTSP")
except Exception as e:
print(f"[ERROR]: Failed to process RTSP stream: {e}")
finally:
for client in self.clients:
client.wait_before_disconnect()
self.multicast_packet(Client.END_OF_AUDIO.encode('utf-8'), True)
self.close_all_clients()
self.write_all_clients_srt()
print("[INFO]: RTSP stream processing finished.")
def process_hls_stream(self, hls_url, save_file): def process_hls_stream(self, hls_url, save_file=None):
""" """
Connect to an HLS source, process the audio stream, and send it for transcription. Connect to an HLS source, process the audio stream, and send it for transcription.
Args: Args:
hls_url (str): The URL of the HLS stream source. hls_url (str): The URL of the HLS stream source.
save_file str, optional): Local path to save the network stream. save_file (str, optional): Local path to save the network stream.
""" """
process = self.get_hls_ffmpeg_process(hls_url, save_file) print("[INFO]: Connecting to HLS stream...")
self.handle_ffmpeg_process(process, stream_type='HLS')
def handle_ffmpeg_process(self, process, stream_type):
print(f"[INFO]: Connecting to {stream_type} stream...")
try: try:
# Process the stream container = av.open(hls_url, format="hls")
while True: self.process_av_stream(container, stream_type="HLS", save_file=save_file)
in_bytes = process.stdout.read(self.chunk * 2) # 2 bytes per sample
if not in_bytes:
break
audio_array = self.bytes_to_float_array(in_bytes)
self.multicast_packet(audio_array.tobytes())
except Exception as e: except Exception as e:
print(f"[ERROR]: Failed to connect to {stream_type} stream: {e}") print(f"[ERROR]: Failed to process HLS stream: {e}")
finally: finally:
for client in self.clients:
client.wait_before_disconnect()
self.multicast_packet(Client.END_OF_AUDIO.encode('utf-8'), True)
self.close_all_clients() self.close_all_clients()
self.write_all_clients_srt() self.write_all_clients_srt()
if process: print("[INFO]: HLS stream processing finished.")
process.kill()
print(f"[INFO]: {stream_type} stream processing finished.") def process_av_stream(self, container, stream_type, save_file=None):
"""
Process an AV container stream and send audio packets to the server.
def get_rtsp_ffmpeg_process(self, rtsp_url): Args:
return ( container (av.container.InputContainer): The input container to process.
ffmpeg stream_type (str): The type of stream being processed ("RTSP" or "HLS").
.input(rtsp_url, threads=0) save_file (str, optional): Local path to save the stream. Default is None.
.output('-', format='s16le', acodec='pcm_s16le', ac=1, ar=self.rate) """
.run_async(pipe_stdout=True, pipe_stderr=True) audio_stream = next((s for s in container.streams if s.type == "audio"), None)
) if not audio_stream:
print(f"[ERROR]: No audio stream found in {stream_type} source.")
return
def get_hls_ffmpeg_process(self, hls_url, save_file): output_container = None
if save_file is None: if save_file:
process = ( output_container = av.open(save_file, mode="w")
ffmpeg output_audio_stream = output_container.add_stream(codec_name="pcm_s16le", rate=self.rate)
.input(hls_url, threads=0)
.output('-', format='s16le', acodec='pcm_s16le', ac=1, ar=self.rate)
.run_async(pipe_stdout=True, pipe_stderr=True)
)
else:
input = ffmpeg.input(hls_url, threads=0)
output_file = input.output(save_file, acodec='copy', vcodec='copy').global_args('-loglevel', 'quiet')
output_std = input.output('-', format='s16le', acodec='pcm_s16le', ac=1, ar=self.rate)
process = (
ffmpeg.merge_outputs(output_file, output_std)
.run_async(pipe_stdout=True, pipe_stderr=True)
)
return process try:
for packet in container.demux(audio_stream):
for frame in packet.decode():
audio_data = frame.to_ndarray().tobytes()
self.multicast_packet(audio_data)
if save_file:
output_container.mux(frame)
except Exception as e:
print(f"[ERROR]: Error during {stream_type} stream processing: {e}")
finally:
# Wait for server to send any leftover transcription.
time.sleep(5)
self.multicast_packet(Client.END_OF_AUDIO.encode('utf-8'), True)
if output_container:
output_container.close()
container.close()
def save_chunk(self, n_audio_file): def save_chunk(self, n_audio_file):
""" """
@@ -639,10 +672,16 @@ class TranscriptionClient(TranscriptionTeeClient):
host (str): The hostname or IP address of the server. host (str): The hostname or IP address of the server.
port (int): The port number to connect to on the server. port (int): The port number to connect to on the server.
lang (str, optional): The primary language for transcription. Default is None, which defaults to English ('en'). lang (str, optional): The primary language for transcription. Default is None, which defaults to English ('en').
translate (bool, optional): Indicates whether translation tasks are required (default is False). translate (bool, optional): If True, the task will be translation instead of transcription. Default is False.
save_output_recording (bool, optional): Indicates whether to save recording from microphone. model (str, optional): The whisper model to use (e.g., "small", "base"). Default is "small".
output_recording_filename (str, optional): File to save the output recording. use_vad (bool, optional): Whether to enable voice activity detection. Default is True.
output_transcription_path (str, optional): File to save the output transcription. save_output_recording (bool, optional): Whether to save the microphone recording. Default is False.
output_recording_filename (str, optional): Path to save the output recording WAV file. Default is "./output_recording.wav".
output_transcription_path (str, optional): File path to save the output transcription (SRT file). Default is "./output.srt".
log_transcription (bool, optional): Whether to log transcription output to the console. Default is True.
max_clients (int, optional): Maximum number of client connections allowed. Default is 4.
max_connection_time (int, optional): Maximum allowed connection time in seconds. Default is 600.
mute_audio_playback (bool, optional): If True, mutes audio playback during file playback. Default is False.
Attributes: Attributes:
client (Client): An instance of the underlying Client class responsible for handling the WebSocket connection. client (Client): An instance of the underlying Client class responsible for handling the WebSocket connection.
@@ -664,9 +703,18 @@ class TranscriptionClient(TranscriptionTeeClient):
use_vad=True, use_vad=True,
save_output_recording=False, save_output_recording=False,
output_recording_filename="./output_recording.wav", output_recording_filename="./output_recording.wav",
output_transcription_path="./output.srt" output_transcription_path="./output.srt",
log_transcription=True,
max_clients=4,
max_connection_time=600,
mute_audio_playback=False,
): ):
self.client = Client(host, port, lang, translate, model, srt_file_path=output_transcription_path, use_vad=use_vad) self.client = Client(
host, port, lang, translate, model, srt_file_path=output_transcription_path,
use_vad=use_vad, log_transcription=log_transcription, max_clients=max_clients,
max_connection_time=max_connection_time
)
if save_output_recording and not output_recording_filename.endswith(".wav"): if save_output_recording and not output_recording_filename.endswith(".wav"):
raise ValueError(f"Please provide a valid `output_recording_filename`: {output_recording_filename}") raise ValueError(f"Please provide a valid `output_recording_filename`: {output_recording_filename}")
if not output_transcription_path.endswith(".srt"): if not output_transcription_path.endswith(".srt"):
@@ -675,5 +723,6 @@ class TranscriptionClient(TranscriptionTeeClient):
self, self,
[self.client], [self.client],
save_output_recording=save_output_recording, save_output_recording=save_output_recording,
output_recording_filename=output_recording_filename output_recording_filename=output_recording_filename,
mute_audio_playback=mute_audio_playback
) )
+152 -71
View File
@@ -4,6 +4,9 @@ import threading
import json import json
import functools import functools
import logging import logging
from enum import Enum
from typing import List, Optional
import torch import torch
import numpy as np import numpy as np
from websockets.sync.server import serve from websockets.sync.server import serve
@@ -121,11 +124,30 @@ class ClientManager:
return False return False
class BackendType(Enum):
FASTER_WHISPER = "faster_whisper"
TENSORRT = "tensorrt"
@staticmethod
def valid_types() -> List[str]:
return [backend_type.value for backend_type in BackendType]
@staticmethod
def is_valid(backend: str) -> bool:
return backend in BackendType.valid_types()
def is_faster_whisper(self) -> bool:
return self == BackendType.FASTER_WHISPER
def is_tensorrt(self) -> bool:
return self == BackendType.TENSORRT
class TranscriptionServer: class TranscriptionServer:
RATE = 16000 RATE = 16000
def __init__(self): def __init__(self):
self.client_manager = ClientManager() self.client_manager = None
self.no_voice_activity_chunks = 0 self.no_voice_activity_chunks = 0
self.use_vad = True self.use_vad = True
self.single_model = False self.single_model = False
@@ -134,7 +156,9 @@ class TranscriptionServer:
self, websocket, options, faster_whisper_custom_model_path, self, websocket, options, faster_whisper_custom_model_path,
whisper_tensorrt_path, trt_multilingual whisper_tensorrt_path, trt_multilingual
): ):
if self.backend == "tensorrt": client: Optional[ServeClientBase] = None
if self.backend.is_tensorrt():
try: try:
client = ServeClientTensorRT( client = ServeClientTensorRT(
websocket, websocket,
@@ -155,24 +179,31 @@ class TranscriptionServer:
"message": "TensorRT-LLM not supported on Server yet. " "message": "TensorRT-LLM not supported on Server yet. "
"Reverting to available backend: 'faster_whisper'" "Reverting to available backend: 'faster_whisper'"
})) }))
self.backend = "faster_whisper" self.backend = BackendType.FASTER_WHISPER
if self.backend == "faster_whisper": try:
if faster_whisper_custom_model_path is not None and os.path.exists(faster_whisper_custom_model_path): if self.backend.is_faster_whisper():
logging.info(f"Using custom model {faster_whisper_custom_model_path}") if faster_whisper_custom_model_path is not None and os.path.exists(faster_whisper_custom_model_path):
options["model"] = faster_whisper_custom_model_path logging.info(f"Using custom model {faster_whisper_custom_model_path}")
client = ServeClientFasterWhisper( options["model"] = faster_whisper_custom_model_path
websocket, client = ServeClientFasterWhisper(
language=options["language"], websocket,
task=options["task"], language=options["language"],
client_uid=options["uid"], task=options["task"],
model=options["model"], client_uid=options["uid"],
initial_prompt=options.get("initial_prompt"), model=options["model"],
vad_parameters=options.get("vad_parameters"), initial_prompt=options.get("initial_prompt"),
use_vad=self.use_vad, vad_parameters=options.get("vad_parameters"),
single_model=self.single_model, use_vad=self.use_vad,
) single_model=self.single_model,
logging.info("Running faster_whisper backend.") )
logging.info("Running faster_whisper backend.")
except Exception as e:
return
if client is None:
raise ValueError(f"Backend type {self.backend.value} not recognised or not handled.")
self.client_manager.add_client(websocket, client) self.client_manager.add_client(websocket, client)
@@ -197,12 +228,18 @@ class TranscriptionServer:
logging.info("New client connected") logging.info("New client connected")
options = websocket.recv() options = websocket.recv()
options = json.loads(options) options = json.loads(options)
if self.client_manager is None:
max_clients = options.get('max_clients', 4)
max_connection_time = options.get('max_connection_time', 600)
self.client_manager = ClientManager(max_clients, max_connection_time)
self.use_vad = options.get('use_vad') self.use_vad = options.get('use_vad')
if self.client_manager.is_server_full(websocket, options): if self.client_manager.is_server_full(websocket, options):
websocket.close() websocket.close()
return False # Indicates that the connection should not continue return False # Indicates that the connection should not continue
if self.backend == "tensorrt": if self.backend.is_tensorrt():
self.vad_detector = VoiceActivityDetector(frame_rate=self.RATE) self.vad_detector = VoiceActivityDetector(frame_rate=self.RATE)
self.initialize_client(websocket, options, faster_whisper_custom_model_path, self.initialize_client(websocket, options, faster_whisper_custom_model_path,
whisper_tensorrt_path, trt_multilingual) whisper_tensorrt_path, trt_multilingual)
@@ -221,11 +258,11 @@ class TranscriptionServer:
frame_np = self.get_audio_from_websocket(websocket) frame_np = self.get_audio_from_websocket(websocket)
client = self.client_manager.get_client(websocket) client = self.client_manager.get_client(websocket)
if frame_np is False: if frame_np is False:
if self.backend == "tensorrt": if self.backend.is_tensorrt():
client.set_eos(True) client.set_eos(True)
return False return False
if self.backend == "tensorrt": if self.backend.is_tensorrt():
voice_active = self.voice_activity(websocket, frame_np) voice_active = self.voice_activity(websocket, frame_np)
if voice_active: if voice_active:
self.no_voice_activity_chunks = 0 self.no_voice_activity_chunks = 0
@@ -238,7 +275,7 @@ class TranscriptionServer:
def recv_audio(self, def recv_audio(self,
websocket, websocket,
backend="faster_whisper", backend: BackendType = BackendType.FASTER_WHISPER,
faster_whisper_custom_model_path=None, faster_whisper_custom_model_path=None,
whisper_tensorrt_path=None, whisper_tensorrt_path=None,
trt_multilingual=False): trt_multilingual=False):
@@ -311,10 +348,12 @@ class TranscriptionServer:
# TODO: load model initially # TODO: load model initially
else: else:
logging.info("Single model mode currently only works with custom models.") logging.info("Single model mode currently only works with custom models.")
if not BackendType.is_valid(backend):
raise ValueError(f"{backend} is not a valid backend type. Choose backend from {BackendType.valid_types()}")
with serve( with serve(
functools.partial( functools.partial(
self.recv_audio, self.recv_audio,
backend=backend, backend=BackendType(backend),
faster_whisper_custom_model_path=faster_whisper_custom_model_path, faster_whisper_custom_model_path=faster_whisper_custom_model_path,
whisper_tensorrt_path=whisper_tensorrt_path, whisper_tensorrt_path=whisper_tensorrt_path,
trt_multilingual=trt_multilingual trt_multilingual=trt_multilingual
@@ -382,7 +421,7 @@ class ServeClientBase(object):
self.prev_out = '' self.prev_out = ''
self.t_start = None self.t_start = None
self.exit = False self.exit = False
self.same_output_threshold = 0 self.same_output_count = 0
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 = []
@@ -440,9 +479,10 @@ class ServeClientBase(object):
Clip audio if the current chunk exceeds 30 seconds, this basically implies that Clip audio if the current chunk exceeds 30 seconds, this basically implies that
no valid segment for the last 30 seconds from whisper no valid segment for the last 30 seconds from whisper
""" """
if self.frames_np[int((self.timestamp_offset - self.frames_offset)*self.RATE):].shape[0] > 25 * self.RATE: with self.lock:
duration = self.frames_np.shape[0] / self.RATE if self.frames_np[int((self.timestamp_offset - self.frames_offset)*self.RATE):].shape[0] > 25 * self.RATE:
self.timestamp_offset = self.frames_offset + duration - 5 duration = self.frames_np.shape[0] / self.RATE
self.timestamp_offset = self.frames_offset + duration - 5
def get_audio_chunk_for_processing(self): def get_audio_chunk_for_processing(self):
""" """
@@ -458,8 +498,9 @@ class ServeClientBase(object):
- input_bytes (np.ndarray): The next chunk of audio data to be processed. - input_bytes (np.ndarray): The next chunk of audio data to be processed.
- duration (float): The duration of the audio chunk in seconds. - duration (float): The duration of the audio chunk in seconds.
""" """
samples_take = max(0, (self.timestamp_offset - self.frames_offset) * self.RATE) with self.lock:
input_bytes = self.frames_np[int(samples_take):].copy() samples_take = max(0, (self.timestamp_offset - self.frames_offset) * self.RATE)
input_bytes = self.frames_np[int(samples_take):].copy()
duration = input_bytes.shape[0] / self.RATE duration = input_bytes.shape[0] / self.RATE
return input_bytes, duration return input_bytes, duration
@@ -676,7 +717,9 @@ class ServeClientTensorRT(ServeClientBase):
self.transcript.append({"text": last_segment + " "}) self.transcript.append({"text": last_segment + " "})
elif self.transcript[-1]["text"].strip() != last_segment: elif self.transcript[-1]["text"].strip() != last_segment:
self.transcript.append({"text": last_segment + " "}) self.transcript.append({"text": last_segment + " "})
self.timestamp_offset += duration
with self.lock:
self.timestamp_offset += duration
def speech_to_text(self): def speech_to_text(self):
""" """
@@ -745,31 +788,49 @@ class ServeClientFasterWhisper(ServeClientBase):
super().__init__(client_uid, websocket) super().__init__(client_uid, websocket)
self.model_sizes = [ self.model_sizes = [
"tiny", "tiny.en", "base", "base.en", "small", "small.en", "tiny", "tiny.en", "base", "base.en", "small", "small.en",
"medium", "medium.en", "large-v2", "large-v3", "medium", "medium.en", "large-v2", "large-v3", "distil-small.en",
"distil-medium.en", "distil-large-v2", "distil-large-v3",
"large-v3-turbo", "turbo"
] ]
if not os.path.exists(model):
self.model_size_or_path = self.check_valid_model(model) self.model_size_or_path = model
else:
self.model_size_or_path = model
self.language = "en" if self.model_size_or_path.endswith("en") else language self.language = "en" if self.model_size_or_path.endswith("en") else language
self.task = task self.task = task
self.initial_prompt = initial_prompt self.initial_prompt = initial_prompt
self.vad_parameters = vad_parameters or {"threshold": 0.5} self.vad_parameters = vad_parameters or {"onset": 0.5}
self.no_speech_thresh = 0.45 self.no_speech_thresh = 0.45
self.same_output_threshold = 10
self.end_time_for_same_output = None
device = "cuda" if torch.cuda.is_available() else "cpu" device = "cuda" if torch.cuda.is_available() else "cpu"
if device == "cuda":
major, _ = torch.cuda.get_device_capability(device)
self.compute_type = "float16" if major >= 7 else "float32"
else:
self.compute_type = "int8"
if self.model_size_or_path is None: if self.model_size_or_path is None:
return return
logging.info(f"Using Device={device} with precision {self.compute_type}")
if single_model:
if ServeClientFasterWhisper.SINGLE_MODEL is None: try:
self.create_model(device) if single_model:
ServeClientFasterWhisper.SINGLE_MODEL = self.transcriber if ServeClientFasterWhisper.SINGLE_MODEL is None:
self.create_model(device)
ServeClientFasterWhisper.SINGLE_MODEL = self.transcriber
else:
self.transcriber = ServeClientFasterWhisper.SINGLE_MODEL
else: else:
self.transcriber = ServeClientFasterWhisper.SINGLE_MODEL self.create_model(device)
else: except Exception as e:
self.create_model(device) logging.error(f"Failed to load model: {e}")
self.websocket.send(json.dumps({
"uid": self.client_uid,
"status": "ERROR",
"message": f"Failed to load model: {str(self.model_size_or_path)}"
}))
self.websocket.close()
return
self.use_vad = use_vad self.use_vad = use_vad
@@ -793,7 +854,7 @@ class ServeClientFasterWhisper(ServeClientBase):
self.transcriber = WhisperModel( self.transcriber = WhisperModel(
self.model_size_or_path, self.model_size_or_path,
device=device, device=device,
compute_type="int8" if device == "cpu" else "float16", compute_type=self.compute_type,
local_files_only=False, local_files_only=False,
) )
@@ -944,6 +1005,7 @@ class ServeClientFasterWhisper(ServeClientBase):
input_bytes, duration = self.get_audio_chunk_for_processing() input_bytes, duration = self.get_audio_chunk_for_processing()
if duration < 1.0: if duration < 1.0:
time.sleep(0.1) # wait for audio chunks to arrive
continue continue
try: try:
input_sample = input_bytes.copy() input_sample = input_bytes.copy()
@@ -959,7 +1021,7 @@ class ServeClientFasterWhisper(ServeClientBase):
logging.error(f"[ERROR]: Failed to transcribe audio chunk: {e}") logging.error(f"[ERROR]: Failed to transcribe audio chunk: {e}")
time.sleep(0.01) time.sleep(0.01)
def format_segment(self, start, end, text): def format_segment(self, start, end, text, completed=False):
""" """
Formats a transcription segment with precise start and end times alongside the transcribed text. Formats a transcription segment with precise start and end times alongside the transcribed text.
@@ -976,7 +1038,8 @@ class ServeClientFasterWhisper(ServeClientBase):
return { return {
'start': "{:.3f}".format(start), 'start': "{:.3f}".format(start),
'end': "{:.3f}".format(end), 'end': "{:.3f}".format(end),
'text': text 'text': text,
'completed': completed
} }
def update_segments(self, segments, duration): def update_segments(self, segments, duration):
@@ -1002,52 +1065,70 @@ class ServeClientFasterWhisper(ServeClientBase):
""" """
offset = None offset = None
self.current_out = '' self.current_out = ''
last_segment = None
# process complete segments # process complete segments
if len(segments) > 1: if len(segments) > 1 and segments[-1].no_speech_prob <= self.no_speech_thresh:
for i, s in enumerate(segments[:-1]): for i, s in enumerate(segments[:-1]):
text_ = s.text text_ = s.text
self.text.append(text_) self.text.append(text_)
start, end = self.timestamp_offset + s.start, self.timestamp_offset + min(duration, s.end) with self.lock:
start, end = self.timestamp_offset + s.start, self.timestamp_offset + min(duration, s.end)
if start >= end: if start >= end:
continue continue
if s.no_speech_prob > self.no_speech_thresh: if s.no_speech_prob > self.no_speech_thresh:
continue continue
self.transcript.append(self.format_segment(start, end, text_)) self.transcript.append(self.format_segment(start, end, text_, completed=True))
offset = min(duration, s.end) offset = min(duration, s.end)
self.current_out += segments[-1].text # only process the last segment if it satisfies the no_speech_thresh
last_segment = self.format_segment( if segments[-1].no_speech_prob <= self.no_speech_thresh:
self.timestamp_offset + segments[-1].start, self.current_out += segments[-1].text
self.timestamp_offset + min(duration, segments[-1].end), with self.lock:
self.current_out last_segment = self.format_segment(
) self.timestamp_offset + segments[-1].start,
self.timestamp_offset + min(duration, segments[-1].end),
self.current_out,
completed=False
)
if self.current_out.strip() == self.prev_out.strip() and self.current_out != '':
self.same_output_count += 1
# if we remove the audio because of same output on the nth reptition we might remove the
# audio thats not yet transcribed so, capturing the time when it was repeated for the first time
if self.end_time_for_same_output is None:
self.end_time_for_same_output = segments[-1].end
time.sleep(0.1) # wait for some voice activity just in case there is an unitended pause from the speaker for better punctuations.
else:
self.same_output_count = 0
self.end_time_for_same_output = None
# if same incomplete segment is seen multiple times then update the offset # if same incomplete segment is seen multiple times then update the offset
# and append the segment to the list # and append the segment to the list
if self.current_out.strip() == self.prev_out.strip() and self.current_out != '': if self.same_output_count > self.same_output_threshold:
self.same_output_threshold += 1
else:
self.same_output_threshold = 0
if self.same_output_threshold > 5:
if not len(self.text) or self.text[-1].strip().lower() != self.current_out.strip().lower(): if not len(self.text) or self.text[-1].strip().lower() != self.current_out.strip().lower():
self.text.append(self.current_out) self.text.append(self.current_out)
self.transcript.append(self.format_segment( with self.lock:
self.timestamp_offset, self.transcript.append(self.format_segment(
self.timestamp_offset + duration, self.timestamp_offset,
self.current_out self.timestamp_offset + min(duration, self.end_time_for_same_output),
)) self.current_out,
completed=True
))
self.current_out = '' self.current_out = ''
offset = duration offset = min(duration, self.end_time_for_same_output)
self.same_output_threshold = 0 self.same_output_count = 0
last_segment = None last_segment = None
self.end_time_for_same_output = None
else: else:
self.prev_out = self.current_out self.prev_out = self.current_out
# update offset # update offset
if offset is not None: if offset is not None:
self.timestamp_offset += offset with self.lock:
self.timestamp_offset += offset
return last_segment return last_segment
+17 -18
View File
@@ -23,8 +23,12 @@ from typing import Dict, Iterable, List, Optional, TextIO, Tuple, Union
import kaldialign import kaldialign
import numpy as np import numpy as np
import soundfile import soundfile
import av
import wave
import torch import torch
import torch.nn.functional as F import torch.nn.functional as F
from whisper_live.utils import resample
Pathlike = Union[str, Path] Pathlike = Union[str, Path]
@@ -35,38 +39,33 @@ CHUNK_LENGTH = 30
N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE # 480000 samples in a 30-second chunk N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE # 480000 samples in a 30-second chunk
def load_audio(file: str, sr: int = SAMPLE_RATE): def load_audio(file: str, sr: int = 16000):
""" """
Open an audio file and read as mono waveform, resampling as necessary Open an audio file, resample it, and read as a mono waveform.
Parameters Parameters
---------- ----------
file: str file: str
The audio file to open The audio file to open.
sr: int sr: int
The sample rate to resample the audio if necessary The sample rate to resample the audio if necessary.
Returns Returns
------- -------
A NumPy array containing the audio waveform, in float32 dtype. A NumPy array containing the audio waveform, in float32 dtype.
""" """
resampled_file = resample(file, sr)
# This launches a subprocess to decode audio while down-mixing with wave.open(resampled_file, "rb") as wav_file:
# and resampling as necessary. Requires the ffmpeg CLI in PATH. num_frames = wav_file.getnframes()
# fmt: off raw_data = wav_file.readframes(num_frames)
cmd = [
"ffmpeg", "-nostdin", "-threads", "0", "-i", file, "-f", "s16le", "-ac",
"1", "-acodec", "pcm_s16le", "-ar",
str(sr), "-"
]
# fmt: on
try:
out = run(cmd, capture_output=True, check=True).stdout
except CalledProcessError as e:
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
return np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0 audio_data = np.frombuffer(raw_data, dtype=np.int16)
audio_data = audio_data.astype(np.float32) / 32768.0
return audio_data
def load_audio_wav_format(wav_path): def load_audio_wav_format(wav_path):
File diff suppressed because it is too large Load Diff
+150 -67
View File
@@ -1,5 +1,6 @@
import json import json
import re import re
import math
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path from pathlib import Path
from typing import Union from typing import Union
@@ -14,7 +15,8 @@ import tensorrt_llm
import tensorrt_llm.logger as logger import tensorrt_llm.logger as logger
from tensorrt_llm._utils import (str_dtype_to_torch, str_dtype_to_trt, from tensorrt_llm._utils import (str_dtype_to_torch, str_dtype_to_trt,
trt_dtype_to_torch) trt_dtype_to_torch)
from tensorrt_llm.runtime import ModelConfig, SamplingConfig from tensorrt_llm.bindings import GptJsonConfig, KVCacheType
from tensorrt_llm.runtime import PYTHON_BINDINGS, ModelConfig, SamplingConfig
from tensorrt_llm.runtime.session import Session, TensorInfo from tensorrt_llm.runtime.session import Session, TensorInfo
@@ -24,49 +26,101 @@ HOP_LENGTH = 160
CHUNK_LENGTH = 30 CHUNK_LENGTH = 30
N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE # 480000 samples in a 30-second chunk N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE # 480000 samples in a 30-second chunk
def read_config(component, engine_dir):
config_path = engine_dir / component / 'config.json'
with open(config_path, 'r') as f:
config = json.load(f)
model_config = OrderedDict()
model_config.update(config['pretrained_config'])
model_config.update(config['build_config'])
return model_config
def remove_tensor_padding(input_tensor,
input_tensor_lengths=None,
pad_value=None):
if pad_value:
assert input_tensor_lengths is None, "input_tensor_lengths should be None when pad_value is provided"
# Text tensor case: batch, seq_len
assert torch.all(
input_tensor[:, 0] != pad_value
), "First token in each sequence should not be pad_value"
assert input_tensor_lengths is None
# Create a mask for all non-pad tokens
mask = input_tensor != pad_value
# Apply the mask to input_tensor to remove pad tokens
output_tensor = input_tensor[mask].view(1, -1)
else:
# Audio tensor case: batch, seq_len, feature_len
# position_ids case: batch, seq_len
assert input_tensor_lengths is not None, "input_tensor_lengths must be provided for 3D input_tensor"
# Initialize a list to collect valid sequences
valid_sequences = []
for i in range(input_tensor.shape[0]):
valid_length = input_tensor_lengths[i]
valid_sequences.append(input_tensor[i, :valid_length])
# Concatenate all valid sequences along the batch dimension
output_tensor = torch.cat(valid_sequences, dim=0)
return output_tensor
class WhisperEncoding: class WhisperEncoding:
def __init__(self, engine_dir): def __init__(self, engine_dir):
self.session = self.get_session(engine_dir) self.session = self.get_session(engine_dir)
config = read_config('encoder', engine_dir)
self.n_mels = config['n_mels']
self.dtype = config['dtype']
self.num_languages = config['num_languages']
self.encoder_config = config
def get_session(self, engine_dir): def get_session(self, engine_dir):
config_path = engine_dir / 'encoder_config.json' serialize_path = engine_dir / 'encoder' / 'rank0.engine'
with open(config_path, 'r') as f:
config = json.load(f)
use_gpt_attention_plugin = config['plugin_config'][
'gpt_attention_plugin']
dtype = config['builder_config']['precision']
n_mels = config['builder_config']['n_mels']
num_languages = config['builder_config']['num_languages']
self.dtype = dtype
self.n_mels = n_mels
self.num_languages = num_languages
serialize_path = engine_dir / f'whisper_encoder_{self.dtype}_tp1_rank0.engine'
with open(serialize_path, 'rb') as f: with open(serialize_path, 'rb') as f:
session = Session.from_serialized_engine(f.read()) session = Session.from_serialized_engine(f.read())
return session return session
def get_audio_features(self, mel): def get_audio_features(self,
mel,
input_lengths = torch.tensor( mel_input_lengths,
[mel.shape[2] // 2 for _ in range(mel.shape[0])], encoder_downsampling_factor=2):
if isinstance(mel, list):
longest_mel = max([f.shape[-1] for f in mel])
mel = [
torch.nn.functional.pad(f, (0, longest_mel - f.shape[-1]),
mode='constant') for f in mel
]
mel = torch.cat(mel, dim=0).type(
str_dtype_to_torch("float16")).contiguous()
bsz, seq_len = mel.shape[0], mel.shape[2]
position_ids = torch.arange(
math.ceil(seq_len / encoder_downsampling_factor),
dtype=torch.int32, dtype=torch.int32,
device=mel.device) device=mel.device).expand(bsz, -1).contiguous()
if self.encoder_config['plugin_config']['remove_input_padding']:
# mel B,D,T -> B,T,D -> BxT, D
mel = mel.transpose(1, 2)
mel = remove_tensor_padding(mel, mel_input_lengths)
position_ids = remove_tensor_padding(
position_ids, mel_input_lengths // encoder_downsampling_factor)
inputs = OrderedDict() inputs = OrderedDict()
inputs['x'] = mel inputs['input_features'] = mel
inputs['input_lengths'] = input_lengths inputs['input_lengths'] = mel_input_lengths
inputs['position_ids'] = position_ids
output_list = [ output_list = [
TensorInfo('x', str_dtype_to_trt(self.dtype), mel.shape), TensorInfo('input_features', str_dtype_to_trt(self.dtype),
mel.shape),
TensorInfo('input_lengths', str_dtype_to_trt('int32'), TensorInfo('input_lengths', str_dtype_to_trt('int32'),
input_lengths.shape) mel_input_lengths.shape),
TensorInfo('position_ids', str_dtype_to_trt('int32'),
inputs['position_ids'].shape)
] ]
output_info = (self.session).infer_shapes(output_list) output_info = (self.session).infer_shapes(output_list)
@@ -84,48 +138,44 @@ class WhisperEncoding:
stream=stream.cuda_stream) stream=stream.cuda_stream)
assert ok, 'Engine execution failed' assert ok, 'Engine execution failed'
stream.synchronize() stream.synchronize()
audio_features = outputs['output'] encoder_output = outputs['encoder_output']
return audio_features encoder_output_lengths = mel_input_lengths // encoder_downsampling_factor
return encoder_output, encoder_output_lengths
class WhisperDecoding: class WhisperDecoding:
def __init__(self, engine_dir, runtime_mapping, debug_mode=False): def __init__(self, engine_dir, runtime_mapping, debug_mode=False):
self.decoder_config = self.get_config(engine_dir) self.decoder_config = read_config('decoder', engine_dir)
self.decoder_generation_session = self.get_session( self.decoder_generation_session = self.get_session(
engine_dir, runtime_mapping, debug_mode) engine_dir, runtime_mapping, debug_mode)
def get_config(self, engine_dir):
config_path = engine_dir / 'decoder_config.json'
with open(config_path, 'r') as f:
config = json.load(f)
decoder_config = OrderedDict()
decoder_config.update(config['plugin_config'])
decoder_config.update(config['builder_config'])
return decoder_config
def get_session(self, engine_dir, runtime_mapping, debug_mode=False): def get_session(self, engine_dir, runtime_mapping, debug_mode=False):
dtype = self.decoder_config['precision'] serialize_path = engine_dir / 'decoder' / 'rank0.engine'
serialize_path = engine_dir / f'whisper_decoder_{dtype}_tp1_rank0.engine'
with open(serialize_path, "rb") as f: with open(serialize_path, "rb") as f:
decoder_engine_buffer = f.read() decoder_engine_buffer = f.read()
decoder_model_config = ModelConfig( decoder_model_config = ModelConfig(
max_batch_size=self.decoder_config['max_batch_size'], max_batch_size=self.decoder_config['max_batch_size'],
max_beam_width=self.decoder_config['max_beam_width'], max_beam_width=self.decoder_config['max_beam_width'],
num_heads=self.decoder_config['num_heads'], num_heads=self.decoder_config['num_attention_heads'],
num_kv_heads=self.decoder_config['num_heads'], num_kv_heads=self.decoder_config['num_attention_heads'],
hidden_size=self.decoder_config['hidden_size'], hidden_size=self.decoder_config['hidden_size'],
vocab_size=self.decoder_config['vocab_size'], vocab_size=self.decoder_config['vocab_size'],
num_layers=self.decoder_config['num_layers'], cross_attention=True,
gpt_attention_plugin=self.decoder_config['gpt_attention_plugin'], num_layers=self.decoder_config['num_hidden_layers'],
remove_input_padding=self.decoder_config['remove_input_padding'], gpt_attention_plugin=self.decoder_config['plugin_config']
cross_attention=self.decoder_config['cross_attention'], ['gpt_attention_plugin'],
remove_input_padding=self.decoder_config['plugin_config']
['remove_input_padding'],
kv_cache_type=KVCacheType.PAGED
if self.decoder_config['plugin_config']['paged_kv_cache'] == True
else KVCacheType.CONTINUOUS,
has_position_embedding=self. has_position_embedding=self.
decoder_config['has_position_embedding'], decoder_config['has_position_embedding'],
has_token_type_embedding=self. dtype=self.decoder_config['dtype'],
decoder_config['has_token_type_embedding'], has_token_type_embedding=False,
) )
decoder_generation_session = tensorrt_llm.runtime.GenerationSession( decoder_generation_session = tensorrt_llm.runtime.GenerationSession(
decoder_model_config, decoder_model_config,
@@ -138,14 +188,12 @@ class WhisperDecoding:
def generate(self, def generate(self,
decoder_input_ids, decoder_input_ids,
encoder_outputs, encoder_outputs,
encoder_max_input_length,
encoder_input_lengths,
eot_id, eot_id,
max_new_tokens=40, max_new_tokens=40,
num_beams=1): num_beams=1):
encoder_input_lengths = torch.tensor( batch_size = decoder_input_ids.shape[0]
[encoder_outputs.shape[1] for x in range(encoder_outputs.shape[0])],
dtype=torch.int32,
device='cuda')
decoder_input_lengths = torch.tensor([ decoder_input_lengths = torch.tensor([
decoder_input_ids.shape[-1] decoder_input_ids.shape[-1]
for _ in range(decoder_input_ids.shape[0]) for _ in range(decoder_input_ids.shape[0])
@@ -154,10 +202,10 @@ class WhisperDecoding:
device='cuda') device='cuda')
decoder_max_input_length = torch.max(decoder_input_lengths).item() decoder_max_input_length = torch.max(decoder_input_lengths).item()
cross_attention_mask = torch.ones( cross_attention_mask = torch.ones([
[encoder_outputs.shape[0], 1, batch_size, decoder_max_input_length + max_new_tokens,
encoder_outputs.shape[1]]).int().cuda() encoder_max_input_length
]).int().cuda()
# generation config # generation config
sampling_config = SamplingConfig(end_id=eot_id, sampling_config = SamplingConfig(end_id=eot_id,
pad_id=eot_id, pad_id=eot_id,
@@ -167,11 +215,24 @@ class WhisperDecoding:
decoder_max_input_length, decoder_max_input_length,
max_new_tokens, max_new_tokens,
beam_width=num_beams, beam_width=num_beams,
encoder_max_input_length=encoder_outputs.shape[1]) encoder_max_input_length=encoder_max_input_length)
torch.cuda.synchronize() torch.cuda.synchronize()
decoder_input_ids = decoder_input_ids.type(torch.int32).cuda() decoder_input_ids = decoder_input_ids.type(torch.int32).cuda()
if self.decoder_config['plugin_config']['remove_input_padding']:
# 50256 is the index of <pad> for all whisper models' decoder
WHISPER_PAD_TOKEN_ID = 50256
decoder_input_ids = remove_tensor_padding(
decoder_input_ids, pad_value=WHISPER_PAD_TOKEN_ID)
if encoder_outputs.dim() == 3:
encoder_output_lens = torch.full((encoder_outputs.shape[0], ),
encoder_outputs.shape[1],
dtype=torch.int32,
device='cuda')
encoder_outputs = remove_tensor_padding(encoder_outputs,
encoder_output_lens)
output_ids = self.decoder_generation_session.decode( output_ids = self.decoder_generation_session.decode(
decoder_input_ids, decoder_input_ids,
decoder_input_lengths, decoder_input_lengths,
@@ -196,18 +257,23 @@ class WhisperTRTLLM(object):
runtime_mapping = tensorrt_llm.Mapping(world_size, runtime_rank) runtime_mapping = tensorrt_llm.Mapping(world_size, runtime_rank)
torch.cuda.set_device(runtime_rank % runtime_mapping.gpus_per_node) torch.cuda.set_device(runtime_rank % runtime_mapping.gpus_per_node)
engine_dir = Path(engine_dir) engine_dir = Path(engine_dir)
encoder_config = read_config('encoder', engine_dir)
decoder_config = read_config('decoder', engine_dir)
self.n_mels = encoder_config['n_mels']
self.num_languages = encoder_config['num_languages']
is_multilingual = (decoder_config['vocab_size'] >= 51865)
self.encoder = WhisperEncoding(engine_dir) self.encoder = WhisperEncoding(engine_dir)
self.decoder = WhisperDecoding(engine_dir, self.decoder = WhisperDecoding(engine_dir,
runtime_mapping, runtime_mapping,
debug_mode=False) debug_mode=False)
self.n_mels = self.encoder.n_mels self.n_mels = self.encoder.n_mels
# self.tokenizer = get_tokenizer(num_languages=self.encoder.num_languages, # self.tokenizer = get_tokenizer(num_languages=self.encoder.num_languages,
# tokenizer_dir=assets_dir) # tokenizer_dir=assets_dir)
self.device = device self.device = device
self.tokenizer = get_tokenizer( self.tokenizer = get_tokenizer(
is_multilingual, is_multilingual,
num_languages=self.encoder.num_languages, num_languages=self.num_languages,
language=language, language=language,
task=task, task=task,
) )
@@ -274,8 +340,10 @@ class WhisperTRTLLM(object):
def process_batch( def process_batch(
self, self,
mel, mel,
mel_input_lengths,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>", text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
num_beams=1): num_beams=1,
max_new_tokens=96):
prompt_id = self.tokenizer.encode( prompt_id = self.tokenizer.encode(
text_prefix, allowed_special=set(self.tokenizer.special_tokens.keys())) text_prefix, allowed_special=set(self.tokenizer.special_tokens.keys()))
@@ -283,11 +351,14 @@ class WhisperTRTLLM(object):
batch_size = mel.shape[0] batch_size = mel.shape[0]
decoder_input_ids = prompt_id.repeat(batch_size, 1) decoder_input_ids = prompt_id.repeat(batch_size, 1)
encoder_output = self.encoder.get_audio_features(mel) encoder_output, encoder_output_lengths = self.encoder.get_audio_features(mel, mel_input_lengths)
encoder_max_input_length = torch.max(encoder_output_lengths).item()
output_ids = self.decoder.generate(decoder_input_ids, output_ids = self.decoder.generate(decoder_input_ids,
encoder_output, encoder_output,
encoder_max_input_length,
encoder_output_lengths,
self.tokenizer.eot, self.tokenizer.eot,
max_new_tokens=96, max_new_tokens=max_new_tokens,
num_beams=num_beams) num_beams=num_beams)
texts = [] texts = []
for i in range(len(output_ids)): for i in range(len(output_ids)):
@@ -302,10 +373,22 @@ class WhisperTRTLLM(object):
dtype='float16', dtype='float16',
batch_size=1, batch_size=1,
num_beams=1, num_beams=1,
padding_strategy="max",
): ):
mel = mel.type(str_dtype_to_torch(dtype)) mel = mel.type(str_dtype_to_torch(dtype))
mel = mel.unsqueeze(0) mel = mel.unsqueeze(0)
predictions = self.process_batch(mel, text_prefix, num_beams) # repeat the mel spectrogram to match the batch size
mel = mel.repeat(batch_size, 1, 1)
if padding_strategy == "longest":
pass
else:
mel = torch.nn.functional.pad(mel, (0, 3000 - mel.shape[2]))
features_input_lengths = torch.full((mel.shape[0], ),
mel.shape[2],
dtype=torch.int32,
device=mel.device)
predictions = self.process_batch(mel, features_input_lengths, text_prefix, num_beams)
prediction = predictions[0] prediction = predictions[0]
# remove all special tokens in the prediction # remove all special tokens in the prediction
+30 -19
View File
@@ -1,8 +1,9 @@
import os import os
import textwrap import textwrap
import scipy import scipy
import ffmpeg
import numpy as np import numpy as np
import av
from pathlib import Path
def clear_screen(): def clear_screen():
@@ -26,8 +27,8 @@ def format_time(s):
return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}" return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
def create_srt_file(segments, output_file): def create_srt_file(segments, resampled_file):
with open(output_file, 'w', encoding='utf-8') as srt_file: with open(resampled_file, 'w', encoding='utf-8') as srt_file:
segment_number = 1 segment_number = 1
for segment in segments: for segment in segments:
start_time = format_time(float(segment['start'])) start_time = format_time(float(segment['start']))
@@ -43,9 +44,7 @@ def create_srt_file(segments, output_file):
def resample(file: str, sr: int = 16000): def resample(file: str, sr: int = 16000):
""" """
# https://github.com/openai/whisper/blob/7858aa9c08d98f75575035ecd6481f462d66ca27/whisper/audio.py#L22 Resample the audio file to 16kHz.
Open an audio file and read as mono waveform, resampling as necessary,
save the resampled audio
Args: Args:
file (str): The audio file to open file (str): The audio file to open
@@ -54,18 +53,30 @@ def resample(file: str, sr: int = 16000):
Returns: Returns:
resampled_file (str): The resampled audio file resampled_file (str): The resampled audio file
""" """
try: container = av.open(file)
# This launches a subprocess to decode audio while down-mixing and resampling as necessary. stream = next(s for s in container.streams if s.type == 'audio')
# Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
out, _ = (
ffmpeg.input(file, threads=0)
.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=sr)
.run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
)
except ffmpeg.Error as e:
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
np_buffer = np.frombuffer(out, dtype=np.int16)
resampled_file = f"{file.split('.')[0]}_resampled.wav" resampler = av.AudioResampler(
scipy.io.wavfile.write(resampled_file, sr, np_buffer.astype(np.int16)) format='s16',
layout='mono',
rate=sr,
)
resampled_file = Path(file).stem + "_resampled.wav"
output_container = av.open(resampled_file, mode='w')
output_stream = output_container.add_stream('pcm_s16le', rate=sr)
output_stream.layout = 'mono'
for frame in container.decode(audio=0):
frame.pts = None
resampled_frames = resampler.resample(frame)
if resampled_frames is not None:
for resampled_frame in resampled_frames:
for packet in output_stream.encode(resampled_frame):
output_container.mux(packet)
for packet in output_stream.encode(None):
output_container.mux(packet)
output_container.close()
return resampled_file return resampled_file
+30 -15
View File
@@ -1,10 +1,9 @@
# original: https://github.com/snakers4/silero-vad/blob/master/utils_vad.py
import os import os
import subprocess import subprocess
import torch import torch
import numpy as np import numpy as np
import onnxruntime import onnxruntime
import warnings
class VoiceActivityDetection(): class VoiceActivityDetection():
@@ -24,7 +23,11 @@ class VoiceActivityDetection():
self.session = onnxruntime.InferenceSession(path, providers=['CUDAExecutionProvider'], sess_options=opts) self.session = onnxruntime.InferenceSession(path, providers=['CUDAExecutionProvider'], sess_options=opts)
self.reset_states() self.reset_states()
self.sample_rates = [8000, 16000] if '16k' in path:
warnings.warn('This model support only 16000 sampling rate!')
self.sample_rates = [16000]
else:
self.sample_rates = [8000, 16000]
def _validate_input(self, x, sr: int): def _validate_input(self, x, sr: int):
if x.dim() == 1: if x.dim() == 1:
@@ -34,27 +37,32 @@ class VoiceActivityDetection():
if sr != 16000 and (sr % 16000 == 0): if sr != 16000 and (sr % 16000 == 0):
step = sr // 16000 step = sr // 16000
x = x[:, ::step] x = x[:,::step]
sr = 16000 sr = 16000
if sr not in self.sample_rates: if sr not in self.sample_rates:
raise ValueError(f"Supported sampling rates: {self.sample_rates} (or multiply of 16000)") raise ValueError(f"Supported sampling rates: {self.sample_rates} (or multiply of 16000)")
if sr / x.shape[1] > 31.25: if sr / x.shape[1] > 31.25:
raise ValueError("Input audio chunk is too short") raise ValueError("Input audio chunk is too short")
return x, sr return x, sr
def reset_states(self, batch_size=1): def reset_states(self, batch_size=1):
self._h = np.zeros((2, batch_size, 64)).astype('float32') self._state = torch.zeros((2, batch_size, 128)).float()
self._c = np.zeros((2, batch_size, 64)).astype('float32') self._context = torch.zeros(0)
self._last_sr = 0 self._last_sr = 0
self._last_batch_size = 0 self._last_batch_size = 0
def __call__(self, x, sr: int): def __call__(self, x, sr: int):
x, sr = self._validate_input(x, sr) x, sr = self._validate_input(x, sr)
num_samples = 512 if sr == 16000 else 256
if x.shape[-1] != num_samples:
raise ValueError(f"Provided number of samples is {x.shape[-1]} (Supported values: 256 for 8000 sample rate, 512 for 16000)")
batch_size = x.shape[0] batch_size = x.shape[0]
context_size = 64 if sr == 16000 else 32
if not self._last_batch_size: if not self._last_batch_size:
self.reset_states(batch_size) self.reset_states(batch_size)
@@ -63,28 +71,35 @@ class VoiceActivityDetection():
if (self._last_batch_size) and (self._last_batch_size != batch_size): if (self._last_batch_size) and (self._last_batch_size != batch_size):
self.reset_states(batch_size) self.reset_states(batch_size)
if not len(self._context):
self._context = torch.zeros(batch_size, context_size)
x = torch.cat([self._context, x], dim=1)
if sr in [8000, 16000]: if sr in [8000, 16000]:
ort_inputs = {'input': x.numpy(), 'h': self._h, 'c': self._c, 'sr': np.array(sr, dtype='int64')} ort_inputs = {'input': x.numpy(), 'state': self._state.numpy(), 'sr': np.array(sr, dtype='int64')}
ort_outs = self.session.run(None, ort_inputs) ort_outs = self.session.run(None, ort_inputs)
out, self._h, self._c = ort_outs out, state = ort_outs
self._state = torch.from_numpy(state)
else: else:
raise ValueError() raise ValueError()
self._context = x[..., -context_size:]
self._last_sr = sr self._last_sr = sr
self._last_batch_size = batch_size self._last_batch_size = batch_size
out = torch.tensor(out) out = torch.from_numpy(out)
return out return out
def audio_forward(self, x, sr: int, num_samples: int = 512): def audio_forward(self, x, sr: int):
outs = [] outs = []
x, sr = self._validate_input(x, sr) x, sr = self._validate_input(x, sr)
self.reset_states()
num_samples = 512 if sr == 16000 else 256
if x.shape[1] % num_samples: if x.shape[1] % num_samples:
pad_num = num_samples - (x.shape[1] % num_samples) pad_num = num_samples - (x.shape[1] % num_samples)
x = torch.nn.functional.pad(x, (0, pad_num), 'constant', value=0.0) x = torch.nn.functional.pad(x, (0, pad_num), 'constant', value=0.0)
self.reset_states(x.shape[0])
for i in range(0, x.shape[1], num_samples): for i in range(0, x.shape[1], num_samples):
wavs_batch = x[:, i:i+num_samples] wavs_batch = x[:, i:i+num_samples]
out_chunk = self.__call__(wavs_batch, sr) out_chunk = self.__call__(wavs_batch, sr)
@@ -94,7 +109,7 @@ class VoiceActivityDetection():
return stacked.cpu() return stacked.cpu()
@staticmethod @staticmethod
def download(model_url="https://github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx"): def download(model_url="https://github.com/snakers4/silero-vad/raw/v5.0/files/silero_vad.onnx"):
target_dir = os.path.expanduser("~/.cache/whisper-live/") target_dir = os.path.expanduser("~/.cache/whisper-live/")
# Ensure the target directory exists # Ensure the target directory exists
@@ -138,5 +153,5 @@ class VoiceActivityDetector:
bool: True if the speech probability exceeds the threshold, indicating the presence of voice activity; bool: True if the speech probability exceeds the threshold, indicating the presence of voice activity;
False otherwise. False otherwise.
""" """
speech_prob = self.model(torch.from_numpy(audio_frame), self.frame_rate).item() speech_probs = self.model.audio_forward(torch.from_numpy(audio_frame.copy()), self.frame_rate)[0]
return speech_prob > self.threshold return torch.any(speech_probs > self.threshold).item()