From d4629c017c4970d7f399a7abac43fa744fb9d8e6 Mon Sep 17 00:00:00 2001 From: killian <63927363+KillianLucas@users.noreply.github.com> Date: Wed, 14 Feb 2024 04:38:10 -0800 Subject: [PATCH] Better LMC accumulator logic, queued audio messages, proper audio streaming via bytes --- 01OS/01OS/clients/base_device.py | 84 +++++++++-------- 01OS/01OS/server/server.py | 152 ++++++++++++++++++++----------- 01OS/01OS/server/tts/tts.py | 38 +++++--- 01OS/01OS/utils/accumulator.py | 40 ++++++++ 01OS/output_audio.wav | Bin 49382 -> 0 bytes 5 files changed, 209 insertions(+), 105 deletions(-) create mode 100644 01OS/01OS/utils/accumulator.py delete mode 100644 01OS/output_audio.wav diff --git a/01OS/01OS/clients/base_device.py b/01OS/01OS/clients/base_device.py index f2197a4..d7dd30f 100644 --- a/01OS/01OS/clients/base_device.py +++ b/01OS/01OS/clients/base_device.py @@ -22,6 +22,7 @@ import wave import tempfile from datetime import datetime from interpreter import interpreter # Just for code execution. Maybe we should let people do from interpreter.computer import run? +# In the future, I guess kernel watching code should be elsewhere? Somewhere server / client agnostic? from ..server.utils.kernel import put_kernel_messages_into_queue from ..server.utils.get_system_info import get_system_info from ..server.stt.stt import stt_wav @@ -30,6 +31,11 @@ from ..server.utils.logs import setup_logging from ..server.utils.logs import logger setup_logging() + +from ..utils.accumulator import Accumulator + +accumulator = Accumulator() + # Configuration for Audio Recording CHUNK = 1024 # Record in chunks of 1024 samples FORMAT = pyaudio.paInt16 # 16 bits per sample @@ -44,19 +50,30 @@ current_platform = get_system_info() # Initialize PyAudio p = pyaudio.PyAudio() -import asyncio - send_queue = queue.Queue() class Device: def __init__(self): + self.audiosegments = [] pass + async def play_audiosegments(self): + """Plays them sequentially.""" + while True: + try: + for audio in self.audiosegments: + play(audio) + self.audiosegments.remove(audio) + await asyncio.sleep(0.1) + except: + traceback.print_exc() + + def record_audio(self): if os.getenv('STT_RUNNER') == "server": # STT will happen on the server. we're sending audio. - send_queue.put({"role": "user", "type": "audio", "format": "audio/wav", "start": True}) + send_queue.put({"role": "user", "type": "audio", "format": "bytes.wav", "start": True}) elif os.getenv('STT_RUNNER') == "client": # STT will happen here, on the client. we're sending text. send_queue.put({"role": "user", "type": "message", "start": True}) @@ -92,8 +109,8 @@ class Device: send_queue.put({"role": "user", "type": "message", "content": "stop"}) send_queue.put({"role": "user", "type": "message", "end": True}) else: - send_queue.put({"role": "user", "type": "audio", "format": "audio/wav", "content": ""}) - send_queue.put({"role": "user", "type": "audio", "format": "audio/wav", "end": True}) + send_queue.put({"role": "user", "type": "audio", "format": "bytes.wav", "content": ""}) + send_queue.put({"role": "user", "type": "audio", "format": "bytes.wav", "end": True}) else: if os.getenv('STT_RUNNER') == "client": # Run stt then send text @@ -105,9 +122,9 @@ class Device: with open(wav_path, 'rb') as audio_file: byte_data = audio_file.read(CHUNK) while byte_data: - send_queue.put({"role": "user", "type": "audio", "format": "audio/wav", "content": str(byte_data)}) + send_queue.put(byte_data) byte_data = audio_file.read(CHUNK) - send_queue.put({"role": "user", "type": "audio", "format": "audio/wav", "end": True}) + send_queue.put({"role": "user", "type": "audio", "format": "bytes.wav", "end": True}) if os.path.exists(wav_path): os.remove(wav_path) @@ -140,8 +157,12 @@ class Device: async def message_sender(self, websocket): while True: message = await asyncio.get_event_loop().run_in_executor(None, send_queue.get) - await websocket.send(json.dumps(message)) + if isinstance(message, bytes): + await websocket.send(message) + else: + await websocket.send(json.dumps(message)) send_queue.task_done() + await asyncio.sleep(0.01) async def websocket_communication(self, WS_URL): while True: @@ -150,52 +171,42 @@ class Device: logger.info("Press the spacebar to start/stop recording. Press ESC to exit.") asyncio.create_task(self.message_sender(websocket)) - initial_message = {"role": None, "type": None, "format": None, "content": None} - message_so_far = initial_message - while True: - message = await websocket.recv() + await asyncio.sleep(0.01) + chunk = await websocket.recv() - logger.debug(f"Got this message from the server: {type(message)} {message}") + logger.debug(f"Got this message from the server: {type(chunk)} {chunk}") - if type(message) == str: - message = json.loads(message) + if type(chunk) == str: + chunk = json.loads(chunk) - if message.get("end"): - logger.debug(f"Complete message from the server: {message_so_far}") - logger.info("\n") - message_so_far = initial_message + message = accumulator.accumulate(chunk) + if message == None: + # Will be None until we have a full message ready + continue - if "content" in message: - print(message['content'], end="", flush=True) - if any(message_so_far[key] != message[key] for key in message_so_far if key != "content"): - message_so_far = message - else: - message_so_far["content"] += message["content"] + # At this point, we have our message - if message["type"] == "audio" and "content" in message: - audio_bytes = bytes(ast.literal_eval(message["content"])) + if message["type"] == "audio" and message["format"].startswith("bytes"): # Convert bytes to audio file - audio_file = io.BytesIO(audio_bytes) - audio = AudioSegment.from_mp3(audio_file) - - # Play the audio - play(audio) + # Format will be bytes.wav or bytes.opus + audio_bytes = io.BytesIO(message["content"]) + audio = AudioSegment.from_file(audio_bytes, codec=message["format"].split(".")[1]) - await asyncio.sleep(1) + self.audiosegments.append(audio) # Run the code if that's the client's job if os.getenv('CODE_RUNNER') == "client": if message["type"] == "code" and "end" in message: - language = message_so_far["format"] - code = message_so_far["content"] + language = message["format"] + code = message["content"] result = interpreter.computer.run(language, code) send_queue.put(result) except: - # traceback.print_exc() + traceback.print_exc() logger.info(f"Connecting to `{WS_URL}`...") await asyncio.sleep(2) @@ -212,6 +223,7 @@ class Device: if os.getenv('CODE_RUNNER') == "client": asyncio.create_task(put_kernel_messages_into_queue(send_queue)) + asyncio.create_task(self.play_audiosegments()) # If Raspberry Pi, add the button listener, otherwise use the spacebar if current_platform.startswith("raspberry-pi"): diff --git a/01OS/01OS/server/server.py b/01OS/01OS/server/server.py index 62ab6d8..9710fd5 100644 --- a/01OS/01OS/server/server.py +++ b/01OS/01OS/server/server.py @@ -9,9 +9,9 @@ import traceback import re from fastapi import FastAPI from fastapi.responses import PlainTextResponse -from starlette.websockets import WebSocket +from starlette.websockets import WebSocket, WebSocketDisconnect from .stt.stt import stt_bytes -from .tts.tts import tts +from .tts.tts import stream_tts from pathlib import Path import asyncio import urllib.parse @@ -19,11 +19,13 @@ from .utils.kernel import put_kernel_messages_into_queue from .i import configure_interpreter from interpreter import interpreter import ngrok +from ..utils.accumulator import Accumulator from .utils.logs import setup_logging from .utils.logs import logger setup_logging() +accumulator = Accumulator() app = FastAPI() @@ -105,54 +107,89 @@ async def websocket_endpoint(websocket: WebSocket): async def receive_messages(websocket: WebSocket): while True: - data = await websocket.receive_json() - if data["role"] == "computer": - from_computer.put(data) # To be handled by interpreter.computer.run - elif data["role"] == "user": - await from_user.put(data) - else: - raise("Unknown role:", data) + try: + try: + data = await websocket.receive() + except Exception as e: + print(str(e)) + return + if 'text' in data: + try: + data = json.loads(data['text']) + if data["role"] == "computer": + from_computer.put(data) # To be handled by interpreter.computer.run + elif data["role"] == "user": + await from_user.put(data) + else: + raise("Unknown role:", data) + except json.JSONDecodeError: + pass # data is not JSON, leave it as is + elif 'bytes' in data: + data = data['bytes'] # binary data + await from_user.put(data) + except WebSocketDisconnect as e: + if e.code == 1000: + logger.info("Websocket connection closed normally.") + return + else: + raise + async def send_messages(websocket: WebSocket): while True: message = await to_device.get() logger.debug(f"Sending to the device: {type(message)} {message}") - await websocket.send_json(message) + + try: + if isinstance(message, dict): + await websocket.send_json(message) + elif isinstance(message, bytes): + await websocket.send_bytes(message) + else: + raise TypeError("Message must be a dict or bytes") + except: + # Make sure to put the message back in the queue if you failed to send it + await to_device.put(message) + raise async def listener(): - audio_bytes = bytearray() + while True: while True: if not from_user.empty(): - message = await from_user.get() + chunk = await from_user.get() break elif not from_computer.empty(): - message = from_computer.get() + chunk = from_computer.get() break await asyncio.sleep(1) - if type(message) == str: - message = json.loads(message) + - # Hold the audio in a buffer. If it's ready (we got end flag, stt it) - if message["type"] == "audio": - if "content" in message: - audio_bytes.extend(bytes(ast.literal_eval(message["content"]))) - if "end" in message: - content = stt_bytes(audio_bytes, message["format"]) - if content == None: # If it was nothing / silence - continue - audio_bytes = bytearray() - message = {"role": "user", "type": "message", "content": content} - else: + message = accumulator.accumulate(chunk) + if message == None: + # Will be None until we have a full message ready + continue + + # print(str(message)[:1000]) + + # At this point, we have our message + + if message["type"] == "audio" and message["format"].startswith("bytes"): + + if not message["content"]: # If it was nothing / silence continue - # Ignore flags, we only needed them for audio ^ - if "content" not in message or message["content"] == None: - continue + # Convert bytes to audio file + # Format will be bytes.wav or bytes.opus + mime_type = "audio/" + message["format"].split(".")[1] + text = stt_bytes(message["content"], mime_type) + message = {"role": "user", "type": "message", "content": text} + + # At this point, we have only text messages # Custom stop message will halt us - if message["content"].lower().strip(".,!") == "stop": + if message["content"].lower().strip(".,! ") == "stop": continue # Load, append, and save conversation history @@ -173,19 +210,31 @@ async def listener(): # Yield to the event loop, so you actually send it out await asyncio.sleep(0.01) - # Speak full sentences out loud - if chunk["role"] == "assistant" and "content" in chunk: - accumulated_text += chunk["content"] - sentences = split_into_sentences(accumulated_text) - if is_full_sentence(sentences[-1]): - for sentence in sentences: - await stream_or_play_tts(sentence) - accumulated_text = "" - else: - for sentence in sentences[:-1]: - await stream_or_play_tts(sentence) - accumulated_text = sentences[-1] - + if os.getenv('TTS_RUNNER') == "server": + # Speak full sentences out loud + if chunk["role"] == "assistant" and "content" in chunk: + accumulated_text += chunk["content"] + sentences = split_into_sentences(accumulated_text) + + # If we're going to speak, say we're going to stop sending text. + # This should be fixed probably, we should be able to do both in parallel, or only one. + if any(is_full_sentence(sentence) for sentence in sentences): + await to_device.put({"role": "assistant", "type": "message", "end": True}) + + if is_full_sentence(sentences[-1]): + for sentence in sentences: + await stream_tts_to_device(sentence) + accumulated_text = "" + else: + for sentence in sentences[:-1]: + await stream_tts_to_device(sentence) + accumulated_text = sentences[-1] + + # If we're going to speak, say we're going to stop sending text. + # This should be fixed probably, we should be able to do both in parallel, or only one. + if any(is_full_sentence(sentence) for sentence in sentences): + await to_device.put({"role": "assistant", "type": "message", "start": True}) + # If we have a new message, save our progress and go back to the top if not from_user.empty(): @@ -215,19 +264,12 @@ async def listener(): break else: with open(conversation_history_path, 'w') as file: - json.dump(interpreter.messages, file, indent=4) - - -async def stream_or_play_tts(sentence): - - if os.getenv('TTS_RUNNER') == "server": - tts(sentence, play_audio=True) - else: - await to_device.put({"role": "assistant", "type": "audio", "format": "audio/mp3", "start": True}) - audio_bytes = tts(sentence, play_audio=False) - await to_device.put({"role": "assistant", "type": "audio", "format": "audio/mp3", "content": str(audio_bytes)}) - await to_device.put({"role": "assistant", "type": "audio", "format": "audio/mp3", "end": True}) + json.dump(interpreter.messages, file, indent=4) +async def stream_tts_to_device(sentence): + for chunk in stream_tts(sentence): + await to_device.put(chunk) + async def setup_ngrok(ngrok_auth_token, parsed_url): # Set up Ngrok logger.info("Setting up Ngrok") diff --git a/01OS/01OS/server/tts/tts.py b/01OS/01OS/server/tts/tts.py index 836a1dd..6106966 100644 --- a/01OS/01OS/server/tts/tts.py +++ b/01OS/01OS/server/tts/tts.py @@ -12,27 +12,28 @@ import os import subprocess import tempfile from pydub import AudioSegment -from pydub.playback import play -import simpleaudio as sa client = OpenAI() -def tts(text, play_audio): +chunk_size = 1024 + +def stream_tts(text): + """ + A generator that streams tts as LMC messages. + """ if os.getenv('ALL_LOCAL') == 'False': response = client.audio.speech.create( model="tts-1", voice="alloy", input=text, - response_format="mp3" + response_format="opus" ) - with tempfile.NamedTemporaryFile(suffix=".mp3") as temp_file: + with tempfile.NamedTemporaryFile(suffix=".opus") as temp_file: response.stream_to_file(temp_file.name) - - if play_audio: - audio = AudioSegment.from_mp3(temp_file.name) - play_audiosegment(audio) - return temp_file.read() + audio_bytes = temp_file.read() + file_type = "bytes.opus" + else: with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file: output_file = temp_file.name @@ -43,13 +44,19 @@ def tts(text, play_audio): '--output_file', output_file ], input=text, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - if play_audio: - audio = AudioSegment.from_wav(temp_file.name) - play_audiosegment(audio) - return temp_file.read() + audio_bytes = temp_file.read() + file_type = "bytes.wav" + + # Stream the audio + yield {"role": "assistant", "type": "audio", "format": file_type, "start": True} + for i in range(0, len(audio_bytes), chunk_size): + chunk = audio_bytes[i:i+chunk_size] + yield chunk + yield {"role": "assistant", "type": "audio", "format": file_type, "end": True} def play_audiosegment(audio): """ + UNUSED the default makes some pops. this fixes that """ @@ -73,3 +80,6 @@ def play_audiosegment(audio): # Wait for the playback to finish play_obj.wait_done() + # Delete the wav file + os.remove("output_audio.wav") + diff --git a/01OS/01OS/utils/accumulator.py b/01OS/01OS/utils/accumulator.py new file mode 100644 index 0000000..93f9c2f --- /dev/null +++ b/01OS/01OS/utils/accumulator.py @@ -0,0 +1,40 @@ +class Accumulator: + def __init__(self): + self.template = {"role": None, "type": None, "format": None, "content": None} + self.message = self.template + + def accumulate(self, chunk): + print(str(chunk)[:100]) + if type(chunk) == dict: + + if "format" in chunk and chunk["format"] == "active_line": + # We don't do anything with these + return None + + if "start" in chunk: + self.message = chunk + self.message.pop("start") + return None + + if "content" in chunk: + if any(self.message[key] != chunk[key] for key in self.message if key != "content"): + self.message = chunk + if "content" not in self.message: + self.message["content"] = chunk["content"] + else: + self.message["content"] += chunk["content"] + return None + + if "end" in chunk: + # We will proceed + message = self.message + self.message = self.template + return message + + if type(chunk) == bytes: + if "content" not in self.message or type(self.message["content"]) != bytes: + self.message["content"] = b"" + self.message["content"] += chunk + return None + + \ No newline at end of file diff --git a/01OS/output_audio.wav b/01OS/output_audio.wav deleted file mode 100644 index 44ec5e663502f7790104b017d96ad3483076cd0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49382 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%0D}=ToL} zjL}U0n6=1&)o|(+s}18gvx3fgJGFwfiM5n{od`-m(}^jwq5oQ3h9EC`)BQJryE+3f7=k=cEguhRyz zvKeRDKfy8E#{$y|XJ9(O`VOZ>b#j>T%*i!&msvN)eWw<%`0n^RdmN7v9x~nj+BU`^ z_FufLHLPc0J&?1W%$hMyIgjkW=bU{IyUVQQ*QX3smd2@ztYsSP3bUNf-h`FFP-S+8 zmBX%_{AVrwst4?;Gakd3A;EHB)|l0ZX#_huQDZ2xQk+L8`x(+-YtE2nDOio1wfKrJ zE5q4iv7@te_D_3GiL!A3s~^MP8BbqQ4_0ek$rI;nrn7!59~M|mzp?^_{^x_6xcgPI z?2M&yqT+n!w8X5I%z`=bcWO4niCGHg(OEL9t&@XHdQNM`Mue=dV&hHL3dR{0hxL!F z=B&roA z4$FtdVn>z_!;)Py`8uV_+R4(fBP;XYBdb5V?>v5G#Mk;Vq*$BThvC6WU@_T|ov{y# z!SZzKG0U4_$L_G0?DMrWRt~$*YT}d(J7YC=w&SaYe$`v2Hvg;LjBiffIhpB<&royb z!E$A#IZJfvfYY`xhA|Cfzbr7R{MW!F$%3=*`g-NO{uH|aTYcJ)jE&FxeWigz%GVECn?E5tbhOkq{&iwzai8G!P35JPN zubDJh9T?^|yb%$+#j+YQi7>`6-Dg_B%5eVvn?FnEtiQ9Af3N=Qldt7|y@zc6lkVR; z&RVhGuS{?r85jQR$0^gVzh8O6uAO?~eDYQHU$6dUgEOY{{(t@buPOe`^=rvY-~PwX z*Sh|XPyXND`JcD_-(3GYMkS#ApFd8%f4%%Sjq~jPF6aMwtpA?MnKvtk#r|5-|Nil7 z+W(r%*Yp4W?CaJ4ei{GQ6#uK7{#Vcb&FBAFtbgOX&Rht%5O5*jLcoQ93jr4bE(BZ% zxDaq5;6lKKfC~W^0xkqx2)Gb%A>cy5g@6kI7XmH>TnM-ja3SDAz=ePd0T%);1Y8KX z5O5*jLcoQ93jr4bE(BZ%xDaq5;6lKKfC~W^0xkqx2)Gb%A>cy5g@6kI7XmH>TnM-j za3SDAz=ePd0T%);1Y8KX5O5*jLcoQ93jr4bE(BZ%xDaq5;6lKKfC~W^0xkqx2)Gb% kA>cy5g@6kI7XmH>TnM-ja3SDAz=ePd0T%-Q|3cvZ08XSD=l}o!