261 Commits

Author SHA1 Message Date
makaveli10 09670dd3c7 add eos to faster_whisper server
Signed-off-by: makaveli10 <vineet.suryan@collabora.com>
2024-07-11 07:01:34 -04: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
makaveli10 7c0b32b85e bump version 0.0.10 2024-01-01 18:12:51 +05:30
makaveli 01665a54c1 Merge pull request #82 from hcljsq/main
Add the `large-v3` model
2024-01-01 18:09:41 +05:30
华晨 02793a93f8 feat: Update transcriber to support large-v3 model with 128 mel filters 2024-01-01 20:22:37 +08:00
makaveli db2e0bbcdd Merge pull request #81 from k0hacuu/main
README Spelling correction
2024-01-01 13:53:38 +05:30
Chronoz e92ddd291a fix exception on overflow 2023-12-31 20:59:33 +07:00
华晨 5918b5ed42 fix: update faster-whisper 2023-12-31 15:45:41 +08:00
华晨 71d207a607 feat: add large-v3 model 2023-12-30 23:19:30 +08:00
Jonny Yang 6ee4cd09f2 Update README.md 2023-12-25 13:12:24 +00:00
Marcus Edel 5de4de4b84 Merge pull request #76 from makaveli10/model_size_option
Model size option.
2023-12-20 09:10:57 -05:00
makaveli10 e006722da7 add model size option to client 2023-12-20 18:06:02 +05:30
makaveli10 a52dc0cbf8 update chrome/firefox plugin readme 2023-12-15 00:09:55 +05:30
makaveli10 048ab0a8f4 remove debugging script 2023-12-15 02:26:01 +08:00
makaveli10 261bb9e961 Merge branch 'model_size_option' of github.com:makaveli10/whisper-live into model_size_option 2023-12-15 02:23:20 +08:00
makaveli10 091f6179d4 add sample audio for testing 2023-12-15 02:23:13 +08:00
makaveli10 1e1349cd80 update firefox plugin with model size dropdown 2023-12-14 23:52:51 +05:30
makaveli10 14beb4f942 update chrome plugin with model size dropdown 2023-12-14 23:52:25 +05:30
makaveli10 7ffcad64ba update readme 2023-12-15 02:21:35 +08:00
makaveli10 402fceb9f3 remove emptyline 2023-12-15 02:21:09 +08:00
makaveli10 09b18e8ab8 add model size option from server 2023-12-14 22:08:12 +08:00
makaveli da72d03073 Merge pull request #73 from jhormigo/hls_support
Support for HLS transcription
2023-12-13 00:05:11 +05:30
Jesús Hormigo a1a8d5f92a Added a HLS stream sample URL 2023-12-12 13:46:52 +01:00
Jesús Hormigo b6dee4e46e Using ffmpeg-python package instead of requiring having ffmpeg installed in system 2023-12-10 19:34:27 +01:00
Jesús Hormigo f3cd20fbf3 Support for HLS transcription (resolves #62) 2023-12-09 21:24:55 +01:00
35 changed files with 3727 additions and 1092 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.8, 3.9, '3.10', 3.11]
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@v2
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 ffmpeg 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.8, 3.9, '3.10', 3.11]
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-tensorrt:
needs: [run-tests, check-code-format]
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.tensorrt
push: true
tags: ghcr.io/collabora/whisperlive-tensorrt: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
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.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Cache Python dependencies
uses: actions/cache@v2
with:
path: |
~/.cache/pip
!~/.cache/pip/log
key: ubuntu-latest-pip-3.8-${{ hashFiles('requirements/server.txt', 'requirements/client.txt') }}
restore-keys: |
ubuntu-latest-pip-3.8-
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y ffmpeg 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 -1
View File
@@ -26,9 +26,9 @@ 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.
### Getting Started ### Getting Started
- Make sure the transcription server is running properly. To know more about how to start the server, see the [documentation here](https://github.com/collabora/whisper-live). - Make sure the transcription server is running properly. To know more about how to start the server, see the [documentation here](https://github.com/collabora/whisper-live).
+3 -1
View File
@@ -156,7 +156,9 @@ async function startCapture(options) {
port: options.port, port: options.port,
multilingual: options.useMultilingual, multilingual: options.useMultilingual,
language: options.language, language: options.language,
task: options.task task: options.task,
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');
+3 -5
View File
@@ -93,16 +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: option.modelSize,
use_vad: option.useVad
}) })
); );
}; };
+114 -97
View File
@@ -16,120 +16,137 @@
<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">
<label for="taskDropdown">Select task:</label> <label for="taskDropdown">Select task:</label>
<select id="taskDropdown" disabled> <select id="taskDropdown" >
<option value="">Select Task</option> <option value="">Select Task</option>
<option value="transcribe" selected>Transcribe</option> <option value="transcribe" selected>Transcribe</option>
<option value="translate">Translate</option> <option value="translate">Translate</option>
</select> </select>
</div> </div>
<div class="dropdown-container">
<label for="modelSizeDropdown">Select Model Size:</label>
<select id="modelSizeDropdown">
<option value="">Select model</option>
<option value="tiny">Tiny </option>
<option value="tiny.en">Tiny (English-only)</option>
<option value="base">Base</option>
<option value="base.en">Base (English-only)</option>
<option value="small" selected>Small</option>
<option value="small.en">Small (English-only)</option>
<option value="medium">Medium</option>
<option value="medium.en">Medium (English-only)</option>
<option value="large-v2">Large-v2</option>
<option value="large-v3">Large-v3</option>
</select>
</div>
</body> </body>
</html> </html>
+29 -21
View File
@@ -4,11 +4,13 @@ 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');
let selectedLanguage = null; let selectedLanguage = null;
let selectedTask = taskDropdown.value; let selectedTask = taskDropdown.value;
let selectedModelSize = modelSizeDropdown.value;
// Add click event listeners to the buttons // Add click event listeners to the buttons
startButton.addEventListener("click", startCapture); startButton.addEventListener("click", startCapture);
@@ -30,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;
} }
}); });
@@ -52,6 +52,13 @@ document.addEventListener("DOMContentLoaded", function () {
} }
}); });
chrome.storage.local.get("selectedModelSize", ({ selectedModelSize: storedModelSize }) => {
if (storedModelSize !== undefined) {
modelSizeDropdown.value = storedModelSize;
selectedModelSize = storedModelSize;
}
});
// Function to handle the start capture button click event // Function to handle the start capture button click event
async function startCapture() { async function startCapture() {
// Ignore click if the button is disabled // Ignore click if the button is disabled
@@ -77,9 +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,
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 } }, () => {
@@ -118,9 +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;
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);
} }
@@ -131,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() {
@@ -157,6 +160,11 @@ document.addEventListener("DOMContentLoaded", function () {
chrome.storage.local.set({ selectedTask }); chrome.storage.local.set({ selectedTask });
}); });
modelSizeDropdown.addEventListener('change', function() {
selectedModelSize = modelSizeDropdown.value;
chrome.storage.local.set({ selectedModelSize });
});
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
if (request.action === "updateSelectedLanguage") { if (request.action === "updateSelectedLanguage") {
const detectedLanguage = request.detectedLanguage; const detectedLanguage = request.detectedLanguage;
+1 -1
View File
@@ -24,9 +24,9 @@ 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.
### Getting Started ### Getting Started
- Make sure the transcription server is running properly. To know more about how to start the server, see the [documentation here](https://github.com/collabora/whisper-live). - Make sure the transcription server is running properly. To know more about how to start the server, see the [documentation here](https://github.com/collabora/whisper-live).
+4 -6
View File
@@ -66,18 +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: data.modelSize,
use_vad: data.useVad
}) })
); );
}; };
@@ -200,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');
+115 -99
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">
@@ -133,5 +133,21 @@
<option value="translate">Translate</option> <option value="translate">Translate</option>
</select> </select>
</div> </div>
<div class="dropdown-container">
<label for="modelSizeDropdown">Select Model Size:</label>
<select id="modelSizeDropdown">
<option value="">Select model</option>
<option value="tiny">Tiny </option>
<option value="tiny.en">Tiny (English-only)</option>
<option value="base">Base</option>
<option value="base.en">Base (English-only)</option>
<option value="small" selected>Small</option>
<option value="small.en">Small (English-only)</option>
<option value="medium">Medium</option>
<option value="medium.en">Medium (English-only)</option>
<option value="large-v2">Large-v2</option>
<option value="large-v3">Large-v3</option>
</select>
</div>
</body> </body>
</html> </html>
+29 -20
View File
@@ -3,11 +3,14 @@ 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');
let selectedLanguage = null; let selectedLanguage = null;
let selectedTask = taskDropdown.value; let selectedTask = taskDropdown.value;
let selectedModelSize = modelSizeDropdown.value;
browser.storage.local.get("capturingState") browser.storage.local.get("capturingState")
.then(function(result) { .then(function(result) {
@@ -32,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;
} }
}); });
@@ -54,6 +55,13 @@ document.addEventListener("DOMContentLoaded", function() {
} }
}); });
browser.storage.local.get("selectedModelSize", ({ selectedModelSize: storedModelSize }) => {
if (storedModelSize !== undefined) {
modelSizeDropdown.value = storedModelSize;
selectedModelSize = storedModelSize;
}
});
startButton.addEventListener("click", function() { startButton.addEventListener("click", function() {
let host = "localhost"; let host = "localhost";
let port = "9090"; let port = "9090";
@@ -73,9 +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,
useVad: useVadCheckbox.checked,
} }
}); });
toggleCaptureButtons(true); toggleCaptureButtons(true);
@@ -114,8 +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;
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);
} }
@@ -126,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() {
@@ -152,6 +156,11 @@ document.addEventListener("DOMContentLoaded", function() {
browser.storage.local.set({ selectedTask }); browser.storage.local.set({ selectedTask });
}); });
modelSizeDropdown.addEventListener('change', function() {
selectedModelSize = modelSizeDropdown.value;
browser.storage.local.set({ selectedModelSize });
});
browser.runtime.onMessage.addListener((request, sender, sendResponse) => { browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "updateSelectedLanguage") { if (request.action === "updateSelectedLanguage") {
const detectedLanguage = request.data; const detectedLanguage = request.data;
+1 -1
View File
@@ -108,4 +108,4 @@ label {
.dropdown-container { .dropdown-container {
padding: 10px; padding: 10px;
} }
+116 -46
View File
@@ -1,14 +1,20 @@
# 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
- Install PyAudio and ffmpeg - Install PyAudio and ffmpeg
```bash ```bash
bash setup.sh bash scripts/setup.sh
``` ```
- Install whisper-live from pip - Install whisper-live from pip
@@ -16,64 +22,129 @@ 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 two backends `faster_whisper` and `tensorrt`. 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("localhost", 9090, is_multilingual=True, lang="hi", translate=True) -b tensorrt \
client(audio_file_path) -trt /home/TensorRT-LLM/examples/whisper/whisper_small_en
```
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 also enables the multilingual feature, allowing transcription in multiple languages. The language option specifies the target language for transcription, in this case, Hindi ("hi"). 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.
- To transcribe from microphone: # Run Multilingual model
```python python3 run_server.py -p 9090 \
from whisper_live.client import TranscriptionClient -b tensorrt \
client = TranscriptionClient(host, port, is_multilingual=True, lang="hi", translate=True) -trt /home/TensorRT-LLM/examples/whisper/whisper_small \
client() -m
``` ```
This command captures audio from the microphone and sends it to the server for transcription. It uses the same options as the previous command, enabling the multilingual feature and specifying the target language and task. #### 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
## Transcribe audio from browser python3 run_server.py --port 9090 \
- Run the server --backend faster_whisper \
```python --omp_num_threads 4
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 #### Single model mode
- Refer to [Audio-Transcription-Chrome](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Chrome#readme) to use Chrome extension. 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.
### Firefox Extension 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.
- Refer to [Audio-Transcription-Firefox](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Firefox#readme) to use Mozilla Firefox extension.
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`.
```python
from whisper_live.client import TranscriptionClient
client = TranscriptionClient(
"localhost",
9090,
lang="en",
translate=False,
model="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
)
```
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 [Audio-Transcription-Firefox](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Firefox#readme) for setup instructions.
## 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.
```bash
docker run -p 9090:9090 --runtime=nvidia --gpus all --entrypoint /bin/bash -it ghcr.io/collabora/whisperlive-tensorrt
# Build tiny.en engine
bash build_whisper_tensorrt.sh /app/TensorRT-LLM-examples small.en
# Run server with tiny.en
python3 run_server.py --port 9090 \
--backend tensorrt \
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small_en"
```
- CPU - CPU
```bash ```bash
docker build . -t whisper-live -f docker/Dockerfile.cpu docker run -it -p 9090:9090 ghcr.io/collabora/whisperlive-cpu:latest
docker run -it -p 9090:9090 whisper-live: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. **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. - [x] TensorRT backend for Whisper.
## Contact ## Contact
@@ -98,6 +169,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}
} }
+42
View File
@@ -0,0 +1,42 @@
# 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.9.0`
## 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)
- Clone this repo.
```bash
git clone https://github.com/collabora/WhisperLive.git
cd WhisperLive
```
- Run WhisperLive TensorRT in docker
```bash
docker run -p 9090:9090 --runtime=nvidia --gpus all --entrypoint /bin/bash -it ghcr.io/collabora/whisperlive-tensorrt:latest
```
## 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
# 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"
# Run Multilingual model
python3 run_server.py --port 9090 \
--backend tensorrt \
--trt_model_path "/app/TensorRT-LLM-examples/whisper/whisper_small" \
--trt_multilingual
```
BIN
View File
Binary file not shown.
+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"]
+28
View File
@@ -0,0 +1,28 @@
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
python3.10 python3-pip openmpi-bin libopenmpi-dev git wget \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install --no-cache-dir -U tensorrt_llm==0.9.0 --extra-index-url https://pypi.nvidia.com
WORKDIR /app
RUN git clone -b v0.9.0 --depth 1 https://github.com/NVIDIA/TensorRT-LLM.git && \
mv TensorRT-LLM/examples ./TensorRT-LLM-examples && \
rm -rf TensorRT-LLM
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 .
+12 -6
View File
@@ -1,7 +1,13 @@
PyAudio faster-whisper==1.0.1
faster-whisper==0.9.0 torch
--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.16.0
numba
openai-whisper
kaldialign
soundfile
ffmpeg-python
scipy
jiwer
evaluate
numpy<2
+47 -2
View File
@@ -1,5 +1,50 @@
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"]')
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('--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,
single_model=not args.no_single_model,
)
+77
View File
@@ -0,0 +1,77 @@
#!/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"
;;
*)
echo "Invalid model name: $model_name"
exit 1
;;
esac
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 output_dir="whisper_${model_name//./_}"
echo "$output_dir"
echo "Running build script for $model_name with output directory $output_dir"
python3 build.py --output_dir "$output_dir" --use_gpt_attention_plugin --use_gemm_plugin --use_bert_attention_plugin --enable_context_fmha --model_name "$model_name"
echo "Whisper $model_name TensorRT engine built."
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}"
cd $1/whisper
pip install --no-deps -r requirements.txt
download_and_build_model "$model_name"
View File
+40 -34
View File
@@ -10,45 +10,51 @@ 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.9.0", "faster-whisper==1.0.1",
"torch", "torch",
"torchaudio", "torchaudio",
"websockets", "websockets",
"onnxruntime", "onnxruntime==1.16.0",
"ffmpeg-python", "ffmpeg-python",
"scipy", "scipy",
"websocket-client", "websocket-client",
], "numba",
python_requires=">=3.8" "openai-whisper",
"kaldialign",
"soundfile",
],
python_requires=">=3.8"
) )
View File
+156
View File
@@ -0,0 +1,156 @@
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
})
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"},
{"start": 1, "end": 2, "text": "Test transcript 2"},
{"start": 2, "end": 3, "text": "Test transcript 3"}
]
})
self.client.on_message(self.mock_ws_app, message)
# Assert that the transcript was updated correctly
self.assertEqual(len(self.client.transcript), 2)
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())
+150
View File
@@ -0,0 +1,150 @@
import subprocess
import time
import json
import unittest
from unittest import mock
import numpy as np
import evaluate
from websockets.exceptions import ConnectionClosed
from whisper_live.server import TranscriptionServer
from whisper_live.client import Client, TranscriptionClient, TranscriptionTeeClient
from whisper.normalizers import EnglishTextNormalizer
class TestTranscriptionServerInitialization(unittest.TestCase):
def test_initialization(self):
server = TranscriptionServer()
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.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, "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, "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.metric = evaluate.load("wer")
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 = self.metric.compute(
predictions=[prediction_normalized],
references=[gt_normalized]
)
self.assertLess(wer, 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")
with self.assertLogs(level="INFO") as log:
self.server.recv_audio(mock_websocket, "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, "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, "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.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.9" __version__ = "0.5.0"
+445 -256
View File
@@ -1,56 +1,34 @@
import os import os
import shutil
import wave import wave
import numpy as np import numpy as np
import scipy
import ffmpeg
import pyaudio import pyaudio
import threading import threading
import textwrap
import json import json
import websocket import websocket
import uuid import uuid
import time import time
import ffmpeg
import whisper_live.utils as utils
def resample(file: str, sr: int = 16000):
"""
# https://github.com/openai/whisper/blob/7858aa9c08d98f75575035ecd6481f462d66ca27/whisper/audio.py#L22
Open an audio file and read as mono waveform, resampling as necessary,
save the resampled audio
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
"""
try:
# This launches a subprocess to decode audio while down-mixing and resampling as necessary.
# Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
out, _ = (
ffmpeg.input(file, threads=0)
.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=sr)
.run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
)
except ffmpeg.Error as e:
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
np_buffer = np.frombuffer(out, dtype=np.int16)
resampled_file = f"{file.split('.')[0]}_resampled.wav"
scipy.io.wavfile.write(resampled_file, sr, np_buffer.astype(np.int16))
return resampled_file
class Client: class Client:
""" """
Handles audio recording, streaming, and communication with a server using WebSocket. Handles communication with a server using WebSocket.
""" """
INSTANCES = {} INSTANCES = {}
END_OF_AUDIO = "END_OF_AUDIO"
def __init__( def __init__(
self, host=None, port=None, is_multilingual=False, lang=None, translate=False self,
host=None,
port=None,
lang=None,
translate=False,
model="small",
srt_file_path="output.srt",
use_vad=True
): ):
""" """
Initializes a Client instance for audio recording and streaming to a server. Initializes a Client instance for audio recording and streaming to a server.
@@ -62,38 +40,28 @@ class Client:
Args: Args:
host (str): The hostname or IP address of the server. host (str): The hostname or IP address of the server.
port (int): The port number for the WebSocket server. port (int): The port number for the WebSocket server.
is_multilingual (bool, optional): Specifies if multilingual transcription is enabled. Default is False. lang (str, optional): The selected language for transcription. Default is None.
lang (str, optional): The selected language for transcription when multilingual is disabled. Default is None.
translate (bool, optional): Specifies if the task is translation. Default is False. translate (bool, optional): Specifies if the task is translation. Default is False.
""" """
self.chunk = 1024
self.format = pyaudio.paInt16
self.channels = 1
self.rate = 16000
self.record_seconds = 60000
self.recording = False self.recording = False
self.multilingual = False
self.language = None
self.task = "transcribe" self.task = "transcribe"
self.uid = str(uuid.uuid4()) self.uid = str(uuid.uuid4())
self.waiting = False self.waiting = False
self.last_response_recieved = None self.last_response_received = None
self.disconnect_if_no_response_for = 15 self.disconnect_if_no_response_for = 15
self.multilingual = is_multilingual self.language = lang
self.language = lang if is_multilingual else "en" self.model = model
self.server_error = False
self.srt_file_path = srt_file_path
self.use_vad = use_vad
self.last_segment = None
self.last_received_segment = None
if translate: if translate:
self.task = "translate" self.task = "translate"
self.timestamp_offset = 0.0 self.timestamp_offset = 0.0
self.audio_bytes = None self.audio_bytes = None
self.p = pyaudio.PyAudio()
self.stream = self.p.open(
format=self.format,
channels=self.channels,
rate=self.rate,
input=True,
frames_per_buffer=self.chunk,
)
if host is not None and port is not None: if host is not None and port is not None:
socket_url = f"ws://{host}:{port}" socket_url = f"ws://{host}:{port}"
@@ -117,13 +85,47 @@ class Client:
self.ws_thread.setDaemon(True) self.ws_thread.setDaemon(True)
self.ws_thread.start() self.ws_thread.start()
self.frames = b"" self.transcript = []
print("[INFO]: * recording") print("[INFO]: * recording")
def handle_status_messages(self, message_data):
"""Handles server status messages."""
status = message_data["status"]
if status == "WAIT":
self.waiting = True
print(f"[INFO]: Server is full. Estimated wait time {round(message_data['message'])} minutes.")
elif status == "ERROR":
print(f"Message from Server: {message_data['message']}")
self.server_error = True
elif status == "WARNING":
print(f"Message from Server: {message_data['message']}")
def process_segments(self, segments):
"""Processes transcript segments."""
text = []
for i, seg in enumerate(segments):
if not text or text[-1] != seg["text"]:
text.append(seg["text"])
if i == len(segments) - 1:
self.last_segment = seg
elif (self.server_backend == "faster_whisper" and
(not self.transcript or
float(seg['start']) >= float(self.transcript[-1]['end']))):
self.transcript.append(seg)
# update last received segment and last valid response time
if self.last_received_segment is None or self.last_received_segment != segments[-1]["text"]:
self.last_response_received = time.time()
self.last_received_segment = segments[-1]["text"]
# Truncate to last 3 entries for brevity.
text = text[-3:]
utils.clear_screen()
utils.print_transcript(text)
def on_message(self, ws, message): def on_message(self, ws, message):
""" """
Callback function called when a message is received from the server. Callback function called when a message is received from the server.
It updates various attributes of the client based on the received message, including It updates various attributes of the client based on the received message, including
recording status, language detection, and server messages. If a disconnect message recording status, language detection, and server messages. If a disconnect message
is received, it sets the recording status to False. is received, it sets the recording status to False.
@@ -133,25 +135,25 @@ class Client:
message (str): The received message from the server. message (str): The received message from the server.
""" """
self.last_response_recieved = time.time()
message = json.loads(message) message = json.loads(message)
if self.uid != message.get("uid"): if self.uid != message.get("uid"):
print("[ERROR]: invalid client uid") print("[ERROR]: invalid client uid")
return return
if "status" in message.keys() and message["status"] == "WAIT": if "status" in message.keys():
self.waiting = True self.handle_status_messages(message)
print( return
f"[INFO]:Server is full. Estimated wait time {round(message['message'])} minutes."
)
if "message" in message.keys() and message["message"] == "DISCONNECT": if "message" in message.keys() and message["message"] == "DISCONNECT":
print("[INFO]: Server overtime disconnected.") print("[INFO]: Server disconnected due to overtime.")
self.recording = False self.recording = False
if "message" in message.keys() and message["message"] == "SERVER_READY": if "message" in message.keys() and message["message"] == "SERVER_READY":
self.last_response_received = time.time()
self.recording = True self.recording = True
self.server_backend = message["backend"]
print(f"[INFO]: Server Running with backend {self.server_backend}")
return return
if "language" in message.keys(): if "language" in message.keys():
@@ -162,78 +164,43 @@ class Client:
) )
return return
if "segments" not in message.keys(): if "segments" in message.keys():
return self.process_segments(message["segments"])
message = message["segments"]
text = []
if len(message):
for seg in message:
if text and text[-1] == seg["text"]:
# already got it
continue
text.append(seg["text"])
# keep only last 3
if len(text) > 3:
text = text[-3:]
wrapper = textwrap.TextWrapper(width=60)
word_list = wrapper.wrap(text="".join(text))
# Print each line.
if os.name == "nt":
os.system("cls")
else:
os.system("clear")
for element in word_list:
print(element)
def on_error(self, ws, error): def on_error(self, ws, error):
print(error) print(f"[ERROR] WebSocket Error: {error}")
self.server_error = True
self.error_message = error
def on_close(self, ws, close_status_code, close_msg): def on_close(self, ws, close_status_code, close_msg):
print(f"[INFO]: Websocket connection closed: {close_status_code}: {close_msg}") print(f"[INFO]: Websocket connection closed: {close_status_code}: {close_msg}")
self.recording = False
self.waiting = False
def on_open(self, ws): def on_open(self, ws):
""" """
Callback function called when the WebSocket connection is successfully opened. Callback function called when the WebSocket connection is successfully opened.
Sends an initial configuration message to the server, including client UID, multilingual mode, Sends an initial configuration message to the server, including client UID,
language selection, and task type. language selection, and task type.
Args: Args:
ws (websocket.WebSocketApp): The WebSocket client instance. ws (websocket.WebSocketApp): The WebSocket client instance.
""" """
print(self.multilingual, self.language, self.task)
print("[INFO]: Opened connection") print("[INFO]: Opened connection")
ws.send( ws.send(
json.dumps( json.dumps(
{ {
"uid": self.uid, "uid": self.uid,
"multilingual": self.multilingual,
"language": self.language, "language": self.language,
"task": self.task, "task": self.task,
"model": self.model,
"use_vad": self.use_vad
} }
) )
) )
@staticmethod
def bytes_to_float_array(audio_bytes):
"""
Convert audio data from bytes to a NumPy float array.
It assumes that the audio data is in 16-bit PCM format. The audio data is normalized to
have values between -1 and 1.
Args:
audio_bytes (bytes): Audio data in bytes.
Returns:
np.ndarray: A NumPy array containing the audio data as float values normalized between -1 and 1.
"""
raw_data = np.frombuffer(buffer=audio_bytes, dtype=np.int16)
return raw_data.astype(np.float32) / 32768.0
def send_packet_to_server(self, message): def send_packet_to_server(self, message):
""" """
Send an audio packet to the server using WebSocket. Send an audio packet to the server using WebSocket.
@@ -247,62 +214,11 @@ class Client:
except Exception as e: except Exception as e:
print(e) print(e)
def play_file(self, filename):
"""
Play an audio file and send it to the server for processing.
Reads an audio file, plays it through the audio output, and simultaneously sends
the audio data to the server for processing. It uses PyAudio to create an audio
stream for playback. The audio data is read from the file in chunks, converted to
floating-point format, and sent to the server using WebSocket communication.
This method is typically used when you want to process pre-recorded audio and send it
to the server in real-time.
Args:
filename (str): The path to the audio file to be played and sent to the server.
"""
# read audio and create pyaudio stream
with wave.open(filename, "rb") as wavfile:
self.stream = self.p.open(
format=self.p.get_format_from_width(wavfile.getsampwidth()),
channels=wavfile.getnchannels(),
rate=wavfile.getframerate(),
input=True,
output=True,
frames_per_buffer=self.chunk,
)
try:
while self.recording:
data = wavfile.readframes(self.chunk)
if data == b"":
break
audio_array = self.bytes_to_float_array(data)
self.send_packet_to_server(audio_array.tobytes())
self.stream.write(data)
wavfile.close()
assert self.last_response_recieved
while time.time() - self.last_response_recieved < self.disconnect_if_no_response_for:
continue
self.stream.close()
self.close_websocket()
except KeyboardInterrupt:
wavfile.close()
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
self.close_websocket()
print("[INFO]: Keyboard interrupt.")
def close_websocket(self): def close_websocket(self):
""" """
Close the WebSocket connection and join the WebSocket thread. Close the WebSocket connection and join the WebSocket thread.
First attempts to close the WebSocket connection using `self.client_socket.close()`. After First attempts to close the WebSocket connection using `self.client_socket.close()`. After
closing the connection, it joins the WebSocket thread to ensure proper termination. closing the connection, it joins the WebSocket thread to ensure proper termination.
""" """
@@ -325,11 +241,323 @@ class Client:
""" """
return self.client_socket return self.client_socket
def write_srt_file(self, output_path="output.srt"):
"""
Writes out the transcript in .srt format.
Args:
message (output_path, optional): The path to the target file. Default is "output.srt".
"""
if self.server_backend == "faster_whisper":
if (self.last_segment):
self.transcript.append(self.last_segment)
utils.create_srt_file(self.transcript, output_path)
def wait_before_disconnect(self):
"""Waits a bit before disconnecting in order to process pending responses."""
assert self.last_response_received
while time.time() - self.last_response_received < self.disconnect_if_no_response_for:
continue
class TranscriptionTeeClient:
"""
Client for handling audio recording, streaming, and transcription tasks via one or more
WebSocket connections.
Acts as a high-level client for audio transcription tasks using a WebSocket connection. It can be used
to send audio data for transcription to one or more servers, and receive transcribed text segments.
Args:
clients (list): one or more previously initialized Client instances
Attributes:
clients (list): the underlying Client instances responsible for handling WebSocket connections.
"""
def __init__(self, clients, save_output_recording=False, output_recording_filename="./output_recording.wav"):
self.clients = clients
if not self.clients:
raise Exception("At least one client is required.")
self.chunk = 4096
self.format = pyaudio.paInt16
self.channels = 1
self.rate = 16000
self.record_seconds = 60000
self.save_output_recording = save_output_recording
self.output_recording_filename = output_recording_filename
self.frames = b""
self.p = pyaudio.PyAudio()
try:
self.stream = self.p.open(
format=self.format,
channels=self.channels,
rate=self.rate,
input=True,
frames_per_buffer=self.chunk,
)
except OSError as error:
print(f"[WARN]: Unable to access microphone. {error}")
self.stream = None
def __call__(self, audio=None, rtsp_url=None, hls_url=None, save_file=None):
"""
Start the transcription process.
Initiates the transcription process by connecting to the server via a WebSocket. It waits for the server
to be ready to receive audio data and then sends audio for transcription. If an audio file is provided, it
will be played and streamed to the server; otherwise, it will perform live recording.
Args:
audio (str, optional): Path to an audio file for transcription. Default is None, which triggers live recording.
"""
assert sum(
source is not None for source in [audio, rtsp_url, hls_url]
) <= 1, 'You must provide only one selected source'
print("[INFO]: Waiting for server ready ...")
for client in self.clients:
while not client.recording:
if client.waiting or client.server_error:
self.close_all_clients()
return
print("[INFO]: Server Ready!")
if hls_url is not None:
self.process_hls_stream(hls_url, save_file)
elif audio is not None:
resampled_file = utils.resample(audio)
self.play_file(resampled_file)
elif rtsp_url is not None:
self.process_rtsp_stream(rtsp_url)
else:
self.record()
def close_all_clients(self):
"""Closes all client websockets."""
for client in self.clients:
client.close_websocket()
def write_all_clients_srt(self):
"""Writes out .srt files for all clients."""
for client in self.clients:
client.write_srt_file(client.srt_file_path)
def multicast_packet(self, packet, unconditional=False):
"""
Sends an identical packet via all clients.
Args:
packet (bytes): The audio data packet in bytes to be sent.
unconditional (bool, optional): If true, send regardless of whether clients are recording. Default is False.
"""
for client in self.clients:
if (unconditional or client.recording):
client.send_packet_to_server(packet)
def play_file(self, filename):
"""
Play an audio file and send it to the server for processing.
Reads an audio file, plays it through the audio output, and simultaneously sends
the audio data to the server for processing. It uses PyAudio to create an audio
stream for playback. The audio data is read from the file in chunks, converted to
floating-point format, and sent to the server using WebSocket communication.
This method is typically used when you want to process pre-recorded audio and send it
to the server in real-time.
Args:
filename (str): The path to the audio file to be played and sent to the server.
"""
# read audio and create pyaudio stream
with wave.open(filename, "rb") as wavfile:
self.stream = self.p.open(
format=self.p.get_format_from_width(wavfile.getsampwidth()),
channels=wavfile.getnchannels(),
rate=wavfile.getframerate(),
input=True,
output=True,
frames_per_buffer=self.chunk,
)
try:
while any(client.recording for client in self.clients):
data = wavfile.readframes(self.chunk)
if data == b"":
break
audio_array = self.bytes_to_float_array(data)
self.multicast_packet(audio_array.tobytes())
self.stream.write(data)
wavfile.close()
for client in self.clients:
client.wait_before_disconnect()
self.multicast_packet(Client.END_OF_AUDIO.encode('utf-8'), True)
self.write_all_clients_srt()
self.stream.close()
self.close_all_clients()
except KeyboardInterrupt:
wavfile.close()
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
self.close_all_clients()
self.write_all_clients_srt()
print("[INFO]: Keyboard interrupt.")
def process_rtsp_stream(self, rtsp_url):
"""
Connect to an RTSP source, process the audio stream, and send it for trascription.
Args:
rtsp_url (str): The URL of the RTSP stream source.
"""
process = self.get_rtsp_ffmpeg_process(rtsp_url)
self.handle_ffmpeg_process(process, stream_type='RTSP')
def process_hls_stream(self, hls_url, save_file):
"""
Connect to an HLS source, process the audio stream, and send it for transcription.
Args:
hls_url (str): The URL of the HLS stream source.
save_file str, optional): Local path to save the network stream.
"""
process = self.get_hls_ffmpeg_process(hls_url, save_file)
self.handle_ffmpeg_process(process, stream_type='HLS')
def handle_ffmpeg_process(self, process, stream_type):
print(f"[INFO]: Connecting to {stream_type} stream...")
try:
# Process the stream
while True:
in_bytes = process.stdout.read(self.chunk * 2) # 2 bytes per sample
if not in_bytes:
break
audio_array = self.bytes_to_float_array(in_bytes)
self.multicast_packet(audio_array.tobytes())
except Exception as e:
print(f"[ERROR]: Failed to connect to {stream_type} stream: {e}")
finally:
self.close_all_clients()
self.write_all_clients_srt()
if process:
process.kill()
print(f"[INFO]: {stream_type} stream processing finished.")
def get_rtsp_ffmpeg_process(self, rtsp_url):
return (
ffmpeg
.input(rtsp_url, threads=0)
.output('-', format='s16le', acodec='pcm_s16le', ac=1, ar=self.rate)
.run_async(pipe_stdout=True, pipe_stderr=True)
)
def get_hls_ffmpeg_process(self, hls_url, save_file):
if save_file is None:
process = (
ffmpeg
.input(hls_url, threads=0)
.output('-', format='s16le', acodec='pcm_s16le', ac=1, ar=self.rate)
.run_async(pipe_stdout=True, pipe_stderr=True)
)
else:
input = ffmpeg.input(hls_url, threads=0)
output_file = input.output(save_file, acodec='copy', vcodec='copy').global_args('-loglevel', 'quiet')
output_std = input.output('-', format='s16le', acodec='pcm_s16le', ac=1, ar=self.rate)
process = (
ffmpeg.merge_outputs(output_file, output_std)
.run_async(pipe_stdout=True, pipe_stderr=True)
)
return process
def save_chunk(self, n_audio_file):
"""
Saves the current audio frames to a WAV file in a separate thread.
Args:
n_audio_file (int): The index of the audio file which determines the filename.
This helps in maintaining the order and uniqueness of each chunk.
"""
t = threading.Thread(
target=self.write_audio_frames_to_file,
args=(self.frames[:], f"chunks/{n_audio_file}.wav",),
)
t.start()
def finalize_recording(self, n_audio_file):
"""
Finalizes the recording process by saving any remaining audio frames,
closing the audio stream, and terminating the process.
Args:
n_audio_file (int): The file index to be used if there are remaining audio frames to be saved.
This index is incremented before use if the last chunk is saved.
"""
if self.save_output_recording and len(self.frames):
self.write_audio_frames_to_file(
self.frames[:], f"chunks/{n_audio_file}.wav"
)
n_audio_file += 1
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
self.close_all_clients()
if self.save_output_recording:
self.write_output_recording(n_audio_file)
self.write_all_clients_srt()
def record(self):
"""
Record audio data from the input stream and save it to a WAV file.
Continuously records audio data from the input stream, sends it to the server via a WebSocket
connection, and simultaneously saves it to multiple WAV files in chunks. It stops recording when
the `RECORD_SECONDS` duration is reached or when the `RECORDING` flag is set to `False`.
Audio data is saved in chunks to the "chunks" directory. Each chunk is saved as a separate WAV file.
The recording will continue until the specified duration is reached or until the `RECORDING` flag is set to `False`.
The recording process can be interrupted by sending a KeyboardInterrupt (e.g., pressing Ctrl+C). After recording,
the method combines all the saved audio chunks into the specified `out_file`.
"""
n_audio_file = 0
if self.save_output_recording:
if os.path.exists("chunks"):
shutil.rmtree("chunks")
os.makedirs("chunks")
try:
for _ in range(0, int(self.rate / self.chunk * self.record_seconds)):
if not any(client.recording for client in self.clients):
break
data = self.stream.read(self.chunk, exception_on_overflow=False)
self.frames += data
audio_array = self.bytes_to_float_array(data)
self.multicast_packet(audio_array.tobytes())
# save frames if more than a minute
if len(self.frames) > 60 * self.rate:
if self.save_output_recording:
self.save_chunk(n_audio_file)
n_audio_file += 1
self.frames = b""
self.write_all_clients_srt()
except KeyboardInterrupt:
self.finalize_recording(n_audio_file)
def write_audio_frames_to_file(self, frames, file_name): def write_audio_frames_to_file(self, frames, file_name):
""" """
Write audio frames to a WAV file. Write audio frames to a WAV file.
The WAV file is created or overwritten with the specified name. The audio frames should be The WAV file is created or overwritten with the specified name. The audio frames should be
in the correct format and match the specified channel, sample width, and sample rate. in the correct format and match the specified channel, sample width, and sample rate.
Args: Args:
@@ -344,68 +572,11 @@ class Client:
wavfile.setframerate(self.rate) wavfile.setframerate(self.rate)
wavfile.writeframes(frames) wavfile.writeframes(frames)
def record(self, out_file="output_recording.wav"): def write_output_recording(self, n_audio_file):
"""
Record audio data from the input stream and save it to a WAV file.
Continuously records audio data from the input stream, sends it to the server via a WebSocket
connection, and simultaneously saves it to multiple WAV files in chunks. It stops recording when
the `RECORD_SECONDS` duration is reached or when the `RECORDING` flag is set to `False`.
Audio data is saved in chunks to the "chunks" directory. Each chunk is saved as a separate WAV file.
The recording will continue until the specified duration is reached or until the `RECORDING` flag is set to `False`.
The recording process can be interrupted by sending a KeyboardInterrupt (e.g., pressing Ctrl+C). After recording,
the method combines all the saved audio chunks into the specified `out_file`.
Args:
out_file (str, optional): The name of the output WAV file to save the entire recording. Default is "output_recording.wav".
"""
n_audio_file = 0
if not os.path.exists("chunks"):
os.makedirs("chunks", exist_ok=True)
try:
for _ in range(0, int(self.rate / self.chunk * self.record_seconds)):
if not self.recording:
break
data = self.stream.read(self.chunk)
self.frames += data
audio_array = Client.bytes_to_float_array(data)
self.send_packet_to_server(audio_array.tobytes())
# save frames if more than a minute
if len(self.frames) > 60 * self.rate:
t = threading.Thread(
target=self.write_audio_frames_to_file,
args=(
self.frames[:],
f"chunks/{n_audio_file}.wav",
),
)
t.start()
n_audio_file += 1
self.frames = b""
except KeyboardInterrupt:
if len(self.frames):
self.write_audio_frames_to_file(
self.frames[:], f"chunks/{n_audio_file}.wav"
)
n_audio_file += 1
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
self.close_websocket()
self.write_output_recording(n_audio_file, out_file)
def write_output_recording(self, n_audio_file, out_file):
""" """
Combine and save recorded audio chunks into a single WAV file. Combine and save recorded audio chunks into a single WAV file.
The individual audio chunk files are expected to be located in the "chunks" directory. Reads each chunk The individual audio chunk files are expected to be located in the "chunks" directory. Reads each chunk
file, appends its audio data to the final recording, and then deletes the chunk file. After combining file, appends its audio data to the final recording, and then deletes the chunk file. After combining
and saving, the final recording is stored in the specified `out_file`. and saving, the final recording is stored in the specified `out_file`.
@@ -420,7 +591,7 @@ class Client:
for i in range(n_audio_file) for i in range(n_audio_file)
if os.path.exists(f"chunks/{i}.wav") if os.path.exists(f"chunks/{i}.wav")
] ]
with wave.open(out_file, "wb") as wavfile: with wave.open(self.output_recording_filename, "wb") as wavfile:
wavfile: wave.Wave_write wavfile: wave.Wave_write
wavfile.setnchannels(self.channels) wavfile.setnchannels(self.channels)
wavfile.setsampwidth(2) wavfile.setsampwidth(2)
@@ -435,11 +606,31 @@ class Client:
# remove this file # remove this file
os.remove(in_file) os.remove(in_file)
wavfile.close() wavfile.close()
# clean up temporary directory to store chunks
if os.path.exists("chunks"):
shutil.rmtree("chunks")
@staticmethod
def bytes_to_float_array(audio_bytes):
"""
Convert audio data from bytes to a NumPy float array.
It assumes that the audio data is in 16-bit PCM format. The audio data is normalized to
have values between -1 and 1.
Args:
audio_bytes (bytes): Audio data in bytes.
Returns:
np.ndarray: A NumPy array containing the audio data as float values normalized between -1 and 1.
"""
raw_data = np.frombuffer(buffer=audio_bytes, dtype=np.int16)
return raw_data.astype(np.float32) / 32768.0
class TranscriptionClient: class TranscriptionClient(TranscriptionTeeClient):
""" """
Client for handling audio transcription tasks via a WebSocket connection. Client for handling audio transcription tasks via a single WebSocket connection.
Acts as a high-level client for audio transcription tasks using a WebSocket connection. It can be used Acts as a high-level client for audio transcription tasks using a WebSocket connection. It can be used
to send audio data for transcription to a server and receive transcribed text segments. to send audio data for transcription to a server and receive transcribed text segments.
@@ -447,9 +638,11 @@ class TranscriptionClient:
Args: Args:
host (str): The hostname or IP address of the server. host (str): The hostname or IP address of the server.
port (int): The port number to connect to on the server. port (int): The port number to connect to on the server.
is_multilingual (bool, optional): Indicates whether the transcription should support multiple languages (default is False). lang (str, optional): The primary language for transcription. Default is None, which defaults to English ('en').
lang (str, optional): The primary language for transcription (used if `is_multilingual` is False). Default is None, which defaults to English ('en').
translate (bool, optional): Indicates whether translation tasks are required (default is False). translate (bool, optional): Indicates whether translation tasks are required (default is False).
save_output_recording (bool, optional): Indicates whether to save recording from microphone.
output_recording_filename (str, optional): File to save the output recording.
output_transcription_path (str, optional): File to save the output transcription.
Attributes: Attributes:
client (Client): An instance of the underlying Client class responsible for handling the WebSocket connection. client (Client): An instance of the underlying Client class responsible for handling the WebSocket connection.
@@ -457,34 +650,30 @@ class TranscriptionClient:
Example: Example:
To create a TranscriptionClient and start transcription on microphone audio: To create a TranscriptionClient and start transcription on microphone audio:
```python ```python
transcription_client = TranscriptionClient(host="localhost", port=9090, is_multilingual=True) transcription_client = TranscriptionClient(host="localhost", port=9090)
transcription_client() transcription_client()
``` ```
""" """
def __init__(self, host, port, is_multilingual=False, lang=None, translate=False): def __init__(
self.client = Client(host, port, is_multilingual, lang, translate) self,
host,
def __call__(self, audio=None): port,
""" lang=None,
Start the transcription process. translate=False,
model="small",
Initiates the transcription process by connecting to the server via a WebSocket. It waits for the server use_vad=True,
to be ready to receive audio data and then sends audio for transcription. If an audio file is provided, it save_output_recording=False,
will be played and streamed to the server; otherwise, it will perform live recording. output_recording_filename="./output_recording.wav",
output_transcription_path="./output.srt"
Args: ):
audio (str, optional): Path to an audio file for transcription. Default is None, which triggers live recording. self.client = Client(host, port, lang, translate, model, srt_file_path=output_transcription_path, use_vad=use_vad)
if save_output_recording and not output_recording_filename.endswith(".wav"):
""" raise ValueError(f"Please provide a valid `output_recording_filename`: {output_recording_filename}")
print("[INFO]: Waiting for server ready ...") if not output_transcription_path.endswith(".srt"):
while not self.client.recording: raise ValueError(f"Please provide a valid `output_transcription_path`: {output_transcription_path}. The file extension should be `.srt`.")
if self.client.waiting: TranscriptionTeeClient.__init__(
self.client.close_websocket() self,
return [self.client],
pass save_output_recording=save_output_recording,
print("[INFO]: Server Ready!") output_recording_filename=output_recording_filename
if audio is not None: )
resampled_file = resample(audio)
self.client.play_file(resampled_file)
else:
self.client.record()
+949 -362
View File
File diff suppressed because it is too large Load Diff
+365
View File
@@ -0,0 +1,365 @@
# 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 torch
import torch.nn.functional as F
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 = SAMPLE_RATE):
"""
Open an audio file and read as mono waveform, resampling as necessary
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.
"""
# This launches a subprocess to decode audio while down-mixing
# and resampling as necessary. Requires the ffmpeg CLI in PATH.
# fmt: off
cmd = [
"ffmpeg", "-nostdin", "-threads", "0", "-i", file, "-f", "s16le", "-ac",
"1", "-acodec", "pcm_s16le", "-ar",
str(sr), "-"
]
# fmt: on
try:
out = run(cmd, capture_output=True, check=True).stdout
except CalledProcessError as e:
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
return np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
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)
+215 -31
View File
@@ -1,20 +1,22 @@
# original https://github.com/guillaumekln/faster-whisper/blob/master/faster_whisper/transcribe.py # original https://github.com/guillaumekln/faster-whisper/blob/master/faster_whisper/transcribe.py
import itertools import itertools
import json
import logging import logging
import os import os
import zlib import zlib
from inspect import signature
from typing import BinaryIO, Iterable, List, NamedTuple, Optional, Tuple, Union from typing import BinaryIO, Iterable, List, NamedTuple, Optional, Tuple, Union
import ctranslate2 import ctranslate2
import numpy as np import numpy as np
import tokenizers import tokenizers
from faster_whisper.audio import decode_audio from faster_whisper.audio import decode_audio, pad_or_trim
from faster_whisper.feature_extractor import FeatureExtractor from faster_whisper.feature_extractor import FeatureExtractor
from faster_whisper.tokenizer import _LANGUAGE_CODES, Tokenizer from faster_whisper.tokenizer import _LANGUAGE_CODES, Tokenizer
from faster_whisper.utils import download_model, format_timestamp, get_logger from faster_whisper.utils import download_model, format_timestamp, get_end, get_logger
from faster_whisper.vad import ( from faster_whisper.vad import (
SpeechTimestampsMap, SpeechTimestampsMap,
VadOptions, VadOptions,
@@ -66,6 +68,9 @@ class TranscriptionOptions(NamedTuple):
word_timestamps: bool word_timestamps: bool
prepend_punctuations: str prepend_punctuations: str
append_punctuations: str append_punctuations: str
max_new_tokens: Optional[int]
clip_timestamps: Union[str, List[float]]
hallucination_silence_threshold: Optional[float]
class TranscriptionInfo(NamedTuple): class TranscriptionInfo(NamedTuple):
@@ -94,8 +99,8 @@ class WhisperModel:
Args: Args:
model_size_or_path: Size of the model to use (tiny, tiny.en, base, base.en, model_size_or_path: Size of the model to use (tiny, tiny.en, base, base.en,
small, small.en, medium, medium.en, large-v1, large-v2, or large), a path to a converted small, small.en, medium, medium.en, large-v1, large-v2, large-v3, or large), a path to a
model directory, or a CTranslate2-converted Whisper model ID from the Hugging Face Hub. converted model directory, or a CTranslate2-converted Whisper model ID from the HF Hub.
When a size or a model ID is configured, the converted model is downloaded When a size or a model ID is configured, the converted model is downloaded
from the Hugging Face Hub. from the Hugging Face Hub.
device: Device to use for computation ("cpu", "cuda", "auto"). device: Device to use for computation ("cpu", "cuda", "auto").
@@ -144,7 +149,8 @@ class WhisperModel:
"openai/whisper-tiny" + ("" if self.model.is_multilingual else ".en") "openai/whisper-tiny" + ("" if self.model.is_multilingual else ".en")
) )
self.feature_extractor = FeatureExtractor() self.feat_kwargs = self._get_feature_kwargs(model_path)
self.feature_extractor = FeatureExtractor(**self.feat_kwargs)
self.num_samples_per_token = self.feature_extractor.hop_length * 2 self.num_samples_per_token = self.feature_extractor.hop_length * 2
self.frames_per_second = ( self.frames_per_second = (
self.feature_extractor.sampling_rate // self.feature_extractor.hop_length self.feature_extractor.sampling_rate // self.feature_extractor.hop_length
@@ -161,7 +167,23 @@ class WhisperModel:
"""The languages supported by the model.""" """The languages supported by the model."""
return list(_LANGUAGE_CODES) if self.model.is_multilingual else ["en"] return list(_LANGUAGE_CODES) if self.model.is_multilingual else ["en"]
def transcribe( def _get_feature_kwargs(self, model_path) -> dict:
preprocessor_config_file = os.path.join(model_path, "preprocessor_config.json")
config = {}
if os.path.isfile(preprocessor_config_file):
try:
with open(preprocessor_config_file, "r", encoding="utf-8") as json_file:
config = json.load(json_file)
valid_keys = signature(FeatureExtractor.__init__).parameters.keys()
config = {k: v for k, v in config.items() if k in valid_keys}
except json.JSONDecodeError as e:
self.logger.warning(
"Could not load preprocessor_config.json: %s", str(e)
)
return config
def transcribe( # noqa: C901
self, self,
audio: Union[str, BinaryIO, np.ndarray], audio: Union[str, BinaryIO, np.ndarray],
language: Optional[str] = None, language: Optional[str] = None,
@@ -196,6 +218,10 @@ class WhisperModel:
append_punctuations: str = "\"'.。,!?::”)]}、", append_punctuations: str = "\"'.。,!?::”)]}、",
vad_filter: bool = False, vad_filter: bool = False,
vad_parameters: Optional[Union[dict, VadOptions]] = None, vad_parameters: Optional[Union[dict, VadOptions]] = None,
max_new_tokens: Optional[int] = None,
chunk_length: Optional[int] = None,
clip_timestamps: Union[str, List[float]] = "0",
hallucination_silence_threshold: Optional[float] = None,
) -> Tuple[Iterable[Segment], TranscriptionInfo]: ) -> Tuple[Iterable[Segment], TranscriptionInfo]:
"""Transcribes an input file. """Transcribes an input file.
@@ -247,6 +273,16 @@ class WhisperModel:
https://github.com/snakers4/silero-vad. https://github.com/snakers4/silero-vad.
vad_parameters: Dictionary of Silero VAD parameters or VadOptions class (see available vad_parameters: Dictionary of Silero VAD parameters or VadOptions class (see available
parameters and default values in the class `VadOptions`). parameters and default values in the class `VadOptions`).
max_new_tokens: Maximum number of new tokens to generate per-chunk. If not set,
the maximum will be set by the default max_length.
chunk_length: The length of audio segments. If it is not None, it will overwrite the
default chunk_length of the FeatureExtractor.
clip_timestamps: Union[str, List[float]]
Comma-separated list start,end,start,end,... timestamps (in seconds) of clips to
process. The last end timestamp defaults to the end of the file.
hallucination_silence_threshold: Optional[float]
When word_timestamps is True, skip silent periods longer than this threshold
(in seconds) when a possible hallucination is detected
Returns: Returns:
A tuple with: A tuple with:
@@ -296,7 +332,10 @@ class WhisperModel:
else: else:
speech_chunks = None speech_chunks = None
features = self.feature_extractor(audio) if audio.shape[0] == 0:
return None, None
features = self.feature_extractor(audio, chunk_length=chunk_length)
encoder_output = None encoder_output = None
all_language_probs = None all_language_probs = None
@@ -362,6 +401,9 @@ class WhisperModel:
word_timestamps=word_timestamps, word_timestamps=word_timestamps,
prepend_punctuations=prepend_punctuations, prepend_punctuations=prepend_punctuations,
append_punctuations=append_punctuations, append_punctuations=append_punctuations,
max_new_tokens=max_new_tokens,
clip_timestamps=clip_timestamps,
hallucination_silence_threshold=hallucination_silence_threshold,
) )
segments = self.generate_segments(features, tokenizer, options, encoder_output) segments = self.generate_segments(features, tokenizer, options, encoder_output)
@@ -389,8 +431,33 @@ class WhisperModel:
encoder_output: Optional[ctranslate2.StorageView] = None, encoder_output: Optional[ctranslate2.StorageView] = None,
) -> Iterable[Segment]: ) -> Iterable[Segment]:
content_frames = features.shape[-1] - self.feature_extractor.nb_max_frames content_frames = features.shape[-1] - self.feature_extractor.nb_max_frames
content_duration = float(content_frames * self.feature_extractor.time_per_frame)
if isinstance(options.clip_timestamps, str):
TranscriptionOptions.clip_timestamps = [
float(ts)
for ts in (
options.clip_timestamps.split(",")
if options.clip_timestamps
else []
)
]
seek_points: List[int] = [
round(ts * self.frames_per_second) for ts in options.clip_timestamps
]
if len(seek_points) == 0:
seek_points.append(0)
if len(seek_points) % 2 == 1:
seek_points.append(content_frames)
seek_clips: List[Tuple[int, int]] = list(
zip(seek_points[::2], seek_points[1::2])
)
punctuation = "\"'“¿([{-\"'.。,!?::”)]}、"
idx = 0 idx = 0
seek = 0 clip_idx = 0
seek = seek_clips[clip_idx][0]
all_tokens = [] all_tokens = []
prompt_reset_since = 0 prompt_reset_since = 0
@@ -404,13 +471,34 @@ class WhisperModel:
last_speech_timestamp = 0.0 last_speech_timestamp = 0.0
all_segments = [] all_segments = []
while seek < content_frames: # NOTE: This loop is obscurely flattened to make the diff readable.
# A later commit should turn this into a simpler nested loop.
# for seek_clip_start, seek_clip_end in seek_clips:
# while seek < seek_clip_end
while clip_idx < len(seek_clips):
seek_clip_start, seek_clip_end = seek_clips[clip_idx]
if seek_clip_end > content_frames:
seek_clip_end = content_frames
if seek < seek_clip_start:
seek = seek_clip_start
if seek >= seek_clip_end:
clip_idx += 1
if clip_idx < len(seek_clips):
seek = seek_clips[clip_idx][0]
continue
time_offset = seek * self.feature_extractor.time_per_frame time_offset = seek * self.feature_extractor.time_per_frame
segment = features[:, seek : seek + self.feature_extractor.nb_max_frames] window_end_time = float(
segment_size = min( (seek + self.feature_extractor.nb_max_frames)
self.feature_extractor.nb_max_frames, content_frames - seek * self.feature_extractor.time_per_frame
) )
segment_size = min(
self.feature_extractor.nb_max_frames,
content_frames - seek,
seek_clip_end - seek,
)
segment = features[:, seek:seek + segment_size]
segment_duration = segment_size * self.feature_extractor.time_per_frame segment_duration = segment_size * self.feature_extractor.time_per_frame
segment = pad_or_trim(segment, self.feature_extractor.nb_max_frames)
if self.logger.isEnabledFor(logging.DEBUG): if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug( self.logger.debug(
@@ -462,10 +550,33 @@ class WhisperModel:
previous_seek = seek previous_seek = seek
current_segments = [] current_segments = []
# anomalous words are very long/short/improbable
def word_anomaly_score(word: dict) -> float:
probability = word.get("probability", 0.0)
duration = word["end"] - word["start"]
score = 0.0
if probability < 0.15:
score += 1.0
if duration < 0.133:
score += (0.133 - duration) * 15
if duration > 2.0:
score += duration - 2.0
return score
def is_segment_anomaly(segment: Optional[dict]) -> bool:
if segment is None or not segment["words"]:
return False
words = [w for w in segment["words"] if w["word"] not in punctuation]
words = words[:8]
score = sum(word_anomaly_score(w) for w in words)
return score >= 3 or score + 0.01 >= len(words)
def next_words_segment(segments: List[dict]) -> Optional[dict]:
return next((s for s in segments if s["words"]), None)
single_timestamp_ending = ( single_timestamp_ending = (
len(tokens) >= 2 len(tokens) >= 2
and tokens[-2] < tokenizer.timestamp_begin and tokens[-2] < tokenizer.timestamp_begin <= tokens[-1]
and tokens[-1] >= tokenizer.timestamp_begin
) )
consecutive_timestamps = [ consecutive_timestamps = [
@@ -548,18 +659,62 @@ class WhisperModel:
last_speech_timestamp=last_speech_timestamp, last_speech_timestamp=last_speech_timestamp,
) )
word_end_timestamps = [ if not single_timestamp_ending:
w["end"] for s in current_segments for w in s["words"] last_word_end = get_end(current_segments)
] if last_word_end is not None and last_word_end > time_offset:
if len(word_end_timestamps) > 0: seek = round(last_word_end * self.frames_per_second)
last_speech_timestamp = word_end_timestamps[-1]
if not single_timestamp_ending and len(word_end_timestamps) > 0:
seek_shift = round(
(word_end_timestamps[-1] - time_offset) * self.frames_per_second
)
if seek_shift > 0: # skip silence before possible hallucinations
seek = previous_seek + seek_shift if options.hallucination_silence_threshold is not None:
threshold = options.hallucination_silence_threshold
# if first segment might be a hallucination, skip leading silence
first_segment = next_words_segment(current_segments)
if first_segment is not None and is_segment_anomaly(first_segment):
gap = first_segment["start"] - time_offset
if gap > threshold:
seek = previous_seek + round(gap * self.frames_per_second)
continue
# skip silence before any possible hallucination that is surrounded
# by silence or more hallucinations
hal_last_end = last_speech_timestamp
for si in range(len(current_segments)):
segment = current_segments[si]
if not segment["words"]:
continue
if is_segment_anomaly(segment):
next_segment = next_words_segment(
current_segments[si + 1:]
)
if next_segment is not None:
hal_next_start = next_segment["words"][0]["start"]
else:
hal_next_start = time_offset + segment_duration
silence_before = (
segment["start"] - hal_last_end > threshold
or segment["start"] < threshold
or segment["start"] - time_offset < 2.0
)
silence_after = (
hal_next_start - segment["end"] > threshold
or is_segment_anomaly(next_segment)
or window_end_time - segment["end"] < 2.0
)
if silence_before and silence_after:
seek = round(
max(time_offset + 1, segment["start"])
* self.frames_per_second
)
if content_duration - segment["end"] < threshold:
seek = content_frames
current_segments[si:] = []
break
hal_last_end = segment["end"]
last_word_end = get_end(current_segments)
if last_word_end is not None:
last_speech_timestamp = last_word_end
for segment in current_segments: for segment in current_segments:
tokens = segment["tokens"] tokens = segment["tokens"]
@@ -586,7 +741,7 @@ class WhisperModel:
[Word(**word) for word in segment["words"]] [Word(**word) for word in segment["words"]]
if options.word_timestamps if options.word_timestamps
else None else None
), ),
)) ))
if ( if (
@@ -627,6 +782,21 @@ class WhisperModel:
max_initial_timestamp_index = int( max_initial_timestamp_index = int(
round(options.max_initial_timestamp / self.time_precision) round(options.max_initial_timestamp / self.time_precision)
) )
if options.max_new_tokens is not None:
max_length = len(prompt) + options.max_new_tokens
else:
max_length = self.max_length
if max_length > self.max_length:
raise ValueError(
f"The length of the prompt is {len(prompt)}, and the `max_new_tokens` "
f"{max_length - len(prompt)}. Thus, the combined length of the prompt "
f"and `max_new_tokens` is: {max_length}. This exceeds the "
f"`max_length` of the Whisper model: {self.max_length}. "
"You should either reduce the length of your prompt, or "
"reduce the value of `max_new_tokens`, "
f"so that their combined length is less that {self.max_length}."
)
for temperature in options.temperatures: for temperature in options.temperatures:
if temperature > 0: if temperature > 0:
@@ -648,7 +818,7 @@ class WhisperModel:
length_penalty=options.length_penalty, length_penalty=options.length_penalty,
repetition_penalty=options.repetition_penalty, repetition_penalty=options.repetition_penalty,
no_repeat_ngram_size=options.no_repeat_ngram_size, no_repeat_ngram_size=options.no_repeat_ngram_size,
max_length=self.max_length, max_length=max_length,
return_scores=True, return_scores=True,
return_no_speech_prob=True, return_no_speech_prob=True,
suppress_blank=options.suppress_blank, suppress_blank=options.suppress_blank,
@@ -706,6 +876,8 @@ class WhisperModel:
if ( if (
options.no_speech_threshold is not None options.no_speech_threshold is not None
and result.no_speech_prob > options.no_speech_threshold and result.no_speech_prob > options.no_speech_threshold
and options.log_prob_threshold is not None
and avg_logprob < options.log_prob_threshold
): ):
needs_fallback = False # silence needs_fallback = False # silence
@@ -716,6 +888,13 @@ class WhisperModel:
decode_result = max( decode_result = max(
below_cr_threshold_results or all_results, key=lambda x: x[1] below_cr_threshold_results or all_results, key=lambda x: x[1]
) )
# to pass final temperature for prompt_reset_on_temperature
decode_result = (
decode_result[0],
decode_result[1],
temperature,
decode_result[3],
)
return decode_result return decode_result
@@ -730,7 +909,7 @@ class WhisperModel:
if previous_tokens: if previous_tokens:
prompt.append(tokenizer.sot_prev) prompt.append(tokenizer.sot_prev)
prompt.extend(previous_tokens[-(self.max_length // 2 - 1) :]) prompt.extend(previous_tokens[-(self.max_length // 2 - 1):])
prompt.extend(tokenizer.sot_sequence) prompt.extend(tokenizer.sot_sequence)
@@ -772,6 +951,7 @@ class WhisperModel:
word_durations = np.array([word["end"] - word["start"] for word in alignment]) word_durations = np.array([word["end"] - word["start"] for word in alignment])
word_durations = word_durations[word_durations.nonzero()] word_durations = word_durations[word_durations.nonzero()]
median_duration = np.median(word_durations) if len(word_durations) > 0 else 0.0 median_duration = np.median(word_durations) if len(word_durations) > 0 else 0.0
median_duration = min(0.7, float(median_duration))
max_duration = median_duration * 2 max_duration = median_duration * 2
# hack: truncate long words at sentence boundaries. # hack: truncate long words at sentence boundaries.
@@ -893,6 +1073,13 @@ class WhisperModel:
words, word_tokens = tokenizer.split_to_word_tokens( words, word_tokens = tokenizer.split_to_word_tokens(
text_tokens + [tokenizer.eot] text_tokens + [tokenizer.eot]
) )
if len(word_tokens) <= 1:
# return on eot only
# >>> np.pad([], (1, 0))
# array([0.])
# This results in crashes when we lookup jump_times with float, like
# IndexError: arrays used as indices must be of integer (or boolean) type
return []
word_boundaries = np.pad(np.cumsum([len(t) for t in word_tokens[:-1]]), (1, 0)) word_boundaries = np.pad(np.cumsum([len(t) for t in word_tokens[:-1]]), (1, 0))
if len(word_boundaries) <= 1: if len(word_boundaries) <= 1:
return [] return []
@@ -914,9 +1101,6 @@ class WhisperModel:
words, word_tokens, start_times, end_times, word_probabilities words, word_tokens, start_times, end_times, word_probabilities
) )
] ]
def destroy(self):
del self.model
def restore_speech_timestamps( def restore_speech_timestamps(
+338
View File
@@ -0,0 +1,338 @@
import json
import re
from collections import OrderedDict
from pathlib import Path
from typing import Union
import torch
import numpy as np
import torch.nn.functional as F
from whisper.tokenizer import get_tokenizer
from whisper_live.tensorrt_utils import (mel_filters, load_audio_wav_format, pad_or_trim, load_audio)
import tensorrt_llm
import tensorrt_llm.logger as logger
from tensorrt_llm._utils import (str_dtype_to_torch, str_dtype_to_trt,
trt_dtype_to_torch)
from tensorrt_llm.runtime import ModelConfig, SamplingConfig
from tensorrt_llm.runtime.session import Session, TensorInfo
SAMPLE_RATE = 16000
N_FFT = 400
HOP_LENGTH = 160
CHUNK_LENGTH = 30
N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE # 480000 samples in a 30-second chunk
class WhisperEncoding:
def __init__(self, engine_dir):
self.session = self.get_session(engine_dir)
def get_session(self, engine_dir):
config_path = engine_dir / 'encoder_config.json'
with open(config_path, 'r') as f:
config = json.load(f)
use_gpt_attention_plugin = config['plugin_config'][
'gpt_attention_plugin']
dtype = config['builder_config']['precision']
n_mels = config['builder_config']['n_mels']
num_languages = config['builder_config']['num_languages']
self.dtype = dtype
self.n_mels = n_mels
self.num_languages = num_languages
serialize_path = engine_dir / f'whisper_encoder_{self.dtype}_tp1_rank0.engine'
with open(serialize_path, 'rb') as f:
session = Session.from_serialized_engine(f.read())
return session
def get_audio_features(self, mel):
input_lengths = torch.tensor(
[mel.shape[2] // 2 for _ in range(mel.shape[0])],
dtype=torch.int32,
device=mel.device)
inputs = OrderedDict()
inputs['x'] = mel
inputs['input_lengths'] = input_lengths
output_list = [
TensorInfo('x', str_dtype_to_trt(self.dtype), mel.shape),
TensorInfo('input_lengths', str_dtype_to_trt('int32'),
input_lengths.shape)
]
output_info = (self.session).infer_shapes(output_list)
logger.debug(f'output info {output_info}')
outputs = {
t.name: torch.empty(tuple(t.shape),
dtype=trt_dtype_to_torch(t.dtype),
device='cuda')
for t in output_info
}
stream = torch.cuda.current_stream()
ok = self.session.run(inputs=inputs,
outputs=outputs,
stream=stream.cuda_stream)
assert ok, 'Engine execution failed'
stream.synchronize()
audio_features = outputs['output']
return audio_features
class WhisperDecoding:
def __init__(self, engine_dir, runtime_mapping, debug_mode=False):
self.decoder_config = self.get_config(engine_dir)
self.decoder_generation_session = self.get_session(
engine_dir, runtime_mapping, debug_mode)
def get_config(self, engine_dir):
config_path = engine_dir / 'decoder_config.json'
with open(config_path, 'r') as f:
config = json.load(f)
decoder_config = OrderedDict()
decoder_config.update(config['plugin_config'])
decoder_config.update(config['builder_config'])
return decoder_config
def get_session(self, engine_dir, runtime_mapping, debug_mode=False):
dtype = self.decoder_config['precision']
serialize_path = engine_dir / f'whisper_decoder_{dtype}_tp1_rank0.engine'
with open(serialize_path, "rb") as f:
decoder_engine_buffer = f.read()
decoder_model_config = ModelConfig(
max_batch_size=self.decoder_config['max_batch_size'],
max_beam_width=self.decoder_config['max_beam_width'],
num_heads=self.decoder_config['num_heads'],
num_kv_heads=self.decoder_config['num_heads'],
hidden_size=self.decoder_config['hidden_size'],
vocab_size=self.decoder_config['vocab_size'],
num_layers=self.decoder_config['num_layers'],
gpt_attention_plugin=self.decoder_config['gpt_attention_plugin'],
remove_input_padding=self.decoder_config['remove_input_padding'],
cross_attention=self.decoder_config['cross_attention'],
has_position_embedding=self.
decoder_config['has_position_embedding'],
has_token_type_embedding=self.
decoder_config['has_token_type_embedding'],
)
decoder_generation_session = tensorrt_llm.runtime.GenerationSession(
decoder_model_config,
decoder_engine_buffer,
runtime_mapping,
debug_mode=debug_mode)
return decoder_generation_session
def generate(self,
decoder_input_ids,
encoder_outputs,
eot_id,
max_new_tokens=40,
num_beams=1):
encoder_input_lengths = torch.tensor(
[encoder_outputs.shape[1] for x in range(encoder_outputs.shape[0])],
dtype=torch.int32,
device='cuda')
decoder_input_lengths = torch.tensor([
decoder_input_ids.shape[-1]
for _ in range(decoder_input_ids.shape[0])
],
dtype=torch.int32,
device='cuda')
decoder_max_input_length = torch.max(decoder_input_lengths).item()
cross_attention_mask = torch.ones(
[encoder_outputs.shape[0], 1,
encoder_outputs.shape[1]]).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_outputs.shape[1])
torch.cuda.synchronize()
decoder_input_ids = decoder_input_ids.type(torch.int32).cuda()
output_ids = self.decoder_generation_session.decode(
decoder_input_ids,
decoder_input_lengths,
sampling_config,
encoder_output=encoder_outputs,
encoder_input_lengths=encoder_input_lengths,
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"):
world_size = 1
runtime_rank = tensorrt_llm.mpi_rank()
runtime_mapping = tensorrt_llm.Mapping(world_size, runtime_rank)
torch.cuda.set_device(runtime_rank % runtime_mapping.gpus_per_node)
engine_dir = Path(engine_dir)
self.encoder = WhisperEncoding(engine_dir)
self.decoder = WhisperDecoding(engine_dir,
runtime_mapping,
debug_mode=False)
self.n_mels = self.encoder.n_mels
# self.tokenizer = get_tokenizer(num_languages=self.encoder.num_languages,
# tokenizer_dir=assets_dir)
self.device = device
self.tokenizer = get_tokenizer(
is_multilingual,
num_languages=self.encoder.num_languages,
language=language,
task=task,
)
self.filters = mel_filters(self.device, self.encoder.n_mels, assets_dir)
def log_mel_spectrogram(
self,
audio: Union[str, np.ndarray, torch.Tensor],
padding: int = 0,
return_duration=True
):
"""
Compute the log-Mel spectrogram of
Parameters
----------
audio: Union[str, np.ndarray, torch.Tensor], shape = (*)
The path to audio or either a NumPy array or Tensor containing the audio waveform in 16 kHz
n_mels: int
The number of Mel-frequency filters, only 80 and 128 are supported
padding: int
Number of zero samples to pad to the right
device: Optional[Union[str, torch.device]]
If given, the audio tensor is moved to this device before STFT
Returns
-------
torch.Tensor, shape = (80 or 128, n_frames)
A Tensor that contains the Mel spectrogram
"""
if not torch.is_tensor(audio):
if isinstance(audio, str):
if audio.endswith('.wav'):
audio, _ = load_audio_wav_format(audio)
else:
audio = load_audio(audio)
assert isinstance(audio, np.ndarray), f"Unsupported audio type: {type(audio)}"
duration = audio.shape[-1] / SAMPLE_RATE
audio = pad_or_trim(audio, N_SAMPLES)
audio = audio.astype(np.float32)
audio = torch.from_numpy(audio)
if self.device is not None:
audio = audio.to(self.device)
if padding > 0:
audio = F.pad(audio, (0, padding))
window = torch.hann_window(N_FFT).to(audio.device)
stft = torch.stft(audio, N_FFT, HOP_LENGTH, window=window, return_complex=True)
magnitudes = stft[..., :-1].abs()**2
mel_spec = self.filters @ magnitudes
log_spec = torch.clamp(mel_spec, min=1e-10).log10()
log_spec = torch.maximum(log_spec, log_spec.max() - 8.0)
log_spec = (log_spec + 4.0) / 4.0
if return_duration:
return log_spec, duration
else:
return log_spec
def process_batch(
self,
mel,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
num_beams=1):
prompt_id = self.tokenizer.encode(
text_prefix, allowed_special=set(self.tokenizer.special_tokens.keys()))
prompt_id = torch.tensor(prompt_id)
batch_size = mel.shape[0]
decoder_input_ids = prompt_id.repeat(batch_size, 1)
encoder_output = self.encoder.get_audio_features(mel)
output_ids = self.decoder.generate(decoder_input_ids,
encoder_output,
self.tokenizer.eot,
max_new_tokens=96,
num_beams=num_beams)
texts = []
for i in range(len(output_ids)):
text = self.tokenizer.decode(output_ids[i][0]).strip()
texts.append(text)
return texts
def transcribe(
self,
mel,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
dtype='float16',
batch_size=1,
num_beams=1,
):
mel = mel.type(str_dtype_to_torch(dtype))
mel = mel.unsqueeze(0)
predictions = self.process_batch(mel, text_prefix, num_beams)
prediction = predictions[0]
# remove all special tokens in the prediction
prediction = re.sub(r'<\|.*?\|>', '', prediction)
return prediction.strip()
def decode_wav_file(
model,
mel,
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
dtype='float16',
batch_size=1,
num_beams=1,
normalizer=None,
mel_filters_dir=None):
mel = mel.type(str_dtype_to_torch(dtype))
mel = mel.unsqueeze(0)
# repeat the mel spectrogram to match the batch size
mel = mel.repeat(batch_size, 1, 1)
predictions = model.process_batch(mel, text_prefix, num_beams)
prediction = predictions[0]
# remove all special tokens in the prediction
prediction = re.sub(r'<\|.*?\|>', '', prediction)
if normalizer:
prediction = normalizer(prediction)
return prediction.strip()
+71
View File
@@ -0,0 +1,71 @@
import os
import textwrap
import scipy
import ffmpeg
import numpy as np
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, output_file):
with open(output_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):
"""
# https://github.com/openai/whisper/blob/7858aa9c08d98f75575035ecd6481f462d66ca27/whisper/audio.py#L22
Open an audio file and read as mono waveform, resampling as necessary,
save the resampled audio
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
"""
try:
# This launches a subprocess to decode audio while down-mixing and resampling as necessary.
# Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
out, _ = (
ffmpeg.input(file, threads=0)
.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=sr)
.run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
)
except ffmpeg.Error as e:
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
np_buffer = np.frombuffer(out, dtype=np.int16)
resampled_file = f"{file.split('.')[0]}_resampled.wav"
scipy.io.wavfile.write(resampled_file, sr, np_buffer.astype(np.int16))
return resampled_file
+142
View File
@@ -0,0 +1,142 @@
# original: https://github.com/snakers4/silero-vad/blob/master/utils_vad.py
import os
import subprocess
import torch
import numpy as np
import onnxruntime
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()
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._h = np.zeros((2, batch_size, 64)).astype('float32')
self._c = np.zeros((2, batch_size, 64)).astype('float32')
self._last_sr = 0
self._last_batch_size = 0
def __call__(self, x, sr: int):
x, sr = self._validate_input(x, sr)
batch_size = x.shape[0]
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 sr in [8000, 16000]:
ort_inputs = {'input': x.numpy(), 'h': self._h, 'c': self._c, 'sr': np.array(sr, dtype='int64')}
ort_outs = self.session.run(None, ort_inputs)
out, self._h, self._c = ort_outs
else:
raise ValueError()
self._last_sr = sr
self._last_batch_size = batch_size
out = torch.tensor(out)
return out
def audio_forward(self, x, sr: int, num_samples: int = 512):
outs = []
x, sr = self._validate_input(x, sr)
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)
self.reset_states(x.shape[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/v4.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_prob = self.model(torch.from_numpy(audio_frame), self.frame_rate).item()
return speech_prob > self.threshold