From 2593ef83c3bafc59c38e0a70450d8ba1c6729f9a Mon Sep 17 00:00:00 2001 From: Zohaib Rauf Date: Fri, 2 Feb 2024 17:40:04 -0800 Subject: [PATCH 1/6] Fixed requirements file --- OS/01/.gitignore | 1 + OS/01/core/i_endpoint.py | 2 +- OS/01/requirements.txt | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 OS/01/.gitignore diff --git a/OS/01/.gitignore b/OS/01/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/OS/01/.gitignore @@ -0,0 +1 @@ +.env diff --git a/OS/01/core/i_endpoint.py b/OS/01/core/i_endpoint.py index db08ba1..d86e0f7 100644 --- a/OS/01/core/i_endpoint.py +++ b/OS/01/core/i_endpoint.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, WebSocket import uvicorn import redis diff --git a/OS/01/requirements.txt b/OS/01/requirements.txt index 9e99ba4..0f942af 100644 --- a/OS/01/requirements.txt +++ b/OS/01/requirements.txt @@ -1 +1,5 @@ -pip install git+https://github.com/KillianLucas/open-interpreter.git +git+https://github.com/KillianLucas/open-interpreter.git +redis==5.0.1 +fastapi==0.109.0 +uvicorn==0.27.0.post1 +websockets==12.0 \ No newline at end of file From 3e10fb80fa63f9d2def6dbe2eb8c221019ccfd28 Mon Sep 17 00:00:00 2001 From: Zohaib Rauf Date: Fri, 2 Feb 2024 21:16:34 -0800 Subject: [PATCH 2/6] Added service for transcription --- OS/01/.gitignore | 1 + OS/01/README.md | 9 + OS/01/core/.env.example | 1 + OS/01/core/i_endpoint.py | 53 +- OS/01/core/stt/__init__.py | 55 + .../stt/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 3773 bytes OS/01/core/stt/whisper-rust/.gitignore | 1 + OS/01/core/stt/whisper-rust/Cargo.lock | 1228 +++++++++++++++++ OS/01/core/stt/whisper-rust/Cargo.toml | 14 + OS/01/core/stt/whisper-rust/src/main.rs | 34 + OS/01/core/stt/whisper-rust/src/transcribe.rs | 64 + OS/01/core/test_cli.py | 40 + OS/01/requirements.txt | 5 +- 13 files changed, 1502 insertions(+), 3 deletions(-) create mode 100644 OS/01/README.md create mode 100644 OS/01/core/.env.example create mode 100644 OS/01/core/stt/__init__.py create mode 100644 OS/01/core/stt/__pycache__/__init__.cpython-311.pyc create mode 100644 OS/01/core/stt/whisper-rust/.gitignore create mode 100644 OS/01/core/stt/whisper-rust/Cargo.lock create mode 100644 OS/01/core/stt/whisper-rust/Cargo.toml create mode 100644 OS/01/core/stt/whisper-rust/src/main.rs create mode 100644 OS/01/core/stt/whisper-rust/src/transcribe.rs create mode 100644 OS/01/core/test_cli.py diff --git a/OS/01/.gitignore b/OS/01/.gitignore index 4c49bd7..4c842e4 100644 --- a/OS/01/.gitignore +++ b/OS/01/.gitignore @@ -1 +1,2 @@ .env +.DS_Store \ No newline at end of file diff --git a/OS/01/README.md b/OS/01/README.md new file mode 100644 index 0000000..e55bcdb --- /dev/null +++ b/OS/01/README.md @@ -0,0 +1,9 @@ + +# Setup + +1. Install Rust and Python dependencies +2. Go to core/stt and run `cargo build --release` +3. Download GGML Whisper model from https://huggingface.co/ggerganov/whisper.cpp +4. Copy .env.example to .env and put the path to model +5. Run `python core/i_endpoint.py` to start the server +6. Run `python core/test_cli.py PATH_TO_FILE` to test sending audio to service and getting transcription back over websocket \ No newline at end of file diff --git a/OS/01/core/.env.example b/OS/01/core/.env.example new file mode 100644 index 0000000..e85c4c7 --- /dev/null +++ b/OS/01/core/.env.example @@ -0,0 +1 @@ +WHISPER_MODEL_PATH=/path/to/ggml-tiny.en.bin \ No newline at end of file diff --git a/OS/01/core/i_endpoint.py b/OS/01/core/i_endpoint.py index d86e0f7..4c76498 100644 --- a/OS/01/core/i_endpoint.py +++ b/OS/01/core/i_endpoint.py @@ -1,6 +1,13 @@ from fastapi import FastAPI, Request, WebSocket import uvicorn import redis +import json +from dotenv import load_dotenv +from stt import get_transcription +import tempfile + +# Load environment variables +load_dotenv() app = FastAPI() @@ -10,7 +17,7 @@ r = redis.Redis(host='localhost', port=6379, db=0) @app.post("/i/") async def i(request: Request): message = await request.json() - + client_host = request.client.host # Get the client's IP address message = f""" @@ -26,5 +33,47 @@ async def i(request: Request): "content": message }) + +@app.websocket("/a") +async def a(ws: WebSocket): + await ws.accept() + audio_file = bytearray() + mime_type = None + + try: + while True: + message = await ws.receive() + + if message['type'] == 'websocket.disconnect': + break + + if message['type'] == 'websocket.receive': + if 'text' in message: + control_message = json.loads(message['text']) + if control_message.get('action') == 'command' and control_message.get('state') == 'start' and 'mimeType' in control_message: + # This indicates the start of a new audio file + mime_type = control_message.get('mimeType') + elif control_message.get('action') == 'command' and control_message.get('state') == 'end': + # This indicates the end of the audio file + # Process the complete audio file here + transcription = get_transcription(audio_file, mime_type) + await ws.send_json({"transcript": transcription}) + + print("SENT TRANSCRIPTION!") + + # Reset the bytearray for the next audio file + audio_file = bytearray() + mime_type = None + elif 'bytes' in message: + # If it's not a control message, it's part of the audio file + audio_file.extend(message['bytes']) + except Exception as e: + print(f"WebSocket connection closed with exception: {e}") + finally: + await ws.close() + print("WebSocket connection closed") + + if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + with tempfile.TemporaryDirectory(): + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/OS/01/core/stt/__init__.py b/OS/01/core/stt/__init__.py new file mode 100644 index 0000000..5fa7198 --- /dev/null +++ b/OS/01/core/stt/__init__.py @@ -0,0 +1,55 @@ +from datetime import datetime +import os +import contextlib +import tempfile +import ffmpeg +import subprocess + +def convert_mime_type_to_format(mime_type: str) -> str: + if mime_type == "audio/x-wav": + return "wav" + if mime_type == "audio/webm": + return "webm" + + return mime_type + +@contextlib.contextmanager +def export_audio_to_wav_ffmpeg(audio: bytearray, mime_type: str) -> str: + temp_dir = tempfile.gettempdir() + + # Create a temporary file with the appropriate extension + input_ext = convert_mime_type_to_format(mime_type) + input_path = os.path.join(temp_dir, f"input_{datetime.now().strftime('%Y%m%d%H%M%S%f')}.{input_ext}") + with open(input_path, 'wb') as f: + f.write(audio) + + # Export to wav + output_path = os.path.join(temp_dir, f"output_{datetime.now().strftime('%Y%m%d%H%M%S%f')}.wav") + ffmpeg.input(input_path).output(output_path, acodec='pcm_s16le', ac=1, ar='16k').run() + + print(f"Temporary file path: {output_path}") + + try: + yield output_path + finally: + os.remove(input_path) + #os.remove(output_path) + +def run_command(command): + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + return result.stdout, result.stderr + +def get_transcription(audio_bytes: bytearray, mime_type): + with export_audio_to_wav_ffmpeg(audio_bytes, mime_type) as wav_file_path: + model_path = os.getenv("WHISPER_MODEL_PATH") + if not model_path: + raise EnvironmentError("WHISPER_MODEL_PATH environment variable is not set.") + + output, error = run_command([ + os.path.join(os.path.dirname(__file__), 'whisper-rust', 'target', 'release', 'whisper-rust'), + '--model-path', model_path, + '--file-path', wav_file_path + ]) + + print("Exciting transcription result:", output) + return output \ No newline at end of file diff --git a/OS/01/core/stt/__pycache__/__init__.cpython-311.pyc b/OS/01/core/stt/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02a3b046083e9d38532a04f6d6ecb27ef6c629fe GIT binary patch literal 3773 zcmb_fZ)_9E6`$Q5|5^XZKL{ZZ5<>`JN@7B2XzveIxl1nu2&ix^ap|ob?>gCB?6qdc zNr)WDs`!vYsh}sMM7=}JCwdi}#7UgQmr4{Bm5=LaCES%FRjQDBUn(O&Li9`DjAJ_v zp=za$ci+63_jc#advAVk{rh0hhoC+9%OACmJqZ1s4ayD1ygk1P-6B$vDommziXwsW z;-nB2crQjp-b+!5_c)4q?~1xqNpq`Mb0@Isx`Lw~O`Pzqy%ni$O|Hn@s^<#61)|$! zF^c+B?-dmFtFk61MAZk+2UPJq3j4ofI$^=_sxhijJ*f>lauPa)UQTO{n`kscQgNa3 zCWJ)bCAgpO1G0#+u$52Q93monC5)xY7_%`f;~Jjz$1YDyzisq9aohLSKyGDe9XR%Grp4*l*1wcBx&1U1z$^cO_m6 zJ&rg%>UfL{<#OD^z(7vNlZx5fKdF7ka@~=7`#%j!jyo1NR4wj^F?N&L<_lUfZIBqb z97^buS|}Z(KA zbFcvb;<%=XP66-|iDX(Eb6k7}j=N;3BasZKq=}xQ3=@;alos|77DsJfcFo2n3qh#V2s<3k4s8ZDM-`4G441$&O34_~4GJty<~8uoKj4=` zPwQLk`aSvjy>|WH+~7+$YCQDS$$Ue<-O!&qx!SnpM$2N`=WR>l)`9VFx8G~~ru|;~ zlitCV-a+f+NWM2}_eMd&u3sifOukXK8};1KsvMl3SlXMHJ8Ze5hy+*5a&N&aU-Mk` zTz7p|w@^1n1(c0Kj_ z7Ub&(^WI&ycb8SZUj16!I15`c+aWN2`q8!%yQPP_@rf?a!vg})M;H&Zqet!A298QK z)srDOubxxDysau^5d~?vO;pf!^I286Bi5qEoRAeJ*ebjMU%`_EEBey#1$MqnszOTg zUL(Z86p}XVa(rfHG);`SW}2i6Uf|$!C(jL#2(aWZkg(`@;zlwVOR4NEn3+jxG7CeY zQYQQ1tSD;=q#p$P;5K`JwdbN12DpU+y+PpqJl4=Q$0$^E3@Z(MkP zY2TOKmVdv6_Y(%8PU*8j8NEz3v!=wdQ^OrO1Bas4ky!+#v!+Zz84_v5Apm<7m#S|R z*{o#NuLr%2rSMk*m0|6*Mk{ZEw^j=AG7Iw??Jc_N;JEq-tpGFHP$pGLg)cx=x&^D< zuB@oKvy$q`ies+Ya&rmno5ORi^<`T10@&C1M?hH|XJ4M1lwVc^HY>lZJcQ+8r|IW| zC(oZ7_^EQ{?1uxVm2*G7FgV-y=3GciP3go)v9Gt#RE+4cQ2=q>45bVjGBw&W8<-x~ z&9p|kNydbZ=P-`&k1zcgqcn2GCDPmP5riKWaq z(bH5nQlV0Iz7zIw2qingy^b4R3Z^&>166wl2r&rHRni7JWEWHP0Gy{{P$0aD!j@7L z!ZJDRCdZf#J5o-Nqcjt8l=pE1IZyU7xeE@KK}?nn>%qPeY*1=UB_DvwLAcF-0fB=e z1Df4!&CBoF^1DS8aBaIkx7yrtW5nv{%Qqjjn~%=>3iYi3sB%C2Yib^NyD>+Sj0BX;YNyTcC#AM4i0SUxar2gVD59ZN%n zKaPXUxPG zj8x|~<9HGHPg-UZ?Eg0>8c#%|hY@_D&-1Wf02&VR+Fc5v4?g4>SWUv8f>ux`dl)$a z#F3z^mr8%#niAktY)pf~%pfF2NbGVc85lBguM>8fIoNRgdhLA=qL`V~j*~$UurCtx zuRx&c2|@wwvTCmal5_l5K#f-UDxmrt{}oU$$A9akMeG)uzv?R@=xah{WB5)yZob&Cf@;Xn~ph&L!~TmE4YLHAa|xBhRHiCEzP literal 0 HcmV?d00001 diff --git a/OS/01/core/stt/whisper-rust/.gitignore b/OS/01/core/stt/whisper-rust/.gitignore new file mode 100644 index 0000000..1de5659 --- /dev/null +++ b/OS/01/core/stt/whisper-rust/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/OS/01/core/stt/whisper-rust/Cargo.lock b/OS/01/core/stt/whisper-rust/Cargo.lock new file mode 100644 index 0000000..27da1ff --- /dev/null +++ b/OS/01/core/stt/whisper-rust/Cargo.lock @@ -0,0 +1,1228 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47" +dependencies = [ + "alsa-sys", + "bitflags 1.3.2", + "libc", + "nix", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bindgen" +version = "0.69.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c69fae65a523209d34240b60abe0c42d33d1045d445c0839d8a4894a736e2d" +dependencies = [ + "bitflags 2.4.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.48", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni 0.19.0", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "once_cell", + "parking_lot", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "indexmap" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libloading" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "oboe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" +dependencies = [ + "jni 0.20.0", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pkg-config" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" + +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +dependencies = [ + "proc-macro2", + "syn 2.0.48", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] +name = "web-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "whisper-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e19ccc66e3746fc15cc9b8cbc67be61d7326f0dd313c3d5dce53e706365e23" +dependencies = [ + "whisper-rs-sys", +] + +[[package]] +name = "whisper-rs-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91adc462e9bc7b3fce6116eeafe76f9bc46c20c2b4f5fe0da00fd1196fa92f1" +dependencies = [ + "bindgen", + "cfg-if", + "cmake", + "fs_extra", +] + +[[package]] +name = "whisper-rust" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "cpal", + "hound", + "whisper-rs", + "whisper-rs-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cad8365489051ae9f054164e459304af2e7e9bb407c958076c8bf4aef52da5" +dependencies = [ + "memchr", +] diff --git a/OS/01/core/stt/whisper-rust/Cargo.toml b/OS/01/core/stt/whisper-rust/Cargo.toml new file mode 100644 index 0000000..f172692 --- /dev/null +++ b/OS/01/core/stt/whisper-rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "whisper-rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.79" +clap = { version = "4.4.18", features = ["derive"] } +cpal = "0.15.2" +hound = "3.5.1" +whisper-rs = "0.10.0" +whisper-rs-sys = "0.8.0" \ No newline at end of file diff --git a/OS/01/core/stt/whisper-rust/src/main.rs b/OS/01/core/stt/whisper-rust/src/main.rs new file mode 100644 index 0000000..0688c89 --- /dev/null +++ b/OS/01/core/stt/whisper-rust/src/main.rs @@ -0,0 +1,34 @@ +mod transcribe; + +use clap::Parser; +use std::path::PathBuf; +use transcribe::transcribe; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// This is the model for Whisper STT + #[arg(short, long, value_parser, required = true)] + model_path: PathBuf, + + /// This is the wav audio file that will be converted from speech to text + #[arg(short, long, value_parser, required = true)] + file_path: Option, +} + +fn main() { + + let args = Args::parse(); + + let file_path = match args.file_path { + Some(fp) => fp, + None => panic!("No file path provided") + }; + + let result = transcribe(&args.model_path, &file_path); + + match result { + Ok(transcription) => print!("{}", transcription), + Err(e) => panic!("Error: {}", e), + } +} \ No newline at end of file diff --git a/OS/01/core/stt/whisper-rust/src/transcribe.rs b/OS/01/core/stt/whisper-rust/src/transcribe.rs new file mode 100644 index 0000000..35970cc --- /dev/null +++ b/OS/01/core/stt/whisper-rust/src/transcribe.rs @@ -0,0 +1,64 @@ +use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters}; +use std::path::PathBuf; + + +/// Transcribes the given audio file using the whisper-rs library. +/// +/// # Arguments +/// * `model_path` - Path to Whisper model file +/// * `file_path` - A string slice that holds the path to the audio file to be transcribed. +/// +/// # Returns +/// +/// A Result containing a String with the transcription if successful, or an error message if not. +pub fn transcribe(model_path: &PathBuf, file_path: &PathBuf) -> Result { + + let model_path_str = model_path.to_str().expect("Not valid model path"); + // Load a context and model + let ctx = WhisperContext::new_with_params( + model_path_str, // Replace with the actual path to the model + WhisperContextParameters::default(), + ) + .map_err(|_| "failed to load model")?; + + // Create a state + let mut state = ctx.create_state().map_err(|_| "failed to create state")?; + + // Create a params object + // Note that currently the only implemented strategy is Greedy, BeamSearch is a WIP + let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 }); + + // Edit parameters as needed + params.set_n_threads(1); // Set the number of threads to use + params.set_translate(true); // Enable translation + params.set_language(Some("en")); // Set the language to translate to English + // Disable printing to stdout + params.set_print_special(false); + params.set_print_progress(false); + params.set_print_realtime(false); + params.set_print_timestamps(false); + + // Load the audio file + let audio_data = std::fs::read(file_path) + .map_err(|e| format!("failed to read audio file: {}", e))? + .chunks_exact(2) + .map(|chunk| i16::from_ne_bytes([chunk[0], chunk[1]])) + .collect::>(); + + // Convert the audio data to the required format (16KHz mono i16 samples) + let audio_data = whisper_rs::convert_integer_to_float_audio(&audio_data); + + // Run the model + state.full(params, &audio_data[..]).map_err(|_| "failed to run model")?; + + // Fetch the results + let num_segments = state.full_n_segments().map_err(|_| "failed to get number of segments")?; + let mut transcription = String::new(); + for i in 0..num_segments { + let segment = state.full_get_segment_text(i).map_err(|_| "failed to get segment")?; + transcription.push_str(&segment); + transcription.push('\n'); + } + + Ok(transcription) +} \ No newline at end of file diff --git a/OS/01/core/test_cli.py b/OS/01/core/test_cli.py new file mode 100644 index 0000000..0569aff --- /dev/null +++ b/OS/01/core/test_cli.py @@ -0,0 +1,40 @@ +import argparse +import asyncio +import websockets +import os +import json + +# Define the function to send audio file in chunks +async def send_audio_in_chunks(file_path, chunk_size=4096): + async with websockets.connect("ws://localhost:8000/a") as websocket: + # Send the start command with mime type + await websocket.send(json.dumps({"action": "command", "state": "start", "mimeType": "audio/webm"})) + + # Open the file in binary mode and send in chunks + with open(file_path, 'rb') as audio_file: + chunk = audio_file.read(chunk_size) + while chunk: + await websocket.send(chunk) + chunk = audio_file.read(chunk_size) + + # Send the end command + await websocket.send(json.dumps({"action": "command", "state": "end"})) + + # Receive a json message and then close the connection + message = await websocket.recv() + print("Received message:", json.loads(message)) + await websocket.close() + +# Parse command line arguments +parser = argparse.ArgumentParser(description="Send a webm audio file to the /a websocket endpoint and print the responses.") +parser.add_argument("file_path", help="The path to the webm audio file to send.") +args = parser.parse_args() + +# Check if the file exists +if not os.path.isfile(args.file_path): + print(args.file_path) + print("Error: The file does not exist.") + exit(1) + +# Run the asyncio event loop +asyncio.get_event_loop().run_until_complete(send_audio_in_chunks(args.file_path)) diff --git a/OS/01/requirements.txt b/OS/01/requirements.txt index 0f942af..56c706a 100644 --- a/OS/01/requirements.txt +++ b/OS/01/requirements.txt @@ -2,4 +2,7 @@ git+https://github.com/KillianLucas/open-interpreter.git redis==5.0.1 fastapi==0.109.0 uvicorn==0.27.0.post1 -websockets==12.0 \ No newline at end of file +websockets==12.0 +pydub==0.25.1 +numpy==1.26.3 +python-dotenv==1.0.1 \ No newline at end of file From ae93a3f67056d86b6c728dfedf2ea105c80adeed Mon Sep 17 00:00:00 2001 From: Zohaib Rauf Date: Fri, 2 Feb 2024 21:17:54 -0800 Subject: [PATCH 3/6] Added ffmpeq in reqs --- OS/01/requirements.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/OS/01/requirements.txt b/OS/01/requirements.txt index 56c706a..044c638 100644 --- a/OS/01/requirements.txt +++ b/OS/01/requirements.txt @@ -3,6 +3,5 @@ redis==5.0.1 fastapi==0.109.0 uvicorn==0.27.0.post1 websockets==12.0 -pydub==0.25.1 -numpy==1.26.3 -python-dotenv==1.0.1 \ No newline at end of file +python-dotenv==1.0.1 +ffmpeg-python==0.2.0 \ No newline at end of file From fcb263129579f452cefd989dd20cf755346a81c2 Mon Sep 17 00:00:00 2001 From: Zohaib Rauf Date: Fri, 2 Feb 2024 21:30:05 -0800 Subject: [PATCH 4/6] Updated readme and gitignore --- OS/01/.gitignore | 160 ++++++++++++++++++++++++- OS/01/README.md | 12 +- OS/01/core/stt/whisper-rust/.gitignore | 11 +- 3 files changed, 175 insertions(+), 8 deletions(-) diff --git a/OS/01/.gitignore b/OS/01/.gitignore index 4c842e4..6769e21 100644 --- a/OS/01/.gitignore +++ b/OS/01/.gitignore @@ -1,2 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env -.DS_Store \ No newline at end of file +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/OS/01/README.md b/OS/01/README.md index e55bcdb..e4c881b 100644 --- a/OS/01/README.md +++ b/OS/01/README.md @@ -1,9 +1,9 @@ # Setup -1. Install Rust and Python dependencies -2. Go to core/stt and run `cargo build --release` -3. Download GGML Whisper model from https://huggingface.co/ggerganov/whisper.cpp -4. Copy .env.example to .env and put the path to model -5. Run `python core/i_endpoint.py` to start the server -6. Run `python core/test_cli.py PATH_TO_FILE` to test sending audio to service and getting transcription back over websocket \ No newline at end of file +1. Install [Rust](https://www.rust-lang.org/tools/install) and Python dependencies `pip install -r requirements.txt`. +2. Go to **core/stt** and run `cargo build --release`. +3. Download GGML Whisper model from [Huggingface](https://huggingface.co/ggerganov/whisper.cpp). +4. In core, copy `.env.example` to `.env` and put the path to model. +5. Run `python core/i_endpoint.py` to start the server. +6. Run `python core/test_cli.py PATH_TO_FILE` to test sending audio to service and getting transcription back over websocket. \ No newline at end of file diff --git a/OS/01/core/stt/whisper-rust/.gitignore b/OS/01/core/stt/whisper-rust/.gitignore index 1de5659..71ab9a4 100644 --- a/OS/01/core/stt/whisper-rust/.gitignore +++ b/OS/01/core/stt/whisper-rust/.gitignore @@ -1 +1,10 @@ -target \ No newline at end of file +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb \ No newline at end of file From 1f658d88377a9723b57fd25672f0c954979728b5 Mon Sep 17 00:00:00 2001 From: Zohaib Rauf Date: Fri, 2 Feb 2024 21:32:20 -0800 Subject: [PATCH 5/6] Removed pycache --- .../stt/__pycache__/__init__.cpython-311.pyc | Bin 3773 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 OS/01/core/stt/__pycache__/__init__.cpython-311.pyc diff --git a/OS/01/core/stt/__pycache__/__init__.cpython-311.pyc b/OS/01/core/stt/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 02a3b046083e9d38532a04f6d6ecb27ef6c629fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3773 zcmb_fZ)_9E6`$Q5|5^XZKL{ZZ5<>`JN@7B2XzveIxl1nu2&ix^ap|ob?>gCB?6qdc zNr)WDs`!vYsh}sMM7=}JCwdi}#7UgQmr4{Bm5=LaCES%FRjQDBUn(O&Li9`DjAJ_v zp=za$ci+63_jc#advAVk{rh0hhoC+9%OACmJqZ1s4ayD1ygk1P-6B$vDommziXwsW z;-nB2crQjp-b+!5_c)4q?~1xqNpq`Mb0@Isx`Lw~O`Pzqy%ni$O|Hn@s^<#61)|$! zF^c+B?-dmFtFk61MAZk+2UPJq3j4ofI$^=_sxhijJ*f>lauPa)UQTO{n`kscQgNa3 zCWJ)bCAgpO1G0#+u$52Q93monC5)xY7_%`f;~Jjz$1YDyzisq9aohLSKyGDe9XR%Grp4*l*1wcBx&1U1z$^cO_m6 zJ&rg%>UfL{<#OD^z(7vNlZx5fKdF7ka@~=7`#%j!jyo1NR4wj^F?N&L<_lUfZIBqb z97^buS|}Z(KA zbFcvb;<%=XP66-|iDX(Eb6k7}j=N;3BasZKq=}xQ3=@;alos|77DsJfcFo2n3qh#V2s<3k4s8ZDM-`4G441$&O34_~4GJty<~8uoKj4=` zPwQLk`aSvjy>|WH+~7+$YCQDS$$Ue<-O!&qx!SnpM$2N`=WR>l)`9VFx8G~~ru|;~ zlitCV-a+f+NWM2}_eMd&u3sifOukXK8};1KsvMl3SlXMHJ8Ze5hy+*5a&N&aU-Mk` zTz7p|w@^1n1(c0Kj_ z7Ub&(^WI&ycb8SZUj16!I15`c+aWN2`q8!%yQPP_@rf?a!vg})M;H&Zqet!A298QK z)srDOubxxDysau^5d~?vO;pf!^I286Bi5qEoRAeJ*ebjMU%`_EEBey#1$MqnszOTg zUL(Z86p}XVa(rfHG);`SW}2i6Uf|$!C(jL#2(aWZkg(`@;zlwVOR4NEn3+jxG7CeY zQYQQ1tSD;=q#p$P;5K`JwdbN12DpU+y+PpqJl4=Q$0$^E3@Z(MkP zY2TOKmVdv6_Y(%8PU*8j8NEz3v!=wdQ^OrO1Bas4ky!+#v!+Zz84_v5Apm<7m#S|R z*{o#NuLr%2rSMk*m0|6*Mk{ZEw^j=AG7Iw??Jc_N;JEq-tpGFHP$pGLg)cx=x&^D< zuB@oKvy$q`ies+Ya&rmno5ORi^<`T10@&C1M?hH|XJ4M1lwVc^HY>lZJcQ+8r|IW| zC(oZ7_^EQ{?1uxVm2*G7FgV-y=3GciP3go)v9Gt#RE+4cQ2=q>45bVjGBw&W8<-x~ z&9p|kNydbZ=P-`&k1zcgqcn2GCDPmP5riKWaq z(bH5nQlV0Iz7zIw2qingy^b4R3Z^&>166wl2r&rHRni7JWEWHP0Gy{{P$0aD!j@7L z!ZJDRCdZf#J5o-Nqcjt8l=pE1IZyU7xeE@KK}?nn>%qPeY*1=UB_DvwLAcF-0fB=e z1Df4!&CBoF^1DS8aBaIkx7yrtW5nv{%Qqjjn~%=>3iYi3sB%C2Yib^NyD>+Sj0BX;YNyTcC#AM4i0SUxar2gVD59ZN%n zKaPXUxPG zj8x|~<9HGHPg-UZ?Eg0>8c#%|hY@_D&-1Wf02&VR+Fc5v4?g4>SWUv8f>ux`dl)$a z#F3z^mr8%#niAktY)pf~%pfF2NbGVc85lBguM>8fIoNRgdhLA=qL`V~j*~$UurCtx zuRx&c2|@wwvTCmal5_l5K#f-UDxmrt{}oU$$A9akMeG)uzv?R@=xah{WB5)yZob&Cf@;Xn~ph&L!~TmE4YLHAa|xBhRHiCEzP From 6c39a5d497734e21825dca12736b03b899ad7fbf Mon Sep 17 00:00:00 2001 From: Zohaib Rauf Date: Sat, 3 Feb 2024 01:49:12 -0800 Subject: [PATCH 6/6] Added Push to talk example --- OS/01/core/i_endpoint.py | 1 + OS/01/core/stt/__init__.py | 4 +- examples/push-to-record/.gitignore | 160 +++++++++++++++++++++++ examples/push-to-record/README.md | 9 ++ examples/push-to-record/main.py | 129 ++++++++++++++++++ examples/push-to-record/output.wav | Bin 0 -> 63532 bytes examples/push-to-record/requirements.txt | 4 + 7 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 examples/push-to-record/.gitignore create mode 100644 examples/push-to-record/README.md create mode 100644 examples/push-to-record/main.py create mode 100644 examples/push-to-record/output.wav create mode 100644 examples/push-to-record/requirements.txt diff --git a/OS/01/core/i_endpoint.py b/OS/01/core/i_endpoint.py index 4c76498..1d37f4f 100644 --- a/OS/01/core/i_endpoint.py +++ b/OS/01/core/i_endpoint.py @@ -67,6 +67,7 @@ async def a(ws: WebSocket): elif 'bytes' in message: # If it's not a control message, it's part of the audio file audio_file.extend(message['bytes']) + except Exception as e: print(f"WebSocket connection closed with exception: {e}") finally: diff --git a/OS/01/core/stt/__init__.py b/OS/01/core/stt/__init__.py index 5fa7198..a7cf036 100644 --- a/OS/01/core/stt/__init__.py +++ b/OS/01/core/stt/__init__.py @@ -6,7 +6,7 @@ import ffmpeg import subprocess def convert_mime_type_to_format(mime_type: str) -> str: - if mime_type == "audio/x-wav": + if mime_type == "audio/x-wav" or mime_type == "audio/wav": return "wav" if mime_type == "audio/webm": return "webm" @@ -33,7 +33,7 @@ def export_audio_to_wav_ffmpeg(audio: bytearray, mime_type: str) -> str: yield output_path finally: os.remove(input_path) - #os.remove(output_path) + os.remove(output_path) def run_command(command): result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) diff --git a/examples/push-to-record/.gitignore b/examples/push-to-record/.gitignore new file mode 100644 index 0000000..6769e21 --- /dev/null +++ b/examples/push-to-record/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/examples/push-to-record/README.md b/examples/push-to-record/README.md new file mode 100644 index 0000000..1f91b70 --- /dev/null +++ b/examples/push-to-record/README.md @@ -0,0 +1,9 @@ + +# Setup + +On Mac install portaudio `brew install portaudio` + +1. Run `pip install -r requirements.txt` +2. Start the core/i_endpoint.py service +3. Run `python main.py` +4. Press spacebar down to talk and release to get the transcription back \ No newline at end of file diff --git a/examples/push-to-record/main.py b/examples/push-to-record/main.py new file mode 100644 index 0000000..080e3a3 --- /dev/null +++ b/examples/push-to-record/main.py @@ -0,0 +1,129 @@ +import os +import pyaudio +import threading +import asyncio +import websockets +import json +from pynput import keyboard +import wave +import tempfile +from datetime import datetime + +# Configuration +chunk = 1024 # Record in chunks of 1024 samples +sample_format = pyaudio.paInt16 # 16 bits per sample +channels = 1 # Stereo +fs = 48000 # Sample rate + +p = pyaudio.PyAudio() # Create an interface to PortAudio +frames = [] # Initialize array to store frames +recording = False # Flag to control recording state + +ws_chunk_size = 4096 # Websocket stream chunk size + +async def start_recording(): + global recording + + if recording: + return # Avoid multiple starts + recording = True + frames.clear() # Clear existing frames + + stream = p.open(format=sample_format, + channels=channels, + rate=fs, + frames_per_buffer=chunk, + input=True) + + print("Recording started...") + async with websockets.connect("ws://localhost:8000/a") as websocket: + # Send the start command with mime type + await websocket.send(json.dumps({"action": "command", "state": "start", "mimeType": "audio/wav"})) + while recording: + data = stream.read(chunk) + frames.append(data) + + stream.stop_stream() + stream.close() + + try: + file_path = save_recording(frames) + with open(file_path, 'rb') as audio_file: + byte_chunk = audio_file.read(ws_chunk_size) + while byte_chunk: + await websocket.send(byte_chunk) + byte_chunk = audio_file.read(ws_chunk_size) + finally: + os.remove(file_path) + + # Send the end command + await websocket.send(json.dumps({"action": "command", "state": "end"})) + + # Receive a json message and then close the connection + message = await websocket.recv() + print("Received message:", json.loads(message)) + + + print("Recording stopped.") + +def save_recording(frames) -> str: + # Save the recorded data as a WAV file + temp_dir = tempfile.gettempdir() + + # Create a temporary file with the appropriate extension + output_path = os.path.join(temp_dir, f"input_{datetime.now().strftime('%Y%m%d%H%M%S%f')}.wav") + with wave.open(output_path, 'wb') as wf: + wf.setnchannels(channels) + wf.setsampwidth(p.get_sample_size(sample_format)) + wf.setframerate(fs) + wf.writeframes(b''.join(frames)) + + return output_path + +def start_recording_sync(): + # Create a new event loop for the thread + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + # Run the asyncio event loop + loop.run_until_complete(start_recording()) + loop.close() + + +def stop_recording(): + global recording + recording = False + print("Stopped recording") + +def toggle_recording(e): + global recording + if recording: + stop_recording() + else: + # Start recording in a new thread to avoid blocking + print("Starting recording") + threading.Thread(target=start_recording_sync).start() + +is_space_pressed = False # Flag to track the state of the spacebar + +def on_press(key): + global is_space_pressed + if key == keyboard.Key.space and not is_space_pressed: + is_space_pressed = True + toggle_recording(key) + +def on_release(key): + global is_space_pressed + if key == keyboard.Key.space and is_space_pressed: + is_space_pressed = False + stop_recording() + if key == keyboard.Key.esc: + # Stop listener + return False + +# Collect events until released +with keyboard.Listener(on_press=on_press, on_release=on_release) as listener: + with tempfile.TemporaryDirectory(): + print("Press the spacebar to start/stop recording. Press ESC to exit.") + listener.join() + +p.terminate() \ No newline at end of file diff --git a/examples/push-to-record/output.wav b/examples/push-to-record/output.wav new file mode 100644 index 0000000000000000000000000000000000000000..83b85d57fd32a489e570aea59ffebb470054dacc GIT binary patch literal 63532 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 G?*9WCm(B|S literal 0 HcmV?d00001 diff --git a/examples/push-to-record/requirements.txt b/examples/push-to-record/requirements.txt new file mode 100644 index 0000000..1528137 --- /dev/null +++ b/examples/push-to-record/requirements.txt @@ -0,0 +1,4 @@ +pynput @ git+https://github.com/moses-palmer/pynput.git@12acf84dc0f721d91a957da65311497acb664933 +pyaudio +websockets==12.0 +asyncio==3.4.3 \ No newline at end of file