332 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
makaveli 8d77f0fa5a bump version v0.4.0 2024-03-20 12:04:38 +05:30
makaveli 2e37216282 Merge pull request #174 from jsichi/tee-client
Add support for processing same audio stream via multiple clients running different tasks.
2024-03-17 22:51:26 +05:30
makaveli 7c7a446478 Fix: mock pyaudio for ci to pass the server tests 2024-03-15 12:29:32 +05:30
John Sichi 37d7f2ed66 Merge branch 'main' into tee-client 2024-03-13 20:12:51 +09:00
makaveli 754f22dfae Merge pull request #175 from FlippFuzz/fix-faster-whisper-version-setup
Fix faster whisper version in setup.py
2024-03-11 14:52:46 +05:30
makaveli ebd2dc9568 Merge pull request #173 from FlippFuzz/fix-os-error-no-mic
Handle failure on systems without microphones
2024-03-11 14:50:15 +05:30
FlippFuzz 9b2e17ec4d Fix faster whisper version in setup.py 2024-03-10 20:28:54 +08:00
John Sichi 5b32dc4130 Fix default value for multicast. 2024-03-10 21:02:50 +09:00
John Sichi c0f37c77e9 Remove camelcase 2024-03-10 20:49:17 +09:00
John Sichi 3b15dc76b4 Add support for processing same audio stream via multiple clients with different tasks. 2024-03-10 20:44:15 +09:00
FlippFuzz 4d477e35e7 Handle failure on systems without microphones
Catch the OSError and print a WARN log.
2024-03-10 19:43:53 +08:00
makaveli a17f4041de Merge pull request #163 from makaveli10/upgrade_faster_whisper
Upgrade faster whisper==1.0.1
2024-03-04 22:26:44 +05:30
makaveli10 8a06ba802b update cuda version 12.2.2 gpu dockerfile 2024-03-04 03:24:49 -05:00
makaveli10 02d4566289 upgrade faster whisper 1.0.1 2024-03-04 07:35:43 +00:00
Marcus Edel acd4902bec Merge pull request #161 from makaveli10/fix_docker_workflow
Build & push docker image on every new tag.
2024-02-29 10:42:49 -05:00
makaveli10 a495a49b06 build & push docker image on every new tag 2024-02-29 19:08:32 +05:30
makaveli 9e5ab408cd bump version 0.3.0 2024-02-28 23:39:44 +05:30
Marcus Edel 5e6c26c3a0 Merge pull request #158 from makaveli10/cpu_usage
fix: cpu usage issue.
2024-02-28 09:11:32 -05:00
makaveli10 18b6168807 fix: cpu usage issue 2024-02-28 13:55:37 +05:30
makaveli ec1349360a Merge pull request #157 from makaveli10/trt-multilingual
fix: lanuguage, task prefix in decoder start ids
2024-02-27 18:46:33 +05:30
makaveli10 a41e714801 fix: lanuguage, task prefix in decoder start ids 2024-02-26 23:31:19 -05:00
Marcus Edel 2d16ee552f Merge pull request #156 from makaveli10/fix_docker_image_gpu
Fix docker image gpu.
2024-02-26 09:24:26 -05:00
makaveli10 9699611000 push docker image to ghcr on push to main 2024-02-26 18:50:38 +05:30
makaveli10 ea64d47899 run server with python3 2024-02-26 18:50:18 +05:30
makaveli c067224474 bump version 0.2.1 2024-02-22 11:16:10 +05:30
makaveli e92f53cfd9 Update ci.yml
install wheel
2024-02-22 11:15:34 +05:30
makaveli 308ac1cff7 bump version 0.2.0 2024-02-22 11:03:30 +05:30
makaveli 2fced08705 Merge pull request #149 from makaveli10/docker-ghcr-ci
Docker ghcr ci
2024-02-22 10:14:51 +05:30
makaveli10 8bdaf9249d only run docker image build and push on new version release 2024-02-21 22:56:35 +05:30
makaveli10 b47a56ca6d update readme to use ghcr docker containers 2024-02-21 22:50:56 +05:30
makaveli10 f975bd452e change ghcr owner 2024-02-21 22:49:16 +05:30
makaveli10 cb963c4834 Merge remote-tracking branch 'upstream/main' into docker-ghcr-ci 2024-02-21 22:43:34 +05:30
makaveli 1db94ea96e Merge pull request #147 from makaveli10/vad_option
add VAD a client option
2024-02-21 16:10:54 +05:30
makaveli10 babe5de074 add vad option to firefox extension 2024-02-20 13:10:46 +05:30
makaveli10 99af50208d add vad option in chrome extension 2024-02-20 13:04:24 +05:30
makaveli10 dc22b7da9f add srt_file_path option 2024-02-20 11:53:45 +05:30
makaveli10 2e9f67ba0b Merge remote-tracking branch 'upstream/main' into vad_option 2024-02-20 11:43:56 +05:30
makaveli c919ba3501 Merge pull request #146 from makaveli10/code_formatting
Code formatting
2024-02-20 11:32:27 +05:30
makaveli10 a38fdb494d remove timeout from tests job 2024-02-20 00:28:38 +05:30
makaveli10 fddc244228 Merge remote-tracking branch 'upstream/main' into code_formatting 2024-02-19 22:02:40 +05:30
Marcus Edel 5e1174ff33 Merge pull request #136 from makaveli10/add_tests
Add tests.
2024-02-19 09:03:01 -05:00
Marcus Edel e40414ab1b Merge pull request #144 from makaveli10/update_readme
add whisper live demo video.
2024-02-19 09:02:27 -05:00
makaveli10 17873c66a0 prune docker cache 2024-02-19 06:36:10 -05:00
makaveli10 1147f58225 increase job timeout 2024-02-19 05:13:03 -05:00
makaveli10 0baa1dc0a6 update ci to build and gpu docker image to ghcr 2024-02-19 04:53:39 -05:00
makaveli10 d1de4948ee update base cuda version to 11.8; some dockerfile-gpu fixes 2024-02-19 04:38:41 -05:00
makaveli10 ff871ad485 update dockerfile name 2024-02-16 20:37:59 +05:30
makaveli10 5fe5e0c8ba Merge branch 'vad_option' into develop 2024-02-16 20:32:01 +05:30
makaveli10 b42ced9816 fix: tests for end of speech message while mocking pyaudio 2024-02-16 20:31:38 +05:30
makaveli10 06794470f8 build docker image on pus develop 2024-02-16 19:01:54 +05:30
makaveli10 d530957b2c test docker ci on fork 2024-02-16 18:58:57 +05:30
makaveli10 6cabbe441b update cpu dockerfile with python-slim-buster base image 2024-02-16 18:58:38 +05:30
makaveli10 78da3f6750 Merge branch 'code_formatting' into vad_option 2024-02-16 17:29:37 +05:30
makaveli10 b04cffc458 update readme; remove common content 2024-02-16 17:11:56 +05:30
makaveli10 fd7c5965b3 add whisper live demo video 2024-02-16 13:28:02 +05:30
makaveli10 4471665085 remove test audio from tests 2024-02-15 19:09:57 +05:30
makaveli10 147e97002e clear_screen for updated transcript 2024-02-15 18:56:14 +05:30
makaveli10 e3c7666cf7 update readme with use_vad 2024-02-15 18:13:26 +05:30
makaveli10 8266099ed0 update tensorrt readme 2024-02-15 18:08:02 +05:30
makaveli10 01dc69e068 close when end of audio from client 2024-02-15 18:07:19 +05:30
makaveli10 9bb92b9bb2 use_vad option and send end of audio message 2024-02-15 17:59:12 +05:30
makaveli10 57c4b60e04 remove websocket.path log from exception logging 2024-02-15 15:08:08 +05:30
makaveli10 3cd96367fb make vad an option 2024-02-15 14:59:43 +05:30
makaveli10 c1420cba0d add tests for server exception handling 2024-02-15 12:16:58 +05:30
makaveli10 4db91eed66 update vad tests after refactor 2024-02-15 12:16:39 +05:30
makaveli10 7bcb92c266 create new method for handling a new connection; expcetion handling 2024-02-15 12:16:18 +05:30
makaveli10 170ba22e5b update method docstrings 2024-02-09 16:08:18 +05:30
makaveli10 ac00e28b86 add: VoiceActivityDetector to manage vad 2024-02-09 16:07:43 +05:30
makaveli10 ceb3cc8747 update timeout log 2024-02-09 14:20:47 +05:30
makaveli10 eaec0ead08 add: handle_transcription_output method 2024-02-09 14:19:49 +05:30
makaveli10 9fbff47126 🔨 refactor whisper_live according to flake8 2024-02-09 13:45:13 +05:30
makaveli10 b4abe95fc6 add: code-format job 2024-02-09 13:44:17 +05:30
makaveli10 14974af951 update ci to run tests 2024-02-08 14:07:44 +05:30
makaveli10 bc474b4a76 update on_close; on_error 2024-02-08 14:06:06 +05:30
makaveli10 9ccf940f51 remove debug import excpetion tensorrt llm 2024-02-08 14:05:46 +05:30
makaveli10 9a9972007e remove debug stats 2024-02-08 14:04:52 +05:30
makaveli10 b2ad6478f5 update requirement for tests 2024-02-08 14:04:32 +05:30
makaveli10 490efdeacc mv test audio to assets 2024-02-08 14:04:16 +05:30
makaveli10 cb570d28ce add vad tests 2024-02-08 14:03:57 +05:30
makaveli10 4e5e086c38 add server tests 2024-02-08 14:03:39 +05:30
makaveli10 cf78d5d608 add client tests 2024-02-08 14:03:13 +05:30
makaveli 9d29b08cea Merge pull request #135 from collabora/revert-134-test_pypi_upload
Revert "Test pypi upload"
2024-02-08 12:23:36 +05:30
makaveli 6071cc1cc5 Revert "Test pypi upload" 2024-02-08 12:23:15 +05:30
makaveli f98e309663 Merge pull request #134 from makaveli10/test_pypi_upload
Test pypi upload
2024-02-08 12:23:07 +05:30
makaveli10 30b00d6c89 upload to testpypi 2024-02-08 12:18:50 +05:30
makaveli10 da2992bcaf add tests to ci.yml 2024-02-08 11:42:39 +05:30
makaveli10 16c5ed8ce9 add more python versions 2024-02-08 11:26:17 +05:30
makaveli10 e14fefb671 cache req 2024-02-08 11:13:12 +05:30
makaveli10 98399707a3 add pyaudio mock 2024-02-08 11:12:57 +05:30
makaveli10 567ceb1246 add pyaudio mock; refactor 🔨 2024-02-08 11:12:40 +05:30
makaveli10 28ea8a20f1 update tests ci 2024-02-07 23:54:02 +05:30
makaveli10 acf6dfe5b7 update python version 2024-02-07 23:47:54 +05:30
makaveli10 84a97f5fdd remove whisper_live from patch to mock websocket 2024-02-07 23:47:29 +05:30
makaveli10 ca2634bbb6 add tests workflow 2024-02-07 23:33:49 +05:30
makaveli10 444ce63440 add unit tests 2024-02-07 23:33:24 +05:30
makaveli10 8db063ee33 update log level to info 2024-02-07 23:31:58 +05:30
makaveli10 92cbc37e9c remove debug stats vad 2024-02-07 23:31:12 +05:30
makaveli10 24fd835356 update requirements 2024-02-07 23:30:34 +05:30
makaveli10 5409d14bcb move audio files to assets 2024-02-07 23:30:16 +05:30
makaveli10 d6edf8e847 update on_error; on_close 2024-02-07 23:29:22 +05:30
makaveli10 cc3ed74c0e remove unused imports 2024-02-07 23:26:42 +05:30
makaveli10 20a8a8ad3d update log level to warning 2024-02-07 23:26:13 +05:30
makaveli10 07387abbc0 silence WhisperTRTLLM import warning 2024-02-07 23:25:50 +05:30
makaveli10 4ecc59783e bump version v0.1.0 2024-02-05 22:37:11 +05:30
makaveli ec9074d712 Merge pull request #128 from lightwastak3n/firefox_remove_multilingual
Firefox remove multilingual
2024-02-05 21:42:10 +05:30
Marcus Edel 3a25db4cb9 Merge pull request #127 from makaveli10/update_setup_requirements
Update required packages for setup.
2024-02-05 08:11:01 -05:00
makaveli 4924ec0adb remove empty lines 2024-02-05 18:40:03 +05:30
makaveli f35abc7f81 Merge branch 'collabora:main' into update_setup_requirements 2024-02-05 18:26:38 +05:30
Sasa Trivic f383121ec3 Remove multilingual option description from the extension readme 2024-02-04 18:03:41 +01:00
Sasa Trivic 17d62272cf Update README.md
Remove multilingual from README
2024-02-04 17:52:21 +01:00
Sasa Trivic 91e1b75bfc Merge branch 'collabora:main' into firefox_remove_multilingual 2024-02-04 17:36:57 +01:00
Sasa Trivic 7aad2ae721 Remove multilingual from Firefox. Sort languages, disable all inputs when capturing. Move both transcripts to the bottom center. 2024-02-04 17:35:20 +01:00
makaveli10 6a1b82f953 update required packages for setup 2024-02-04 16:02:02 +05:30
makaveli dc84839873 Merge pull request #126 from makaveli10/fix_typo_multilingual
fix: typo; remove multilingual debug stat
2024-02-04 14:38:54 +05:30
makaveli10 56d19f5469 fix: typo; remove multilingual debug stat 2024-02-04 14:34:16 +05:30
makaveli 08fa183ba4 Merge pull request #123 from lightwastak3n/main
Chrome extension update; remove multililingual option faster-whisper
2024-02-03 19:24:45 +05:30
Marcus Edel d89b27b8aa Merge pull request #124 from Stinosko/patch-1
Add scipy to server.txt.
2024-02-02 15:31:35 -05:00
Stinosko ce68cc6c87 Add scipy to server.txt
The server script uses scipy but is not installed with the current server requirements file.
2024-02-02 21:07:57 +01:00
Sasa Trivic e697574870 Remove multilingual from client. Remove multilingual from faster whisper backend. Disable task dropdown when capturing in chrome extension. 2024-02-02 13:56:26 +01:00
Sasa Trivic b098b52a4d Merge branch 'main' of github.com:lightwastak3n/WhisperLive 2024-02-01 17:37:18 +01:00
Sasa Trivic ad5543b03e Merge remote-tracking branch 'upstream/main' 2024-02-01 17:31:35 +01:00
Marcus Edel 60455b1583 Merge pull request #121 from makaveli10/save_transcript
Save transcript.
2024-02-01 11:11:05 -05:00
Sasa Trivic cb458fc207 Merge pull request #1 from lightwastak3n/extension_rewrite
Extension rewrite
2024-02-01 15:16:47 +01:00
Sasa Trivic 32ed089a76 Change faster whisper to work with new extension 2024-02-01 14:54:29 +01:00
Sasa Trivic 5b28ddefbd Chrome extension - QOL. Remove multilingual part. 2024-02-01 14:44:07 +01:00
Sasa Trivic e1f531eccf Remove duplicate assignment 2024-02-01 13:56:09 +01:00
Sasa Trivic 8200207530 Center transcription div 2024-02-01 13:16:23 +01:00
makaveli10 08575a03c2 write srt file only for faster_whisper backend 2024-02-01 14:22:45 +05:30
makaveli10 f590446865 Merge remote-tracking branch 'upstream/main' into save_transcript 2024-02-01 12:14:09 +05:30
Sasa Trivic 36d137888e Merge branch 'collabora:main' into main 2024-01-31 18:03:29 +01:00
Marcus Edel e64bc9f3d6 Merge pull request #116 from makaveli10/tensorrt_model_warmup
Tensorrt model warmup.
2024-01-31 11:49:40 -05:00
Sasa Trivic 7cc945aded self.client_uid accessed without being defined 2024-01-31 16:45:31 +01:00
makaveli 2c8a25d355 Merge pull request #119 from gchust/main
fix: keyError: 'model' in server, when using browser extension
2024-01-31 18:40:27 +05:30
makaveli10 f4027de343 add: save_transcript to srt file 2024-01-31 17:37:30 +05:30
makaveli10 d1754d2c46 fix: model_size, no_speech, segment timings 2024-01-31 17:37:03 +05:30
gchust 0e6b1c0632 fix: keyError: 'model' in server, when using browser extension 2024-01-31 11:33:48 +00:00
makaveli d6b51ccd7d Update TensorRT_whisper.md
typo: setup.sh file path
2024-01-29 12:33:12 +05:30
makaveli 2f3c1cd172 Update TensorRT_whisper.md
fix: typo in bash script name to build tensorrt engine
2024-01-29 12:27:51 +05:30
makaveli 703263b375 wamrup tensorrt engine 2024-01-29 12:17:27 +05:30
makaveli 30d2cffb93 load audio for warmup 2024-01-29 12:15:39 +05:30
makaveli 4d94c6b38b Update server.txt
install ffmpeg-python to load test file for model warmup
2024-01-29 12:10:18 +05:30
makaveli d5a0f5859e Update TensorRT_whisper.md
ffmpeg is needed for model warmup
2024-01-29 12:06:56 +05:30
makaveli 025873d2ca Update TensorRT server requirements 2024-01-29 11:57:03 +05:30
makaveli 8c36768f7f Merge pull request #112 from lightwastak3n/main
Readme: Fix transcribe examples
2024-01-26 00:27:43 +05:30
Sasa Trivic ce13e7b622 Fix transcribe examples 2024-01-25 18:44:23 +01:00
makaveli 3498787ccd Merge pull request #104 from makaveli10/tensorrt_backend
Tensorrt backend
2024-01-24 16:42:18 +05:30
makaveli 5cd59b1e4c Update README.md
Co-authored-by: Marcus Edel <marcus.edel@fu-berlin.de>
2024-01-24 10:09:22 +05:30
makaveli10 bd543295f3 update readme 2024-01-22 11:54:21 +00:00
makaveli10 8e2642283a update tensorrt readme 2024-01-22 11:48:23 +00:00
makaveli10 3bf5b47947 fix: server; remove debug stats 2024-01-22 11:47:01 +00:00
makaveli10 634dae835b add numba to req(trt-llm) 2024-01-22 11:45:11 +00:00
makaveli10 969a5aa9e5 remove trt_llm install script 2024-01-22 11:44:48 +00:00
makaveli10 44a2e20c68 remove trt-llm dockerfile 2024-01-22 11:44:21 +00:00
makaveli10 986823dbef Merge remote-tracking branch 'upstream/main' into tensorrt_backend 2024-01-19 10:45:01 -05:00
makaveli10 1e2faa3f2b fix: tensorrt llm idocker setup & docs 2024-01-19 10:40:59 -05:00
makaveli10 e3084b34cb update tensorrt docker & readme 2024-01-19 07:30:50 -05:00
makaveli10 b955e63dc1 update READM 2024-01-19 12:02:18 +00:00
makaveli10 f25ff1785a increase chunk size from 64ms to 256ms 2024-01-19 12:02:05 +00:00
makaveli10 867ff522ae add tensorrt installation & whisper conversion script 2024-01-19 11:59:16 +00:00
makaveli10 75001ae6b7 updatetensorrt-llm dockerfile 2024-01-19 11:58:40 +00:00
makaveli10 6f1d13f25b update requirements 2024-01-19 11:57:46 +00:00
makaveli10 7a9dc6db40 add tensorrt readme 2024-01-19 11:53:47 +00:00
makaveli10 735d6c7763 merge with main 2024-01-19 11:43:21 +00:00
Marcus Edel 0942dc2cfd Merge pull request #102 from makaveli10/change_model_size_param_name
Server to control custom model usage.
2024-01-18 11:07:18 -05:00
makaveli10 881fd55776 run server with custom model from args 2024-01-18 15:02:51 +08:00
makaveli 0c01d7b1e5 Merge pull request #98 from makaveli10/change_model_size_param_name
Change model size param name
2024-01-15 21:08:26 +05:30
makaveli10 c810369324 revert the default port of chrom/firefox extension to 9090 2024-01-15 23:30:05 +08:00
makaveli10 71d0fe69c6 add option to use custom model 2024-01-15 23:28:02 +08:00
makaveli10 67232fffd5 install whl 2024-01-12 11:39:05 +00:00
makaveli10 076aebf3b6 bump version v0.0.11 2024-01-12 15:21:27 +05:30
makaveli10 4cf9d95f73 merge main 2024-01-12 08:17:46 +00:00
makaveli10 389bb5ae37 add docker setup for tensorrt-llm; update readme 2024-01-12 08:15:53 +00:00
Marcus Edel a7eedc5d84 Merge pull request #94 from makaveli10/fix_error_messages
Fix: error messages.
2024-01-11 09:52:07 -05:00
makaveli d91330d790 Merge branch 'collabora:main' into fix_error_messages 2024-01-11 18:18:58 +05:30
makaveli 783d147316 Merge pull request #96 from makaveli10/fix_key_error
fix: key error
2024-01-11 16:29:07 +05:30
makaveli10 058c93e55e fix: key error 2024-01-11 18:55:43 +08:00
makaveli10 f06b9bc827 remove torch req 2024-01-11 08:18:51 +00:00
makaveli10 3c202bf836 update README; add TensorRT doc 2024-01-11 08:18:25 +00:00
makaveli10 647c576e6a update with multilingual option 2024-01-11 08:17:56 +00:00
makaveli10 71a062b726 update dockerfiles 2024-01-10 14:30:57 +00:00
makaveli10 a26f990586 update readme to new setup.sh path 2024-01-10 14:22:51 +00:00
makaveli10 ddb1e0947f move setup.sh to scripts 2024-01-10 14:22:20 +00:00
makaveli10 6dff4fbdd3 add tensorrt_llm installation script 2024-01-10 14:21:39 +00:00
makaveli10 244ca9e6ba remove duplicate code 2024-01-10 14:14:48 +00:00
makaveli10 0f9e93d203 add: tensorrt backend to server 2024-01-09 18:10:17 +00:00
makaveli10 fd86340f30 add: tensorrt backend 2024-01-09 18:09:50 +00:00
makaveli10 2300eedc8b fix: error messages 2024-01-09 13:35:38 +08:00
makaveli cafcb04fbc Merge pull request #92 from hcljsq/main
feat: set initial_prompt and vad_parameters in the first message
2024-01-09 10:00:48 +05:30
Chen Hua 72ead71eeb feat: set initial_prompt and vad_parameters in the first message 2024-01-08 15:33:55 +08:00
makaveli 7b2f5cff72 Merge pull request #89 from hcljsq/main
format segment timestamps
2024-01-03 21:27:06 +05:30
Chen Hua 32c6a565d7 refactor(server): add format_segment helper to standardize timestamp output 2024-01-03 12:25:25 +08:00
makaveli e30286c046 Merge pull request #83 from Chronoz/fix_exception_on_overflow
fix exception on overflow
2024-01-02 19:01:36 +05:30
Chronoz e92ddd291a fix exception on overflow 2023-12-31 20:59:33 +07:00
46 changed files with 6141 additions and 2273 deletions
+182 -36
View File
@@ -1,4 +1,4 @@
name: CI name: Test & Build CI/CD
on: on:
push: push:
@@ -7,46 +7,192 @@ on:
tags: tags:
- v* - v*
pull_request: pull_request:
branches: branches: [ main ]
- main types: [opened, synchronize, reopened]
jobs: jobs:
build-and-push-package: run-tests:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [3.9, '3.10', 3.11, 3.12]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Cache Python dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/pip
!~/.cache/pip/log
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements/server.txt', 'requirements/client.txt') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y portaudio19-dev
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements/server.txt --extra-index-url https://download.pytorch.org/whl/cpu
pip install -r requirements/client.txt
- name: Run tests
run: |
echo "Running tests with Python ${{ matrix.python-version }}"
python -m unittest discover -s tests
check-code-format:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [3.9, '3.10', 3.11, 3.12]
steps: steps:
- name: Check Out Repository - uses: actions/checkout@v2
uses: actions/checkout@v2
- name: Set up Python - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: ${{ matrix.python-version }}
- name: Set up FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v2
- name: Install Additional requirements
run: |
sudo apt-get -y install portaudio19-dev wget
shell: bash
- name: Install Client Requirements - name: Install dependencies
run: pip install -r requirements/client.txt run: |
python -m pip install --upgrade pip
python -m pip install flake8
- name: Install Server Requirements - name: Lint with flake8
run: pip install -r requirements/server.txt run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Install Wheel for build build-and-push-docker-cpu:
run: pip install wheel twine needs: [run-tests, check-code-format]
runs-on: ubuntu-22.04
- name: Build wheel if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
run: | steps:
python setup.py sdist bdist_wheel - uses: actions/checkout@v2
- name: Push package on Test PyPI - name: Log in to GitHub Container Registry
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: docker/login-action@v1
uses: pypa/gh-action-pypi-publish@release/v1 with:
with: registry: ghcr.io
user: __token__ username: ${{ github.repository_owner }}
password: ${{ secrets.PYPI_API_TOKEN }} password: ${{ secrets.GHCR_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
file: docker/Dockerfile.cpu
push: true
tags: ghcr.io/collabora/whisperlive-cpu:latest
build-and-push-docker-gpu:
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.gpu
push: true
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:
needs: [run-tests, check-code-format]
runs-on: ubuntu-22.04
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Cache Python dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/pip
!~/.cache/pip/log
key: ubuntu-latest-pip-3.9-${{ hashFiles('requirements/server.txt', 'requirements/client.txt') }}
restore-keys: |
ubuntu-latest-pip-3.9-
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y portaudio19-dev
- name: Install Python dependencies
run: |
pip install -r requirements/server.txt
pip install -r requirements/client.txt
pip install wheel
- name: Build package
run: python setup.py sdist bdist_wheel
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
-1
View File
@@ -26,7 +26,6 @@ To capture the audio in the current tab, we used the chrome `tabCapture` API to
### Options ### Options
When using the Audio Transcription extension, you have the following options: When using the Audio Transcription extension, you have the following options:
- **Use Collabora Server**: We provide a demo server which runs the whisper small model. - **Use Collabora Server**: We provide a demo server which runs the whisper small model.
- **Use Multilingual Model**: Enable this option to utilize the multilingual capabilities of OpenAI-whisper.
- **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper. - **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper.
- **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English. - **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English.
- **Model Size**: Select the whisper model size to run the server with. - **Model Size**: Select the whisper model size to run the server with.
+2 -1
View File
@@ -157,7 +157,8 @@ async function startCapture(options) {
multilingual: options.useMultilingual, multilingual: options.useMultilingual,
language: options.language, language: options.language,
task: options.task, task: options.task,
modelSize: options.modelSize modelSize: options.modelSize,
useVad: options.useVad,
}, },
}); });
} else { } else {
+1 -1
View File
@@ -59,7 +59,7 @@ function init_element() {
elem_container = document.createElement('div'); elem_container = document.createElement('div');
elem_container.id = "transcription"; elem_container.id = "transcription";
elem_container.style.cssText = 'padding-top:16px;font-size:18px;line-height:18px;top:0px;position:absolute;width:500px;height:90px;opacity:0.9;z-index:100;background:black;border-radius:10px;color:white;'; elem_container.style.cssText = 'padding-top:16px;font-size:18px;position: fixed; top: 85%; left: 50%; transform: translate(-50%, -50%);line-height:18px;width:500px;height:90px;opacity:0.9;z-index:100;background:black;border-radius:10px;color:white;';
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
elem_text = document.createElement('span'); elem_text = document.createElement('span');
+2 -5
View File
@@ -93,17 +93,14 @@ async function startRecord(option) {
const socket = new WebSocket(`ws://${option.host}:${option.port}/`); const socket = new WebSocket(`ws://${option.host}:${option.port}/`);
let isServerReady = false; let isServerReady = false;
let language = option.language; let language = option.language;
if (language === null && !option.multilingual) {
language = 'en';
}
socket.onopen = function(e) { socket.onopen = function(e) {
socket.send( socket.send(
JSON.stringify({ JSON.stringify({
uid: uuid, uid: uuid,
multilingual: option.multilingual,
language: option.language, language: option.language,
task: option.task, task: option.task,
model_size: option.modelSize model: option.modelSize,
use_vad: option.useVad
}) })
); );
}; };
+103 -98
View File
@@ -16,111 +16,112 @@
<label for="useServerCheckbox">Use Collabora Whisper-Live Server</label> <label for="useServerCheckbox">Use Collabora Whisper-Live Server</label>
</div> </div>
<div class="checkbox-container"> <div class="checkbox-container">
<input type="checkbox" id="useMultilingualCheckbox"> <input type="checkbox" id="useVadCheckbox">
<label for="useMultilingualCheckbox">Use Multilingual Model</label> <label for="useVadCheckbox">Use Voice Activity Detection</label>
</div> </div>
<div class="dropdown-container"> <div class="dropdown-container">
<label for="languageDropdown">Select Language:</label> <label for="languageDropdown">Select Language:</label>
<select id="languageDropdown" disabled> <select id="languageDropdown">
<option value="">Select Language</option> <option value="" selected>Automatically detect</option>
<option value="zh">Chinese</option>
<option value="de">German</option>
<option value="es">Spanish</option>
<option value="ru">Russian</option>
<option value="ko">Korean</option>
<option value="fr">French</option>
<option value="ja">Japanese</option>
<option value="pt">Portuguese</option>
<option value="tr">Turkish</option>
<option value="pl">Polish</option>
<option value="ca">Catalan</option>
<option value="nl">Dutch</option>
<option value="ar">Arabic</option>
<option value="sv">Swedish</option>
<option value="it">Italian</option>
<option value="id">Indonesian</option>
<option value="hi">Hindi</option>
<option value="fi">Finnish</option>
<option value="vi">Vietnamese</option>
<option value="he">Hebrew</option>
<option value="uk">Ukrainian</option>
<option value="el">Greek</option>
<option value="ms">Malay</option>
<option value="cs">Czech</option>
<option value="ro">Romanian</option>
<option value="da">Danish</option>
<option value="hu">Hungarian</option>
<option value="ta">Tamil</option>
<option value="no">Norwegian</option>
<option value="th">Thai</option>
<option value="ur">Urdu</option>
<option value="hr">Croatian</option>
<option value="bg">Bulgarian</option>
<option value="lt">Lithuanian</option>
<option value="la">Latin</option>
<option value="mi">Maori</option>
<option value="ml">Malayalam</option>
<option value="cy">Welsh</option>
<option value="sk">Slovak</option>
<option value="te">Telugu</option>
<option value="fa">Persian</option>
<option value="lv">Latvian</option>
<option value="bn">Bengali</option>
<option value="sr">Serbian</option>
<option value="az">Azerbaijani</option>
<option value="sl">Slovenian</option>
<option value="kn">Kannada</option>
<option value="et">Estonian</option>
<option value="mk">Macedonian</option>
<option value="br">Breton</option>
<option value="eu">Basque</option>
<option value="is">Icelandic</option>
<option value="hy">Armenian</option>
<option value="ne">Nepali</option>
<option value="mn">Mongolian</option>
<option value="bs">Bosnian</option>
<option value="kk">Kazakh</option>
<option value="sq">Albanian</option>
<option value="sw">Swahili</option>
<option value="gl">Galician</option>
<option value="mr">Marathi</option>
<option value="pa">Punjabi</option>
<option value="si">Sinhala</option>
<option value="km">Khmer</option>
<option value="sn">Shona</option>
<option value="yo">Yoruba</option>
<option value="so">Somali</option>
<option value="af">Afrikaans</option> <option value="af">Afrikaans</option>
<option value="oc">Occitan</option> <option value="sq">Albanian</option>
<option value="ka">Georgian</option>
<option value="be">Belarusian</option>
<option value="tg">Tajik</option>
<option value="sd">Sindhi</option>
<option value="gu">Gujarati</option>
<option value="am">Amharic</option> <option value="am">Amharic</option>
<option value="yi">Yiddish</option> <option value="ar">Arabic</option>
<option value="lo">Lao</option> <option value="hy">Armenian</option>
<option value="uz">Uzbek</option>
<option value="fo">Faroese</option>
<option value="ht">Haitian Creole</option>
<option value="ps">Pashto</option>
<option value="tk">Turkmen</option>
<option value="nn">Nynorsk</option>
<option value="mt">Maltese</option>
<option value="sa">Sanskrit</option>
<option value="lb">Luxembourgish</option>
<option value="my">Myanmar</option>
<option value="bo">Tibetan</option>
<option value="tl">Tagalog</option>
<option value="mg">Malagasy</option>
<option value="as">Assamese</option> <option value="as">Assamese</option>
<option value="tt">Tatar</option> <option value="az">Azerbaijani</option>
<option value="haw">Hawaiian</option>
<option value="ln">Lingala</option>
<option value="ha">Hausa</option>
<option value="ba">Bashkir</option> <option value="ba">Bashkir</option>
<option value="eu">Basque</option>
<option value="be">Belarusian</option>
<option value="bn">Bengali</option>
<option value="bs">Bosnian</option>
<option value="br">Breton</option>
<option value="bg">Bulgarian</option>
<option value="ca">Catalan</option>
<option value="zh">Chinese</option>
<option value="hr">Croatian</option>
<option value="cs">Czech</option>
<option value="da">Danish</option>
<option value="nl">Dutch</option>
<option value="en">English</option>
<option value="et">Estonian</option>
<option value="fo">Faroese</option>
<option value="fi">Finnish</option>
<option value="fr">French</option>
<option value="gl">Galician</option>
<option value="ka">Georgian</option>
<option value="de">German</option>
<option value="el">Greek</option>
<option value="gu">Gujarati</option>
<option value="ht">Haitian Creole</option>
<option value="ha">Hausa</option>
<option value="haw">Hawaiian</option>
<option value="he">Hebrew</option>
<option value="hi">Hindi</option>
<option value="hu">Hungarian</option>
<option value="is">Icelandic</option>
<option value="id">Indonesian</option>
<option value="it">Italian</option>
<option value="ja">Japanese</option>
<option value="jw">Javanese</option> <option value="jw">Javanese</option>
<option value="kn">Kannada</option>
<option value="kk">Kazakh</option>
<option value="km">Khmer</option>
<option value="ko">Korean</option>
<option value="lo">Lao</option>
<option value="la">Latin</option>
<option value="lv">Latvian</option>
<option value="ln">Lingala</option>
<option value="lt">Lithuanian</option>
<option value="lb">Luxembourgish</option>
<option value="mk">Macedonian</option>
<option value="mg">Malagasy</option>
<option value="ms">Malay</option>
<option value="ml">Malayalam</option>
<option value="mt">Maltese</option>
<option value="mi">Maori</option>
<option value="mr">Marathi</option>
<option value="mn">Mongolian</option>
<option value="my">Myanmar</option>
<option value="ne">Nepali</option>
<option value="no">Norwegian</option>
<option value="nn">Nynorsk</option>
<option value="oc">Occitan</option>
<option value="ps">Pashto</option>
<option value="fa">Persian</option>
<option value="pl">Polish</option>
<option value="pt">Portuguese</option>
<option value="pa">Punjabi</option>
<option value="ro">Romanian</option>
<option value="ru">Russian</option>
<option value="sa">Sanskrit</option>
<option value="sr">Serbian</option>
<option value="sn">Shona</option>
<option value="sd">Sindhi</option>
<option value="si">Sinhala</option>
<option value="sk">Slovak</option>
<option value="sl">Slovenian</option>
<option value="so">Somali</option>
<option value="es">Spanish</option>
<option value="su">Sundanese</option> <option value="su">Sundanese</option>
<option value="sw">Swahili</option>
<option value="sv">Swedish</option>
<option value="tl">Tagalog</option>
<option value="tg">Tajik</option>
<option value="ta">Tamil</option>
<option value="tt">Tatar</option>
<option value="te">Telugu</option>
<option value="th">Thai</option>
<option value="bo">Tibetan</option>
<option value="tr">Turkish</option>
<option value="tk">Turkmen</option>
<option value="uk">Ukrainian</option>
<option value="ur">Urdu</option>
<option value="uz">Uzbek</option>
<option value="vi">Vietnamese</option>
<option value="cy">Welsh</option>
<option value="yi">Yiddish</option>
<option value="yo">Yoruba</option>
</select> </select>
</div> </div>
<div class="dropdown-container"> <div class="dropdown-container">
@@ -134,11 +135,15 @@
<div class="dropdown-container"> <div class="dropdown-container">
<label for="modelSizeDropdown">Select Model Size:</label> <label for="modelSizeDropdown">Select Model Size:</label>
<select id="modelSizeDropdown"> <select id="modelSizeDropdown">
<option value="">Select Task</option> <option value="">Select model</option>
<option value="tiny">Tiny</option> <option value="tiny">Tiny </option>
<option value="tiny.en">Tiny (English-only)</option>
<option value="base">Base</option> <option value="base">Base</option>
<option value="base.en">Base (English-only)</option>
<option value="small" selected>Small</option> <option value="small" selected>Small</option>
<option value="small.en">Small (English-only)</option>
<option value="medium">Medium</option> <option value="medium">Medium</option>
<option value="medium.en">Medium (English-only)</option>
<option value="large-v2">Large-v2</option> <option value="large-v2">Large-v2</option>
<option value="large-v3">Large-v3</option> <option value="large-v3">Large-v3</option>
</select> </select>
+14 -22
View File
@@ -4,7 +4,7 @@ document.addEventListener("DOMContentLoaded", function () {
const stopButton = document.getElementById("stopCapture"); const stopButton = document.getElementById("stopCapture");
const useServerCheckbox = document.getElementById("useServerCheckbox"); const useServerCheckbox = document.getElementById("useServerCheckbox");
const useMultilingualCheckbox = document.getElementById('useMultilingualCheckbox'); const useVadCheckbox = document.getElementById("useVadCheckbox");
const languageDropdown = document.getElementById('languageDropdown'); const languageDropdown = document.getElementById('languageDropdown');
const taskDropdown = document.getElementById('taskDropdown'); const taskDropdown = document.getElementById('taskDropdown');
const modelSizeDropdown = document.getElementById('modelSizeDropdown'); const modelSizeDropdown = document.getElementById('modelSizeDropdown');
@@ -32,11 +32,9 @@ document.addEventListener("DOMContentLoaded", function () {
} }
}); });
chrome.storage.local.get("useMultilingualModelState", ({ useMultilingualModelState }) => { chrome.storage.local.get("useVadState", ({ useVadState }) => {
if (useMultilingualModelState !== undefined) { if (useVadState !== undefined) {
useMultilingualCheckbox.checked = useMultilingualModelState; useVadCheckbox.checked = useVadState;
languageDropdown.disabled = !useMultilingualModelState;
taskDropdown.disabled = !useMultilingualModelState;
} }
}); });
@@ -73,7 +71,7 @@ document.addEventListener("DOMContentLoaded", function () {
// Send a message to the background script to start capturing // Send a message to the background script to start capturing
let host = "localhost"; let host = "localhost";
let port = "5901"; let port = "9090";
const useCollaboraServer = useServerCheckbox.checked; const useCollaboraServer = useServerCheckbox.checked;
if (useCollaboraServer){ if (useCollaboraServer){
host = "transcription.kurg.org" host = "transcription.kurg.org"
@@ -86,10 +84,10 @@ document.addEventListener("DOMContentLoaded", function () {
tabId: currentTab.id, tabId: currentTab.id,
host: host, host: host,
port: port, port: port,
useMultilingual: useMultilingualCheckbox.checked,
language: selectedLanguage, language: selectedLanguage,
task: selectedTask, task: selectedTask,
modelSize: selectedModelSize modelSize: selectedModelSize,
useVad: useVadCheckbox.checked,
}, () => { }, () => {
// Update capturing state in storage and toggle the buttons // Update capturing state in storage and toggle the buttons
chrome.storage.local.set({ capturingState: { isCapturing: true } }, () => { chrome.storage.local.set({ capturingState: { isCapturing: true } }, () => {
@@ -128,10 +126,11 @@ document.addEventListener("DOMContentLoaded", function () {
function toggleCaptureButtons(isCapturing) { function toggleCaptureButtons(isCapturing) {
startButton.disabled = isCapturing; startButton.disabled = isCapturing;
stopButton.disabled = !isCapturing; stopButton.disabled = !isCapturing;
useServerCheckbox.disabled = isCapturing; useServerCheckbox.disabled = isCapturing;
useMultilingualCheckbox.disabled = isCapturing; useVadCheckbox.disabled = isCapturing;
modelSizeDropdown.disabled = isCapturing; modelSizeDropdown.disabled = isCapturing;
languageDropdown.disabled = isCapturing;
taskDropdown.disabled = isCapturing;
startButton.classList.toggle("disabled", isCapturing); startButton.classList.toggle("disabled", isCapturing);
stopButton.classList.toggle("disabled", !isCapturing); stopButton.classList.toggle("disabled", !isCapturing);
} }
@@ -142,16 +141,9 @@ document.addEventListener("DOMContentLoaded", function () {
chrome.storage.local.set({ useServerState }); chrome.storage.local.set({ useServerState });
}); });
useMultilingualCheckbox.addEventListener('change', function() { useVadCheckbox.addEventListener("change", () => {
const useMultilingualModelState = useMultilingualCheckbox.checked; const useVadState = useVadCheckbox.checked;
if (useMultilingualModelState) { chrome.storage.local.set({ useVadState });
languageDropdown.disabled = false;
taskDropdown.disabled = false;
} else {
languageDropdown.disabled = true;
taskDropdown.disabled = true;
}
chrome.storage.local.set({ useMultilingualModelState });
}); });
languageDropdown.addEventListener('change', function() { languageDropdown.addEventListener('change', function() {
-1
View File
@@ -24,7 +24,6 @@ To capture the audio in the current tab, we used the chrome `tabCapture` API to
### Options ### Options
When using the Audio Transcription extension, you have the following options: When using the Audio Transcription extension, you have the following options:
- **Use Collabora Server**: We provide a demo server which runs the whisper small model. - **Use Collabora Server**: We provide a demo server which runs the whisper small model.
- **Use Multilingual Model**: Enable this option to utilize the multilingual capabilities of OpenAI-whisper.
- **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper. - **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper.
- **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English. - **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English.
- **Model Size**: Select the whisper model size to run the server with. - **Model Size**: Select the whisper model size to run the server with.
+3 -6
View File
@@ -66,19 +66,16 @@ function resampleTo16kHZ(audioData, origSampleRate = 44100) {
function startRecording(data) { function startRecording(data) {
socket = new WebSocket(`ws://${data.host}:${data.port}/`); socket = new WebSocket(`ws://${data.host}:${data.port}/`);
language = data.language; language = data.language;
if (language === null && !data.useMultilingual) {
language = 'en';
}
const uuid = generateUUID(); const uuid = generateUUID();
socket.onopen = function(e) { socket.onopen = function(e) {
socket.send( socket.send(
JSON.stringify({ JSON.stringify({
uid: uuid, uid: uuid,
multilingual: data.useMultilingual,
language: data.language, language: data.language,
task: data.task, task: data.task,
model_size: data.modelSize model: data.modelSize,
use_vad: data.useVad
}) })
); );
}; };
@@ -201,7 +198,7 @@ function init_element() {
elem_container = document.createElement('div'); elem_container = document.createElement('div');
elem_container.id = "transcription"; elem_container.id = "transcription";
elem_container.style.cssText = 'padding-top:16px;font-size:18px;line-height:18px;top:0px;position:absolute;width:500px;height:90px;opacity:0.9;z-index:100;background:black;border-radius:10px;color:white;'; elem_container.style.cssText = 'padding-top:16px;font-size:18px;line-height:18px;position:fixed;top:85%;left:50%;transform:translate(-50%,-50%);width:500px;height:90px;opacity:0.9;z-index:100;background:black;border-radius:10px;color:white;';
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
elem_text = document.createElement('span'); elem_text = document.createElement('span');
+105 -101
View File
@@ -15,114 +15,114 @@
<input type="checkbox" id="useServerCheckbox"> <input type="checkbox" id="useServerCheckbox">
<label for="useServerCheckbox">Use Collabora Whisper-Live Server</label> <label for="useServerCheckbox">Use Collabora Whisper-Live Server</label>
</div> </div>
<textarea id="waitTextBox" style="display: none;"></textarea>
<div class="checkbox-container"> <div class="checkbox-container">
<input type="checkbox" id="useMultilingualCheckbox"> <input type="checkbox" id="useVadCheckbox">
<label for="useMultilingualCheckbox">Use Multilingual Model</label> <label for="useVadCheckbox">Use Voice Activity Detection</label>
</div> </div>
<textarea id="waitTextBox" style="display: none;"></textarea>
<div class="dropdown-container"> <div class="dropdown-container">
<label for="languageDropdown">Select Language:</label> <label for="languageDropdown">Select Language:</label>
<select id="languageDropdown" disabled> <select id="languageDropdown">
<option value="">Select Language</option> <option value="" selected>Automatically detect</option>
<option value="zh">Chinese</option>
<option value="de">German</option>
<option value="es">Spanish</option>
<option value="ru">Russian</option>
<option value="ko">Korean</option>
<option value="fr">French</option>
<option value="ja">Japanese</option>
<option value="pt">Portuguese</option>
<option value="tr">Turkish</option>
<option value="pl">Polish</option>
<option value="ca">Catalan</option>
<option value="nl">Dutch</option>
<option value="ar">Arabic</option>
<option value="sv">Swedish</option>
<option value="it">Italian</option>
<option value="id">Indonesian</option>
<option value="hi">Hindi</option>
<option value="fi">Finnish</option>
<option value="vi">Vietnamese</option>
<option value="he">Hebrew</option>
<option value="uk">Ukrainian</option>
<option value="el">Greek</option>
<option value="ms">Malay</option>
<option value="cs">Czech</option>
<option value="ro">Romanian</option>
<option value="da">Danish</option>
<option value="hu">Hungarian</option>
<option value="ta">Tamil</option>
<option value="no">Norwegian</option>
<option value="th">Thai</option>
<option value="ur">Urdu</option>
<option value="hr">Croatian</option>
<option value="bg">Bulgarian</option>
<option value="lt">Lithuanian</option>
<option value="la">Latin</option>
<option value="mi">Maori</option>
<option value="ml">Malayalam</option>
<option value="cy">Welsh</option>
<option value="sk">Slovak</option>
<option value="te">Telugu</option>
<option value="fa">Persian</option>
<option value="lv">Latvian</option>
<option value="bn">Bengali</option>
<option value="sr">Serbian</option>
<option value="az">Azerbaijani</option>
<option value="sl">Slovenian</option>
<option value="kn">Kannada</option>
<option value="et">Estonian</option>
<option value="mk">Macedonian</option>
<option value="br">Breton</option>
<option value="eu">Basque</option>
<option value="is">Icelandic</option>
<option value="hy">Armenian</option>
<option value="ne">Nepali</option>
<option value="mn">Mongolian</option>
<option value="bs">Bosnian</option>
<option value="kk">Kazakh</option>
<option value="sq">Albanian</option>
<option value="sw">Swahili</option>
<option value="gl">Galician</option>
<option value="mr">Marathi</option>
<option value="pa">Punjabi</option>
<option value="si">Sinhala</option>
<option value="km">Khmer</option>
<option value="sn">Shona</option>
<option value="yo">Yoruba</option>
<option value="so">Somali</option>
<option value="af">Afrikaans</option> <option value="af">Afrikaans</option>
<option value="oc">Occitan</option> <option value="sq">Albanian</option>
<option value="ka">Georgian</option>
<option value="be">Belarusian</option>
<option value="tg">Tajik</option>
<option value="sd">Sindhi</option>
<option value="gu">Gujarati</option>
<option value="am">Amharic</option> <option value="am">Amharic</option>
<option value="yi">Yiddish</option> <option value="ar">Arabic</option>
<option value="lo">Lao</option> <option value="hy">Armenian</option>
<option value="uz">Uzbek</option>
<option value="fo">Faroese</option>
<option value="ht">Haitian Creole</option>
<option value="ps">Pashto</option>
<option value="tk">Turkmen</option>
<option value="nn">Nynorsk</option>
<option value="mt">Maltese</option>
<option value="sa">Sanskrit</option>
<option value="lb">Luxembourgish</option>
<option value="my">Myanmar</option>
<option value="bo">Tibetan</option>
<option value="tl">Tagalog</option>
<option value="mg">Malagasy</option>
<option value="as">Assamese</option> <option value="as">Assamese</option>
<option value="tt">Tatar</option> <option value="az">Azerbaijani</option>
<option value="haw">Hawaiian</option>
<option value="ln">Lingala</option>
<option value="ha">Hausa</option>
<option value="ba">Bashkir</option> <option value="ba">Bashkir</option>
<option value="eu">Basque</option>
<option value="be">Belarusian</option>
<option value="bn">Bengali</option>
<option value="bs">Bosnian</option>
<option value="br">Breton</option>
<option value="bg">Bulgarian</option>
<option value="ca">Catalan</option>
<option value="zh">Chinese</option>
<option value="hr">Croatian</option>
<option value="cs">Czech</option>
<option value="da">Danish</option>
<option value="nl">Dutch</option>
<option value="en">English</option>
<option value="et">Estonian</option>
<option value="fo">Faroese</option>
<option value="fi">Finnish</option>
<option value="fr">French</option>
<option value="gl">Galician</option>
<option value="ka">Georgian</option>
<option value="de">German</option>
<option value="el">Greek</option>
<option value="gu">Gujarati</option>
<option value="ht">Haitian Creole</option>
<option value="ha">Hausa</option>
<option value="haw">Hawaiian</option>
<option value="he">Hebrew</option>
<option value="hi">Hindi</option>
<option value="hu">Hungarian</option>
<option value="is">Icelandic</option>
<option value="id">Indonesian</option>
<option value="it">Italian</option>
<option value="ja">Japanese</option>
<option value="jw">Javanese</option> <option value="jw">Javanese</option>
<option value="kn">Kannada</option>
<option value="kk">Kazakh</option>
<option value="km">Khmer</option>
<option value="ko">Korean</option>
<option value="lo">Lao</option>
<option value="la">Latin</option>
<option value="lv">Latvian</option>
<option value="ln">Lingala</option>
<option value="lt">Lithuanian</option>
<option value="lb">Luxembourgish</option>
<option value="mk">Macedonian</option>
<option value="mg">Malagasy</option>
<option value="ms">Malay</option>
<option value="ml">Malayalam</option>
<option value="mt">Maltese</option>
<option value="mi">Maori</option>
<option value="mr">Marathi</option>
<option value="mn">Mongolian</option>
<option value="my">Myanmar</option>
<option value="ne">Nepali</option>
<option value="no">Norwegian</option>
<option value="nn">Nynorsk</option>
<option value="oc">Occitan</option>
<option value="ps">Pashto</option>
<option value="fa">Persian</option>
<option value="pl">Polish</option>
<option value="pt">Portuguese</option>
<option value="pa">Punjabi</option>
<option value="ro">Romanian</option>
<option value="ru">Russian</option>
<option value="sa">Sanskrit</option>
<option value="sr">Serbian</option>
<option value="sn">Shona</option>
<option value="sd">Sindhi</option>
<option value="si">Sinhala</option>
<option value="sk">Slovak</option>
<option value="sl">Slovenian</option>
<option value="so">Somali</option>
<option value="es">Spanish</option>
<option value="su">Sundanese</option> <option value="su">Sundanese</option>
<option value="sw">Swahili</option>
<option value="sv">Swedish</option>
<option value="tl">Tagalog</option>
<option value="tg">Tajik</option>
<option value="ta">Tamil</option>
<option value="tt">Tatar</option>
<option value="te">Telugu</option>
<option value="th">Thai</option>
<option value="bo">Tibetan</option>
<option value="tr">Turkish</option>
<option value="tk">Turkmen</option>
<option value="uk">Ukrainian</option>
<option value="ur">Urdu</option>
<option value="uz">Uzbek</option>
<option value="vi">Vietnamese</option>
<option value="cy">Welsh</option>
<option value="yi">Yiddish</option>
<option value="yo">Yoruba</option>
</select> </select>
</div> </div>
<div class="dropdown-container"> <div class="dropdown-container">
@@ -136,14 +136,18 @@
<div class="dropdown-container"> <div class="dropdown-container">
<label for="modelSizeDropdown">Select Model Size:</label> <label for="modelSizeDropdown">Select Model Size:</label>
<select id="modelSizeDropdown"> <select id="modelSizeDropdown">
<option value="">Select Task</option> <option value="">Select model</option>
<option value="tiny">Tiny</option> <option value="tiny">Tiny </option>
<option value="tiny.en">Tiny (English-only)</option>
<option value="base">Base</option> <option value="base">Base</option>
<option value="base.en">Base (English-only)</option>
<option value="small" selected>Small</option> <option value="small" selected>Small</option>
<option value="small.en">Small (English-only)</option>
<option value="medium">Medium</option> <option value="medium">Medium</option>
<option value="medium.en">Medium (English-only)</option>
<option value="large-v2">Large-v2</option> <option value="large-v2">Large-v2</option>
<option value="large-v3">Large-v3</option> <option value="large-v3">Large-v3</option>
</select> </select>
</div> </div>
</body> </body>
</html> </html>
+13 -21
View File
@@ -3,7 +3,7 @@ document.addEventListener("DOMContentLoaded", function() {
const stopButton = document.getElementById("stopCapture"); const stopButton = document.getElementById("stopCapture");
const useServerCheckbox = document.getElementById("useServerCheckbox"); const useServerCheckbox = document.getElementById("useServerCheckbox");
const useMultilingualCheckbox = document.getElementById('useMultilingualCheckbox'); const useVadCheckbox = document.getElementById("useVadCheckbox");
const languageDropdown = document.getElementById('languageDropdown'); const languageDropdown = document.getElementById('languageDropdown');
const taskDropdown = document.getElementById('taskDropdown'); const taskDropdown = document.getElementById('taskDropdown');
const modelSizeDropdown = document.getElementById('modelSizeDropdown'); const modelSizeDropdown = document.getElementById('modelSizeDropdown');
@@ -35,11 +35,9 @@ document.addEventListener("DOMContentLoaded", function() {
} }
}); });
browser.storage.local.get("useMultilingualModelState", ({ useMultilingualModelState }) => { browser.storage.local.get("useVadState", ({ useVadState }) => {
if (useMultilingualModelState !== undefined) { if (useVadState !== undefined) {
useMultilingualCheckbox.checked = useMultilingualModelState; useVadCheckbox.checked = useVadState;
languageDropdown.disabled = !useMultilingualModelState;
taskDropdown.disabled = !useMultilingualModelState;
} }
}); });
@@ -66,7 +64,7 @@ document.addEventListener("DOMContentLoaded", function() {
startButton.addEventListener("click", function() { startButton.addEventListener("click", function() {
let host = "localhost"; let host = "localhost";
let port = "5901"; let port = "9090";
const useCollaboraServer = useServerCheckbox.checked; const useCollaboraServer = useServerCheckbox.checked;
if (useCollaboraServer){ if (useCollaboraServer){
@@ -83,10 +81,10 @@ document.addEventListener("DOMContentLoaded", function() {
data: { data: {
host: host, host: host,
port: port, port: port,
useMultilingual: useMultilingualCheckbox.checked,
language: selectedLanguage, language: selectedLanguage,
task: selectedTask, task: selectedTask,
modelSize: selectedModelSize modelSize: selectedModelSize,
useVad: useVadCheckbox.checked,
} }
}); });
toggleCaptureButtons(true); toggleCaptureButtons(true);
@@ -125,9 +123,10 @@ document.addEventListener("DOMContentLoaded", function() {
startButton.disabled = isCapturing; startButton.disabled = isCapturing;
stopButton.disabled = !isCapturing; stopButton.disabled = !isCapturing;
useServerCheckbox.disabled = isCapturing; useServerCheckbox.disabled = isCapturing;
useMultilingualCheckbox.disabled = isCapturing; useVadCheckbox.disabled = isCapturing;
modelSizeDropdown.disabled = isCapturing; modelSizeDropdown.disabled = isCapturing;
languageDropdown.disabled = isCapturing;
taskDropdown.disabled = isCapturing;
startButton.classList.toggle("disabled", isCapturing); startButton.classList.toggle("disabled", isCapturing);
stopButton.classList.toggle("disabled", !isCapturing); stopButton.classList.toggle("disabled", !isCapturing);
} }
@@ -138,16 +137,9 @@ document.addEventListener("DOMContentLoaded", function() {
browser.storage.local.set({ useServerState }); browser.storage.local.set({ useServerState });
}); });
useMultilingualCheckbox.addEventListener('change', function() { useVadCheckbox.addEventListener("change", () => {
const useMultilingualModelState = useMultilingualCheckbox.checked; const useVadState = useVadCheckbox.checked;
if (useMultilingualModelState) { browser.storage.local.set({ useVadState });
languageDropdown.disabled = false;
taskDropdown.disabled = false;
} else {
languageDropdown.disabled = true;
taskDropdown.disabled = true;
}
browser.storage.local.set({ useMultilingualModelState });
}); });
languageDropdown.addEventListener('change', function() { languageDropdown.addEventListener('change', function() {
+1 -1
View File
@@ -108,4 +108,4 @@ label {
.dropdown-container { .dropdown-container {
padding: 10px; padding: 10px;
} }
+156 -71
View File
@@ -1,14 +1,30 @@
# whisper-live # WhisperLive
A nearly-live implementation of OpenAI's Whisper.
This project is a real-time transcription application that uses the OpenAI Whisper model to convert speech input into text output. It can be used to transcribe both live audio input from microphone and pre-recorded audio files. <h2 align="center">
<a href="https://www.youtube.com/watch?v=0PHWCApIcCI"><img
src="https://img.youtube.com/vi/0PHWCApIcCI/0.jpg" style="background-color:rgba(0,0,0,0);" height=300 alt="WhisperLive"></a>
<br><br>A nearly-live implementation of OpenAI's Whisper.
<br><br>
</h2>
Unlike traditional speech recognition systems that rely on continuous audio streaming, we use [voice activity detection (VAD)](https://github.com/snakers4/silero-vad) to detect the presence of speech and only send the audio data to whisper when speech is detected. This helps to reduce the amount of data sent to the whisper model and improves the accuracy of the transcription output. This project is a real-time transcription application that uses the OpenAI Whisper model
to convert speech input into text output. It can be used to transcribe both live audio
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 setup.sh bash scripts/setup.sh
``` ```
- Install whisper-live from pip - Install whisper-live from pip
@@ -16,85 +32,155 @@ Unlike traditional speech recognition systems that rely on continuous audio stre
pip install whisper-live pip install whisper-live
``` ```
### Setting up NVIDIA/TensorRT-LLM for TensorRT backend
- 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
- Run the server 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)
```python
from whisper_live.server import TranscriptionServer ### Running the Server
server = TranscriptionServer() - [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) backend
server.run("0.0.0.0", 9090) ```bash
python3 run_server.py --port 9090 \
--backend faster_whisper
# running with custom model
python3 run_server.py --port 9090 \
--backend faster_whisper \
-fw "/path/to/custom/faster/whisper/model"
``` ```
- On the client side - TensorRT backend. Currently, we recommend to only use the docker setup for TensorRT. Follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md) which works as expected. Make sure to build your TensorRT Engines before running the server with TensorRT backend.
- To transcribe an audio file: ```bash
```python # Run English only model
from whisper_live.client import TranscriptionClient python3 run_server.py -p 9090 \
client = TranscriptionClient( -b tensorrt \
"localhost", -trt /home/TensorRT-LLM/examples/whisper/whisper_small_en
9090,
is_multilingual=False,
lang="en",
translate=False,
model_size="small"
)
client("tests/jfk.wav") # Run Multilingual model
``` python3 run_server.py -p 9090 \
This command transcribes the specified audio file (audio.wav) using the Whisper model. It connects to the server running on localhost at port 9090. It can also enable the multilingual feature, allowing transcription in multiple languages. The language option specifies the target language for 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. -b tensorrt \
-trt /home/TensorRT-LLM/examples/whisper/whisper_small \
- To transcribe from microphone: -m
```python
from whisper_live.client import TranscriptionClient
client = TranscriptionClient(
"localhost",
9090,
is_multilingual=True,
lang="hi",
translate=True,
model_size="small"
)
client()
```
This command captures audio from the microphone and sends it to the server for transcription. It uses the multilingual option with `hi` as the selected language, enabling the multilingual feature and specifying the target language and task. We use whisper `small` by default but can be changed to any other option based on the requirements and the hardware running the server.
- To transcribe from a HLS stream:
```python
client = TranscriptionClient(host, port, is_multilingual=True, lang="en", translate=False)
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")
```
This command streams audio into the server from a HLS stream. It uses the same options as the previous command, enabling the multilingual feature and specifying the target language and task.
## Transcribe audio from browser
- Run the server
```python
from whisper_live.server import TranscriptionServer
server = TranscriptionServer()
server.run("0.0.0.0", 9090)
``` ```
This would start the websocket server on port ```9090```.
### Chrome Extension - 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).
- Refer to [Audio-Transcription-Chrome](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Chrome#readme) to use Chrome extension. - > **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#).
### Firefox Extension ```
- Refer to [Audio-Transcription-Firefox](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Firefox#readme) to use Mozilla Firefox extension. 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
- 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
from whisper_live.client import TranscriptionClient
client = TranscriptionClient(
"localhost",
9090,
lang="en",
translate=False,
model="small", # also support hf_model => `Systran/faster-whisper-small`
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.
- Transcribe an audio file:
```python
client("tests/jfk.wav")
```
- To transcribe from microphone:
```python
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:
```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")
```
## Browser Extensions
- 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 https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md
## Whisper Live Server in Docker ## Whisper Live Server in Docker
- GPU - GPU
```bash - Faster-Whisper
docker build . -t whisper-live -f docker/Dockerfile.gpu ```bash
docker run -it --gpus all -p 9090:9090 whisper-live:latest docker run -it --gpus all -p 9090:9090 ghcr.io/collabora/whisperlive-gpu:latest
``` ```
- 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 build . -t whisper-live -f docker/Dockerfile.cpu ```bash
docker run -it -p 9090:9090 whisper-live:latest 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.
- [ ] TensorRT backend for Whisper.
## Contact ## Contact
@@ -119,6 +205,5 @@ We are available to help you with both Open Source and proprietary AI projects.
publisher = {GitHub}, publisher = {GitHub},
journal = {GitHub repository}, journal = {GitHub repository},
howpublished = {\url{https://github.com/snakers4/silero-vad}}, howpublished = {\url{https://github.com/snakers4/silero-vad}},
commit = {insert_some_commit_here},
email = {hello@silero.ai} email = {hello@silero.ai}
} }
+47
View File
@@ -0,0 +1,47 @@
# WhisperLive-TensorRT
We have only tested the TensorRT backend in docker so, we recommend docker for a smooth TensorRT backend setup.
**Note**: We use `tensorrt_llm==0.18.2`
## Installation
- 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)
- Run WhisperLive TensorRT in docker
```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
```
## Whisper TensorRT Engine
- 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
# convert 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
bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small
```
## Run WhisperLive Server with TensorRT Backend
```bash
# Run English only model
python3 run_server.py --port 9090 \
--backend tensorrt \
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_en_float16"
# Run Multilingual model
python3 run_server.py --port 9090 \
--backend tensorrt \
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_float16" \
--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
```
View File
+12 -32
View File
@@ -1,45 +1,25 @@
FROM ubuntu:focal 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 \ RUN pip install --no-cache-dir -U "pip>=24"
curl \
ca-certificates \
sudo \
git \
bzip2 \
libx11-6 \
&& rm -rf /var/lib/apt/lists/*
RUN apt update # create a working directory
# install python
RUN apt install software-properties-common -y && \
add-apt-repository ppa:deadsnakes/ppa && \
apt update
RUN apt install python3-dev -y && \
apt install python-is-python3
# install pip
RUN apt install python3-pip -y
# Create a working directory.
RUN mkdir /app RUN mkdir /app
WORKDIR /app WORKDIR /app
COPY setup.sh /app # install pytorch, but without the nvidia-libs that are only necessary for gpu
COPY requirements/ /app RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
RUN bash setup.sh # install the requirements for running the whisper-live server
RUN pip install -r server.txt 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
CMD ["python", "run_server.py"] CMD ["python", "run_server.py"]
+12 -33
View File
@@ -1,47 +1,26 @@
FROM nvidia/cuda:11.2.2-cudnn8-runtime-ubuntu20.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 \ RUN pip install --no-cache-dir -U "pip>=24"
curl \
ca-certificates \
sudo \
git \
bzip2 \
libx11-6 \
&& rm -rf /var/lib/apt/lists/*
RUN apt update # create a working directory
# install python
RUN apt install software-properties-common -y && \
add-apt-repository ppa:deadsnakes/ppa && \
apt update
RUN apt install python3-dev -y && \
apt install python-is-python3
# install pip
RUN apt install python3-pip -y
# Create a working directory.
RUN mkdir /app RUN mkdir /app
WORKDIR /app WORKDIR /app
COPY setup.sh /app # install the requirements for running the whisper-live server
COPY requirements/ /app COPY requirements/server.txt /app/
RUN pip install --no-cache-dir -r server.txt && rm server.txt
RUN apt update --fix-missing # make the paths of the nvidia libs installed as wheels visible. equivalent to:
RUN bash setup.sh # 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__))'`
RUN pip install -r server.txt 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 ["python", "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
+20 -6
View File
@@ -1,7 +1,21 @@
PyAudio faster-whisper==1.1.0
faster-whisper==0.10.0
--extra-index-url https://download.pytorch.org/whl/cu111
torch==1.10.1
torchaudio==0.10.1
websockets websockets
onnxruntime==1.16.0 onnxruntime==1.17.0
numba
kaldialign
soundfile
scipy
av
jiwer
evaluate
numpy<2
openai-whisper==20240930
tokenizers==0.20.3
# openvino
librosa
openvino
openvino-genai
openvino-tokenizers
optimum
optimum-intel
+51 -2
View File
@@ -1,5 +1,54 @@
from whisper_live.server import TranscriptionServer import argparse
import os
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--port', '-p',
type=int,
default=9090,
help="Websocket port to run the server on.")
parser.add_argument('--backend', '-b',
type=str,
default='faster_whisper',
help='Backends from ["tensorrt", "faster_whisper", "openvino"]')
parser.add_argument('--faster_whisper_custom_model_path', '-fw',
type=str, default=None,
help="Custom Faster Whisper Model")
parser.add_argument('--trt_model_path', '-trt',
type=str,
default=None,
help='Whisper TensorRT model path')
parser.add_argument('--trt_multilingual', '-m',
action="store_true",
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()
if args.backend == "tensorrt":
if args.trt_model_path is None:
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("0.0.0.0") server.run(
"0.0.0.0",
port=args.port,
backend=args.backend,
faster_whisper_custom_model_path=args.faster_whisper_custom_model_path,
whisper_tensorrt_path=args.trt_model_path,
trt_multilingual=args.trt_multilingual,
trt_py_session=args.trt_py_session,
single_model=not args.no_single_model,
)
+120
View File
@@ -0,0 +1,120 @@
#!/bin/bash
download_and_build_model() {
local model_name="$1"
local model_url=""
case "$model_name" in
"tiny.en")
model_url="https://openaipublic.azureedge.net/main/whisper/models/d3dd57d32accea0b295c96e26691aa14d8822fac7d9d27d5dc00b4ca2826dd03/tiny.en.pt"
;;
"tiny")
model_url="https://openaipublic.azureedge.net/main/whisper/models/65147644a518d12f04e32d6f3b26facc3f8dd46e5390956a9424a650c0ce22b9/tiny.pt"
;;
"base.en")
model_url="https://openaipublic.azureedge.net/main/whisper/models/25a8566e1d0c1e2231d1c762132cd20e0f96a85d16145c3a00adf5d1ac670ead/base.en.pt"
;;
"base")
model_url="https://openaipublic.azureedge.net/main/whisper/models/ed3a0b6b1c0edf879ad9b11b1af5a0e6ab5db9205f891f668f8b0e6c6326e34e/base.pt"
;;
"small.en")
model_url="https://openaipublic.azureedge.net/main/whisper/models/f953ad0fd29cacd07d5a9eda5624af0f6bcf2258be67c92b79389873d91e0872/small.en.pt"
;;
"small")
model_url="https://openaipublic.azureedge.net/main/whisper/models/9ecf779972d90ba49c06d968637d720dd632c55bbf19d441fb42bf17a411e794/small.pt"
;;
"medium.en")
model_url="https://openaipublic.azureedge.net/main/whisper/models/d7440d1dc186f76616474e0ff0b3b6b879abc9d1a4926b7adfa41db2d497ab4f/medium.en.pt"
;;
"medium")
model_url="https://openaipublic.azureedge.net/main/whisper/models/345ae4da62f9b3d59415adc60127b97c714f32e89e936602e85993674d08dcb1/medium.pt"
;;
"large-v1")
model_url="https://openaipublic.azureedge.net/main/whisper/models/e4b87e7e0bf463eb8e6956e646f1e277e901512310def2c24bf0e11bd3c28e9a/large-v1.pt"
;;
"large-v2")
model_url="https://openaipublic.azureedge.net/main/whisper/models/81f7c96c852ee8fc832187b0132e569d6c3065a3252ed18e56effd0b6a73e524/large-v2.pt"
;;
"large-v3" | "large")
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"
exit 1
;;
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..."
# wget --directory-prefix=assets "$model_url"
# echo "Download completed: ${model_name}.pt"
if [ ! -f "assets/${model_name}.pt" ]; then
wget --directory-prefix=assets "$model_url"
echo "Download completed: ${model_name}.pt"
else
echo "${model_name}.pt already exists in assets directory."
fi
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 "Converting model weights for $model_name..."
python3 convert_checkpoint.py \
$( [[ "$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 "Model is located at: $(pwd)/$output_dir"
}
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <path-to-tensorrt-examples-dir> [model-name]"
exit 1
fi
tensorrt_examples_dir="$1"
model_name="${2:-small.en}"
weight_only_precision="${3:-float16}" # Default to float16 if not provided
cd $tensorrt_examples_dir/whisper
pip install --no-deps -r requirements.txt
download_and_build_model "$model_name" "$weight_only_precision"
+3
View File
@@ -0,0 +1,3 @@
#! /bin/bash
apt-get install portaudio19-dev wget -y
+48 -35
View File
@@ -10,45 +10,58 @@ HERE = pathlib.Path(__file__).parent
README = (HERE / "README.md").read_text() README = (HERE / "README.md").read_text()
# This call to setup() does all the work # This call to setup() does all the work
setup(name="whisper-live", setup(
version=__version__, name="whisper_live",
description="A nearly-live implementation of OpenAI's Whisper.", version=__version__,
long_description=README, description="A nearly-live implementation of OpenAI's Whisper.",
long_description_content_type="text/markdown", long_description=README,
include_package_data=True, long_description_content_type="text/markdown",
url="https://github.com/collabora/WhisperLive", include_package_data=True,
author="Collabora Ltd", url="https://github.com/collabora/WhisperLive",
author_email="vineet.suryan@collabora.com", author="Collabora Ltd",
license="MIT", author_email="vineet.suryan@collabora.com",
classifiers=[ license="MIT",
"Development Status :: 4 - Beta", classifiers=[
"Intended Audience :: Developers", "Development Status :: 4 - Beta",
"Intended Audience :: Science/Research", "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License", "Intended Audience :: Science/Research",
"Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.8",
"Topic :: Scientific/Engineering :: Artificial Intelligence", "Programming Language :: Python :: 3.9",
], "Topic :: Scientific/Engineering :: Artificial Intelligence",
packages=find_packages( ],
exclude=("examples", packages=find_packages(
"Audio-Transcription-Chrome", exclude=(
"Audio-Transcription-Firefox", "examples",
"requirements", "Audio-Transcription-Chrome",
"whisper-finetuning" "Audio-Transcription-Firefox",
) "requirements",
), "whisper-finetuning"
install_requires=[ )
),
install_requires=[
"PyAudio", "PyAudio",
"faster-whisper==0.10.0", "faster-whisper==1.1.0",
"torch", "torch",
"torchaudio", "torchaudio",
"websockets", "websockets",
"onnxruntime", "onnxruntime==1.17.0",
"ffmpeg-python",
"scipy", "scipy",
"websocket-client", "websocket-client",
], "numba",
python_requires=">=3.8" "openai-whisper==20240930",
"kaldialign",
"soundfile",
"tokenizers==0.20.3",
"librosa",
"numpy==1.26.4",
"openvino",
"openvino-genai",
"openvino-tokenizers",
"optimum",
"optimum-intel",
],
python_requires=">=3.9"
) )
-3
View File
@@ -1,3 +0,0 @@
#! /bin/bash
apt-get install portaudio19-dev ffmpeg wget -y
View File
+162
View File
@@ -0,0 +1,162 @@
import json
import os
import scipy
import websocket
import copy
import unittest
from unittest.mock import patch, MagicMock
from whisper_live.client import Client, TranscriptionClient, TranscriptionTeeClient
from whisper_live.utils import resample
from pathlib import Path
class BaseTestCase(unittest.TestCase):
@patch('whisper_live.client.websocket.WebSocketApp')
@patch('whisper_live.client.pyaudio.PyAudio')
def setUp(self, mock_pyaudio, mock_websocket):
self.mock_pyaudio_instance = MagicMock()
mock_pyaudio.return_value = self.mock_pyaudio_instance
self.mock_stream = MagicMock()
self.mock_pyaudio_instance.open.return_value = self.mock_stream
self.mock_ws_app = mock_websocket.return_value
self.mock_ws_app.send = MagicMock()
self.client = TranscriptionClient(host='localhost', port=9090, lang="en").client
self.mock_pyaudio = mock_pyaudio
self.mock_websocket = mock_websocket
self.mock_audio_packet = b'\x00\x01\x02\x03'
def tearDown(self):
self.client.close_websocket()
self.mock_pyaudio.stop()
self.mock_websocket.stop()
del self.client
class TestClientWebSocketCommunication(BaseTestCase):
def test_websocket_communication(self):
expected_url = 'ws://localhost:9090'
self.mock_websocket.assert_called()
self.assertEqual(self.mock_websocket.call_args[0][0], expected_url)
class TestClientCallbacks(BaseTestCase):
def test_on_open(self):
expected_message = json.dumps({
"uid": self.client.uid,
"language": self.client.language,
"task": self.client.task,
"model": self.client.model,
"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.mock_ws_app.send.assert_called_with(expected_message)
def test_on_message(self):
message = json.dumps(
{
"uid": self.client.uid,
"message": "SERVER_READY",
"backend": "faster_whisper"
}
)
self.client.on_message(self.mock_ws_app, message)
message = json.dumps({
"uid": self.client.uid,
"segments": [
{"start": 0, "end": 1, "text": "Test transcript", "completed": True},
{"start": 1, "end": 2, "text": "Test transcript 2", "completed": True},
{"start": 2, "end": 3, "text": "Test transcript 3", "completed": True}
]
})
self.client.on_message(self.mock_ws_app, message)
# Assert that the transcript was updated correctly
self.assertEqual(len(self.client.transcript), 3)
self.assertEqual(self.client.transcript[1]['text'], "Test transcript 2")
def test_on_close(self):
close_status_code = 1000
close_msg = "Normal closure"
self.client.on_close(self.mock_ws_app, close_status_code, close_msg)
self.assertFalse(self.client.recording)
self.assertFalse(self.client.server_error)
self.assertFalse(self.client.waiting)
def test_on_error(self):
error_message = "Test Error"
self.client.on_error(self.mock_ws_app, error_message)
self.assertTrue(self.client.server_error)
self.assertEqual(self.client.error_message, error_message)
class TestAudioResampling(unittest.TestCase):
def test_resample_audio(self):
original_audio = "assets/jfk.flac"
expected_sr = 16000
resampled_audio = resample(original_audio, expected_sr)
sr, _ = scipy.io.wavfile.read(resampled_audio)
self.assertEqual(sr, expected_sr)
os.remove(resampled_audio)
class TestSendingAudioPacket(BaseTestCase):
def test_send_packet(self):
self.client.send_packet_to_server(self.mock_audio_packet)
self.client.client_socket.send.assert_called_with(self.mock_audio_packet, websocket.ABNF.OPCODE_BINARY)
class TestTee(BaseTestCase):
@patch('whisper_live.client.websocket.WebSocketApp')
@patch('whisper_live.client.pyaudio.PyAudio')
def setUp(self, mock_audio, mock_websocket):
super().setUp()
self.client2 = Client(host='localhost', port=9090, lang="es", translate=False, srt_file_path="transcript.srt")
self.client3 = Client(host='localhost', port=9090, lang="es", translate=True, srt_file_path="translation.srt")
# need a separate mock for each websocket
self.client3.client_socket = copy.deepcopy(self.client3.client_socket)
self.tee = TranscriptionTeeClient([self.client2, self.client3])
def tearDown(self):
self.tee.close_all_clients()
del self.tee
super().tearDown()
def test_invalid_constructor(self):
with self.assertRaises(Exception) as context:
TranscriptionTeeClient([])
def test_multicast_unconditional(self):
self.tee.multicast_packet(self.mock_audio_packet, True)
for client in self.tee.clients:
client.client_socket.send.assert_called_with(self.mock_audio_packet, websocket.ABNF.OPCODE_BINARY)
def test_multicast_conditional(self):
self.client2.recording = False
self.client3.recording = True
self.tee.multicast_packet(self.mock_audio_packet, False)
self.client2.client_socket.send.assert_not_called()
self.client3.client_socket.send.assert_called_with(self.mock_audio_packet, websocket.ABNF.OPCODE_BINARY)
def test_close_all(self):
self.tee.close_all_clients()
for client in self.tee.clients:
client.client_socket.close.assert_called()
def test_write_all_srt(self):
for client in self.tee.clients:
client.server_backend = "faster_whisper"
self.tee.write_all_clients_srt()
self.assertTrue(Path("transcript.srt").is_file())
self.assertTrue(Path("translation.srt").is_file())
+148
View File
@@ -0,0 +1,148 @@
import subprocess
import time
import json
import unittest
from unittest import mock
import numpy as np
import jiwer
from websockets.exceptions import ConnectionClosed
from whisper_live.server import TranscriptionServer, BackendType, ClientManager
from whisper_live.client import Client, TranscriptionClient, TranscriptionTeeClient
from whisper.normalizers import EnglishTextNormalizer
class TestTranscriptionServerInitialization(unittest.TestCase):
def test_initialization(self):
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_connection_time, 600)
self.assertDictEqual(server.client_manager.clients, {})
self.assertDictEqual(server.client_manager.start_times, {})
class TestGetWaitTime(unittest.TestCase):
def setUp(self):
self.server = TranscriptionServer()
self.server.client_manager = ClientManager(max_clients=4, max_connection_time=600)
self.server.client_manager.start_times = {
'client1': time.time() - 120,
'client2': time.time() - 300
}
self.server.client_manager.max_connection_time = 600
def test_get_wait_time(self):
expected_wait_time = (600 - (time.time() - self.server.client_manager.start_times['client2'])) / 60
print(self.server.client_manager.get_wait_time(), expected_wait_time)
self.assertAlmostEqual(self.server.client_manager.get_wait_time(), expected_wait_time, places=2)
class TestServerConnection(unittest.TestCase):
def setUp(self):
self.server = TranscriptionServer()
@mock.patch('websockets.WebSocketCommonProtocol')
def test_connection(self, mock_websocket):
mock_websocket.recv.return_value = json.dumps({
'uid': 'test_client',
'language': 'en',
'task': 'transcribe',
'model': 'tiny.en'
})
self.server.recv_audio(mock_websocket, BackendType("faster_whisper"))
@mock.patch('websockets.WebSocketCommonProtocol')
def test_recv_audio_exception_handling(self, mock_websocket):
mock_websocket.recv.side_effect = [json.dumps({
'uid': 'test_client',
'language': 'en',
'task': 'transcribe',
'model': 'tiny.en'
}), np.array([1, 2, 3]).tobytes()]
with self.assertLogs(level="ERROR"):
self.server.recv_audio(mock_websocket, BackendType("faster_whisper"))
self.assertNotIn(mock_websocket, self.server.client_manager.clients)
class TestServerInferenceAccuracy(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.mock_pyaudio_patch = mock.patch('pyaudio.PyAudio')
cls.mock_pyaudio = cls.mock_pyaudio_patch.start()
cls.mock_pyaudio.return_value.open.return_value = mock.MagicMock()
cls.server_process = subprocess.Popen(["python", "run_server.py"])
time.sleep(2)
@classmethod
def tearDownClass(cls):
cls.server_process.terminate()
cls.server_process.wait()
def setUp(self):
self.normalizer = EnglishTextNormalizer()
def check_prediction(self, srt_path):
gt = "And so my fellow Americans, ask not, what your country can do for you. Ask what you can do for your country!"
with open(srt_path, "r") as f:
lines = f.readlines()
prediction = " ".join([line.strip() for line in lines[2::4]])
prediction_normalized = self.normalizer(prediction)
gt_normalized = self.normalizer(gt)
# calculate WER
wer_score = jiwer.wer(gt_normalized, prediction_normalized)
self.assertLess(wer_score, 0.05)
def test_inference(self):
client = TranscriptionClient(
"localhost", "9090", model="base.en", lang="en",
)
client("assets/jfk.flac")
self.check_prediction("output.srt")
def test_simultaneous_inference(self):
client1 = Client(
"localhost", "9090", model="base.en", lang="en", srt_file_path="transcript1.srt")
client2 = Client(
"localhost", "9090", model="base.en", lang="en", srt_file_path="transcript2.srt")
tee = TranscriptionTeeClient([client1, client2])
tee("assets/jfk.flac")
self.check_prediction("transcript1.srt")
self.check_prediction("transcript2.srt")
class TestExceptionHandling(unittest.TestCase):
def setUp(self):
self.server = TranscriptionServer()
@mock.patch('websockets.WebSocketCommonProtocol')
def test_connection_closed_exception(self, mock_websocket):
mock_websocket.recv.side_effect = ConnectionClosed(1001, "testing connection closed", rcvd_then_sent=mock.Mock())
with self.assertLogs(level="INFO") as log:
self.server.recv_audio(mock_websocket, BackendType("faster_whisper"))
self.assertTrue(any("Connection closed by client" in message for message in log.output))
@mock.patch('websockets.WebSocketCommonProtocol')
def test_json_decode_exception(self, mock_websocket):
mock_websocket.recv.return_value = "invalid json"
with self.assertLogs(level="ERROR") as log:
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))
@mock.patch('websockets.WebSocketCommonProtocol')
def test_unexpected_exception_handling(self, mock_websocket):
mock_websocket.recv.side_effect = RuntimeError("Unexpected error")
with self.assertLogs(level="ERROR") as log:
self.server.recv_audio(mock_websocket, BackendType("faster_whisper"))
for message in log.output:
print(message)
print()
self.assertTrue(any("Unexpected error" in message for message in log.output))
+26
View File
@@ -0,0 +1,26 @@
import unittest
import numpy as np
from whisper_live.transcriber.tensorrt_utils import load_audio
from whisper_live.vad import VoiceActivityDetector
class TestVoiceActivityDetection(unittest.TestCase):
def setUp(self):
self.vad = VoiceActivityDetector()
self.sample_rate = 16000
def generate_silence(self, duration_seconds):
return np.zeros(int(self.sample_rate * duration_seconds), dtype=np.float32)
def load_speech_segment(self, filepath):
return load_audio(filepath)
def test_vad_silence_detection(self):
silence = self.generate_silence(3)
is_speech_present = self.vad(silence.copy())
self.assertFalse(is_speech_present, "VAD incorrectly identified silence as speech.")
def test_vad_speech_detection(self):
audio_tensor = load_audio("assets/jfk.flac")
is_speech_present = self.vad(audio_tensor)
self.assertTrue(is_speech_present, "VAD failed to identify speech segment.")
+1 -1
View File
@@ -1 +1 @@
__version__="0.0.10" __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}")
+547 -301
View File
File diff suppressed because it is too large Load Diff
+383 -452
View File
@@ -1,67 +1,326 @@
import websockets import os
import time import time
import threading import threading
import json import json
import textwrap import functools
import logging import logging
# logging.basicConfig(level = logging.INFO) from enum import Enum
from typing import List, Optional
from websockets.sync.server import serve
import torch
import numpy as np import numpy as np
import time from websockets.sync.server import serve
from whisper_live.transcriber import WhisperModel from websockets.exceptions import ConnectionClosed
from whisper_live.vad import VoiceActivityDetector
from whisper_live.backend.base import ServeClientBase
logging.basicConfig(level=logging.INFO)
class TranscriptionServer: class ClientManager:
""" def __init__(self, max_clients=4, max_connection_time=600):
Represents a transcription server that handles incoming audio from clients. """
Initializes the ClientManager with specified limits on client connections and connection durations.
Attributes:
RATE (int): The audio sampling rate (constant) set to 16000.
vad_model (torch.Module): The voice activity detection model.
vad_threshold (float): The voice activity detection threshold.
clients (dict): A dictionary to store connected clients.
websockets (dict): A dictionary to store WebSocket connections.
clients_start_time (dict): A dictionary to track client start times.
max_clients (int): Maximum allowed connected clients.
max_connection_time (int): Maximum allowed connection time in seconds.
"""
RATE = 16000
def __init__(self):
# voice activity detection model
Args:
max_clients (int, optional): The maximum number of simultaneous client connections allowed. Defaults to 4.
max_connection_time (int, optional): The maximum duration (in seconds) a client can stay connected. Defaults
to 600 seconds (10 minutes).
"""
self.clients = {} self.clients = {}
self.websockets = {} self.start_times = {}
self.clients_start_time = {} self.max_clients = max_clients
self.max_clients = 4 self.max_connection_time = max_connection_time
self.max_connection_time = 600
def add_client(self, websocket, client):
"""
Adds a client and their connection start time to the tracking dictionaries.
Args:
websocket: The websocket associated with the client to add.
client: The client object to be added and tracked.
"""
self.clients[websocket] = client
self.start_times[websocket] = time.time()
def get_client(self, websocket):
"""
Retrieves a client associated with the given websocket.
Args:
websocket: The websocket associated with the client to retrieve.
Returns:
The client object if found, False otherwise.
"""
if websocket in self.clients:
return self.clients[websocket]
return False
def remove_client(self, websocket):
"""
Removes a client and their connection start time from the tracking dictionaries. Performs cleanup on the
client if necessary.
Args:
websocket: The websocket associated with the client to be removed.
"""
client = self.clients.pop(websocket, None)
if client:
client.cleanup()
self.start_times.pop(websocket, None)
def get_wait_time(self): def get_wait_time(self):
""" """
Calculate and return the estimated wait time for clients. Calculates the estimated wait time for new clients based on the remaining connection times of current clients.
Returns: Returns:
float: The estimated wait time in minutes. The estimated wait time in minutes for new clients to connect. Returns 0 if there are available slots.
""" """
wait_time = None wait_time = None
for start_time in self.start_times.values():
for k, v in self.clients_start_time.items(): current_client_time_remaining = self.max_connection_time - (time.time() - start_time)
current_client_time_remaining = self.max_connection_time - (time.time() - v)
if wait_time is None or current_client_time_remaining < wait_time: if wait_time is None or current_client_time_remaining < wait_time:
wait_time = current_client_time_remaining wait_time = current_client_time_remaining
return wait_time / 60 if wait_time is not None else 0
return wait_time / 60 def is_server_full(self, websocket, options):
"""
Checks if the server is at its maximum client capacity and sends a wait message to the client if necessary.
def recv_audio(self, websocket): Args:
websocket: The websocket of the client attempting to connect.
options: A dictionary of options that may include the client's unique identifier.
Returns:
True if the server is full, False otherwise.
"""
if len(self.clients) >= self.max_clients:
wait_time = self.get_wait_time()
response = {"uid": options["uid"], "status": "WAIT", "message": wait_time}
websocket.send(json.dumps(response))
return True
return False
def is_client_timeout(self, websocket):
"""
Checks if a client has exceeded the maximum allowed connection time and disconnects them if so, issuing a warning.
Args:
websocket: The websocket associated with the client to check.
Returns:
True if the client's connection time has exceeded the maximum limit, False otherwise.
"""
elapsed_time = time.time() - self.start_times[websocket]
if elapsed_time >= self.max_connection_time:
self.clients[websocket].disconnect()
logging.warning(f"Client with uid '{self.clients[websocket].client_uid}' disconnected due to overtime.")
return True
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:
RATE = 16000
def __init__(self):
self.client_manager = None
self.no_voice_activity_chunks = 0
self.use_vad = True
self.single_model = False
def initialize_client(
self, websocket, options, faster_whisper_custom_model_path,
whisper_tensorrt_path, trt_multilingual, trt_py_session=False,
):
client: Optional[ServeClientBase] = None
if self.backend.is_tensorrt():
try:
from whisper_live.backend.trt_backend import ServeClientTensorRT
client = ServeClientTensorRT(
websocket,
multilingual=trt_multilingual,
language=options["language"],
task=options["task"],
client_uid=options["uid"],
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.")
except Exception as e:
logging.error(f"TensorRT-LLM not supported: {e}")
self.client_uid = options["uid"]
websocket.send(json.dumps({
"uid": self.client_uid,
"status": "WARNING",
"message": "TensorRT-LLM not supported on Server yet. "
"Reverting to available 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'"
}))
try:
if self.backend.is_faster_whisper():
from whisper_live.backend.faster_whisper_backend import ServeClientFasterWhisper
if faster_whisper_custom_model_path is not None and os.path.exists(faster_whisper_custom_model_path):
logging.info(f"Using custom model {faster_whisper_custom_model_path}")
options["model"] = faster_whisper_custom_model_path
client = ServeClientFasterWhisper(
websocket,
language=options["language"],
task=options["task"],
client_uid=options["uid"],
model=options["model"],
initial_prompt=options.get("initial_prompt"),
vad_parameters=options.get("vad_parameters"),
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)
def get_audio_from_websocket(self, websocket):
"""
Receives audio buffer from websocket and creates a numpy array out of it.
Args:
websocket: The websocket to receive audio from.
Returns:
A numpy array containing the audio.
"""
frame_data = websocket.recv()
if frame_data == b"END_OF_AUDIO":
return False
return np.frombuffer(frame_data, dtype=np.float32)
def handle_new_connection(self, websocket, faster_whisper_custom_model_path,
whisper_tensorrt_path, trt_multilingual, trt_py_session=False):
try:
logging.info("New client connected")
options = websocket.recv()
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')
if self.client_manager.is_server_full(websocket, options):
websocket.close()
return False # Indicates that the connection should not continue
if self.backend.is_tensorrt():
self.vad_detector = VoiceActivityDetector(frame_rate=self.RATE)
self.initialize_client(websocket, options, faster_whisper_custom_model_path,
whisper_tensorrt_path, trt_multilingual, trt_py_session=trt_py_session)
return True
except json.JSONDecodeError:
logging.error("Failed to decode JSON from client")
return False
except ConnectionClosed:
logging.info("Connection closed by client")
return False
except Exception as e:
logging.error(f"Error during new connection initialization: {str(e)}")
return False
def process_audio_frames(self, websocket):
frame_np = self.get_audio_from_websocket(websocket)
client = self.client_manager.get_client(websocket)
if frame_np is False:
if self.backend.is_tensorrt():
client.set_eos(True)
return False
if self.backend.is_tensorrt():
voice_active = self.voice_activity(websocket, frame_np)
if voice_active:
self.no_voice_activity_chunks = 0
client.set_eos(False)
if self.use_vad and not voice_active:
return True
client.add_frames(frame_np)
return True
def recv_audio(self,
websocket,
backend: BackendType = BackendType.FASTER_WHISPER,
faster_whisper_custom_model_path=None,
whisper_tensorrt_path=None,
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.
Continuously receives audio frames from a connected client Continuously receives audio frames from a connected client
over a WebSocket connection. It processes the audio frames using a over a WebSocket connection. It processes the audio frames using a
voice activity detection (VAD) model to determine if they contain speech voice activity detection (VAD) model to determine if they contain speech
@@ -75,69 +334,42 @@ class TranscriptionServer:
Args: Args:
websocket (WebSocket): The WebSocket connection for the client. websocket (WebSocket): The WebSocket connection for the client.
backend (str): The backend to run the server with.
faster_whisper_custom_model_path (str): path to custom faster whisper model.
whisper_tensorrt_path (str): Required for tensorrt backend.
trt_multilingual(bool): Only used for tensorrt, True if multilingual model.
Raises: Raises:
Exception: If there is an error during the audio frame processing. Exception: If there is an error during the audio frame processing.
""" """
logging.info("New client connected") self.backend = backend
options = websocket.recv() if not self.handle_new_connection(websocket, faster_whisper_custom_model_path,
options = json.loads(options) whisper_tensorrt_path, trt_multilingual, trt_py_session=trt_py_session):
if len(self.clients) >= self.max_clients:
logging.warning("Client Queue Full. Asking client to wait ...")
wait_time = self.get_wait_time()
response = {
"uid": options["uid"],
"status": "WAIT",
"message": wait_time,
}
websocket.send(json.dumps(response))
websocket.close()
del websocket
return return
client = ServeClient( try:
websocket, while not self.client_manager.is_client_timeout(websocket):
multilingual=options["multilingual"], if not self.process_audio_frames(websocket):
language=options["language"],
task=options["task"],
client_uid=options["uid"],
model_size=options["model_size"]
)
self.clients[websocket] = client
self.clients_start_time[websocket] = time.time()
while True:
try:
frame_data = websocket.recv()
frame_np = np.frombuffer(frame_data, dtype=np.float32)
self.clients[websocket].add_frames(frame_np)
elapsed_time = time.time() - self.clients_start_time[websocket]
if elapsed_time >= self.max_connection_time:
self.clients[websocket].disconnect()
logging.warning(f"{self.clients[websocket]} Client disconnected due to overtime.")
self.clients[websocket].cleanup()
self.clients.pop(websocket)
self.clients_start_time.pop(websocket)
websocket.close()
del websocket
break break
except ConnectionClosed:
logging.info("Connection closed by client")
except Exception as e:
logging.error(f"Unexpected error: {str(e)}")
finally:
if self.client_manager.get_client(websocket):
self.cleanup(websocket)
websocket.close()
del websocket
except Exception as e: def run(self,
logging.error(e) host,
if self.clients[websocket].model_size is not None: port=9090,
self.clients[websocket].cleanup() backend="tensorrt",
self.clients.pop(websocket) faster_whisper_custom_model_path=None,
self.clients_start_time.pop(websocket) whisper_tensorrt_path=None,
logging.info("Connection Closed.") trt_multilingual=False,
logging.info(self.clients) trt_py_session=False,
del websocket single_model=False):
break
def run(self, host, port=9090):
""" """
Run the transcription server. Run the transcription server.
@@ -145,371 +377,70 @@ 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.
""" """
with serve(self.recv_audio, host, port) as 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(
functools.partial(
self.recv_audio,
backend=BackendType(backend),
faster_whisper_custom_model_path=faster_whisper_custom_model_path,
whisper_tensorrt_path=whisper_tensorrt_path,
trt_multilingual=trt_multilingual,
trt_py_session=trt_py_session,
),
host,
port
) as server:
server.serve_forever() server.serve_forever()
def voice_activity(self, websocket, frame_np):
class ServeClient:
"""
Attributes:
RATE (int): The audio sampling rate (constant) set to 16000.
SERVER_READY (str): A constant message indicating that the server is ready.
DISCONNECT (str): A constant message indicating that the client should disconnect.
client_uid (str): A unique identifier for the client.
data (bytes): Accumulated audio data.
frames (bytes): Accumulated audio frames.
language (str): The language for transcription.
task (str): The task type, e.g., "transcribe."
transcriber (WhisperModel): The Whisper model for speech-to-text.
timestamp_offset (float): The offset in audio timestamps.
frames_np (numpy.ndarray): NumPy array to store audio frames.
frames_offset (float): The offset in audio frames.
text (list): List of transcribed text segments.
current_out (str): The current incomplete transcription.
prev_out (str): The previous incomplete transcription.
t_start (float): Timestamp for the start of transcription.
exit (bool): A flag to exit the transcription thread.
same_output_threshold (int): Threshold for consecutive same output segments.
show_prev_out_thresh (int): Threshold for showing previous output segments.
add_pause_thresh (int): Threshold for adding a pause (blank) segment.
transcript (list): List of transcribed segments.
send_last_n_segments (int): Number of last segments to send to the client.
wrapper (textwrap.TextWrapper): Text wrapper for formatting text.
pick_previous_segments (int): Number of previous segments to include in the output.
websocket: The WebSocket connection for the client.
"""
RATE = 16000
SERVER_READY = "SERVER_READY"
DISCONNECT = "DISCONNECT"
def __init__(
self,
websocket,
task="transcribe",
device=None,
multilingual=False,
language=None,
client_uid=None,
model_size="small"
):
""" """
Initialize a ServeClient instance. Evaluates the voice activity in a given audio frame and manages the state of voice activity detection.
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 This method uses the configured voice activity detection (VAD) model to assess whether the given audio frame
to the client to indicate that the server is ready. contains speech. If the VAD model detects no voice activity for more than three consecutive frames,
it sets an end-of-speech (EOS) flag for the associated client. This method aims to efficiently manage
speech detection to improve subsequent processing steps.
Args: Args:
websocket (WebSocket): The WebSocket connection for the client. websocket: The websocket associated with the current client. Used to retrieve the client object
task (str, optional): The task type, e.g., "transcribe." Defaults to "transcribe". from the client manager for state management.
device (str, optional): The device type for Whisper, "cuda" or "cpu". Defaults to None. frame_np (numpy.ndarray): The audio frame to be analyzed. This should be a NumPy array containing
multilingual (bool, optional): Whether the client supports multilingual transcription. Defaults to False. the audio data for the current frame.
language (str, optional): The language for transcription. Defaults to None.
client_uid (str, optional): A unique identifier for the client. Defaults to None.
"""
self.client_uid = client_uid
self.data = b""
self.frames = b""
self.model_sizes = [
"tiny", "base", "small", "medium", "large-v2", "large-v3"
]
self.multilingual = multilingual
self.model_size = self.get_model_size(model_size)
self.language = language if self.multilingual else "en"
self.task = task
self.websocket = websocket
device = "cuda" if torch.cuda.is_available() else "cpu"
if self.model_size == None:
return
self.transcriber = WhisperModel(
self.model_size,
device=device,
compute_type="int8" if device=="cpu" else "float16",
local_files_only=False,
)
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.wrapper = textwrap.TextWrapper(width=50)
self.pick_previous_segments = 2
# 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
}
)
)
def get_model_size(self, model_size):
"""
Returns the whisper model size based on multilingual.
"""
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
if model_size in ["large-v2", "large-v3"]:
self.multilingual = True
return model_size
if not self.multilingual:
model_size = model_size + ".en"
return model_size
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.
"""
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):]
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)
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
# 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
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
if duration<1.0:
continue
try:
input_sample = input_bytes.copy()
# whisper transcribe with prompt
result, info = self.transcriber.transcribe(
input_sample,
initial_prompt=None,
language=self.language,
task=self.task,
vad_filter=True,
vad_parameters={"threshold": 0.5}
)
if self.language is None:
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}))
else:
# detect language again
continue
if len(result):
self.t_start = None
last_segment = self.update_segments(result, duration)
if len(self.transcript) < self.send_last_n_segments:
segments = self.transcript
else:
segments = self.transcript[-self.send_last_n_segments:]
if last_segment is not None:
segments = segments + [last_segment]
else:
# show previous output if there is pause i.e. no output from whisper
segments = []
if self.t_start is None: self.t_start = time.time()
if time.time() - self.t_start < self.show_prev_out_thresh:
if len(self.transcript) < self.send_last_n_segments:
segments = self.transcript
else:
segments = self.transcript[-self.send_last_n_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('')
try:
self.websocket.send(
json.dumps({
"uid": self.client_uid,
"segments": segments
})
)
except Exception as e:
logging.error(f"[ERROR]: {e}")
except Exception as e:
logging.error(f"[ERROR]: {e}")
time.sleep(0.01)
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: Returns:
dict or None: The last processed segment with its start time, end time, and transcribed text. bool: True if voice activity is detected in the current frame, False otherwise. When returning False
Returns None if there are no valid segments to process. after detecting no voice activity for more than three consecutive frames, it also triggers the
end-of-speech (EOS) flag for the client.
""" """
offset = None if not self.vad_detector(frame_np):
self.current_out = '' self.no_voice_activity_chunks += 1
last_segment = None if self.no_voice_activity_chunks > 3:
# process complete segments client = self.client_manager.get_client(websocket)
if len(segments) > 1: if not client.eos:
for i, s in enumerate(segments[:-1]): client.set_eos(True)
text_ = s.text time.sleep(0.1) # Sleep 100m; wait some voice activity.
self.text.append(text_) return False
start, end = self.timestamp_offset + s.start, self.timestamp_offset + min(duration, s.end) return True
self.transcript.append(
{
'start': start,
'end': end,
'text': text_
}
)
offset = min(duration, s.end)
self.current_out += segments[-1].text def cleanup(self, websocket):
last_segment = {
'start': self.timestamp_offset + segments[-1].start,
'end': self.timestamp_offset + min(duration, segments[-1].end),
'text': 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(
{
'start': self.timestamp_offset,
'end': self.timestamp_offset + duration,
'text': 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
def disconnect(self):
""" """
Notify the client of disconnection and send a disconnect message. Cleans up resources associated with a given client's websocket.
This method sends a disconnect message to the client via the WebSocket connection to notify them
that the transcription service is disconnecting gracefully.
Args:
websocket: The websocket associated with the client to be cleaned up.
""" """
self.websocket.send( if self.client_manager.get_client(websocket):
json.dumps( self.client_manager.remove_client(websocket)
{
"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
self.transcriber.destroy()
File diff suppressed because it is too large Load Diff
+364
View File
@@ -0,0 +1,364 @@
# SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
from collections import defaultdict
from functools import lru_cache
from pathlib import Path
from subprocess import CalledProcessError, run
from typing import Dict, Iterable, List, Optional, TextIO, Tuple, Union
import kaldialign
import numpy as np
import soundfile
import av
import wave
import torch
import torch.nn.functional as F
from whisper_live.utils import resample
Pathlike = Union[str, Path]
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 load_audio(file: str, sr: int = 16000):
"""
Open an audio file, resample it, and read as a mono waveform.
Parameters
----------
file: str
The audio file to open.
sr: int
The sample rate to resample the audio if necessary.
Returns
-------
A NumPy array containing the audio waveform, in float32 dtype.
"""
resampled_file = resample(file, sr)
with wave.open(resampled_file, "rb") as wav_file:
num_frames = wav_file.getnframes()
raw_data = wav_file.readframes(num_frames)
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):
# make sure audio in .wav format
assert wav_path.endswith(
'.wav'), f"Only support .wav format, but got {wav_path}"
waveform, sample_rate = soundfile.read(wav_path)
assert sample_rate == 16000, f"Only support 16k sample rate, but got {sample_rate}"
return waveform, sample_rate
def pad_or_trim(array, length: int = N_SAMPLES, *, axis: int = -1):
"""
Pad or trim the audio array to N_SAMPLES, as expected by the encoder.
"""
if torch.is_tensor(array):
if array.shape[axis] > length:
array = array.index_select(dim=axis,
index=torch.arange(length,
device=array.device))
if array.shape[axis] < length:
pad_widths = [(0, 0)] * array.ndim
pad_widths[axis] = (0, length - array.shape[axis])
array = F.pad(array,
[pad for sizes in pad_widths[::-1] for pad in sizes])
else:
if array.shape[axis] > length:
array = array.take(indices=range(length), axis=axis)
if array.shape[axis] < length:
pad_widths = [(0, 0)] * array.ndim
pad_widths[axis] = (0, length - array.shape[axis])
array = np.pad(array, pad_widths)
return array
@lru_cache(maxsize=None)
def mel_filters(device,
n_mels: int,
mel_filters_dir: str = None) -> torch.Tensor:
"""
load the mel filterbank matrix for projecting STFT into a Mel spectrogram.
Allows decoupling librosa dependency; saved using:
np.savez_compressed(
"mel_filters.npz",
mel_80=librosa.filters.mel(sr=16000, n_fft=400, n_mels=80),
)
"""
assert n_mels in {80, 128}, f"Unsupported n_mels: {n_mels}"
if mel_filters_dir is None:
mel_filters_path = os.path.join(os.path.dirname(__file__), "assets",
"mel_filters.npz")
else:
mel_filters_path = os.path.join(mel_filters_dir, "mel_filters.npz")
with np.load(mel_filters_path) as f:
return torch.from_numpy(f[f"mel_{n_mels}"]).to(device)
def log_mel_spectrogram(
audio: Union[str, np.ndarray, torch.Tensor],
n_mels: int,
padding: int = 0,
device: Optional[Union[str, torch.device]] = None,
return_duration: bool = False,
mel_filters_dir: str = None,
):
"""
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 device is not None:
audio = audio.to(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
filters = mel_filters(audio.device, n_mels, mel_filters_dir)
mel_spec = 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 store_transcripts(filename: Pathlike, texts: Iterable[Tuple[str, str,
str]]) -> None:
"""Save predicted results and reference transcripts to a file.
https://github.com/k2-fsa/icefall/blob/master/icefall/utils.py
Args:
filename:
File to save the results to.
texts:
An iterable of tuples. The first element is the cur_id, the second is
the reference transcript and the third element is the predicted result.
Returns:
Return None.
"""
with open(filename, "w") as f:
for cut_id, ref, hyp in texts:
print(f"{cut_id}:\tref={ref}", file=f)
print(f"{cut_id}:\thyp={hyp}", file=f)
def write_error_stats( # noqa: C901
f: TextIO,
test_set_name: str,
results: List[Tuple[str, str]],
enable_log: bool = True,
) -> float:
"""Write statistics based on predicted results and reference transcripts.
https://github.com/k2-fsa/icefall/blob/master/icefall/utils.py
It will write the following to the given file:
- WER
- number of insertions, deletions, substitutions, corrects and total
reference words. For example::
Errors: 23 insertions, 57 deletions, 212 substitutions, over 2606
reference words (2337 correct)
- The difference between the reference transcript and predicted result.
An instance is given below::
THE ASSOCIATION OF (EDISON->ADDISON) ILLUMINATING COMPANIES
The above example shows that the reference word is `EDISON`,
but it is predicted to `ADDISON` (a substitution error).
Another example is::
FOR THE FIRST DAY (SIR->*) I THINK
The reference word `SIR` is missing in the predicted
results (a deletion error).
results:
An iterable of tuples. The first element is the cur_id, the second is
the reference transcript and the third element is the predicted result.
enable_log:
If True, also print detailed WER to the console.
Otherwise, it is written only to the given file.
Returns:
Return None.
"""
subs: Dict[Tuple[str, str], int] = defaultdict(int)
ins: Dict[str, int] = defaultdict(int)
dels: Dict[str, int] = defaultdict(int)
# `words` stores counts per word, as follows:
# corr, ref_sub, hyp_sub, ins, dels
words: Dict[str, List[int]] = defaultdict(lambda: [0, 0, 0, 0, 0])
num_corr = 0
ERR = "*"
for cut_id, ref, hyp in results:
ali = kaldialign.align(ref, hyp, ERR)
for ref_word, hyp_word in ali:
if ref_word == ERR:
ins[hyp_word] += 1
words[hyp_word][3] += 1
elif hyp_word == ERR:
dels[ref_word] += 1
words[ref_word][4] += 1
elif hyp_word != ref_word:
subs[(ref_word, hyp_word)] += 1
words[ref_word][1] += 1
words[hyp_word][2] += 1
else:
words[ref_word][0] += 1
num_corr += 1
ref_len = sum([len(r) for _, r, _ in results])
sub_errs = sum(subs.values())
ins_errs = sum(ins.values())
del_errs = sum(dels.values())
tot_errs = sub_errs + ins_errs + del_errs
tot_err_rate = "%.2f" % (100.0 * tot_errs / ref_len)
if enable_log:
logging.info(f"[{test_set_name}] %WER {tot_errs / ref_len:.2%} "
f"[{tot_errs} / {ref_len}, {ins_errs} ins, "
f"{del_errs} del, {sub_errs} sub ]")
print(f"%WER = {tot_err_rate}", file=f)
print(
f"Errors: {ins_errs} insertions, {del_errs} deletions, "
f"{sub_errs} substitutions, over {ref_len} reference "
f"words ({num_corr} correct)",
file=f,
)
print(
"Search below for sections starting with PER-UTT DETAILS:, "
"SUBSTITUTIONS:, DELETIONS:, INSERTIONS:, PER-WORD STATS:",
file=f,
)
print("", file=f)
print("PER-UTT DETAILS: corr or (ref->hyp) ", file=f)
for cut_id, ref, hyp in results:
ali = kaldialign.align(ref, hyp, ERR)
combine_successive_errors = True
if combine_successive_errors:
ali = [[[x], [y]] for x, y in ali]
for i in range(len(ali) - 1):
if ali[i][0] != ali[i][1] and ali[i + 1][0] != ali[i + 1][1]:
ali[i + 1][0] = ali[i][0] + ali[i + 1][0]
ali[i + 1][1] = ali[i][1] + ali[i + 1][1]
ali[i] = [[], []]
ali = [[
list(filter(lambda a: a != ERR, x)),
list(filter(lambda a: a != ERR, y)),
] for x, y in ali]
ali = list(filter(lambda x: x != [[], []], ali))
ali = [[
ERR if x == [] else " ".join(x),
ERR if y == [] else " ".join(y),
] for x, y in ali]
print(
f"{cut_id}:\t" + " ".join((ref_word if ref_word == hyp_word else
f"({ref_word}->{hyp_word})"
for ref_word, hyp_word in ali)),
file=f,
)
print("", file=f)
print("SUBSTITUTIONS: count ref -> hyp", file=f)
for count, (ref, hyp) in sorted([(v, k) for k, v in subs.items()],
reverse=True):
print(f"{count} {ref} -> {hyp}", file=f)
print("", file=f)
print("DELETIONS: count ref", file=f)
for count, ref in sorted([(v, k) for k, v in dels.items()], reverse=True):
print(f"{count} {ref}", file=f)
print("", file=f)
print("INSERTIONS: count hyp", file=f)
for count, hyp in sorted([(v, k) for k, v in ins.items()], reverse=True):
print(f"{count} {hyp}", file=f)
print("", file=f)
print("PER-WORD STATS: word corr tot_errs count_in_ref count_in_hyp",
file=f)
for _, word, counts in sorted([(sum(v[1:]), k, v)
for k, v in words.items()],
reverse=True):
(corr, ref_sub, hyp_sub, ins, dels) = counts
tot_errs = ref_sub + hyp_sub + ins + dels
ref_count = corr + ref_sub + dels
hyp_count = corr + hyp_sub + ins
print(f"{word} {corr} {tot_errs} {ref_count} {hyp_count}", file=f)
return float(tot_err_rate)
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()
+82
View File
@@ -0,0 +1,82 @@
import os
import textwrap
import scipy
import numpy as np
import av
from pathlib import Path
def clear_screen():
"""Clears the console screen."""
os.system("cls" if os.name == "nt" else "clear")
def print_transcript(text):
"""Prints formatted transcript text."""
wrapper = textwrap.TextWrapper(width=60)
for line in wrapper.wrap(text="".join(text)):
print(line)
def format_time(s):
"""Convert seconds (float) to SRT time format."""
hours = int(s // 3600)
minutes = int((s % 3600) // 60)
seconds = int(s % 60)
milliseconds = int((s - int(s)) * 1000)
return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
def create_srt_file(segments, resampled_file):
with open(resampled_file, 'w', encoding='utf-8') as srt_file:
segment_number = 1
for segment in segments:
start_time = format_time(float(segment['start']))
end_time = format_time(float(segment['end']))
text = segment['text']
srt_file.write(f"{segment_number}\n")
srt_file.write(f"{start_time} --> {end_time}\n")
srt_file.write(f"{text}\n\n")
segment_number += 1
def resample(file: str, sr: int = 16000):
"""
Resample the audio file to 16kHz.
Args:
file (str): The audio file to open
sr (int): The sample rate to resample the audio if necessary
Returns:
resampled_file (str): The resampled audio file
"""
container = av.open(file)
stream = next(s for s in container.streams if s.type == 'audio')
resampler = av.AudioResampler(
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
+157
View File
@@ -0,0 +1,157 @@
import os
import subprocess
import torch
import numpy as np
import onnxruntime
import warnings
class VoiceActivityDetection():
def __init__(self, force_onnx_cpu=True):
path = self.download()
opts = onnxruntime.SessionOptions()
opts.log_severity_level = 3
opts.inter_op_num_threads = 1
opts.intra_op_num_threads = 1
if force_onnx_cpu and 'CPUExecutionProvider' in onnxruntime.get_available_providers():
self.session = onnxruntime.InferenceSession(path, providers=['CPUExecutionProvider'], sess_options=opts)
else:
self.session = onnxruntime.InferenceSession(path, providers=['CUDAExecutionProvider'], sess_options=opts)
self.reset_states()
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):
if x.dim() == 1:
x = x.unsqueeze(0)
if x.dim() > 2:
raise ValueError(f"Too many dimensions for input audio chunk {x.dim()}")
if sr != 16000 and (sr % 16000 == 0):
step = sr // 16000
x = x[:,::step]
sr = 16000
if sr not in self.sample_rates:
raise ValueError(f"Supported sampling rates: {self.sample_rates} (or multiply of 16000)")
if sr / x.shape[1] > 31.25:
raise ValueError("Input audio chunk is too short")
return x, sr
def reset_states(self, batch_size=1):
self._state = torch.zeros((2, batch_size, 128)).float()
self._context = torch.zeros(0)
self._last_sr = 0
self._last_batch_size = 0
def __call__(self, x, sr: int):
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]
context_size = 64 if sr == 16000 else 32
if not self._last_batch_size:
self.reset_states(batch_size)
if (self._last_sr) and (self._last_sr != sr):
self.reset_states(batch_size)
if (self._last_batch_size) and (self._last_batch_size != 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]:
ort_inputs = {'input': x.numpy(), 'state': self._state.numpy(), 'sr': np.array(sr, dtype='int64')}
ort_outs = self.session.run(None, ort_inputs)
out, state = ort_outs
self._state = torch.from_numpy(state)
else:
raise ValueError()
self._context = x[..., -context_size:]
self._last_sr = sr
self._last_batch_size = batch_size
out = torch.from_numpy(out)
return out
def audio_forward(self, x, sr: int):
outs = []
x, sr = self._validate_input(x, sr)
self.reset_states()
num_samples = 512 if sr == 16000 else 256
if 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)
for i in range(0, x.shape[1], num_samples):
wavs_batch = x[:, i:i+num_samples]
out_chunk = self.__call__(wavs_batch, sr)
outs.append(out_chunk)
stacked = torch.cat(outs, dim=1)
return stacked.cpu()
@staticmethod
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/")
# Ensure the target directory exists
os.makedirs(target_dir, exist_ok=True)
# Define the target file path
model_filename = os.path.join(target_dir, "silero_vad.onnx")
# Check if the model file already exists
if not os.path.exists(model_filename):
# If it doesn't exist, download the model using wget
try:
subprocess.run(["wget", "-O", model_filename, model_url], check=True)
except subprocess.CalledProcessError:
print("Failed to download the model using wget.")
return model_filename
class VoiceActivityDetector:
def __init__(self, threshold=0.5, frame_rate=16000):
"""
Initializes the VoiceActivityDetector with a voice activity detection model and a threshold.
Args:
threshold (float, optional): The probability threshold for detecting voice activity. Defaults to 0.5.
"""
self.model = VoiceActivityDetection()
self.threshold = threshold
self.frame_rate = frame_rate
def __call__(self, audio_frame):
"""
Determines if the given audio frame contains speech by comparing the detected speech probability against
the threshold.
Args:
audio_frame (np.ndarray): The audio frame to be analyzed for voice activity. It is expected to be a
NumPy array of audio samples.
Returns:
bool: True if the speech probability exceeds the threshold, indicating the presence of voice activity;
False otherwise.
"""
speech_probs = self.model.audio_forward(torch.from_numpy(audio_frame.copy()), self.frame_rate)[0]
return torch.any(speech_probs > self.threshold).item()