139 Commits

Author SHA1 Message Date
makaveli 4b46371dac Bump version v0.7.1 2025-05-15 10:25:17 +05:30
makaveli 1f4c918d01 Merge pull request #376 from xXLosKrachosXx/main
Add transcription callback parameter to TranscriptionClient #361
2025-05-15 10:08:59 +05:30
makaveli cd327bab50 Bump version to v0.7.0 2025-05-15 09:47:44 +05:30
Erik b91b3664c2 Add transcription callback parameter to TranscriptionClient #361 2025-05-14 23:03:14 +02:00
Marcus Edel 2375924b45 Merge pull request #375 from makaveli10/update_trt_docs
Update tensorrt_llm docker setup
2025-05-14 11:05:36 -04:00
makaveli ae169245a1 Update tensorrt_llm docker setup
Remove tensorrt build from ci due to space limitations

Signed-off-by: makaveli <vineet.suryan@collabora.com>
2025-05-14 20:29:13 +05:30
makaveli 4ba576fb06 Merge pull request #374 from xXLosKrachosXx/main
Add transcription callback to Client for handling transcription results
2025-05-14 20:19:25 +05:30
makaveli a27ac16d1f Merge pull request #373 from rover0811/main
Add: support for secure WebSocket (WSS) connections
2025-05-13 15:46:37 +05:30
Erik 188b21f1d0 Add transcription callback to Client for handling transcription results 2025-05-12 20:56:04 +02:00
rover0811 d29993048d Fix: Enable support for WebSocket streaming in client.
Added the `use_wss` parameter to allow the client to handle WebSocket-based streaming. This enhances flexibility for real-time transcription scenarios.
2025-05-12 16:42:11 +09:00
rover0811 41d9f683a8 Add: support for secure WebSocket (WSS) connections
Introduce an optional `use_wss` flag to enable secure WebSocket protocol. Updated socket URL generation to dynamically select between `ws` or `wss` based on the flag value. Ensures greater flexibility when connecting to secure servers.
2025-05-12 13:53:41 +09:00
makaveli d9d8d511c7 Merge pull request #367 from giubots/configure-more-params
Add possibility to configure more parameters
2025-05-06 11:29:18 +05:30
giubots 275ed4e45b Merge branch 'main' into configure-more-params 2025-05-02 12:23:45 +02:00
giubots 9cfd8f85b6 test: add new parameters to tests 2025-05-02 11:58:17 +02:00
makaveli 7fb2d356f9 Merge pull request #368 from makaveli10/upgrade_trt_v0_18
Upgrade tensorrt_llm to v0.18.2
2025-04-30 12:19:39 +05:30
makaveli af50fed180 Merge pull request #366 from emmanuel-ferdman/main
Resolve daemon warnings for threading methods
2025-04-30 12:09:03 +05:30
giubots a2271806c3 feat: client sends new parameters to server 2025-04-28 17:21:33 +02:00
giubots 0abf8693ef refactor: include additional parameters
Refactor ServeClientBase and its subclasses to include additional parameters for segment handling and audio clipping.
2025-04-25 13:09:44 +02:00
Emmanuel Ferdman 444a1df740 Resolve daemon warnings for threading methods
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-04-25 00:30:08 -07:00
makaveli10 47ee035f65 Upgrade tensorrt_llm to v0.18.2
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-04-22 12:33:03 +00:00
makaveli d9cb4ffdd0 Merge pull request #359 from makaveli10/remove_blank_segment
Remove blank segment feature
2025-04-22 17:59:10 +05:30
makaveli10 9b364f267a Remove blank segment feature
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-04-17 08:29:14 +00:00
makaveli 617f587699 Merge pull request #354 from makaveli10/remove_audio_clipping
Remove clip_audio from faster_whisper backend
2025-04-15 18:27:07 +05:30
makaveli10 fb3deb2745 Remove clip_audio from faster_whisper backend
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-04-15 08:00:46 +00:00
makaveli 5e430f8154 Merge pull request #353 from Perseus14/patch-1
Fix typo in setup.py
2025-04-15 13:30:30 +05:30
Rishabh Manoj efb51bf0fa Fix typo in setup.py 2025-04-13 00:36:22 +05:30
makaveli 2abca69c9d Merge pull request #348 from makaveli10/integrate_openvino
Integrate openvino
2025-04-08 23:31:53 +05:30
makaveli a62495b090 Integrate OpenVINO backend
Signed-off-by: makaveli <vineet.suryan@collabora.com>
2025-03-31 12:57:19 +05:30
makaveli10 c1ac71ada0 Refactor 🔨
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2025-03-24 16:48:33 +05:30
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
makaveli 815441e8bb Bump version v0.5.0 2024-06-07 11:21:28 +05:30
makaveli 5b9bc2bc0e Merge pull request #223 from peldszus/single-model-mode
Single model mode
2024-06-07 11:10:52 +05:30
Marcus Edel ee132517fa Merge pull request #228 from anshulkharb/patch-1
fix spelling of detection in README.md.
2024-06-05 20:57:44 -04:00
Anshul Kharb 761bb61e87 fix spelling of detection in README.md 2024-06-05 23:14:13 +05:30
Andreas Peldszus 14077315ae Fix argparser option 2024-06-05 10:34:22 +02:00
Andreas Peldszus ab17c4dbc6 Make single model mode the default, update readme 2024-06-05 09:47:52 +02:00
makaveli 5e2421118d Merge pull request #227 from makaveli10/update_tensorrt_llm
Update tensorrt llm to v0.9.0
2024-06-05 08:50:26 +05:30
makaveli d1de2ec3ce Merge pull request #224 from chien-liu/expose-client-srt-location
Expose the srt file location of Transcription client
2024-06-03 21:26:07 +05:30
makaveli10 22a37e7843 Update ci to build and push teensorrt docker image
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-06-03 11:53:27 -04:00
makaveli10 e4579ef291 Dockerfile tensorrt use cuda-runtimee as base image to reduce size
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-06-03 06:44:09 -04:00
makaveli10 f73a146eb9 Update TensorRT backend tensorrt_llm==0.9.0
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-06-03 05:24:41 -04:00
chien-liu cfba5b3e54 Expose the srt file location of Transcription client 2024-06-01 00:14:59 +02:00
Andreas Peldszus 1ac7a278bb Update README 2024-05-31 15:07:01 +02:00
Andreas Peldszus 3a96f60006 Raise error for invalid model paths 2024-05-31 15:06:54 +02:00
Andreas Peldszus 3c09289dea Add single model mode for custom models
- Use a threadlock around the model in single model mode
2024-05-31 15:06:47 +02:00
makaveli e1a42c22d2 Merge pull request #216 from makaveli10/feature/writing_audio_frames_optional
Make writing audio frames optional
2024-05-29 09:10:43 +05:30
makaveli10 3d043dc906 Remove flake8 warning suppression
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-05-28 21:29:38 +05:30
makaveli10 399e9e7efe Fix README typo
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-05-28 11:18:35 +05:30
makaveli10 9d2ea75247 Refactor to make record function more readable
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-05-28 11:06:49 +05:30
makaveli10 225a98be0c Ignore linting as this file is a copy from faster_whisper
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-05-28 11:06:49 +05:30
makaveli10 8f373c3537 Update README
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-05-28 11:06:49 +05:30
makaveli10 61d07edabb Make writing output audio file optional when using microphone
Signed-off-by: makaveli10 <suryanvineet47@gmail.com>
2024-05-28 11:06:49 +05:30
makaveli 03e30e1fed Merge pull request #212 from dshepelev15/feat/RTSP_support
Add support for RTSP stream
2024-05-28 10:48:13 +05:30
makaveli c0a947a8f6 Merge pull request #215 from makaveli10/fix/omp-num-threads
fix: limit CPU usage for VAD onnxruntime inference session by setting…
2024-05-24 23:42:51 +05:30
makaveli10 819ab35b28 fix: limit CPU usage for VAD onnxruntime inference session by setting OMP_NUM_THREADS
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-05-24 04:58:49 -04:00
dshepelev15 615c9c7aed Add support for RTSP stream 2024-05-17 17:32:09 +03:00
makaveli a9683319e0 Merge pull request #192 from fraic/dev1
Add option: save network stream to local file while transcribing
2024-05-05 17:46:39 +05:30
makaveli 0a2d92c5b8 Merge pull request #206 from peldszus/smaller-dockerimages
Improve cpu and gpu Dockerfiles, resulting in much smaller images
2024-05-02 11:02:16 +05:30
Andreas Peldszus dccfce2a3c Improve cpu and gpu Dockerfiles, resulting in much smaller images 2024-04-26 15:51:11 +02:00
fraic f78fc473c5 Add option: save network stream to local file while transcribing 2024-03-25 19:11:24 +08:00
makaveli 0dfbdb2477 bump version v0.4.1 2024-03-22 12:30:42 +05:30
makaveli e171b9c460 Merge pull request #190 from makaveli10/fix_client_close
Fix client close
2024-03-22 12:29:49 +05:30
makaveli10 66b5dc7c15 Merge remote-tracking branch 'upstream/main' into fix_client_close 2024-03-22 12:13:44 +05:30
makaveli10 0f1d36fc06 fix: microphone client close 2024-03-22 12:13:27 +05:30
makaveli d24c53198c Merge pull request #187 from jsichi/preserve-server-error
Don't clear server_error flag on close.
2024-03-22 12:12:16 +05:30
John Sichi fe1640695c Don't clear server_error flag on close. 2024-03-21 13:28:18 +09:00
33 changed files with 4148 additions and 2476 deletions
+39 -10
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.9, '3.10', 3.11, 3.12]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -25,7 +25,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Cache Python dependencies - name: Cache Python dependencies
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: | path: |
~/.cache/pip ~/.cache/pip
@@ -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.9, '3.10', 3.11, 3.12]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -128,6 +128,35 @@ jobs:
push: true push: true
tags: ghcr.io/collabora/whisperlive-gpu:latest tags: ghcr.io/collabora/whisperlive-gpu:latest
build-and-push-docker-openvino:
needs: [run-tests, check-code-format, build-and-push-docker-cpu]
timeout-minutes: 20
runs-on: ubuntu-22.04
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
steps:
- uses: actions/checkout@v2
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Docker Prune
run: docker system prune -af
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push Docker GPU image
uses: docker/build-push-action@v2
with:
context: .
file: docker/Dockerfile.openvino
push: true
tags: ghcr.io/collabora/whisperlive-openvino:latest
publish-to-pypi: publish-to-pypi:
needs: [run-tests, check-code-format] needs: [run-tests, check-code-format]
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@@ -135,23 +164,23 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.8 - name: Set up Python 3.9
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.9
- name: Cache Python dependencies - name: Cache Python dependencies
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: | path: |
~/.cache/pip ~/.cache/pip
!~/.cache/pip/log !~/.cache/pip/log
key: ubuntu-latest-pip-3.8-${{ hashFiles('requirements/server.txt', 'requirements/client.txt') }} key: ubuntu-latest-pip-3.9-${{ hashFiles('requirements/server.txt', 'requirements/client.txt') }}
restore-keys: | restore-keys: |
ubuntu-latest-pip-3.8- ubuntu-latest-pip-3.9-
- 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: |
+88 -14
View File
@@ -11,8 +11,18 @@ This project is a real-time transcription application that uses the OpenAI Whisp
to convert speech input into text output. It can be used to transcribe both live audio to convert speech input into text output. It can be used to transcribe both live audio
input from microphone and pre-recorded audio files. input from microphone and pre-recorded audio files.
- [Installation](#installation)
- [Getting Started](#getting-started)
- [Running the Server](#running-the-server)
- [Running the Client](#running-the-client)
- [Browser Extensions](#browser-extensions)
- [Whisper Live Server in Docker](#whisper-live-server-in-docker)
- [Future Work](#future-work)
- [Contact](#contact)
- [Citations](#citations)
## Installation ## Installation
- Install PyAudio and ffmpeg - Install PyAudio
```bash ```bash
bash scripts/setup.sh bash scripts/setup.sh
``` ```
@@ -26,7 +36,7 @@ input from microphone and pre-recorded audio files.
- Please follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md) for setup of [NVIDIA/TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) and for building Whisper-TensorRT engine. - Please follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md) for setup of [NVIDIA/TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) and for building Whisper-TensorRT engine.
## Getting Started ## Getting Started
The server supports two backends `faster_whisper` and `tensorrt`. If running `tensorrt` backend follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md) The server supports 3 backends `faster_whisper`, `tensorrt` and `openvino`. If running `tensorrt` backend follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md)
### Running the Server ### Running the Server
- [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) backend - [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) backend
@@ -36,7 +46,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"
``` ```
@@ -54,9 +64,43 @@ python3 run_server.py -p 9090 \
-m -m
``` ```
- WhisperLive now supports the [OpenVINO](https://github.com/openvinotoolkit/openvino) backend for efficient inference on Intel CPUs, iGPU and dGPUs. Currently, we tested the models uploaded to [huggingface by OpenVINO](https://huggingface.co/OpenVINO?search_models=whisper).
- > **Docker Recommended:** Running WhisperLive with OpenVINO inside Docker automatically enables GPU support (iGPU/dGPU) without requiring additional host setup.
- > **Native (non-Docker) Use:** If you prefer running outside Docker, ensure the Intel drivers and OpenVINO runtime are installed and properly configured on your system. Refer to the documentation for [installing OpenVINO](https://docs.openvino.ai/2025/get-started/install-openvino.html?PACKAGE=OPENVINO_BASE&VERSION=v_2025_0_0&OP_SYSTEM=LINUX&DISTRIBUTION=PIP#).
```
python3 run_server.py -p 9090 -b openvino
```
#### Controlling OpenMP Threads
To control the number of threads used by OpenMP, you can set the `OMP_NUM_THREADS` environment variable. This is useful for managing CPU resources and ensuring consistent performance. If not specified, `OMP_NUM_THREADS` is set to `1` by default. You can change this by using the `--omp_num_threads` argument:
```bash
python3 run_server.py --port 9090 \
--backend faster_whisper \
--omp_num_threads 4
```
#### Single model mode
By default, when running the server without specifying a model, the server will instantiate a new whisper model for every client connection. This has the advantage, that the server can use different model sizes, based on the client's requested model size. On the other hand, it also means you have to wait for the model to be loaded upon client connection and you will have increased (V)RAM usage.
When serving a custom TensorRT model using the `-trt` or a custom faster_whisper model using the `-fw` option, the server will instead only instantiate the custom model once and then reuse it for all client connections.
If you don't want this, set `--no_single_model`.
### Running the Client ### Running the Client
- Initializing the client: - Initializing the client with below parameters:
- `lang`: Language of the input audio, applicable only if using a multilingual model.
- `translate`: If set to `True` then translate from any language to `en`.
- `model`: Whisper model size.
- `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`.
- `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(
@@ -64,13 +108,18 @@ 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
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.
- Trancribe an audio file: - Transcribe an audio file:
```python ```python
client("tests/jfk.wav") client("tests/jfk.wav")
``` ```
@@ -80,14 +129,19 @@ client("tests/jfk.wav")
client() client()
``` ```
- To transcribe from a RTSP stream:
```python
client(rtsp_url="rtsp://admin:admin@192.168.0.1/rtsp")
```
- To transcribe from a HLS stream: - To transcribe from a HLS stream:
```python ```python
client(hls_url="http://as-hls-ww-live.akamaized.net/pool_904/live/ww/bbc_1xtra/bbc_1xtra.isml/bbc_1xtra-audio%3d96000.norewind.m3u8") client(hls_url="http://as-hls-ww-live.akamaized.net/pool_904/live/ww/bbc_1xtra/bbc_1xtra.isml/bbc_1xtra-audio%3d96000.norewind.m3u8")
``` ```
## Browser Extensions ## Browser Extensions
- Run the server with your desired backend as shown [here](https://github.com/collabora/WhisperLive?tab=readme-ov-file#running-the-server). - Run the server with your desired backend as shown [here](https://github.com/collabora/WhisperLive?tab=readme-ov-file#running-the-server).
- Transcribe audio directly from your browser using our Chrome or Firefox extensions. Refer to [Audio-Transcription-Chrome](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Chrome#readme) and [Audio-Transcription-Firefox](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Firefox#readme) for setup instructions. - Transcribe audio directly from your browser using our Chrome or Firefox extensions. Refer to [Audio-Transcription-Chrome](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Chrome#readme) and https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md
## Whisper Live Server in Docker ## Whisper Live Server in Docker
- GPU - GPU
@@ -96,17 +150,37 @@ client(hls_url="http://as-hls-ww-live.akamaized.net/pool_904/live/ww/bbc_1xtra/b
docker run -it --gpus all -p 9090:9090 ghcr.io/collabora/whisperlive-gpu:latest docker run -it --gpus all -p 9090:9090 ghcr.io/collabora/whisperlive-gpu:latest
``` ```
- TensorRT. Follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md) in order to setup docker and use TensorRT backend. We provide a pre-built docker image which has TensorRT-LLM built and ready to use. - TensorRT. Refer to [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md) for setup and more tensorrt backend configurations.
```bash
docker build . -f docker/Dockerfile.tensorrt -t whisperlive-tensorrt
docker run -p 9090:9090 --runtime=nvidia --gpus all --entrypoint /bin/bash -it whisperlive-tensorrt
# Build small.en engine
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 small.en
python3 run_server.py --port 9090 \
--backend tensorrt \
--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"
```
- OpenVINO
```
docker run -it --device=/dev/dri -p 9090:9090 ghcr.io/collabora/whisperlive-openvino
```
- CPU - CPU
```bash - Faster-whisper
docker run -it -p 9090:9090 ghcr.io/collabora/whisperlive-cpu:latest ```bash
``` docker run -it -p 9090:9090 ghcr.io/collabora/whisperlive-cpu:latest
**Note**: By default we use "small" model size. To build docker image for a different model size, change the size in server.py and then build the docker image. ```
## Future Work ## Future Work
- [ ] Add translation to other languages on top of transcription. - [ ] Add translation to other languages on top of transcription.
- [x] TensorRT backend for Whisper.
## Contact ## Contact
+20 -40
View File
@@ -1,67 +1,47 @@
# Whisper-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 [our fork to setup TensorRT](https://github.com/makaveli10/TensorRT-LLM) **Note**: We use `tensorrt_llm==0.18.2`
## 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. - Run WhisperLive TensorRT in docker
```bash ```bash
git clone https://github.com/collabora/WhisperLive.git docker build . -f docker/Dockerfile.tensorrt -t whisperlive-tensorrt
cd WhisperLive docker run -p 9090:9090 --runtime=nvidia --gpus all --entrypoint /bin/bash -it whisperlive-tensorrt
``` ```
- Pull the TensorRT-LLM docker image which we prebuilt for WhisperLive TensorRT backend.
```bash
docker pull ghcr.io/collabora/whisperbot-base:latest
```
- Next, we run the docker image and mount WhisperLive repo to the containers `/home` directory.
```bash
docker run -it --gpus all --shm-size=8g \
--ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \
-p 9090:9090 -v /path/to/WhisperLive:/home/WhisperLive \
ghcr.io/collabora/whisperbot-base:latest
```
- Make sure to test the installation.
```bash
# export ENV=${ENV:-/etc/shinit_v2}
# source $ENV
python -c "import torch; import tensorrt; import tensorrt_llm"
```
**NOTE**: Uncomment and update library paths if imports fail.
## Whisper TensorRT Engine ## Whisper TensorRT Engine
- We build `small.en` and `small` multilingual TensorRT engine. The script logs the path of the directory with Whisper TensorRT engine. We need the 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 scripts/build_whisper_tensorrt.sh /root/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 scripts/build_whisper_tensorrt.sh /root/TensorRT-LLM-examples small bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small
``` ```
## Run WhisperLive Server with TensorRT Backend ## Run WhisperLive Server with TensorRT Backend
```bash ```bash
cd /home/WhisperLive
# Install requirements
apt update && bash scripts/setup.sh
pip install -r requirements/server.txt
# Required to create mel spectogram
wget --directory-prefix=assets assets/mel_filters.npz https://raw.githubusercontent.com/openai/whisper/main/whisper/assets/mel_filters.npz
# 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 "path/to/whisper_trt/from/build/step" --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 "path/to/whisper_trt/from/build/step" \ --trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_float16" \
--trt_multilingual --trt_multilingual
``` ```
By default trt_backend uses cpp_session, to use python session pass `--trt_py_session` to run_server.py
```bash
python3 run_server.py --port 9090 \
--backend tensorrt \
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_float16" \
--trt_py_session
```
+13 -12
View File
@@ -1,22 +1,23 @@
FROM python:3.8-slim-buster FROM python:3.10-bookworm
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \ # install lib required for pyaudio
curl \ RUN apt update && apt install -y portaudio19-dev && apt-get clean && rm -rf /var/lib/apt/lists/*
ca-certificates \
sudo \
git \
bzip2 \
libx11-6 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# update pip to support for whl.metadata -> less downloading
RUN pip install --no-cache-dir -U "pip>=24"
# create a working directory
RUN mkdir /app
WORKDIR /app WORKDIR /app
COPY scripts/setup.sh requirements/server.txt /app/ # install pytorch, but without the nvidia-libs that are only necessary for gpu
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
RUN apt update && bash setup.sh && pip install -r server.txt # install the requirements for running the whisper-live server
COPY requirements/server.txt /app/
RUN pip install --no-cache-dir -r server.txt && rm server.txt
COPY whisper_live /app/whisper_live COPY whisper_live /app/whisper_live
COPY run_server.py /app COPY run_server.py /app
+14 -21
View File
@@ -1,33 +1,26 @@
FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04 FROM python:3.10-bookworm
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
# Remove any third-party apt sources to avoid issues with expiring keys. # install lib required for pyaudio
RUN rm -f /etc/apt/sources.list.d/*.list RUN apt update && apt install -y portaudio19-dev && apt-get clean && rm -rf /var/lib/apt/lists/*
# Install some basic utilities. # update pip to support for whl.metadata -> less downloading
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN pip install --no-cache-dir -U "pip>=24"
curl \
ca-certificates \
sudo \
git \
bzip2 \
libx11-6 \
python3-dev \
python3-pip \
&& python3 -m pip install --upgrade pip \
&& rm -rf /var/lib/apt/lists/*
# Create a working directory. # create a working directory
RUN mkdir /app RUN mkdir /app
WORKDIR /app WORKDIR /app
COPY scripts/setup.sh requirements/server.txt /app # install the requirements for running the whisper-live server
COPY requirements/server.txt /app/
RUN pip install --no-cache-dir -r server.txt && rm server.txt
RUN apt update && bash setup.sh && rm setup.sh # make the paths of the nvidia libs installed as wheels visible. equivalent to:
RUN pip install -r server.txt && rm server.txt # export LD_LIBRARY_PATH=`python3 -c 'import os; import nvidia.cublas.lib; import nvidia.cudnn.lib; print(os.path.dirname(nvidia.cublas.lib.__file__) + ":" + os.path.dirname(nvidia.cudnn.lib.__file__))'`
ENV LD_LIBRARY_PATH="/usr/local/lib/python3.10/site-packages/nvidia/cublas/lib:/usr/local/lib/python3.10/site-packages/nvidia/cudnn/lib"
COPY whisper_live /app/whisper_live COPY whisper_live /app/whisper_live
COPY run_server.py /app COPY run_server.py /app
CMD ["python3", "run_server.py"] CMD ["python", "run_server.py"]
+19
View File
@@ -0,0 +1,19 @@
FROM openvino/ubuntu22_runtime:latest
ARG DEBIAN_FRONTEND=noninteractive
USER root
RUN apt update && apt install -y portaudio19-dev python-is-python3 && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -U "pip>=24"
RUN mkdir /app
WORKDIR /app
COPY requirements/server.txt /app/
RUN pip install --no-cache-dir -r server.txt && rm server.txt
COPY whisper_live /app/whisper_live
COPY run_server.py /app
CMD ["python", "run_server.py", "--backend", "openvino"]
+30
View File
@@ -0,0 +1,30 @@
FROM nvidia/cuda:12.8.1-base-ubuntu22.04 AS base
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
python3.10 python3-pip openmpi-bin libopenmpi-dev git git-lfs wget \
&& apt install python-is-python3 \
&& pip install --upgrade pip setuptools \
&& rm -rf /var/lib/apt/lists/*
FROM base AS devel
RUN pip install --no-cache-dir -U tensorrt_llm==0.18.2 --extra-index-url https://pypi.nvidia.com
WORKDIR /app
RUN git clone -b v0.18.2 https://github.com/NVIDIA/TensorRT-LLM.git \
&& mv TensorRT-LLM/examples ./TensorRT-LLM-examples \
&& rm -rf TensorRT-LLM
FROM devel AS release
WORKDIR /app
COPY assets/ ./assets
RUN wget -nc -P assets/ https://raw.githubusercontent.com/openai/whisper/main/whisper/assets/mel_filters.npz
COPY scripts/setup.sh ./
RUN apt update && bash setup.sh && rm setup.sh
COPY requirements/server.txt .
RUN pip install --no-cache-dir -r server.txt && rm server.txt
COPY whisper_live ./whisper_live
COPY scripts/build_whisper_tensorrt.sh .
COPY run_server.py .
+1 -1
View File
@@ -1,4 +1,4 @@
PyAudio PyAudio
ffmpeg-python av
scipy scipy
websocket-client websocket-client
+15 -6
View File
@@ -1,12 +1,21 @@
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
# openvino
librosa
openvino
openvino-genai
openvino-tokenizers
optimum
optimum-intel
+19 -3
View File
@@ -1,5 +1,5 @@
import argparse import argparse
from whisper_live.server import TranscriptionServer import os
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -10,7 +10,7 @@ if __name__ == "__main__":
parser.add_argument('--backend', '-b', parser.add_argument('--backend', '-b',
type=str, type=str,
default='faster_whisper', default='faster_whisper',
help='Backends from ["tensorrt", "faster_whisper"]') help='Backends from ["tensorrt", "faster_whisper", "openvino"]')
parser.add_argument('--faster_whisper_custom_model_path', '-fw', parser.add_argument('--faster_whisper_custom_model_path', '-fw',
type=str, default=None, type=str, default=None,
help="Custom Faster Whisper Model") help="Custom Faster Whisper Model")
@@ -21,12 +21,26 @@ if __name__ == "__main__":
parser.add_argument('--trt_multilingual', '-m', parser.add_argument('--trt_multilingual', '-m',
action="store_true", action="store_true",
help='Boolean only for TensorRT model. True if multilingual.') help='Boolean only for TensorRT model. True if multilingual.')
parser.add_argument('--trt_py_session',
action="store_true",
help='Boolean only for TensorRT model. Use python session or cpp session, By default uses Cpp.')
parser.add_argument('--omp_num_threads', '-omp',
type=int,
default=1,
help="Number of threads to use for OpenMP")
parser.add_argument('--no_single_model', '-nsm',
action='store_true',
help='Set this if every connection should instantiate its own model. Only relevant for custom model, passed using -trt or -fw.')
args = parser.parse_args() args = parser.parse_args()
if args.backend == "tensorrt": if args.backend == "tensorrt":
if args.trt_model_path is None: if args.trt_model_path is None:
raise ValueError("Please Provide a valid tensorrt model path") raise ValueError("Please Provide a valid tensorrt model path")
if "OMP_NUM_THREADS" not in os.environ:
os.environ["OMP_NUM_THREADS"] = str(args.omp_num_threads)
from whisper_live.server import TranscriptionServer
server = TranscriptionServer() server = TranscriptionServer()
server.run( server.run(
"0.0.0.0", "0.0.0.0",
@@ -34,5 +48,7 @@ if __name__ == "__main__":
backend=args.backend, backend=args.backend,
faster_whisper_custom_model_path=args.faster_whisper_custom_model_path, faster_whisper_custom_model_path=args.faster_whisper_custom_model_path,
whisper_tensorrt_path=args.trt_model_path, whisper_tensorrt_path=args.trt_model_path,
trt_multilingual=args.trt_multilingual trt_multilingual=args.trt_multilingual,
trt_py_session=args.trt_py_session,
single_model=not args.no_single_model,
) )
+49 -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=4
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,41 @@ 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 --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 \
--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 \
--max_beam_width "$max_beam_width" \
--max_batch_size "$max_batch_size" \
--max_seq_len 225 \
--max_input_len 32 \
--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 +112,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
+13 -6
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,25 @@ 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",
"librosa",
"numpy==1.26.4",
"openvino",
"openvino-genai",
"openvino-tokenizers",
"optimum",
"optimum-intel",
], ],
python_requires=">=3.8" python_requires=">=3.9"
) )
+11 -5
View File
@@ -48,7 +48,13 @@ 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,
"send_last_n_segments": 10,
"no_speech_thresh": 0.45,
"clip_audio": False,
"same_output_threshold": 10,
}) })
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 +72,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,6 +1,6 @@
import unittest import unittest
import numpy as np import numpy as np
from whisper_live.tensorrt_utils import load_audio from whisper_live.transcriber.tensorrt_utils import load_audio
from whisper_live.vad import VoiceActivityDetector from whisper_live.vad import VoiceActivityDetector
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.4.0" __version__ = "0.7.1"
View File
+361
View File
@@ -0,0 +1,361 @@
import json
import logging
import threading
import time
import numpy as np
class ServeClientBase(object):
RATE = 16000
SERVER_READY = "SERVER_READY"
DISCONNECT = "DISCONNECT"
client_uid: str
"""A unique identifier for the client."""
websocket: object
"""The WebSocket connection for the client."""
send_last_n_segments: int
"""Number of most recent segments to send to the client."""
no_speech_thresh: float
"""Segments with no speech probability above this threshold will be discarded."""
clip_audio: bool
"""Whether to clip audio with no valid segments."""
same_output_threshold: int
"""Number of repeated outputs before considering it as a valid segment."""
def __init__(
self,
client_uid,
websocket,
send_last_n_segments=10,
no_speech_thresh=0.45,
clip_audio=False,
same_output_threshold=10,
):
self.client_uid = client_uid
self.websocket = websocket
self.send_last_n_segments = send_last_n_segments
self.no_speech_thresh = no_speech_thresh
self.clip_audio = clip_audio
self.same_output_threshold = same_output_threshold
self.frames = b""
self.timestamp_offset = 0.0
self.frames_np = None
self.frames_offset = 0.0
self.text = []
self.current_out = ""
self.prev_out = ""
self.exit = False
self.same_output_count = 0
self.transcript = []
self.end_time_for_same_output = None
# threading
self.lock = threading.Lock()
def speech_to_text(self):
"""
Process an audio stream in an infinite loop, continuously transcribing the speech.
This method continuously receives audio frames, performs real-time transcription, and sends
transcribed segments to the client via a WebSocket connection.
If the client's language is not detected, it waits for 30 seconds of audio input to make a language prediction.
It utilizes the Whisper ASR model to transcribe the audio, continuously processing and streaming results. Segments
are sent to the client in real-time, and a history of segments is maintained to provide context.
Raises:
Exception: If there is an issue with audio processing or WebSocket communication.
"""
while True:
if self.exit:
logging.info("Exiting speech to text thread")
break
if self.frames_np is None:
continue
if self.clip_audio:
self.clip_audio_if_no_valid_segment()
input_bytes, duration = self.get_audio_chunk_for_processing()
if duration < 1.0:
time.sleep(0.1) # wait for audio chunks to arrive
continue
try:
input_sample = input_bytes.copy()
result = self.transcribe_audio(input_sample)
if result is None or self.language is None:
self.timestamp_offset += duration
time.sleep(0.25) # wait for voice activity, result is None when no voice activity
continue
self.handle_transcription_output(result, duration)
except Exception as e:
logging.error(f"[ERROR]: Failed to transcribe audio chunk: {e}")
time.sleep(0.01)
def transcribe_audio(self):
raise NotImplementedError
def handle_transcription_output(self, result, duration):
raise NotImplementedError
def format_segment(self, start, end, text, completed=False):
"""
Formats a transcription segment with precise start and end times alongside the transcribed text.
Args:
start (float): The start time of the transcription segment in seconds.
end (float): The end time of the transcription segment in seconds.
text (str): The transcribed text corresponding to the segment.
Returns:
dict: A dictionary representing the formatted transcription segment, including
'start' and 'end' times as strings with three decimal places and the 'text'
of the transcription.
"""
return {
'start': "{:.3f}".format(start),
'end': "{:.3f}".format(end),
'text': text,
'completed': completed
}
def add_frames(self, frame_np):
"""
Add audio frames to the ongoing audio stream buffer.
This method is responsible for maintaining the audio stream buffer, allowing the continuous addition
of audio frames as they are received. It also ensures that the buffer does not exceed a specified size
to prevent excessive memory usage.
If the buffer size exceeds a threshold (45 seconds of audio data), it discards the oldest 30 seconds
of audio data to maintain a reasonable buffer size. If the buffer is empty, it initializes it with the provided
audio frame. The audio stream buffer is used for real-time processing of audio data for transcription.
Args:
frame_np (numpy.ndarray): The audio frame data as a NumPy array.
"""
self.lock.acquire()
if self.frames_np is not None and self.frames_np.shape[0] > 45*self.RATE:
self.frames_offset += 30.0
self.frames_np = self.frames_np[int(30*self.RATE):]
# check timestamp offset(should be >= self.frame_offset)
# this basically means that there is no speech as timestamp offset hasnt updated
# and is less than frame_offset
if self.timestamp_offset < self.frames_offset:
self.timestamp_offset = self.frames_offset
if self.frames_np is None:
self.frames_np = frame_np.copy()
else:
self.frames_np = np.concatenate((self.frames_np, frame_np), axis=0)
self.lock.release()
def clip_audio_if_no_valid_segment(self):
"""
Update the timestamp offset based on audio buffer status.
Clip audio if the current chunk exceeds 30 seconds, this basically implies that
no valid segment for the last 30 seconds from whisper
"""
with self.lock:
if self.frames_np[int((self.timestamp_offset - self.frames_offset)*self.RATE):].shape[0] > 25 * self.RATE:
duration = self.frames_np.shape[0] / self.RATE
self.timestamp_offset = self.frames_offset + duration - 5
def get_audio_chunk_for_processing(self):
"""
Retrieves the next chunk of audio data for processing based on the current offsets.
Calculates which part of the audio data should be processed next, based on
the difference between the current timestamp offset and the frame's offset, scaled by
the audio sample rate (RATE). It then returns this chunk of audio data along with its
duration in seconds.
Returns:
tuple: A tuple containing:
- input_bytes (np.ndarray): The next chunk of audio data to be processed.
- duration (float): The duration of the audio chunk in seconds.
"""
with self.lock:
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
return input_bytes, duration
def prepare_segments(self, last_segment=None):
"""
Prepares the segments of transcribed text to be sent to the client.
This method compiles the recent segments of transcribed text, ensuring that only the
specified number of the most recent segments are included. It also appends the most
recent segment of text if provided (which is considered incomplete because of the possibility
of the last word being truncated in the audio chunk).
Args:
last_segment (str, optional): The most recent segment of transcribed text to be added
to the list of segments. Defaults to None.
Returns:
list: A list of transcribed text segments to be sent to the client.
"""
segments = []
if len(self.transcript) >= self.send_last_n_segments:
segments = self.transcript[-self.send_last_n_segments:].copy()
else:
segments = self.transcript.copy()
if last_segment is not None:
segments = segments + [last_segment]
return segments
def get_audio_chunk_duration(self, input_bytes):
"""
Calculates the duration of the provided audio chunk.
Args:
input_bytes (numpy.ndarray): The audio chunk for which to calculate the duration.
Returns:
float: The duration of the audio chunk in seconds.
"""
return input_bytes.shape[0] / self.RATE
def send_transcription_to_client(self, segments):
"""
Sends the specified transcription segments to the client over the websocket connection.
This method formats the transcription segments into a JSON object and attempts to send
this object to the client. If an error occurs during the send operation, it logs the error.
Returns:
segments (list): A list of transcription segments to be sent to the client.
"""
try:
self.websocket.send(
json.dumps({
"uid": self.client_uid,
"segments": segments,
})
)
except Exception as e:
logging.error(f"[ERROR]: Sending data to client: {e}")
def disconnect(self):
"""
Notify the client of disconnection and send a disconnect message.
This method sends a disconnect message to the client via the WebSocket connection to notify them
that the transcription service is disconnecting gracefully.
"""
self.websocket.send(json.dumps({
"uid": self.client_uid,
"message": self.DISCONNECT
}))
def cleanup(self):
"""
Perform cleanup tasks before exiting the transcription service.
This method performs necessary cleanup tasks, including stopping the transcription thread, marking
the exit flag to indicate the transcription thread should exit gracefully, and destroying resources
associated with the transcription process.
"""
logging.info("Cleaning up.")
self.exit = True
def get_segment_no_speech_prob(self, segment):
return getattr(segment, "no_speech_prob", 0)
def get_segment_start(self, segment):
return getattr(segment, "start", getattr(segment, "start_ts", 0))
def get_segment_end(self, segment):
return getattr(segment, "end", getattr(segment, "end_ts", 0))
def update_segments(self, segments, duration):
"""
Processes the segments from Whisper and updates the transcript.
Uses helper methods to account for differences between backends.
Args:
segments (list): List of segments returned by the transcriber.
duration (float): Duration of the current audio chunk.
Returns:
dict or None: The last processed segment (if any).
"""
offset = None
self.current_out = ''
last_segment = None
# Process complete segments only if there are more than one
# and if the last segment's no_speech_prob is below the threshold.
if len(segments) > 1 and self.get_segment_no_speech_prob(segments[-1]) <= self.no_speech_thresh:
for s in segments[:-1]:
text_ = s.text
self.text.append(text_)
with self.lock:
start = self.timestamp_offset + self.get_segment_start(s)
end = self.timestamp_offset + min(duration, self.get_segment_end(s))
if start >= end:
continue
if self.get_segment_no_speech_prob(s) > self.no_speech_thresh:
continue
self.transcript.append(self.format_segment(start, end, text_, completed=True))
offset = min(duration, self.get_segment_end(s))
# Process the last segment if its no_speech_prob is acceptable.
if self.get_segment_no_speech_prob(segments[-1]) <= self.no_speech_thresh:
self.current_out += segments[-1].text
with self.lock:
last_segment = self.format_segment(
self.timestamp_offset + self.get_segment_start(segments[-1]),
self.timestamp_offset + min(duration, self.get_segment_end(segments[-1])),
self.current_out,
completed=False
)
# Handle repeated output logic.
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 = self.get_segment_end(segments[-1])
time.sleep(0.1) # wait briefly for any new voice activity
else:
self.same_output_count = 0
self.end_time_for_same_output = None
# If the same incomplete segment is repeated too many times,
# append it to the transcript and update the offset.
if self.same_output_count > self.same_output_threshold:
if not self.text or self.text[-1].strip().lower() != self.current_out.strip().lower():
self.text.append(self.current_out)
with self.lock:
self.transcript.append(self.format_segment(
self.timestamp_offset,
self.timestamp_offset + min(duration, self.end_time_for_same_output),
self.current_out,
completed=True
))
self.current_out = ''
offset = min(duration, self.end_time_for_same_output)
self.same_output_count = 0
last_segment = None
self.end_time_for_same_output = None
else:
self.prev_out = self.current_out
if offset is not None:
with self.lock:
self.timestamp_offset += offset
return last_segment
@@ -0,0 +1,216 @@
import json
import logging
import threading
import time
import torch
from whisper_live.transcriber.transcriber_faster_whisper import WhisperModel
from whisper_live.backend.base import ServeClientBase
class ServeClientFasterWhisper(ServeClientBase):
SINGLE_MODEL = None
SINGLE_MODEL_LOCK = threading.Lock()
def __init__(
self,
websocket,
task="transcribe",
device=None,
language=None,
client_uid=None,
model="small.en",
initial_prompt=None,
vad_parameters=None,
use_vad=True,
single_model=False,
send_last_n_segments=10,
no_speech_thresh=0.45,
clip_audio=False,
same_output_threshold=10,
):
"""
Initialize a ServeClient instance.
The Whisper model is initialized based on the client's language and device availability.
The transcription thread is started upon initialization. A "SERVER_READY" message is sent
to the client to indicate that the server is ready.
Args:
websocket (WebSocket): The WebSocket connection for the client.
task (str, optional): The task type, e.g., "transcribe". Defaults to "transcribe".
device (str, optional): The device type for Whisper, "cuda" or "cpu". Defaults to None.
language (str, optional): The language for transcription. Defaults to None.
client_uid (str, optional): A unique identifier for the client. Defaults to None.
model (str, optional): The whisper model size. Defaults to 'small.en'
initial_prompt (str, optional): Prompt for whisper inference. Defaults to None.
single_model (bool, optional): Whether to instantiate a new model for each client connection. Defaults to False.
send_last_n_segments (int, optional): Number of most recent segments to send to the client. Defaults to 10.
no_speech_thresh (float, optional): Segments with no speech probability above this threshold will be discarded. Defaults to 0.45.
clip_audio (bool, optional): Whether to clip audio with no valid segments. Defaults to False.
same_output_threshold (int, optional): Number of repeated outputs before considering it as a valid segment. Defaults to 10.
"""
super().__init__(
client_uid,
websocket,
send_last_n_segments,
no_speech_thresh,
clip_audio,
same_output_threshold,
)
self.model_sizes = [
"tiny", "tiny.en", "base", "base.en", "small", "small.en",
"medium", "medium.en", "large-v2", "large-v3", "distil-small.en",
"distil-medium.en", "distil-large-v2", "distil-large-v3",
"large-v3-turbo", "turbo"
]
self.model_size_or_path = model
self.language = "en" if self.model_size_or_path.endswith("en") else language
self.task = task
self.initial_prompt = initial_prompt
self.vad_parameters = vad_parameters or {"onset": 0.5}
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:
return
logging.info(f"Using Device={device} with precision {self.compute_type}")
try:
if single_model:
if ServeClientFasterWhisper.SINGLE_MODEL is None:
self.create_model(device)
ServeClientFasterWhisper.SINGLE_MODEL = self.transcriber
else:
self.transcriber = ServeClientFasterWhisper.SINGLE_MODEL
else:
self.create_model(device)
except Exception as e:
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
# threading
self.trans_thread = threading.Thread(target=self.speech_to_text)
self.trans_thread.start()
self.websocket.send(
json.dumps(
{
"uid": self.client_uid,
"message": self.SERVER_READY,
"backend": "faster_whisper"
}
)
)
def create_model(self, device):
"""
Instantiates a new model, sets it as the transcriber.
"""
self.transcriber = WhisperModel(
self.model_size_or_path,
device=device,
compute_type=self.compute_type,
local_files_only=False,
)
def check_valid_model(self, model_size):
"""
Check if it's a valid whisper model size.
Args:
model_size (str): The name of the model size to check.
Returns:
str: The model size if valid, None otherwise.
"""
if model_size not in self.model_sizes:
self.websocket.send(
json.dumps(
{
"uid": self.client_uid,
"status": "ERROR",
"message": f"Invalid model size {model_size}. Available choices: {self.model_sizes}"
}
)
)
return None
return model_size
def set_language(self, info):
"""
Updates the language attribute based on the detected language information.
Args:
info (object): An object containing the detected language and its probability. This object
must have at least two attributes: `language`, a string indicating the detected
language, and `language_probability`, a float representing the confidence level
of the language detection.
"""
if info.language_probability > 0.5:
self.language = info.language
logging.info(f"Detected language {self.language} with probability {info.language_probability}")
self.websocket.send(json.dumps(
{"uid": self.client_uid, "language": self.language, "language_prob": info.language_probability}))
def transcribe_audio(self, input_sample):
"""
Transcribes the provided audio sample using the configured transcriber instance.
If the language has not been set, it updates the session's language based on the transcription
information.
Args:
input_sample (np.array): The audio chunk to be transcribed. This should be a NumPy
array representing the audio data.
Returns:
The transcription result from the transcriber. The exact format of this result
depends on the implementation of the `transcriber.transcribe` method but typically
includes the transcribed text.
"""
if ServeClientFasterWhisper.SINGLE_MODEL:
ServeClientFasterWhisper.SINGLE_MODEL_LOCK.acquire()
result, info = self.transcriber.transcribe(
input_sample,
initial_prompt=self.initial_prompt,
language=self.language,
task=self.task,
vad_filter=self.use_vad,
vad_parameters=self.vad_parameters if self.use_vad else None)
if ServeClientFasterWhisper.SINGLE_MODEL:
ServeClientFasterWhisper.SINGLE_MODEL_LOCK.release()
if self.language is None and info is not None:
self.set_language(info)
return result
def handle_transcription_output(self, result, duration):
"""
Handle the transcription output, updating the transcript and sending data to the client.
Args:
result (str): The result from whisper inference i.e. the list of segments.
duration (float): Duration of the transcribed audio chunk.
"""
segments = []
if len(result):
self.t_start = None
last_segment = self.update_segments(result, duration)
segments = self.prepare_segments(last_segment)
if len(segments):
self.send_transcription_to_client(segments)
+148
View File
@@ -0,0 +1,148 @@
import json
import logging
import threading
import time
from openvino import Core
from whisper_live.backend.base import ServeClientBase
from whisper_live.transcriber.transcriber_openvino import WhisperOpenVINO
class ServeClientOpenVINO(ServeClientBase):
SINGLE_MODEL = None
SINGLE_MODEL_LOCK = threading.Lock()
def __init__(
self,
websocket,
task="transcribe",
device=None,
language=None,
client_uid=None,
model="small.en",
initial_prompt=None,
vad_parameters=None,
use_vad=True,
single_model=False,
send_last_n_segments=10,
no_speech_thresh=0.45,
clip_audio=False,
same_output_threshold=10,
):
"""
Initialize a ServeClient instance.
The Whisper model is initialized based on the client's language and device availability.
The transcription thread is started upon initialization. A "SERVER_READY" message is sent
to the client to indicate that the server is ready.
Args:
websocket (WebSocket): The WebSocket connection for the client.
task (str, optional): The task type, e.g., "transcribe." Defaults to "transcribe".
device (str, optional): The device type for Whisper, "cuda" or "cpu". Defaults to None.
language (str, optional): The language for transcription. Defaults to None.
client_uid (str, optional): A unique identifier for the client. Defaults to None.
model (str, optional): Huggingface model_id for a valid OpenVINO model.
initial_prompt (str, optional): Prompt for whisper inference. Defaults to None.
single_model (bool, optional): Whether to instantiate a new model for each client connection. Defaults to False.
send_last_n_segments (int, optional): Number of most recent segments to send to the client. Defaults to 10.
no_speech_thresh (float, optional): Segments with no speech probability above this threshold will be discarded. Defaults to 0.45.
clip_audio (bool, optional): Whether to clip audio with no valid segments. Defaults to False.
same_output_threshold (int, optional): Number of repeated outputs before considering it as a valid segment. Defaults to 10.
"""
super().__init__(
client_uid,
websocket,
send_last_n_segments,
no_speech_thresh,
clip_audio,
same_output_threshold,
)
self.language = "en" if language is None else language
if not self.language.startswith("<|"):
self.language = f"<|{self.language}|>"
self.task = "transcribe" if task is None else task
self.clip_audio = True
core = Core()
available_devices = core.available_devices
if 'GPU' in available_devices:
selected_device = 'GPU'
else:
gpu_devices = [d for d in available_devices if d.startswith('GPU')]
selected_device = gpu_devices[0] if gpu_devices else 'CPU'
self.device = selected_device
if single_model:
if ServeClientOpenVINO.SINGLE_MODEL is None:
self.create_model(model)
ServeClientOpenVINO.SINGLE_MODEL = self.transcriber
else:
self.transcriber = ServeClientOpenVINO.SINGLE_MODEL
else:
self.create_model(model)
# threading
self.trans_thread = threading.Thread(target=self.speech_to_text)
self.trans_thread.start()
self.websocket.send(json.dumps({
"uid": self.client_uid,
"message": self.SERVER_READY,
"backend": "openvino"
}))
logging.info(f"Using OpenVINO device: {self.device}")
logging.info(f"Running OpenVINO backend with language: {self.language} and task: {self.task}")
def create_model(self, model_id):
"""
Instantiates a new model, sets it as the transcriber.
"""
self.transcriber = WhisperOpenVINO(
model_id,
device=self.device,
language=self.language,
task=self.task
)
def transcribe_audio(self, input_sample):
"""
Transcribes the provided audio sample using the configured transcriber instance.
If the language has not been set, it updates the session's language based on the transcription
information.
Args:
input_sample (np.array): The audio chunk to be transcribed. This should be a NumPy
array representing the audio data.
Returns:
The transcription result from the transcriber. The exact format of this result
depends on the implementation of the `transcriber.transcribe` method but typically
includes the transcribed text.
"""
if ServeClientOpenVINO.SINGLE_MODEL:
ServeClientOpenVINO.SINGLE_MODEL_LOCK.acquire()
result = self.transcriber.transcribe(input_sample)
if ServeClientOpenVINO.SINGLE_MODEL:
ServeClientOpenVINO.SINGLE_MODEL_LOCK.release()
return result
def handle_transcription_output(self, result, duration):
"""
Handle the transcription output, updating the transcript and sending data to the client.
Args:
result (str): The result from whisper inference i.e. the list of segments.
duration (float): Duration of the transcribed audio chunk.
"""
segments = []
if len(result):
self.t_start = None
last_segment = self.update_segments(result, duration)
segments = self.prepare_segments(last_segment)
if len(segments):
self.send_transcription_to_client(segments)
+210
View File
@@ -0,0 +1,210 @@
import json
import logging
import threading
import time
from whisper_live.backend.base import ServeClientBase
from whisper_live.transcriber.transcriber_tensorrt import WhisperTRTLLM
class ServeClientTensorRT(ServeClientBase):
SINGLE_MODEL = None
SINGLE_MODEL_LOCK = threading.Lock()
def __init__(
self,
websocket,
task="transcribe",
multilingual=False,
language=None,
client_uid=None,
model=None,
single_model=False,
use_py_session=False,
max_new_tokens=225,
send_last_n_segments=10,
no_speech_thresh=0.45,
clip_audio=False,
same_output_threshold=10,
):
"""
Initialize a ServeClient instance.
The Whisper model is initialized based on the client's language and device availability.
The transcription thread is started upon initialization. A "SERVER_READY" message is sent
to the client to indicate that the server is ready.
Args:
websocket (WebSocket): The WebSocket connection for the client.
task (str, optional): The task type, e.g., "transcribe." Defaults to "transcribe".
device (str, optional): The device type for Whisper, "cuda" or "cpu". Defaults to None.
multilingual (bool, optional): Whether the client supports multilingual transcription. Defaults to False.
language (str, optional): The language for transcription. Defaults to None.
client_uid (str, optional): A unique identifier for the client. Defaults to None.
single_model (bool, optional): Whether to instantiate a new model for each client connection. Defaults to False.
use_py_session (bool, optional): Use python session or cpp session. Defaults to Cpp Session.
max_new_tokens (int, optional): Max number of tokens to generate.
send_last_n_segments (int, optional): Number of most recent segments to send to the client. Defaults to 10.
no_speech_thresh (float, optional): Segments with no speech probability above this threshold will be discarded. Defaults to 0.45.
clip_audio (bool, optional): Whether to clip audio with no valid segments. Defaults to False.
same_output_threshold (int, optional): Number of repeated outputs before considering it as a valid segment. Defaults to 10.
"""
super().__init__(
client_uid,
websocket,
send_last_n_segments,
no_speech_thresh,
clip_audio,
same_output_threshold,
)
self.language = language if multilingual else "en"
self.task = task
self.eos = False
self.max_new_tokens = max_new_tokens
if single_model:
if ServeClientTensorRT.SINGLE_MODEL is None:
self.create_model(model, multilingual, use_py_session=use_py_session)
ServeClientTensorRT.SINGLE_MODEL = self.transcriber
else:
self.transcriber = ServeClientTensorRT.SINGLE_MODEL
else:
self.create_model(model, multilingual, use_py_session=use_py_session)
# threading
self.trans_thread = threading.Thread(target=self.speech_to_text)
self.trans_thread.start()
self.websocket.send(json.dumps({
"uid": self.client_uid,
"message": self.SERVER_READY,
"backend": "tensorrt"
}))
def create_model(self, model, multilingual, warmup=True, use_py_session=False):
"""
Instantiates a new model, sets it as the transcriber and does warmup if desired.
"""
self.transcriber = WhisperTRTLLM(
model,
assets_dir="assets",
device="cuda",
is_multilingual=multilingual,
language=self.language,
task=self.task,
use_py_session=use_py_session,
max_output_len=self.max_new_tokens,
)
if warmup:
self.warmup()
def warmup(self, warmup_steps=10):
"""
Warmup TensorRT since first few inferences are slow.
Args:
warmup_steps (int): Number of steps to warm up the model for.
"""
logging.info("[INFO:] Warming up TensorRT engine..")
mel, _ = self.transcriber.log_mel_spectrogram("assets/jfk.flac")
for i in range(warmup_steps):
self.transcriber.transcribe(mel)
def set_eos(self, eos):
"""
Sets the End of Speech (EOS) flag.
Args:
eos (bool): The value to set for the EOS flag.
"""
self.lock.acquire()
self.eos = eos
self.lock.release()
def handle_transcription_output(self, last_segment, duration):
"""
Handle the transcription output, updating the transcript and sending data to the client.
Args:
last_segment (str): The last segment from the whisper output which is considered to be incomplete because
of the possibility of word being truncated.
duration (float): Duration of the transcribed audio chunk.
"""
segments = self.prepare_segments({"text": last_segment})
self.send_transcription_to_client(segments)
if self.eos:
self.update_timestamp_offset(last_segment, duration)
def transcribe_audio(self, input_bytes):
"""
Transcribe the audio chunk and send the results to the client.
Args:
input_bytes (np.array): The audio chunk to transcribe.
"""
if ServeClientTensorRT.SINGLE_MODEL:
ServeClientTensorRT.SINGLE_MODEL_LOCK.acquire()
logging.info(f"[WhisperTensorRT:] Processing audio with duration: {input_bytes.shape[0] / self.RATE}")
mel, duration = self.transcriber.log_mel_spectrogram(input_bytes)
last_segment = self.transcriber.transcribe(
mel,
text_prefix=f"<|startoftranscript|><|{self.language}|><|{self.task}|><|notimestamps|>",
)
if ServeClientTensorRT.SINGLE_MODEL:
ServeClientTensorRT.SINGLE_MODEL_LOCK.release()
if last_segment:
self.handle_transcription_output(last_segment, duration)
def update_timestamp_offset(self, last_segment, duration):
"""
Update timestamp offset and transcript.
Args:
last_segment (str): Last transcribed audio from the whisper model.
duration (float): Duration of the last audio chunk.
"""
if not len(self.transcript):
self.transcript.append({"text": last_segment + " "})
elif self.transcript[-1]["text"].strip() != last_segment:
self.transcript.append({"text": last_segment + " "})
with self.lock:
self.timestamp_offset += duration
def speech_to_text(self):
"""
Process an audio stream in an infinite loop, continuously transcribing the speech.
This method continuously receives audio frames, performs real-time transcription, and sends
transcribed segments to the client via a WebSocket connection.
If the client's language is not detected, it waits for 30 seconds of audio input to make a language prediction.
It utilizes the Whisper ASR model to transcribe the audio, continuously processing and streaming results. Segments
are sent to the client in real-time, and a history of segments is maintained to provide context.
Raises:
Exception: If there is an issue with audio processing or WebSocket communication.
"""
while True:
if self.exit:
logging.info("Exiting speech to text thread")
break
if self.frames_np is None:
time.sleep(0.02) # wait for any audio to arrive
continue
self.clip_audio_if_no_valid_segment()
input_bytes, duration = self.get_audio_chunk_for_processing()
if duration < 0.4:
continue
try:
input_sample = input_bytes.copy()
logging.info(f"[WhisperTensorRT:] Processing audio with duration: {duration}")
self.transcribe_audio(input_sample)
except Exception as e:
logging.error(f"[ERROR]: {e}")
+266 -78
View File
@@ -1,6 +1,8 @@
import os import os
import shutil
import wave import wave
import logging
import numpy as np import numpy as np
import pyaudio import pyaudio
import threading import threading
@@ -8,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
@@ -27,7 +29,16 @@ 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,
use_wss=False,
log_transcription=True,
max_clients=4,
max_connection_time=600,
send_last_n_segments=10,
no_speech_thresh=0.45,
clip_audio=False,
same_output_threshold=10,
transcription_callback=None,
): ):
""" """
Initializes a Client instance for audio recording and streaming to a server. Initializes a Client instance for audio recording and streaming to a server.
@@ -41,6 +52,17 @@ 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.
send_last_n_segments (int, optional): Number of most recent segments to send to the client. Defaults to 10.
no_speech_thresh (float, optional): Segments with no speech probability above this threshold will be discarded. Defaults to 0.45.
clip_audio (bool, optional): Whether to clip audio with no valid segments. Defaults to False.
same_output_threshold (int, optional): Number of repeated outputs before considering it as a valid segment. Defaults to 10.
transcription_callback (callable, optional): A callback function to handle transcription results. Default is None.
""" """
self.recording = False self.recording = False
self.task = "transcribe" self.task = "transcribe"
@@ -53,17 +75,26 @@ class Client:
self.server_error = False self.server_error = False
self.srt_file_path = srt_file_path self.srt_file_path = srt_file_path
self.use_vad = use_vad self.use_vad = use_vad
self.use_wss = use_wss
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
self.send_last_n_segments = send_last_n_segments
self.no_speech_thresh = no_speech_thresh
self.clip_audio = clip_audio
self.same_output_threshold = same_output_threshold
self.transcription_callback = transcription_callback
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:
socket_url = f"ws://{host}:{port}" socket_protocol = 'wss' if self.use_wss else "ws"
socket_url = f"{socket_protocol}://{host}:{port}"
self.client_socket = websocket.WebSocketApp( self.client_socket = websocket.WebSocketApp(
socket_url, socket_url,
on_open=lambda ws: self.on_open(ws), on_open=lambda ws: self.on_open(ws),
@@ -81,7 +112,7 @@ class Client:
# start websocket client in a thread # start websocket client in a thread
self.ws_thread = threading.Thread(target=self.client_socket.run_forever) self.ws_thread = threading.Thread(target=self.client_socket.run_forever)
self.ws_thread.setDaemon(True) self.ws_thread.daemon = True
self.ws_thread.start() self.ws_thread.start()
self.transcript = [] self.transcript = []
@@ -105,9 +136,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)
@@ -116,10 +147,19 @@ 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. # call the transcription callback if provided
text = text[-3:] if self.transcription_callback and callable(self.transcription_callback):
utils.clear_screen() try:
utils.print_transcript(text) self.transcription_callback(" ".join(text), segments) # string, list
except Exception as e:
print(f"[WARN] transcription_callback raised: {e}")
return
if self.log_transcription:
# Truncate to last 3 entries for brevity.
text = text[-3:]
utils.clear_screen()
utils.print_transcript(text)
def on_message(self, ws, message): def on_message(self, ws, message):
""" """
@@ -174,7 +214,6 @@ class Client:
def on_close(self, ws, close_status_code, close_msg): def on_close(self, ws, close_status_code, close_msg):
print(f"[INFO]: Websocket connection closed: {close_status_code}: {close_msg}") print(f"[INFO]: Websocket connection closed: {close_status_code}: {close_msg}")
self.recording = False self.recording = False
self.server_error = False
self.waiting = False self.waiting = False
def on_open(self, ws): def on_open(self, ws):
@@ -196,7 +235,13 @@ 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,
"send_last_n_segments": self.send_last_n_segments,
"no_speech_thresh": self.no_speech_thresh,
"clip_audio": self.clip_audio,
"same_output_threshold": self.same_output_threshold,
} }
) )
) )
@@ -250,7 +295,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)
@@ -260,6 +307,7 @@ class Client:
while time.time() - self.last_response_received < self.disconnect_if_no_response_for: while time.time() - self.last_response_received < self.disconnect_if_no_response_for:
continue continue
class TranscriptionTeeClient: class TranscriptionTeeClient:
""" """
Client for handling audio recording, streaming, and transcription tasks via one or more Client for handling audio recording, streaming, and transcription tasks via one or more
@@ -273,7 +321,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): 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.")
@@ -282,6 +330,9 @@ class TranscriptionTeeClient:
self.channels = 1 self.channels = 1
self.rate = 16000 self.rate = 16000
self.record_seconds = 60000 self.record_seconds = 60000
self.save_output_recording = save_output_recording
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:
@@ -296,7 +347,7 @@ class TranscriptionTeeClient:
print(f"[WARN]: Unable to access microphone. {error}") print(f"[WARN]: Unable to access microphone. {error}")
self.stream = None self.stream = None
def __call__(self, audio=None, hls_url=None): def __call__(self, audio=None, rtsp_url=None, hls_url=None, save_file=None):
""" """
Start the transcription process. Start the transcription process.
@@ -308,6 +359,10 @@ class TranscriptionTeeClient:
audio (str, optional): Path to an audio file for transcription. Default is None, which triggers live recording. audio (str, optional): Path to an audio file for transcription. Default is None, which triggers live recording.
""" """
assert sum(
source is not None for source in [audio, rtsp_url, hls_url]
) <= 1, 'You must provide only one selected source'
print("[INFO]: Waiting for server ready ...") print("[INFO]: Waiting for server ready ...")
for client in self.clients: for client in self.clients:
while not client.recording: while not client.recording:
@@ -317,10 +372,12 @@ class TranscriptionTeeClient:
print("[INFO]: Server Ready!") print("[INFO]: Server Ready!")
if hls_url is not None: if hls_url is not None:
self.process_hls_stream(hls_url) self.process_hls_stream(hls_url, save_file)
elif audio is not None: elif audio is not None:
resampled_file = utils.resample(audio) resampled_file = utils.resample(audio)
self.play_file(resampled_file) self.play_file(resampled_file)
elif rtsp_url is not None:
self.process_rtsp_stream(rtsp_url)
else: else:
self.record() self.record()
@@ -371,6 +428,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)
@@ -379,8 +437,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:
@@ -399,42 +460,123 @@ class TranscriptionTeeClient:
self.write_all_clients_srt() self.write_all_clients_srt()
print("[INFO]: Keyboard interrupt.") print("[INFO]: Keyboard interrupt.")
def process_hls_stream(self, hls_url): def process_rtsp_stream(self, rtsp_url):
"""
Connect to an RTSP source, process the audio stream, and send it for transcription.
Args:
rtsp_url (str): The URL of the RTSP stream source.
"""
print("[INFO]: Connecting to RTSP stream...")
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=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.
""" """
print("[INFO]: Connecting to HLS stream...") print("[INFO]: Connecting to HLS stream...")
process = None # Initialize process to None
try: try:
# Connecting to the HLS stream using ffmpeg-python container = av.open(hls_url, format="hls")
process = ( self.process_av_stream(container, stream_type="HLS", save_file=save_file)
ffmpeg
.input(hls_url, threads=0)
.output('-', format='s16le', acodec='pcm_s16le', ac=1, ar=self.rate)
.run_async(pipe_stdout=True, pipe_stderr=True)
)
# Process the stream
while True:
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 HLS stream: {e}") print(f"[ERROR]: Failed to process HLS stream: {e}")
finally: finally:
if process: for client in self.clients:
process.kill() 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]: HLS stream processing finished.") print("[INFO]: HLS stream processing finished.")
def record(self, out_file="output_recording.wav"): def process_av_stream(self, container, stream_type, save_file=None):
"""
Process an AV container stream and send audio packets to the server.
Args:
container (av.container.InputContainer): The input container to process.
stream_type (str): The type of stream being processed ("RTSP" or "HLS").
save_file (str, optional): Local path to save the stream. Default is None.
"""
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
output_container = None
if save_file:
output_container = av.open(save_file, mode="w")
output_audio_stream = output_container.add_stream(codec_name="pcm_s16le", rate=self.rate)
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):
"""
Saves the current audio frames to a WAV file in a separate thread.
Args:
n_audio_file (int): The index of the audio file which determines the filename.
This helps in maintaining the order and uniqueness of each chunk.
"""
t = threading.Thread(
target=self.write_audio_frames_to_file,
args=(self.frames[:], f"chunks/{n_audio_file}.wav",),
)
t.start()
def finalize_recording(self, n_audio_file):
"""
Finalizes the recording process by saving any remaining audio frames,
closing the audio stream, and terminating the process.
Args:
n_audio_file (int): The file index to be used if there are remaining audio frames to be saved.
This index is incremented before use if the last chunk is saved.
"""
if self.save_output_recording and len(self.frames):
self.write_audio_frames_to_file(
self.frames[:], f"chunks/{n_audio_file}.wav"
)
n_audio_file += 1
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
self.close_all_clients()
if self.save_output_recording:
self.write_output_recording(n_audio_file)
self.write_all_clients_srt()
def record(self):
""" """
Record audio data from the input stream and save it to a WAV file. Record audio data from the input stream and save it to a WAV file.
@@ -446,15 +588,12 @@ class TranscriptionTeeClient:
The recording will continue until the specified duration is reached or until the `RECORDING` flag is set to `False`. The recording will continue until the specified duration is reached or until the `RECORDING` flag is set to `False`.
The recording process can be interrupted by sending a KeyboardInterrupt (e.g., pressing Ctrl+C). After recording, The recording process can be interrupted by sending a KeyboardInterrupt (e.g., pressing Ctrl+C). After recording,
the method combines all the saved audio chunks into the specified `out_file`. the method combines all the saved audio chunks into the specified `out_file`.
Args:
out_file (str, optional): The name of the output WAV file to save the entire recording.
Default is "output_recording.wav".
""" """
n_audio_file = 0 n_audio_file = 0
if not os.path.exists("chunks"): if self.save_output_recording:
os.makedirs("chunks", exist_ok=True) if os.path.exists("chunks"):
shutil.rmtree("chunks")
os.makedirs("chunks")
try: try:
for _ in range(0, int(self.rate / self.chunk * self.record_seconds)): for _ in range(0, int(self.rate / self.chunk * self.record_seconds)):
if not any(client.recording for client in self.clients): if not any(client.recording for client in self.clients):
@@ -468,32 +607,14 @@ class TranscriptionTeeClient:
# save frames if more than a minute # save frames if more than a minute
if len(self.frames) > 60 * self.rate: if len(self.frames) > 60 * self.rate:
t = threading.Thread( if self.save_output_recording:
target=self.write_audio_frames_to_file, self.save_chunk(n_audio_file)
args=( n_audio_file += 1
self.frames[:],
f"chunks/{n_audio_file}.wav",
),
)
t.start()
n_audio_file += 1
self.frames = b"" self.frames = b""
self.write_all_clients_srt() self.write_all_clients_srt()
except KeyboardInterrupt: except KeyboardInterrupt:
if len(self.frames): self.finalize_recording(n_audio_file)
self.write_audio_frames_to_file(
self.frames[:], f"chunks/{n_audio_file}.wav"
)
n_audio_file += 1
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
for client in self.clients:
client.close_all_clients()
self.write_output_recording(n_audio_file, out_file)
self.write_all_clients_srt()
def write_audio_frames_to_file(self, frames, file_name): def write_audio_frames_to_file(self, frames, file_name):
""" """
@@ -514,7 +635,7 @@ class TranscriptionTeeClient:
wavfile.setframerate(self.rate) wavfile.setframerate(self.rate)
wavfile.writeframes(frames) wavfile.writeframes(frames)
def write_output_recording(self, n_audio_file, out_file): def write_output_recording(self, n_audio_file):
""" """
Combine and save recorded audio chunks into a single WAV file. Combine and save recorded audio chunks into a single WAV file.
@@ -533,7 +654,7 @@ class TranscriptionTeeClient:
for i in range(n_audio_file) for i in range(n_audio_file)
if os.path.exists(f"chunks/{i}.wav") if os.path.exists(f"chunks/{i}.wav")
] ]
with wave.open(out_file, "wb") as wavfile: with wave.open(self.output_recording_filename, "wb") as wavfile:
wavfile: wave.Wave_write wavfile: wave.Wave_write
wavfile.setnchannels(self.channels) wavfile.setnchannels(self.channels)
wavfile.setsampwidth(2) wavfile.setsampwidth(2)
@@ -548,6 +669,9 @@ class TranscriptionTeeClient:
# remove this file # remove this file
os.remove(in_file) os.remove(in_file)
wavfile.close() wavfile.close()
# clean up temporary directory to store chunks
if os.path.exists("chunks"):
shutil.rmtree("chunks")
@staticmethod @staticmethod
def bytes_to_float_array(audio_bytes): def bytes_to_float_array(audio_bytes):
@@ -566,6 +690,7 @@ class TranscriptionTeeClient:
raw_data = np.frombuffer(buffer=audio_bytes, dtype=np.int16) raw_data = np.frombuffer(buffer=audio_bytes, dtype=np.int16)
return raw_data.astype(np.float32) / 32768.0 return raw_data.astype(np.float32) / 32768.0
class TranscriptionClient(TranscriptionTeeClient): class TranscriptionClient(TranscriptionTeeClient):
""" """
Client for handling audio transcription tasks via a single WebSocket connection. Client for handling audio transcription tasks via a single WebSocket connection.
@@ -577,7 +702,21 @@ 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.
model (str, optional): The whisper model to use (e.g., "small", "base"). Default is "small".
use_vad (bool, optional): Whether to enable voice activity detection. Default is True.
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.
send_last_n_segments (int, optional): Number of most recent segments to send to the client. Defaults to 10.
no_speech_thresh (float, optional): Segments with no speech probability above this threshold will be discarded. Defaults to 0.45.
clip_audio (bool, optional): Whether to clip audio with no valid segments. Defaults to False.
same_output_threshold (int, optional): Number of repeated outputs before considering it as a valid segment. Defaults to 10.
transcription_callback (callable, optional): A callback function to handle transcription results. Default is None.
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.
@@ -589,6 +728,55 @@ class TranscriptionClient(TranscriptionTeeClient):
transcription_client() transcription_client()
``` ```
""" """
def __init__(self, host, port, lang=None, translate=False, model="small", use_vad=True): def __init__(
self.client = Client(host, port, lang, translate, model, srt_file_path="output.srt", use_vad=use_vad) self,
TranscriptionTeeClient.__init__(self, [self.client]) host,
port,
lang=None,
translate=False,
model="small",
use_vad=True,
use_wss=False,
save_output_recording=False,
output_recording_filename="./output_recording.wav",
output_transcription_path="./output.srt",
log_transcription=True,
max_clients=4,
max_connection_time=600,
mute_audio_playback=False,
send_last_n_segments=10,
no_speech_thresh=0.45,
clip_audio=False,
same_output_threshold=10,
transcription_callback=None,
):
self.client = Client(
host,
port,
lang,
translate,
model,
srt_file_path=output_transcription_path,
use_vad=use_vad,
use_wss=use_wss,
log_transcription=log_transcription,
max_clients=max_clients,
max_connection_time=max_connection_time,
send_last_n_segments=send_last_n_segments,
no_speech_thresh=no_speech_thresh,
clip_audio=clip_audio,
same_output_threshold=same_output_threshold,
transcription_callback=transcription_callback,
)
if save_output_recording and not output_recording_filename.endswith(".wav"):
raise ValueError(f"Please provide a valid `output_recording_filename`: {output_recording_filename}")
if not output_transcription_path.endswith(".srt"):
raise ValueError(f"Please provide a valid `output_transcription_path`: {output_transcription_path}. The file extension should be `.srt`.")
TranscriptionTeeClient.__init__(
self,
[self.client],
save_output_recording=save_output_recording,
output_recording_filename=output_recording_filename,
mute_audio_playback=mute_audio_playback
)
+133 -678
View File
@@ -4,16 +4,14 @@ import threading
import json import json
import functools import functools
import logging import logging
import torch from enum import Enum
from typing import List, Optional
import numpy as np import numpy as np
from websockets.sync.server import serve from websockets.sync.server import serve
from websockets.exceptions import ConnectionClosed from websockets.exceptions import ConnectionClosed
from whisper_live.vad import VoiceActivityDetector from whisper_live.vad import VoiceActivityDetector
from whisper_live.transcriber import WhisperModel from whisper_live.backend.base import ServeClientBase
try:
from whisper_live.transcriber_tensorrt import WhisperTRTLLM
except Exception:
pass
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@@ -121,27 +119,60 @@ class ClientManager:
return False return False
class BackendType(Enum):
FASTER_WHISPER = "faster_whisper"
TENSORRT = "tensorrt"
OPENVINO = "openvino"
@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
def is_openvino(self) -> bool:
return self == BackendType.OPENVINO
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
def initialize_client( def initialize_client(
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, trt_py_session=False,
): ):
if self.backend == "tensorrt": client: Optional[ServeClientBase] = None
if self.backend.is_tensorrt():
try: try:
from whisper_live.backend.trt_backend import ServeClientTensorRT
client = ServeClientTensorRT( client = ServeClientTensorRT(
websocket, websocket,
multilingual=trt_multilingual, multilingual=trt_multilingual,
language=options["language"], language=options["language"],
task=options["task"], task=options["task"],
client_uid=options["uid"], client_uid=options["uid"],
model=whisper_tensorrt_path model=whisper_tensorrt_path,
single_model=self.single_model,
use_py_session=trt_py_session,
send_last_n_segments=options.get("send_last_n_segments", 10),
no_speech_thresh=options.get("no_speech_thresh", 0.45),
clip_audio=options.get("clip_audio", False),
same_output_threshold=options.get("same_output_threshold", 10),
) )
logging.info("Running TensorRT backend.") logging.info("Running TensorRT backend.")
except Exception as e: except Exception as e:
@@ -153,23 +184,64 @@ 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.is_openvino():
try:
from whisper_live.backend.openvino_backend import ServeClientOpenVINO
client = ServeClientOpenVINO(
websocket,
language=options["language"],
task=options["task"],
client_uid=options["uid"],
model=options["model"],
single_model=self.single_model,
send_last_n_segments=options.get("send_last_n_segments", 10),
no_speech_thresh=options.get("no_speech_thresh", 0.45),
clip_audio=options.get("clip_audio", False),
same_output_threshold=options.get("same_output_threshold", 10),
)
logging.info("Running OpenVINO backend.")
except Exception as e:
logging.error(f"OpenVINO not supported: {e}")
self.backend = BackendType.FASTER_WHISPER
self.client_uid = options["uid"]
websocket.send(json.dumps({
"uid": self.client_uid,
"status": "WARNING",
"message": "OpenVINO not supported on Server yet. "
"Reverting to available backend: '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}") from whisper_live.backend.faster_whisper_backend import ServeClientFasterWhisper
options["model"] = faster_whisper_custom_model_path if faster_whisper_custom_model_path is not None and os.path.exists(faster_whisper_custom_model_path):
client = ServeClientFasterWhisper( logging.info(f"Using custom model {faster_whisper_custom_model_path}")
websocket, options["model"] = faster_whisper_custom_model_path
language=options["language"], client = ServeClientFasterWhisper(
task=options["task"], websocket,
client_uid=options["uid"], language=options["language"],
model=options["model"], task=options["task"],
initial_prompt=options.get("initial_prompt"), client_uid=options["uid"],
vad_parameters=options.get("vad_parameters"), model=options["model"],
use_vad=self.use_vad, initial_prompt=options.get("initial_prompt"),
) vad_parameters=options.get("vad_parameters"),
logging.info("Running faster_whisper backend.") use_vad=self.use_vad,
single_model=self.single_model,
send_last_n_segments=options.get("send_last_n_segments", 10),
no_speech_thresh=options.get("no_speech_thresh", 0.45),
clip_audio=options.get("clip_audio", False),
same_output_threshold=options.get("same_output_threshold", 10),
)
logging.info("Running faster_whisper backend.")
except Exception as e:
logging.error(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)
@@ -189,20 +261,26 @@ class TranscriptionServer:
return np.frombuffer(frame_data, dtype=np.float32) return np.frombuffer(frame_data, dtype=np.float32)
def handle_new_connection(self, websocket, faster_whisper_custom_model_path, def handle_new_connection(self, websocket, faster_whisper_custom_model_path,
whisper_tensorrt_path, trt_multilingual): whisper_tensorrt_path, trt_multilingual, trt_py_session=False):
try: try:
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, trt_py_session=trt_py_session)
return True return True
except json.JSONDecodeError: except json.JSONDecodeError:
logging.error("Failed to decode JSON from client") logging.error("Failed to decode JSON from client")
@@ -218,11 +296,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
@@ -234,11 +312,12 @@ class TranscriptionServer:
return True return True
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,
trt_py_session=False):
""" """
Receive audio chunks from a client in an infinite loop. Receive audio chunks from a client in an infinite loop.
@@ -265,7 +344,7 @@ class TranscriptionServer:
""" """
self.backend = backend self.backend = backend
if not self.handle_new_connection(websocket, faster_whisper_custom_model_path, if not self.handle_new_connection(websocket, faster_whisper_custom_model_path,
whisper_tensorrt_path, trt_multilingual): whisper_tensorrt_path, trt_multilingual, trt_py_session=trt_py_session):
return return
try: try:
@@ -288,7 +367,9 @@ class TranscriptionServer:
backend="tensorrt", backend="tensorrt",
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,
trt_py_session=False,
single_model=False):
""" """
Run the transcription server. Run the transcription server.
@@ -296,13 +377,27 @@ class TranscriptionServer:
host (str): The host address to bind the server. host (str): The host address to bind the server.
port (int): The port number to bind the server. port (int): The port number to bind the server.
""" """
if faster_whisper_custom_model_path is not None and not os.path.exists(faster_whisper_custom_model_path):
raise ValueError(f"Custom faster_whisper model '{faster_whisper_custom_model_path}' is not a valid path.")
if whisper_tensorrt_path is not None and not os.path.exists(whisper_tensorrt_path):
raise ValueError(f"TensorRT model '{whisper_tensorrt_path}' is not a valid path.")
if single_model:
if faster_whisper_custom_model_path or whisper_tensorrt_path:
logging.info("Custom model option was provided. Switching to single model mode.")
self.single_model = True
# TODO: load model initially
else:
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,
trt_py_session=trt_py_session,
), ),
host, host,
port port
@@ -349,643 +444,3 @@ class TranscriptionServer:
if self.client_manager.get_client(websocket): if self.client_manager.get_client(websocket):
self.client_manager.remove_client(websocket) self.client_manager.remove_client(websocket)
class ServeClientBase(object):
RATE = 16000
SERVER_READY = "SERVER_READY"
DISCONNECT = "DISCONNECT"
def __init__(self, client_uid, websocket):
self.client_uid = client_uid
self.websocket = websocket
self.frames = b""
self.timestamp_offset = 0.0
self.frames_np = None
self.frames_offset = 0.0
self.text = []
self.current_out = ''
self.prev_out = ''
self.t_start = None
self.exit = False
self.same_output_threshold = 0
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.transcript = []
self.send_last_n_segments = 10
# text formatting
self.pick_previous_segments = 2
# threading
self.lock = threading.Lock()
def speech_to_text(self):
raise NotImplementedError
def transcribe_audio(self):
raise NotImplementedError
def handle_transcription_output(self):
raise NotImplementedError
def add_frames(self, frame_np):
"""
Add audio frames to the ongoing audio stream buffer.
This method is responsible for maintaining the audio stream buffer, allowing the continuous addition
of audio frames as they are received. It also ensures that the buffer does not exceed a specified size
to prevent excessive memory usage.
If the buffer size exceeds a threshold (45 seconds of audio data), it discards the oldest 30 seconds
of audio data to maintain a reasonable buffer size. If the buffer is empty, it initializes it with the provided
audio frame. The audio stream buffer is used for real-time processing of audio data for transcription.
Args:
frame_np (numpy.ndarray): The audio frame data as a NumPy array.
"""
self.lock.acquire()
if self.frames_np is not None and self.frames_np.shape[0] > 45*self.RATE:
self.frames_offset += 30.0
self.frames_np = self.frames_np[int(30*self.RATE):]
# check timestamp offset(should be >= self.frame_offset)
# this basically means that there is no speech as timestamp offset hasnt updated
# and is less than frame_offset
if self.timestamp_offset < self.frames_offset:
self.timestamp_offset = self.frames_offset
if self.frames_np is None:
self.frames_np = frame_np.copy()
else:
self.frames_np = np.concatenate((self.frames_np, frame_np), axis=0)
self.lock.release()
def clip_audio_if_no_valid_segment(self):
"""
Update the timestamp offset based on audio buffer status.
Clip audio if the current chunk exceeds 30 seconds, this basically implies that
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:
duration = self.frames_np.shape[0] / self.RATE
self.timestamp_offset = self.frames_offset + duration - 5
def get_audio_chunk_for_processing(self):
"""
Retrieves the next chunk of audio data for processing based on the current offsets.
Calculates which part of the audio data should be processed next, based on
the difference between the current timestamp offset and the frame's offset, scaled by
the audio sample rate (RATE). It then returns this chunk of audio data along with its
duration in seconds.
Returns:
tuple: A tuple containing:
- input_bytes (np.ndarray): The next chunk of audio data to be processed.
- duration (float): The duration of the audio chunk in seconds.
"""
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
return input_bytes, duration
def prepare_segments(self, last_segment=None):
"""
Prepares the segments of transcribed text to be sent to the client.
This method compiles the recent segments of transcribed text, ensuring that only the
specified number of the most recent segments are included. It also appends the most
recent segment of text if provided (which is considered incomplete because of the possibility
of the last word being truncated in the audio chunk).
Args:
last_segment (str, optional): The most recent segment of transcribed text to be added
to the list of segments. Defaults to None.
Returns:
list: A list of transcribed text segments to be sent to the client.
"""
segments = []
if len(self.transcript) >= self.send_last_n_segments:
segments = self.transcript[-self.send_last_n_segments:].copy()
else:
segments = self.transcript.copy()
if last_segment is not None:
segments = segments + [last_segment]
return segments
def get_audio_chunk_duration(self, input_bytes):
"""
Calculates the duration of the provided audio chunk.
Args:
input_bytes (numpy.ndarray): The audio chunk for which to calculate the duration.
Returns:
float: The duration of the audio chunk in seconds.
"""
return input_bytes.shape[0] / self.RATE
def send_transcription_to_client(self, segments):
"""
Sends the specified transcription segments to the client over the websocket connection.
This method formats the transcription segments into a JSON object and attempts to send
this object to the client. If an error occurs during the send operation, it logs the error.
Returns:
segments (list): A list of transcription segments to be sent to the client.
"""
try:
self.websocket.send(
json.dumps({
"uid": self.client_uid,
"segments": segments,
})
)
except Exception as e:
logging.error(f"[ERROR]: Sending data to client: {e}")
def disconnect(self):
"""
Notify the client of disconnection and send a disconnect message.
This method sends a disconnect message to the client via the WebSocket connection to notify them
that the transcription service is disconnecting gracefully.
"""
self.websocket.send(json.dumps({
"uid": self.client_uid,
"message": self.DISCONNECT
}))
def cleanup(self):
"""
Perform cleanup tasks before exiting the transcription service.
This method performs necessary cleanup tasks, including stopping the transcription thread, marking
the exit flag to indicate the transcription thread should exit gracefully, and destroying resources
associated with the transcription process.
"""
logging.info("Cleaning up.")
self.exit = True
class ServeClientTensorRT(ServeClientBase):
def __init__(self, websocket, task="transcribe", multilingual=False, language=None, client_uid=None, model=None):
"""
Initialize a ServeClient instance.
The Whisper model is initialized based on the client's language and device availability.
The transcription thread is started upon initialization. A "SERVER_READY" message is sent
to the client to indicate that the server is ready.
Args:
websocket (WebSocket): The WebSocket connection for the client.
task (str, optional): The task type, e.g., "transcribe." Defaults to "transcribe".
device (str, optional): The device type for Whisper, "cuda" or "cpu". Defaults to None.
multilingual (bool, optional): Whether the client supports multilingual transcription. Defaults to False.
language (str, optional): The language for transcription. Defaults to None.
client_uid (str, optional): A unique identifier for the client. Defaults to None.
"""
super().__init__(client_uid, websocket)
self.language = language if multilingual else "en"
self.task = task
self.eos = False
self.transcriber = WhisperTRTLLM(
model,
assets_dir="assets",
device="cuda",
is_multilingual=multilingual,
language=self.language,
task=self.task
)
self.warmup()
# threading
self.trans_thread = threading.Thread(target=self.speech_to_text)
self.trans_thread.start()
self.websocket.send(json.dumps({
"uid": self.client_uid,
"message": self.SERVER_READY,
"backend": "tensorrt"
}))
def warmup(self, warmup_steps=10):
"""
Warmup TensorRT since first few inferences are slow.
Args:
warmup_steps (int): Number of steps to warm up the model for.
"""
logging.info("[INFO:] Warming up TensorRT engine..")
mel, _ = self.transcriber.log_mel_spectrogram("assets/jfk.flac")
for i in range(warmup_steps):
self.transcriber.transcribe(mel)
def set_eos(self, eos):
"""
Sets the End of Speech (EOS) flag.
Args:
eos (bool): The value to set for the EOS flag.
"""
self.lock.acquire()
self.eos = eos
self.lock.release()
def handle_transcription_output(self, last_segment, duration):
"""
Handle the transcription output, updating the transcript and sending data to the client.
Args:
last_segment (str): The last segment from the whisper output which is considered to be incomplete because
of the possibility of word being truncated.
duration (float): Duration of the transcribed audio chunk.
"""
segments = self.prepare_segments({"text": last_segment})
self.send_transcription_to_client(segments)
if self.eos:
self.update_timestamp_offset(last_segment, duration)
def transcribe_audio(self, input_bytes):
"""
Transcribe the audio chunk and send the results to the client.
Args:
input_bytes (np.array): The audio chunk to transcribe.
"""
logging.info(f"[WhisperTensorRT:] Processing audio with duration: {input_bytes.shape[0] / self.RATE}")
mel, duration = self.transcriber.log_mel_spectrogram(input_bytes)
last_segment = self.transcriber.transcribe(
mel,
text_prefix=f"<|startoftranscript|><|{self.language}|><|{self.task}|><|notimestamps|>"
)
if last_segment:
self.handle_transcription_output(last_segment, duration)
def update_timestamp_offset(self, last_segment, duration):
"""
Update timestamp offset and transcript.
Args:
last_segment (str): Last transcribed audio from the whisper model.
duration (float): Duration of the last audio chunk.
"""
if not len(self.transcript):
self.transcript.append({"text": last_segment + " "})
elif self.transcript[-1]["text"].strip() != last_segment:
self.transcript.append({"text": last_segment + " "})
self.timestamp_offset += duration
def speech_to_text(self):
"""
Process an audio stream in an infinite loop, continuously transcribing the speech.
This method continuously receives audio frames, performs real-time transcription, and sends
transcribed segments to the client via a WebSocket connection.
If the client's language is not detected, it waits for 30 seconds of audio input to make a language prediction.
It utilizes the Whisper ASR model to transcribe the audio, continuously processing and streaming results. Segments
are sent to the client in real-time, and a history of segments is maintained to provide context.Pauses in speech
(no output from Whisper) are handled by showing the previous output for a set duration. A blank segment is added if
there is no speech for a specified duration to indicate a pause.
Raises:
Exception: If there is an issue with audio processing or WebSocket communication.
"""
while True:
if self.exit:
logging.info("Exiting speech to text thread")
break
if self.frames_np is None:
time.sleep(0.02) # wait for any audio to arrive
continue
self.clip_audio_if_no_valid_segment()
input_bytes, duration = self.get_audio_chunk_for_processing()
if duration < 0.4:
continue
try:
input_sample = input_bytes.copy()
logging.info(f"[WhisperTensorRT:] Processing audio with duration: {duration}")
self.transcribe_audio(input_sample)
except Exception as e:
logging.error(f"[ERROR]: {e}")
class ServeClientFasterWhisper(ServeClientBase):
def __init__(self, websocket, task="transcribe", device=None, language=None, client_uid=None, model="small.en",
initial_prompt=None, vad_parameters=None, use_vad=True):
"""
Initialize a ServeClient instance.
The Whisper model is initialized based on the client's language and device availability.
The transcription thread is started upon initialization. A "SERVER_READY" message is sent
to the client to indicate that the server is ready.
Args:
websocket (WebSocket): The WebSocket connection for the client.
task (str, optional): The task type, e.g., "transcribe." Defaults to "transcribe".
device (str, optional): The device type for Whisper, "cuda" or "cpu". Defaults to None.
language (str, optional): The language for transcription. Defaults to None.
client_uid (str, optional): A unique identifier for the client. Defaults to None.
model (str, optional): The whisper model size. Defaults to 'small.en'
initial_prompt (str, optional): Prompt for whisper inference. Defaults to None.
"""
super().__init__(client_uid, websocket)
self.model_sizes = [
"tiny", "tiny.en", "base", "base.en", "small", "small.en",
"medium", "medium.en", "large-v2", "large-v3",
]
if not os.path.exists(model):
self.model_size_or_path = self.check_valid_model(model)
else:
self.model_size_or_path = model
self.language = "en" if self.model_size_or_path.endswith("en") else language
self.task = task
self.initial_prompt = initial_prompt
self.vad_parameters = vad_parameters or {"threshold": 0.5}
self.no_speech_thresh = 0.45
device = "cuda" if torch.cuda.is_available() else "cpu"
if self.model_size_or_path is None:
return
self.transcriber = WhisperModel(
self.model_size_or_path,
device=device,
compute_type="int8" if device == "cpu" else "float16",
local_files_only=False,
)
self.use_vad = use_vad
# threading
self.trans_thread = threading.Thread(target=self.speech_to_text)
self.trans_thread.start()
self.websocket.send(
json.dumps(
{
"uid": self.client_uid,
"message": self.SERVER_READY,
"backend": "faster_whisper"
}
)
)
def check_valid_model(self, model_size):
"""
Check if it's a valid whisper model size.
Args:
model_size (str): The name of the model size to check.
Returns:
str: The model size if valid, None otherwise.
"""
if model_size not in self.model_sizes:
self.websocket.send(
json.dumps(
{
"uid": self.client_uid,
"status": "ERROR",
"message": f"Invalid model size {model_size}. Available choices: {self.model_sizes}"
}
)
)
return None
return model_size
def set_language(self, info):
"""
Updates the language attribute based on the detected language information.
Args:
info (object): An object containing the detected language and its probability. This object
must have at least two attributes: `language`, a string indicating the detected
language, and `language_probability`, a float representing the confidence level
of the language detection.
"""
if info.language_probability > 0.5:
self.language = info.language
logging.info(f"Detected language {self.language} with probability {info.language_probability}")
self.websocket.send(json.dumps(
{"uid": self.client_uid, "language": self.language, "language_prob": info.language_probability}))
def transcribe_audio(self, input_sample):
"""
Transcribes the provided audio sample using the configured transcriber instance.
If the language has not been set, it updates the session's language based on the transcription
information.
Args:
input_sample (np.array): The audio chunk to be transcribed. This should be a NumPy
array representing the audio data.
Returns:
The transcription result from the transcriber. The exact format of this result
depends on the implementation of the `transcriber.transcribe` method but typically
includes the transcribed text.
"""
result, info = self.transcriber.transcribe(
input_sample,
initial_prompt=self.initial_prompt,
language=self.language,
task=self.task,
vad_filter=self.use_vad,
vad_parameters=self.vad_parameters if self.use_vad else None)
if self.language is None and info is not None:
self.set_language(info)
return result
def get_previous_output(self):
"""
Retrieves previously generated transcription outputs if no new transcription is available
from the current audio chunks.
Checks the time since the last transcription output and, if it is within a specified
threshold, returns the most recent segments of transcribed text. It also manages
adding a pause (blank segment) to indicate a significant gap in speech based on a defined
threshold.
Returns:
segments (list): A list of transcription segments. This may include the most recent
transcribed text segments or a blank segment to indicate a pause
in speech.
"""
segments = []
if self.t_start is None:
self.t_start = time.time()
if time.time() - self.t_start < self.show_prev_out_thresh:
segments = self.prepare_segments()
# add a blank if there is no speech for 3 seconds
if len(self.text) and self.text[-1] != '':
if time.time() - self.t_start > self.add_pause_thresh:
self.text.append('')
return segments
def handle_transcription_output(self, result, duration):
"""
Handle the transcription output, updating the transcript and sending data to the client.
Args:
result (str): The result from whisper inference i.e. the list of segments.
duration (float): Duration of the transcribed audio chunk.
"""
segments = []
if len(result):
self.t_start = None
last_segment = self.update_segments(result, duration)
segments = self.prepare_segments(last_segment)
else:
# show previous output if there is pause i.e. no output from whisper
segments = self.get_previous_output()
if len(segments):
self.send_transcription_to_client(segments)
def speech_to_text(self):
"""
Process an audio stream in an infinite loop, continuously transcribing the speech.
This method continuously receives audio frames, performs real-time transcription, and sends
transcribed segments to the client via a WebSocket connection.
If the client's language is not detected, it waits for 30 seconds of audio input to make a language prediction.
It utilizes the Whisper ASR model to transcribe the audio, continuously processing and streaming results. Segments
are sent to the client in real-time, and a history of segments is maintained to provide context.Pauses in speech
(no output from Whisper) are handled by showing the previous output for a set duration. A blank segment is added if
there is no speech for a specified duration to indicate a pause.
Raises:
Exception: If there is an issue with audio processing or WebSocket communication.
"""
while True:
if self.exit:
logging.info("Exiting speech to text thread")
break
if self.frames_np is None:
continue
self.clip_audio_if_no_valid_segment()
input_bytes, duration = self.get_audio_chunk_for_processing()
if duration < 1.0:
continue
try:
input_sample = input_bytes.copy()
result = self.transcribe_audio(input_sample)
if result is None or self.language is None:
self.timestamp_offset += duration
time.sleep(0.25) # wait for voice activity, result is None when no voice activity
continue
self.handle_transcription_output(result, duration)
except Exception as e:
logging.error(f"[ERROR]: Failed to transcribe audio chunk: {e}")
time.sleep(0.01)
def format_segment(self, start, end, text):
"""
Formats a transcription segment with precise start and end times alongside the transcribed text.
Args:
start (float): The start time of the transcription segment in seconds.
end (float): The end time of the transcription segment in seconds.
text (str): The transcribed text corresponding to the segment.
Returns:
dict: A dictionary representing the formatted transcription segment, including
'start' and 'end' times as strings with three decimal places and the 'text'
of the transcription.
"""
return {
'start': "{:.3f}".format(start),
'end': "{:.3f}".format(end),
'text': text
}
def update_segments(self, segments, duration):
"""
Processes the segments from whisper. Appends all the segments to the list
except for the last segment assuming that it is incomplete.
Updates the ongoing transcript with transcribed segments, including their start and end times.
Complete segments are appended to the transcript in chronological order. Incomplete segments
(assumed to be the last one) are processed to identify repeated content. If the same incomplete
segment is seen multiple times, it updates the offset and appends the segment to the transcript.
A threshold is used to detect repeated content and ensure it is only included once in the transcript.
The timestamp offset is updated based on the duration of processed segments. The method returns the
last processed segment, allowing it to be sent to the client for real-time updates.
Args:
segments(dict) : dictionary of segments as returned by whisper
duration(float): duration of the current chunk
Returns:
dict or None: The last processed segment with its start time, end time, and transcribed text.
Returns None if there are no valid segments to process.
"""
offset = None
self.current_out = ''
# process complete segments
if len(segments) > 1:
for i, s in enumerate(segments[:-1]):
text_ = s.text
self.text.append(text_)
start, end = self.timestamp_offset + s.start, self.timestamp_offset + min(duration, s.end)
if start >= end:
continue
if s.no_speech_prob > self.no_speech_thresh:
continue
self.transcript.append(self.format_segment(start, end, text_))
offset = min(duration, s.end)
self.current_out += segments[-1].text
last_segment = self.format_segment(
self.timestamp_offset + segments[-1].start,
self.timestamp_offset + min(duration, segments[-1].end),
self.current_out
)
# if same incomplete segment is seen multiple times then update the offset
# and append the segment to the list
if self.current_out.strip() == self.prev_out.strip() and self.current_out != '':
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():
self.text.append(self.current_out)
self.transcript.append(self.format_segment(
self.timestamp_offset,
self.timestamp_offset + duration,
self.current_out
))
self.current_out = ''
offset = duration
self.same_output_threshold = 0
last_segment = None
else:
self.prev_out = self.current_out
# update offset
if offset is not None:
self.timestamp_offset += offset
return last_segment
File diff suppressed because it is too large Load Diff
@@ -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
@@ -0,0 +1,23 @@
import librosa
import os
import openvino_genai as ov_genai
import huggingface_hub as hf_hub
class WhisperOpenVINO(object):
def __init__(self, model_id="OpenVINO/whisper-tiny-fp16-ov", device="CPU", language="en", task="transcribe"):
model_path = model_id.split('/')[-1]
cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "openvino_whisper_models")
os.makedirs(cache_dir, exist_ok=True)
model_path = os.path.join(cache_dir, model_path)
if not os.path.exists(model_path):
hf_hub.snapshot_download(model_id, local_dir=model_path)
self.model = ov_genai.WhisperPipeline(str(model_path), device=device)
self.language = language
self.task = task
def transcribe(self, input_audio):
outputs = self.model.generate(input_audio, return_timestamps=True, language=self.language, task=self.task)
outputs = [seg for seg in outputs.chunks]
return outputs
@@ -0,0 +1,479 @@
import json
import re
import math
from collections import OrderedDict
from pathlib import Path
from typing import Union
import torch
import numpy as np
import torch.nn.functional as F
from whisper.tokenizer import get_tokenizer
from whisper_live.transcriber.tensorrt_utils import (
mel_filters,
load_audio_wav_format,
pad_or_trim,
load_audio
)
import tensorrt_llm
import tensorrt_llm.logger as logger
from tensorrt_llm._utils import (str_dtype_to_torch, str_dtype_to_trt,
trt_dtype_to_torch)
from tensorrt_llm.bindings import GptJsonConfig, KVCacheType
from tensorrt_llm.runtime import PYTHON_BINDINGS, ModelConfig, SamplingConfig
from tensorrt_llm.runtime.session import Session, TensorInfo
if PYTHON_BINDINGS:
from tensorrt_llm.runtime import ModelRunnerCpp
SAMPLE_RATE = 16000
N_FFT = 400
HOP_LENGTH = 160
CHUNK_LENGTH = 30
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:
def __init__(self, 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):
serialize_path = engine_dir / 'encoder' / 'rank0.engine'
with open(serialize_path, 'rb') as f:
session = Session.from_serialized_engine(f.read())
return session
def get_audio_features(self,
mel,
mel_input_lengths,
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,
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['input_features'] = mel
inputs['input_lengths'] = mel_input_lengths
inputs['position_ids'] = position_ids
output_list = [
TensorInfo('input_features', str_dtype_to_trt(self.dtype),
mel.shape),
TensorInfo('input_lengths', str_dtype_to_trt('int32'),
mel_input_lengths.shape),
TensorInfo('position_ids', str_dtype_to_trt('int32'),
inputs['position_ids'].shape)
]
output_info = (self.session).infer_shapes(output_list)
logger.debug(f'output info {output_info}')
outputs = {
t.name: torch.empty(tuple(t.shape),
dtype=trt_dtype_to_torch(t.dtype),
device='cuda')
for t in output_info
}
stream = torch.cuda.current_stream()
ok = self.session.run(inputs=inputs,
outputs=outputs,
stream=stream.cuda_stream)
assert ok, 'Engine execution failed'
stream.synchronize()
encoder_output = outputs['encoder_output']
encoder_output_lengths = mel_input_lengths // encoder_downsampling_factor
return encoder_output, encoder_output_lengths
class WhisperDecoding:
def __init__(self, engine_dir, runtime_mapping, debug_mode=False):
self.decoder_config = read_config('decoder', engine_dir)
self.decoder_generation_session = self.get_session(
engine_dir, runtime_mapping, debug_mode)
def get_session(self, engine_dir, runtime_mapping, debug_mode=False):
serialize_path = engine_dir / 'decoder' / 'rank0.engine'
with open(serialize_path, "rb") as f:
decoder_engine_buffer = f.read()
decoder_model_config = ModelConfig(
max_batch_size=self.decoder_config['max_batch_size'],
max_beam_width=self.decoder_config['max_beam_width'],
num_heads=self.decoder_config['num_attention_heads'],
num_kv_heads=self.decoder_config['num_attention_heads'],
hidden_size=self.decoder_config['hidden_size'],
vocab_size=self.decoder_config['vocab_size'],
cross_attention=True,
num_layers=self.decoder_config['num_hidden_layers'],
gpt_attention_plugin=self.decoder_config['plugin_config']
['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.
decoder_config['has_position_embedding'],
dtype=self.decoder_config['dtype'],
has_token_type_embedding=False,
)
decoder_generation_session = tensorrt_llm.runtime.GenerationSession(
decoder_model_config,
decoder_engine_buffer,
runtime_mapping,
debug_mode=debug_mode)
return decoder_generation_session
def generate(self,
decoder_input_ids,
encoder_outputs,
encoder_max_input_length,
encoder_input_lengths,
eot_id,
max_new_tokens=40,
num_beams=1):
batch_size = decoder_input_ids.shape[0]
decoder_input_lengths = torch.tensor([
decoder_input_ids.shape[-1]
for _ in range(decoder_input_ids.shape[0])
],
dtype=torch.int32,
device='cuda')
decoder_max_input_length = torch.max(decoder_input_lengths).item()
cross_attention_mask = torch.ones([
batch_size, decoder_max_input_length + max_new_tokens,
encoder_max_input_length
]).int().cuda()
# generation config
sampling_config = SamplingConfig(end_id=eot_id,
pad_id=eot_id,
num_beams=num_beams)
self.decoder_generation_session.setup(
decoder_input_lengths.size(0),
decoder_max_input_length,
max_new_tokens,
beam_width=num_beams,
encoder_max_input_length=encoder_max_input_length)
torch.cuda.synchronize()
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(
decoder_input_ids,
decoder_input_lengths,
sampling_config,
encoder_output=encoder_outputs,
encoder_input_lengths=encoder_input_lengths,
cross_attention_mask=cross_attention_mask,
)
torch.cuda.synchronize()
# get the list of int from output_ids tensor
output_ids = output_ids.cpu().numpy().tolist()
return output_ids
class WhisperTRTLLM(object):
def __init__(self,
engine_dir,
assets_dir=None,
device=None,
is_multilingual=False,
language="en",
task="transcribe",
use_py_session=False,
num_beams=1,
debug_mode=False,
max_output_len=96):
world_size = 1
runtime_rank = tensorrt_llm.mpi_rank()
runtime_mapping = tensorrt_llm.Mapping(world_size, runtime_rank)
torch.cuda.set_device(runtime_rank % runtime_mapping.gpus_per_node)
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.device = device
self.tokenizer = get_tokenizer(
is_multilingual,
num_languages=self.num_languages,
language=language,
task=task,
)
if use_py_session:
self.encoder = WhisperEncoding(engine_dir)
self.decoder = WhisperDecoding(engine_dir,
runtime_mapping,
debug_mode=False)
else:
json_config = GptJsonConfig.parse_file(engine_dir / 'decoder' /
'config.json')
assert json_config.model_config.supports_inflight_batching
runner_kwargs = dict(engine_dir=engine_dir,
is_enc_dec=True,
max_batch_size=1,
max_input_len=3000,
max_output_len=max_output_len,
max_beam_width=num_beams,
debug_mode=debug_mode,
kv_cache_free_gpu_memory_fraction=0.9,
cross_kv_cache_fraction=0.5)
self.model_runner_cpp = ModelRunnerCpp.from_dir(**runner_kwargs)
self.filters = mel_filters(self.device, self.n_mels, assets_dir)
self.use_py_session = use_py_session
def log_mel_spectrogram(
self,
audio: Union[str, np.ndarray, torch.Tensor],
padding: int = 0,
return_duration=True
):
"""
Compute the log-Mel spectrogram of
Parameters
----------
audio: Union[str, np.ndarray, torch.Tensor], shape = (*)
The path to audio or either a NumPy array or Tensor containing the audio waveform in 16 kHz
n_mels: int
The number of Mel-frequency filters, only 80 and 128 are supported
padding: int
Number of zero samples to pad to the right
device: Optional[Union[str, torch.device]]
If given, the audio tensor is moved to this device before STFT
Returns
-------
torch.Tensor, shape = (80 or 128, n_frames)
A Tensor that contains the Mel spectrogram
"""
if not torch.is_tensor(audio):
if isinstance(audio, str):
if audio.endswith('.wav'):
audio, _ = load_audio_wav_format(audio)
else:
audio = load_audio(audio)
assert isinstance(audio, np.ndarray), f"Unsupported audio type: {type(audio)}"
duration = audio.shape[-1] / SAMPLE_RATE
audio = pad_or_trim(audio, N_SAMPLES)
audio = audio.astype(np.float32)
audio = torch.from_numpy(audio)
if self.device is not None:
audio = audio.to(self.device)
if padding > 0:
audio = F.pad(audio, (0, padding))
window = torch.hann_window(N_FFT).to(audio.device)
stft = torch.stft(audio, N_FFT, HOP_LENGTH, window=window, return_complex=True)
magnitudes = stft[..., :-1].abs()**2
mel_spec = self.filters @ magnitudes
log_spec = torch.clamp(mel_spec, min=1e-10).log10()
log_spec = torch.maximum(log_spec, log_spec.max() - 8.0)
log_spec = (log_spec + 4.0) / 4.0
if return_duration:
return log_spec, duration
else:
return log_spec
def process_batch(
self,
mel,
mel_input_lengths,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
num_beams=1,
max_new_tokens=96):
prompt_id = self.tokenizer.encode(
text_prefix, allowed_special=set(self.tokenizer.special_tokens.keys()))
prompt_id = torch.tensor(prompt_id)
batch_size = mel.shape[0]
decoder_input_ids = prompt_id.repeat(batch_size, 1)
if self.use_py_session:
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,
encoder_output,
encoder_max_input_length,
encoder_output_lengths,
self.tokenizer.eot,
max_new_tokens=max_new_tokens,
num_beams=num_beams)
else:
with torch.no_grad():
if isinstance(mel, list):
mel = [
m.transpose(1, 2).type(
str_dtype_to_torch("float16")).squeeze(0)
for m in mel
]
else:
mel = mel.transpose(1, 2)
outputs = self.model_runner_cpp.generate(
batch_input_ids=decoder_input_ids,
encoder_input_features=mel,
encoder_output_lengths=mel_input_lengths // 2,
max_new_tokens=max_new_tokens,
end_id=self.tokenizer.eot,
pad_id=self.tokenizer.eot,
num_beams=num_beams,
output_sequence_lengths=True,
return_dict=True)
torch.cuda.synchronize()
output_ids = outputs['output_ids'].cpu().numpy().tolist()
texts = []
for i in range(len(output_ids)):
text = self.tokenizer.decode(output_ids[i][0]).strip()
texts.append(text)
return texts
def transcribe(
self,
mel,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
dtype='float16',
batch_size=1,
num_beams=1,
padding_strategy="max",
max_new_tokens=96,
):
mel = mel.type(str_dtype_to_torch(dtype))
mel = mel.unsqueeze(0)
# 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,
max_new_tokens=max_new_tokens
)
prediction = predictions[0]
# remove all special tokens in the prediction
prediction = re.sub(r'<\|.*?\|>', '', prediction)
return prediction.strip()
def decode_wav_file(
model,
mel,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
dtype='float16',
batch_size=1,
num_beams=1,
normalizer=None,
mel_filters_dir=None):
mel = mel.type(str_dtype_to_torch(dtype))
mel = mel.unsqueeze(0)
# repeat the mel spectrogram to match the batch size
mel = mel.repeat(batch_size, 1, 1)
predictions = model.process_batch(mel, text_prefix, num_beams)
prediction = predictions[0]
# remove all special tokens in the prediction
prediction = re.sub(r'<\|.*?\|>', '', prediction)
if normalizer:
prediction = normalizer(prediction)
return prediction.strip()
-320
View File
@@ -1,320 +0,0 @@
import json
import re
from collections import OrderedDict
from pathlib import Path
from typing import Union
import torch
import numpy as np
import torch.nn.functional as F
from whisper.tokenizer import get_tokenizer
from whisper_live.tensorrt_utils import (mel_filters, load_audio_wav_format, pad_or_trim, load_audio)
import tensorrt_llm
import tensorrt_llm.logger as logger
from tensorrt_llm._utils import (str_dtype_to_torch, str_dtype_to_trt,
trt_dtype_to_torch)
from tensorrt_llm.runtime import ModelConfig, SamplingConfig
from tensorrt_llm.runtime.session import Session, TensorInfo
SAMPLE_RATE = 16000
N_FFT = 400
HOP_LENGTH = 160
CHUNK_LENGTH = 30
N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE # 480000 samples in a 30-second chunk
class WhisperEncoding:
def __init__(self, engine_dir):
self.session = self.get_session(engine_dir)
def get_session(self, engine_dir):
config_path = engine_dir / 'encoder_config.json'
with open(config_path, 'r') as f:
config = json.load(f)
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:
session = Session.from_serialized_engine(f.read())
return session
def get_audio_features(self, mel):
inputs = OrderedDict()
output_list = []
inputs.update({'x': mel})
output_list.append(
TensorInfo('x', str_dtype_to_trt(self.dtype), mel.shape))
output_info = (self.session).infer_shapes(output_list)
logger.debug(f'output info {output_info}')
outputs = {
t.name: torch.empty(tuple(t.shape),
dtype=trt_dtype_to_torch(t.dtype),
device='cuda')
for t in output_info
}
stream = torch.cuda.current_stream()
ok = self.session.run(inputs=inputs,
outputs=outputs,
stream=stream.cuda_stream)
assert ok, 'Engine execution failed'
stream.synchronize()
audio_features = outputs['output']
return audio_features
class WhisperDecoding:
def __init__(self, engine_dir, runtime_mapping, debug_mode=False):
self.decoder_config = self.get_config(engine_dir)
self.decoder_generation_session = self.get_session(
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):
dtype = self.decoder_config['precision']
serialize_path = engine_dir / f'whisper_decoder_{dtype}_tp1_rank0.engine'
with open(serialize_path, "rb") as f:
decoder_engine_buffer = f.read()
decoder_model_config = ModelConfig(
num_heads=self.decoder_config['num_heads'],
num_kv_heads=self.decoder_config['num_heads'],
hidden_size=self.decoder_config['hidden_size'],
vocab_size=self.decoder_config['vocab_size'],
num_layers=self.decoder_config['num_layers'],
gpt_attention_plugin=self.decoder_config['gpt_attention_plugin'],
remove_input_padding=self.decoder_config['remove_input_padding'],
cross_attention=self.decoder_config['cross_attention'],
has_position_embedding=self.
decoder_config['has_position_embedding'],
has_token_type_embedding=self.
decoder_config['has_token_type_embedding'],
)
decoder_generation_session = tensorrt_llm.runtime.GenerationSession(
decoder_model_config,
decoder_engine_buffer,
runtime_mapping,
debug_mode=debug_mode)
return decoder_generation_session
def generate(self,
decoder_input_ids,
encoder_outputs,
eot_id,
max_new_tokens=40,
num_beams=1):
encoder_input_lengths = torch.tensor(
[encoder_outputs.shape[1] for x in range(encoder_outputs.shape[0])],
dtype=torch.int32,
device='cuda')
decoder_input_lengths = torch.tensor([
decoder_input_ids.shape[-1]
for _ in range(decoder_input_ids.shape[0])
],
dtype=torch.int32,
device='cuda')
decoder_max_input_length = torch.max(decoder_input_lengths).item()
# generation config
sampling_config = SamplingConfig(end_id=eot_id,
pad_id=eot_id,
num_beams=num_beams)
self.decoder_generation_session.setup(
decoder_input_lengths.size(0),
decoder_max_input_length,
max_new_tokens,
beam_width=num_beams,
encoder_max_input_length=encoder_outputs.shape[1])
torch.cuda.synchronize()
decoder_input_ids = decoder_input_ids.type(torch.int32).cuda()
output_ids = self.decoder_generation_session.decode(
decoder_input_ids,
decoder_input_lengths,
sampling_config,
encoder_output=encoder_outputs,
encoder_input_lengths=encoder_input_lengths,
)
torch.cuda.synchronize()
# get the list of int from output_ids tensor
output_ids = output_ids.cpu().numpy().tolist()
return output_ids
class WhisperTRTLLM(object):
def __init__(self, engine_dir, assets_dir=None, device=None, is_multilingual=False,
language="en", task="transcribe"):
world_size = 1
runtime_rank = tensorrt_llm.mpi_rank()
runtime_mapping = tensorrt_llm.Mapping(world_size, runtime_rank)
torch.cuda.set_device(runtime_rank % runtime_mapping.gpus_per_node)
engine_dir = Path(engine_dir)
self.encoder = WhisperEncoding(engine_dir)
self.decoder = WhisperDecoding(engine_dir,
runtime_mapping,
debug_mode=False)
self.n_mels = self.encoder.n_mels
# self.tokenizer = get_tokenizer(num_languages=self.encoder.num_languages,
# tokenizer_dir=assets_dir)
self.device = device
self.tokenizer = get_tokenizer(
is_multilingual,
num_languages=self.encoder.num_languages,
language=language,
task=task,
)
self.filters = mel_filters(self.device, self.encoder.n_mels, assets_dir)
def log_mel_spectrogram(
self,
audio: Union[str, np.ndarray, torch.Tensor],
padding: int = 0,
return_duration=True
):
"""
Compute the log-Mel spectrogram of
Parameters
----------
audio: Union[str, np.ndarray, torch.Tensor], shape = (*)
The path to audio or either a NumPy array or Tensor containing the audio waveform in 16 kHz
n_mels: int
The number of Mel-frequency filters, only 80 and 128 are supported
padding: int
Number of zero samples to pad to the right
device: Optional[Union[str, torch.device]]
If given, the audio tensor is moved to this device before STFT
Returns
-------
torch.Tensor, shape = (80 or 128, n_frames)
A Tensor that contains the Mel spectrogram
"""
if not torch.is_tensor(audio):
if isinstance(audio, str):
if audio.endswith('.wav'):
audio, _ = load_audio_wav_format(audio)
else:
audio = load_audio(audio)
assert isinstance(audio, np.ndarray), f"Unsupported audio type: {type(audio)}"
duration = audio.shape[-1] / SAMPLE_RATE
audio = pad_or_trim(audio, N_SAMPLES)
audio = audio.astype(np.float32)
audio = torch.from_numpy(audio)
if self.device is not None:
audio = audio.to(self.device)
if padding > 0:
audio = F.pad(audio, (0, padding))
window = torch.hann_window(N_FFT).to(audio.device)
stft = torch.stft(audio, N_FFT, HOP_LENGTH, window=window, return_complex=True)
magnitudes = stft[..., :-1].abs()**2
mel_spec = self.filters @ magnitudes
log_spec = torch.clamp(mel_spec, min=1e-10).log10()
log_spec = torch.maximum(log_spec, log_spec.max() - 8.0)
log_spec = (log_spec + 4.0) / 4.0
if return_duration:
return log_spec, duration
else:
return log_spec
def process_batch(
self,
mel,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
num_beams=1):
prompt_id = self.tokenizer.encode(
text_prefix, allowed_special=set(self.tokenizer.special_tokens.keys()))
prompt_id = torch.tensor(prompt_id)
batch_size = mel.shape[0]
decoder_input_ids = prompt_id.repeat(batch_size, 1)
encoder_output = self.encoder.get_audio_features(mel)
output_ids = self.decoder.generate(decoder_input_ids,
encoder_output,
self.tokenizer.eot,
max_new_tokens=96,
num_beams=num_beams)
texts = []
for i in range(len(output_ids)):
text = self.tokenizer.decode(output_ids[i][0]).strip()
texts.append(text)
return texts
def transcribe(
self,
mel,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
dtype='float16',
batch_size=1,
num_beams=1,
):
mel = mel.type(str_dtype_to_torch(dtype))
mel = mel.unsqueeze(0)
predictions = self.process_batch(mel, text_prefix, num_beams)
prediction = predictions[0]
# remove all special tokens in the prediction
prediction = re.sub(r'<\|.*?\|>', '', prediction)
return prediction.strip()
def decode_wav_file(
model,
mel,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
dtype='float16',
batch_size=1,
num_beams=1,
normalizer=None,
mel_filters_dir=None):
mel = mel.type(str_dtype_to_torch(dtype))
mel = mel.unsqueeze(0)
# repeat the mel spectrogram to match the batch size
mel = mel.repeat(batch_size, 1, 1)
predictions = model.process_batch(mel, text_prefix, num_beams)
prediction = predictions[0]
# remove all special tokens in the prediction
prediction = re.sub(r'<\|.*?\|>', '', prediction)
if normalizer:
prediction = normalizer(prediction)
return prediction.strip()
+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()