Added tests
This commit is contained in:
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
|
||||
|
||||
# Install Poetry and project dependencies
|
||||
- name: Install Poetry Package
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install poetry==1.3.2
|
||||
poetry config virtualenvs.create false
|
||||
poetry install --no-interaction --with dev
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
# 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:
|
||||
self.output_stream.write(data)
|
||||
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 RealtimeSTT import AudioToTextRecorder
|
||||
import types
|
||||
import time
|
||||
import wave
|
||||
import asyncio
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from RealtimeSTT import AudioToTextRecorder
|
||||
import importlib
|
||||
import asyncio
|
||||
import types
|
||||
import wave
|
||||
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:
|
||||
|
||||
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
|
||||
else:
|
||||
play_audio = False
|
||||
# if client:
|
||||
# play_audio = True
|
||||
|
||||
server_thread = threading.Thread(
|
||||
target=start_server,
|
||||
args=(
|
||||
|
@ -178,11 +181,12 @@ def _run(
|
|||
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!
|
||||
if server:
|
||||
play_audio = False
|
||||
else:
|
||||
play_audio = True
|
||||
# if server:
|
||||
# play_audio = False
|
||||
|
||||
client_thread = threading.Thread(target=module.main, args=[server_url, debug, play_audio])
|
||||
client_thread.start()
|
||||
|
|
Loading…
Reference in New Issue