Added tests

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

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

@ -59,11 +59,10 @@ class Device:
while True: while True:
try: try:
data = await self.websocket.recv() data = await self.websocket.recv()
if isinstance(data, bytes) and not self.recording: if self.play_audio and isinstance(data, bytes) and not self.recording:
if self.play_audio: self.output_stream.write(data)
self.output_stream.write(data)
except Exception as e: except Exception as e:
print(f"Error in receive_audio: {e}") await self.connect_with_retry()
def on_press(self, key): def on_press(self, key):
if key == keyboard.Key.space and not self.recording: 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 RealtimeTTS import TextToAudioStream, CoquiEngine, OpenAIEngine, ElevenlabsEngine
from fastapi.responses import PlainTextResponse
from RealtimeSTT import AudioToTextRecorder from RealtimeSTT import AudioToTextRecorder
import importlib
import asyncio
import types import types
import time
import wave import wave
import asyncio import os
from fastapi.responses import PlainTextResponse
def start_server(server_host, server_port, profile, debug, play_audio): 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 interpreter.stt.stop() # It needs this for some reason
# TTS # TTS
if not hasattr(interpreter, 'tts'): if not hasattr(interpreter, 'tts'):
print("Setting TTS provider to default: openai") 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.verbose = debug
interpreter.server.host = server_host interpreter.server.host = server_host
interpreter.server.port = server_port interpreter.server.port = server_port
interpreter.play_audio = play_audio interpreter.play_audio = play_audio
interpreter.audio_chunks = [] interpreter.audio_chunks = []
old_input = interpreter.input ### Swap out the input function for one that supports voice
old_output = interpreter.output
old_input = interpreter.input
async def new_input(self, chunk): async def new_input(self, chunk):
await asyncio.sleep(0) 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}) 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): async def new_output(self):
while True: while True:
output = await old_output() output = await old_output()
@ -100,25 +97,29 @@ def start_server(server_host, server_port, profile, debug, play_audio):
delimiters = ".?!;,\n…)]}" delimiters = ".?!;,\n…)]}"
if output["type"] == "message" and len(output.get("content", "")) > 0: if output["type"] == "message" and len(output.get("content", "")) > 0:
self.tts.feed(output.get("content")) 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. 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} return {"role": "assistant", "type": "audio", "format": "bytes.wav", "start": True}
if output == {"role": "assistant", "type": "message", "end": 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^ 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", "start": True}
return {"role": "assistant", "type": "audio", "format": "bytes.wav", "end": True} return {"role": "assistant", "type": "audio", "format": "bytes.wav", "end": True}
def on_tts_chunk(self, chunk): def on_tts_chunk(self, chunk):
self.output_queue.sync_q.put(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.input = types.MethodType(new_input, interpreter)
interpreter.output = types.MethodType(new_output, interpreter) interpreter.output = types.MethodType(new_output, interpreter)
interpreter.on_tts_chunk = types.MethodType(on_tts_chunk, interpreter) interpreter.on_tts_chunk = types.MethodType(on_tts_chunk, interpreter)
# Add ping route, required by device
@interpreter.server.app.get("/ping") @interpreter.server.app.get("/ping")
async def ping(): async def ping():
return PlainTextResponse("pong") 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 import pytest
@pytest.mark.skip(reason="pytest hanging") import subprocess
def test_ping(client): import time
response = client.get("/ping")
assert response.status_code == 200 def test_poetry_run_01():
assert response.text == "pong" 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): # def test_interpreter_chat(mock_interpreter):

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

Loading…
Cancel
Save