Skip to content

Commit 8171c8d

Browse files
Merge pull request #13 from akshatvasisht/unified-test-suite
Unified test suite
2 parents d5e22c5 + 6c1b72a commit 8171c8d

32 files changed

+1169
-185
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,7 @@ logs/
3939

4040
# Generated assets
4141
**/*.wav
42+
43+
# Cache and Agent Context
44+
.hf/
45+
agentcontext/

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
![Python](https://img.shields.io/badge/Python-3.10%2B-blue?logo=python&logoColor=white)
1111
![FastAPI](https://img.shields.io/badge/FastAPI-0.109-009688?logo=fastapi&logoColor=white)
1212
![PyTorch](https://img.shields.io/badge/PyTorch-2.0-EE4C2C?logo=pytorch&logoColor=white)
13-
![Whisper](https://img.shields.io/badge/AI-Faster--Whisper-violet)
13+
![Whisper](https://img.shields.io/badge/STT-Faster--Whisper-violet)
14+
![TTS](https://img.shields.io/badge/TTS-Fish%20Audio-8B5CF6)
1415
![MsgPack](https://img.shields.io/badge/Protocol-MessagePack-informational)
16+
![TypeScript](https://img.shields.io/badge/TypeScript-5.0-3178C6?logo=typescript&logoColor=white)
17+
![Next.js](https://img.shields.io/badge/Next.js-15.0-black?logo=next.js&logoColor=white)
18+
![Tailwind](https://img.shields.io/badge/Tailwind_CSS-3.4-38B2AC?logo=tailwind-css&logoColor=white)
1519
![License](https://img.shields.io/badge/License-MIT-green)
1620
![Event](https://img.shields.io/badge/Submission-MadHacks%202025-FFD700)
1721

backend/api/socket_manager.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
1212

1313
from ..common import engine_state
14-
from .types import ControlMessage, JanusOutboundMessage
14+
from .types import ControlMessage, JanusOutboundMessage, ControlStateMessage
1515

1616
router = APIRouter()
1717

@@ -32,6 +32,17 @@ async def janus_ws(websocket: WebSocket) -> None:
3232
"""
3333
await websocket.accept()
3434

35+
# Send initial state to sync UI
36+
state = engine_state.control_state
37+
initial_state = ControlStateMessage(
38+
type="control_state",
39+
is_streaming=state.is_streaming,
40+
is_recording=state.is_recording,
41+
mode=state.mode,
42+
emotion_override=state.emotion_override,
43+
)
44+
await _send_event(websocket, initial_state)
45+
3546
recv_task = asyncio.create_task(_recv_loop(websocket))
3647
send_task = asyncio.create_task(_send_loop(websocket))
3748

@@ -56,10 +67,22 @@ async def janus_ws(websocket: WebSocket) -> None:
5667
except Exception as e:
5768
print(f"WebSocket handler error: {e}")
5869
finally:
70+
_reset_control_state()
5971
recv_task.cancel()
6072
send_task.cancel()
6173

6274

75+
def _reset_control_state() -> None:
76+
"""
77+
Reset engine_state.control_state to idle values.
78+
"""
79+
state = engine_state.control_state
80+
state.is_streaming = False
81+
state.is_recording = False
82+
state.is_talking = False
83+
print(f"Control State Reset on Disconnect: {state}")
84+
85+
6386
async def _recv_loop(websocket: WebSocket) -> None:
6487
"""
6588
Receive ControlMessage payloads from frontend and update control_state.

backend/api/types.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,15 @@ class PacketSummaryMessage(BaseModel):
5454
emotion: Optional[str] = None
5555
snippet: Optional[str] = None
5656

57+
class ControlStateMessage(BaseModel):
58+
"""
59+
Current full state of the engine, sent to synchronise frontend.
60+
"""
61+
type: Literal["control_state"]
62+
is_streaming: bool
63+
is_recording: bool
64+
mode: JanusMode
65+
emotion_override: EmotionOverride
66+
5767
# Union type for outbound messages
58-
JanusOutboundMessage = TranscriptMessage | PacketSummaryMessage
68+
JanusOutboundMessage = TranscriptMessage | PacketSummaryMessage | ControlStateMessage

backend/scripts/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
Backend scripts package.
3+
4+
Contains CLI utility scripts for testing and development, including the main
5+
sender and receiver entry points for the Janus audio pipeline.
6+
"""
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Utility script to list all available PyAudio input and output devices.
4+
Helps in identifying device indices for configuration.
5+
"""
6+
import pyaudio
7+
import sys
8+
9+
def list_devices():
10+
p = pyaudio.PyAudio()
11+
print(f"PyAudio Version: {pyaudio.__version__}")
12+
print(f"Default Input Device: {p.get_default_input_device_info() if p.get_device_count() > 0 else 'None'}")
13+
print(f"Default Output Device: {p.get_default_output_device_info() if p.get_device_count() > 0 else 'None'}")
14+
15+
print("\nAll Devices:")
16+
for i in range(p.get_device_count()):
17+
info = p.get_device_info_by_index(i)
18+
print(f"Index {i}: {info['name']} (Inputs: {info['maxInputChannels']}, Outputs: {info['maxOutputChannels']})")
19+
20+
p.terminate()
21+
22+
if __name__ == "__main__":
23+
try:
24+
list_devices()
25+
except Exception as e:
26+
print(f"Error: {e}")

backend/scripts/receiver_main.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,19 @@ def playback_worker(
8080
playback_queue.task_done()
8181

8282

83-
def receiver_loop() -> None:
83+
def receiver_loop(stop_event: threading.Event | None = None) -> None:
8484
"""
8585
Main receiver loop entry point.
8686
8787
Listens for incoming packets, deserializes JanusPackets, synthesizes audio,
8888
and plays it through the audio service. Supports both TCP and UDP protocols.
8989
Configuration is loaded from environment variables.
9090
91+
Args:
92+
stop_event: Optional threading event to signal shutdown. If None, creates
93+
an internal event and waits for KeyboardInterrupt. If provided,
94+
exits when the event is set.
95+
9196
Returns:
9297
None
9398
@@ -122,7 +127,13 @@ def receiver_loop() -> None:
122127
logger.info(f"Listening for Transmissions on UDP port {receiver_port}...")
123128

124129
playback_queue = queue.Queue(maxsize=100)
125-
stop_event = threading.Event()
130+
131+
# Use provided stop_event or create internal one
132+
if stop_event is None:
133+
stop_event = threading.Event()
134+
use_keyboard_interrupt = True
135+
else:
136+
use_keyboard_interrupt = False
126137

127138
playback_thread = threading.Thread(
128139
target=playback_worker,
@@ -133,6 +144,10 @@ def receiver_loop() -> None:
133144

134145
try:
135146
while True:
147+
# Exit if stop_event is set (test mode)
148+
if not use_keyboard_interrupt and stop_event.is_set():
149+
break
150+
136151
try:
137152
if use_tcp:
138153
length_bytes = recv_exact(sock, 4)
@@ -146,7 +161,12 @@ def receiver_loop() -> None:
146161
logger.info("Connection closed while reading packet")
147162
break
148163
else:
149-
data, addr = sock.recvfrom(4096)
164+
# Set timeout for UDP to allow periodic stop_event checking
165+
sock.settimeout(0.5)
166+
try:
167+
data, addr = sock.recvfrom(4096)
168+
except socket.timeout:
169+
continue
150170

151171
try:
152172
packet = JanusPacket.deserialize(data)

0 commit comments

Comments
 (0)