Skip to content

Commit fc97ef6

Browse files
committed
Added tests
1 parent d2496fa commit fc97ef6

File tree

7 files changed

+116
-118
lines changed

7 files changed

+116
-118
lines changed

.github/workflows/test.yml

+19-21
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ jobs:
1717
strategy:
1818
fail-fast: false
1919
matrix:
20-
os: [macos-latest]
21-
# os: [ubuntu-latest, windows-latest, macos-latest]
20+
# os: [macos-latest]
21+
os: [ubuntu-latest, windows-latest, macos-latest]
2222
python-version: ["3.11"]
2323

2424
defaults:
@@ -31,32 +31,30 @@ jobs:
3131
- name: Checkout
3232
uses: actions/checkout@v4
3333

34-
- name: Install poetry
35-
run: pipx install poetry
36-
3734
- name: Setup Python ${{ matrix.python-version }}
3835
uses: actions/setup-python@v5
3936
with:
4037
python-version: ${{ matrix.python-version }}
41-
cache: "poetry"
42-
43-
# Install Portaudio on Ubuntu
44-
- name: Installing Portaudio in Ubuntu
45-
if: matrix.os == 'ubuntu-latest'
46-
run: sudo apt-get install portaudio19-dev python-all-dev
4738

48-
# Install Portaudio on macOS using Homebrew
49-
- name: Installing Portaudio in Mac
50-
if: matrix.os == 'macos-latest'
51-
run: brew install portaudio
39+
- name: Install poetry
40+
run: |
41+
curl -sSL https://install.python-poetry.org | python3 -
5242
53-
# Install Poetry and project dependencies
54-
- name: Install Poetry Package
43+
- name: Install dependencies
5544
run: |
56-
pip install --upgrade pip
57-
pip install poetry==1.3.2
58-
poetry config virtualenvs.create false
59-
poetry install --no-interaction --with dev
45+
# Ensure dependencies are installed without relying on a lock file.
46+
poetry update
47+
poetry install
48+
49+
# # Install Portaudio on Ubuntu
50+
# - name: Installing Portaudio in Ubuntu
51+
# if: matrix.os == 'ubuntu-latest'
52+
# run: sudo apt-get install portaudio19-dev python-all-dev
53+
54+
# # Install Portaudio on macOS using Homebrew
55+
# - name: Installing Portaudio in Mac
56+
# if: matrix.os == 'macos-latest'
57+
# run: brew install portaudio
6058

6159
# Run pytest
6260
- name: Run Pytest

software/source/clients/base_device.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,10 @@ async def receive_audio(self):
5959
while True:
6060
try:
6161
data = await self.websocket.recv()
62-
if isinstance(data, bytes) and not self.recording:
63-
if self.play_audio:
64-
self.output_stream.write(data)
62+
if self.play_audio and isinstance(data, bytes) and not self.recording:
63+
self.output_stream.write(data)
6564
except Exception as e:
66-
print(f"Error in receive_audio: {e}")
65+
await self.connect_with_retry()
6766

6867
def on_press(self, key):
6968
if key == keyboard.Key.space and not self.recording:

