Added tests

pull/266/merge
killian 6 months ago
parent d2496fa8a2
commit fc97ef6a24

@ -17,8 +17,8 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-latest]
# os: [ubuntu-latest, windows-latest, macos-latest]
# os: [macos-latest]
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.11"]
defaults:
@ -31,32 +31,30 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
# Install Portaudio on Ubuntu
- name: Installing Portaudio in Ubuntu
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get install portaudio19-dev python-all-dev
# Install Portaudio on macOS using Homebrew
- name: Installing Portaudio in Mac
if: matrix.os == 'macos-latest'
run: brew install portaudio
- name: Install poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
# Install Poetry and project dependencies
- name: Install Poetry Package
- name: Install dependencies
run: |
pip install --upgrade pip
pip install poetry==1.3.2
poetry config virtualenvs.create false
poetry install --no-interaction --with dev
# Ensure dependencies are installed without relying on a lock file.
poetry update
poetry install
# # Install Portaudio on Ubuntu
# - name: Installing Portaudio in Ubuntu
# if: matrix.os == 'ubuntu-latest'
# run: sudo apt-get install portaudio19-dev python-all-dev
# # Install Portaudio on macOS using Homebrew
# - name: Installing Portaudio in Mac
# if: matrix.os == 'macos-latest'
# run: brew install portaudio
# Run pytest
- name: Run Pytest

@ -59,11 +59,10 @@ class Device:
while True:
try:
data = await self.websocket.recv()
if isinstance(data, bytes) and not self.recording:
if self.play_audio:
if self.play_audio and isinstance(data, bytes) and not self.recording:
self.output_stream.write(data)
except Exception as e:
print(f"Error in receive_audio: {e}")
await self.connect_with_retry()
def on_press(self, key):
if key == keyboard.Key.space and not self.recording:

@ -0,0 +1,42 @@
"""
Mac only.
"""
import subprocess
import threading
import time
def beep(sound):
if "." not in sound:
sound = sound + ".aiff"
try:
subprocess.Popen(["afplay", f"/System/Library/Sounds/{sound}"])
except:
pass # No big deal
class RepeatedBeep:
def __init__(self):
self.sound = "Pop"
self.running = False
self.thread = threading.Thread(target=self._play_sound, daemon=True)
self.thread.start()
def _play_sound(self):
while True:
if self.running:
try:
subprocess.call(["afplay", f"/System/Library/Sounds/{self.sound}.aiff"])
except:
pass # No big deal
time.sleep(0.6)
time.sleep(0.05)
def start(self):
if not self.running:
time.sleep(0.6*4)
self.running = True
def stop(self):
self.running = False
beeper = RepeatedBeep()

@ -1,14 +1,11 @@
import importlib
import traceback
import json
import os
from RealtimeTTS import TextToAudioStream, CoquiEngine, OpenAIEngine, ElevenlabsEngine
from fastapi.responses import PlainTextResponse
from RealtimeSTT import AudioToTextRecorder
import importlib
import asyncio
import types
import time
import wave
import asyncio
from fastapi.responses import PlainTextResponse
import os
def start_server(server_host, server_port, profile, debug, play_audio):
@ -26,7 +23,6 @@ def start_server(server_host, server_port, profile, debug, play_audio):
)
interpreter.stt.stop() # It needs this for some reason
# TTS
if not hasattr(interpreter, 'tts'):
print("Setting TTS provider to default: openai")
@ -46,16 +42,13 @@ def start_server(server_host, server_port, profile, debug, play_audio):
interpreter.verbose = debug
interpreter.server.host = server_host
interpreter.server.port = server_port
interpreter.play_audio = play_audio
interpreter.audio_chunks = []
old_input = interpreter.input
old_output = interpreter.output
### Swap out the input function for one that supports voice
old_input = interpreter.input
async def new_input(self, chunk):
await asyncio.sleep(0)
@ -86,6 +79,10 @@ def start_server(server_host, server_port, profile, debug, play_audio):
await old_input({"role": "user", "type": "message", "end": True})
### Swap out the output function for one that supports voice
old_output = interpreter.output
async def new_output(self):
while True:
output = await old_output()
@ -100,25 +97,29 @@ def start_server(server_host, server_port, profile, debug, play_audio):
delimiters = ".?!;,\n…)]}"
if output["type"] == "message" and len(output.get("content", "")) > 0:
self.tts.feed(output.get("content"))
if not self.tts.is_playing() and any([c in delimiters for c in output.get("content")]): # Start playing once the first delimiter is encountered.
self.tts.play_async(on_audio_chunk=self.on_tts_chunk, muted=not self.play_audio, sentence_fragment_delimiters=delimiters)
self.tts.play_async(on_audio_chunk=self.on_tts_chunk, muted=not self.play_audio, sentence_fragment_delimiters=delimiters, minimum_sentence_length=9)
return {"role": "assistant", "type": "audio", "format": "bytes.wav", "start": True}
if output == {"role": "assistant", "type": "message", "end": True}:
if not self.tts.is_playing(): # We put this here in case it never outputs a delimiter and never triggers play_async^
self.tts.play_async(on_audio_chunk=self.on_tts_chunk, muted=not self.play_audio, sentence_fragment_delimiters=delimiters)
self.tts.play_async(on_audio_chunk=self.on_tts_chunk, muted=not self.play_audio, sentence_fragment_delimiters=delimiters, minimum_sentence_length=9)
return {"role": "assistant", "type": "audio", "format": "bytes.wav", "start": True}
return {"role": "assistant", "type": "audio", "format": "bytes.wav", "end": True}
def on_tts_chunk(self, chunk):
self.output_queue.sync_q.put(chunk)
# Wrap in voice interface
# Set methods on interpreter object
interpreter.input = types.MethodType(new_input, interpreter)
interpreter.output = types.MethodType(new_output, interpreter)
interpreter.on_tts_chunk = types.MethodType(on_tts_chunk, interpreter)
# Add ping route, required by device
@interpreter.server.app.get("/ping")
async def ping():
return PlainTextResponse("pong")

@ -1,64 +0,0 @@
import os
from datetime import datetime
from pytimeparse import parse
from crontab import CronTab
from uuid import uuid4
from platformdirs import user_data_dir
def schedule(message="", start=None, interval=None) -> None:
"""
Schedules a task at a particular time, or at a particular interval
"""
if start and interval:
raise ValueError("Cannot specify both start time and interval.")
if not start and not interval:
raise ValueError("Either start time or interval must be specified.")
# Read the temp file to see what the current session is
session_file_path = os.path.join(user_data_dir("01"), "01-session.txt")
with open(session_file_path, "r") as session_file:
file_session_value = session_file.read().strip()
prefixed_message = "AUTOMATED MESSAGE FROM SCHEDULER: " + message
# Escape the message and the json, cron is funky with quotes
escaped_question = prefixed_message.replace('"', '\\"')
json_data = f'{{\\"text\\": \\"{escaped_question}\\"}}'
command = f"""bash -c 'if [ "$(cat "{session_file_path}")" == "{file_session_value}" ]; then /usr/bin/curl -X POST -H "Content-Type: application/json" -d "{json_data}" http://localhost:10001/; fi' """
cron = CronTab(user=True)
job = cron.new(command=command)
# Prefix with 01 dev preview so we can delete them all in the future
job_id = "01-dev-preview-" + str(uuid4())
job.set_comment(job_id)
if start:
try:
start_time = datetime.strptime(start, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise ValueError(f"Invalid datetime format: {start}.")
job.setall(start_time)
print(f"Task scheduled for {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
elif interval:
seconds = parse(interval)
if seconds <= 60:
job.minute.every(1)
print("Task scheduled every minute")
elif seconds < 3600:
minutes = max(int(seconds / 60), 1)
job.minute.every(minutes)
print(f"Task scheduled every {minutes} minutes")
elif seconds < 86400:
hours = max(int(seconds / 3600), 1)
job.hour.every(hours)
print(f"Task scheduled every {hours} hour(s)")
else:
days = max(int(seconds / 86400), 1)
job.day.every(days)
print(f"Task scheduled every {days} day(s)")
cron.write()

@ -2,11 +2,29 @@
import pytest
@pytest.mark.skip(reason="pytest hanging")
def test_ping(client):
response = client.get("/ping")
assert response.status_code == 200
assert response.text == "pong"
import subprocess
import time
def test_poetry_run_01():
process = subprocess.Popen(['poetry', 'run', '01'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
timeout = time.time() + 30 # 30 seconds from now
while True:
output = process.stdout.readline().decode('utf-8')
if "Hold spacebar to record." in output:
assert True
return
if time.time() > timeout:
assert False, "Timeout reached without finding expected output."
return
# @pytest.mark.skip(reason="pytest hanging")
# def test_ping(client):
# response = client.get("/ping")
# assert response.status_code == 200
# assert response.text == "pong"
# def test_interpreter_chat(mock_interpreter):

@ -134,11 +134,14 @@ def _run(
signal.signal(signal.SIGINT, handle_exit)
if server:
# Have the server play audio if we're running this on the same device. Needless pops and clicks otherwise!
if client:
play_audio = True
else:
play_audio = False
# (DISABLED)
# Have the server play audio if we're running this on the same device. Needless pops and clicks otherwise!
# if client:
# play_audio = True
server_thread = threading.Thread(
target=start_server,
args=(
@ -178,12 +181,13 @@ def _run(
f".clients.{client_type}.device", package="source"
)
# Have the server play audio if we're running this on the same device. Needless pops and clicks otherwise!
if server:
play_audio = False
else:
play_audio = True
# (DISABLED)
# Have the server play audio if we're running this on the same device. Needless pops and clicks otherwise!
# if server:
# play_audio = False
client_thread = threading.Thread(target=module.main, args=[server_url, debug, play_audio])
client_thread.start()

Loading…
Cancel
Save