software/source/clients/mac/beeps.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
Mac only.
3+
"""
4+
5+
import subprocess
6+
import threading
7+
import time
8+
9+
def beep(sound):
10+
if "." not in sound:
11+
sound = sound + ".aiff"
12+
try:
13+
subprocess.Popen(["afplay", f"/System/Library/Sounds/{sound}"])
14+
except:
15+
pass # No big deal
16+
17+
class RepeatedBeep:
18+
def __init__(self):
19+
self.sound = "Pop"
20+
self.running = False
21+
self.thread = threading.Thread(target=self._play_sound, daemon=True)
22+
self.thread.start()
23+
24+
def _play_sound(self):
25+
while True:
26+
if self.running:
27+
try:
28+
subprocess.call(["afplay", f"/System/Library/Sounds/{self.sound}.aiff"])
29+
except:
30+
pass # No big deal
31+
time.sleep(0.6)
32+
time.sleep(0.05)
33+
34+
def start(self):
35+
if not self.running:
36+
time.sleep(0.6*4)
37+
self.running = True
38+
39+
def stop(self):
40+
self.running = False
41+
42+
beeper = RepeatedBeep()

software/source/server/async_server.py

+17-16
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import importlib
2-
import traceback
3-
import json
4-
import os
51
from RealtimeTTS import TextToAudioStream, CoquiEngine, OpenAIEngine, ElevenlabsEngine
2+
from fastapi.responses import PlainTextResponse
63
from RealtimeSTT import AudioToTextRecorder
4+
import importlib
5+
import asyncio
76
import types
8-
import time
97
import wave
10-
import asyncio
11-
from fastapi.responses import PlainTextResponse
8+
import os
129

1310
def start_server(server_host, server_port, profile, debug, play_audio):
1411

@@ -26,7 +23,6 @@ def start_server(server_host, server_port, profile, debug, play_audio):
2623
)
2724
interpreter.stt.stop() # It needs this for some reason
2825

29-
3026
# TTS
3127
if not hasattr(interpreter, 'tts'):
3228
print("Setting TTS provider to default: openai")
@@ -46,16 +42,13 @@ def start_server(server_host, server_port, profile, debug, play_audio):
4642
interpreter.verbose = debug
4743
interpreter.server.host = server_host
4844
interpreter.server.port = server_port
49-
5045
interpreter.play_audio = play_audio
51-
52-
5346
interpreter.audio_chunks = []
5447

5548

56-
old_input = interpreter.input
57-
old_output = interpreter.output
49+
### Swap out the input function for one that supports voice
5850

51+
old_input = interpreter.input
5952

6053
async def new_input(self, chunk):
6154
await asyncio.sleep(0)
@@ -86,6 +79,10 @@ async def new_input(self, chunk):
8679
await old_input({"role": "user", "type": "message", "end": True})
8780

8881

82+
### Swap out the output function for one that supports voice
83+
84+
old_output = interpreter.output
85+
8986
async def new_output(self):
9087
while True:
9188
output = await old_output()
@@ -100,25 +97,29 @@ async def new_output(self):
10097
delimiters = ".?!;,\n…)]}"
10198

10299
if output["type"] == "message" and len(output.get("content", "")) > 0:
100+
103101
self.tts.feed(output.get("content"))
102+
104103
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.
105-
self.tts.play_async(on_audio_chunk=self.on_tts_chunk, muted=not self.play_audio, sentence_fragment_delimiters=delimiters)
104+
self.tts.play_async(on_audio_chunk=self.on_tts_chunk, muted=not self.play_audio, sentence_fragment_delimiters=delimiters, minimum_sentence_length=9)
106105
return {"role": "assistant", "type": "audio", "format": "bytes.wav", "start": True}
107106

108107
if output == {"role": "assistant", "type": "message", "end": True}:
109108
if not self.tts.is_playing(): # We put this here in case it never outputs a delimiter and never triggers play_async^
110-
self.tts.play_async(on_audio_chunk=self.on_tts_chunk, muted=not self.play_audio, sentence_fragment_delimiters=delimiters)
109+
self.tts.play_async(on_audio_chunk=self.on_tts_chunk, muted=not self.play_audio, sentence_fragment_delimiters=delimiters, minimum_sentence_length=9)
111110
return {"role": "assistant", "type": "audio", "format": "bytes.wav", "start": True}
112111
return {"role": "assistant", "type": "audio", "format": "bytes.wav", "end": True}
113112

114113
def on_tts_chunk(self, chunk):
115114
self.output_queue.sync_q.put(chunk)
116115

117-
# Wrap in voice interface
116+
117+
# Set methods on interpreter object
118118
interpreter.input = types.MethodType(new_input, interpreter)
119119
interpreter.output = types.MethodType(new_output, interpreter)
120120
interpreter.on_tts_chunk = types.MethodType(on_tts_chunk, interpreter)
121121

122+
# Add ping route, required by device
122123
@interpreter.server.app.get("/ping")
123124
async def ping():
124125
return PlainTextResponse("pong")

software/source/server/skills/schedule.py

-64
This file was deleted.

software/source/server/tests/test_run.py

+23-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,29 @@
22
import pytest
33

44

5-
@pytest.mark.skip(reason="pytest hanging")
6-
def test_ping(client):
7-
response = client.get("/ping")
8-
assert response.status_code == 200
9-
assert response.text == "pong"
5+
import subprocess
6+
import time
7+
8+
def test_poetry_run_01():
9+
process = subprocess.Popen(['poetry', 'run', '01'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
10+
timeout = time.time() + 30 # 30 seconds from now
11+
12+
while True:
13+
output = process.stdout.readline().decode('utf-8')
14+
if "Hold spacebar to record." in output:
15+
assert True
16+
return
17+
if time.time() > timeout:
18+
assert False, "Timeout reached without finding expected output."
19+
return
20+
21+
22+
23+
# @pytest.mark.skip(reason="pytest hanging")
24+
# def test_ping(client):
25+
# response = client.get("/ping")
26+
# assert response.status_code == 200
27+
# assert response.text == "pong"
1028

1129

1230
# def test_interpreter_chat(mock_interpreter):

software/start.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,14 @@ def handle_exit(signum, frame):
134134
signal.signal(signal.SIGINT, handle_exit)
135135

136136
if server:
137+
138+
play_audio = False
139+
140+
# (DISABLED)
137141
# Have the server play audio if we're running this on the same device. Needless pops and clicks otherwise!
138-
if client:
139-
play_audio = True
140-
else:
141-
play_audio = False
142+
# if client:
143+
# play_audio = True
144+
142145
server_thread = threading.Thread(
143146
target=start_server,
144147
args=(
@@ -178,11 +181,12 @@ def handle_exit(signum, frame):
178181
f".clients.{client_type}.device", package="source"
179182
)
180183

184+
play_audio = True
185+
186+
# (DISABLED)
181187
# Have the server play audio if we're running this on the same device. Needless pops and clicks otherwise!
182-
if server:
183-
play_audio = False
184-
else:
185-
play_audio = True
188+
# if server:
189+
# play_audio = False
186190

187191
client_thread = threading.Thread(target=module.main, args=[server_url, debug, play_audio])
188192
client_thread.start()

0 commit comments

Comments
 (0)