Skip to content

Commit 1508930

Browse files
committed
Add comprehensive tests covering e2e, audio modules, and client utilities
1 parent 3995fb0 commit 1508930

14 files changed

+577
-0
lines changed

tests/__init__.py

Whitespace-only changes.

tests/e2e_test_convai.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import os
2+
import time
3+
import asyncio
4+
5+
import pytest
6+
from neuralaudio import NeuralAudio
7+
from neuralaudio.conversational_ai.conversation import Conversation, ClientTools
8+
from neuralaudio.conversational_ai.default_audio_interface import DefaultAudioInterface
9+
10+
11+
@pytest.mark.skipif(os.getenv("CI") == "true", reason="Skip live conversation test in CI environment")
12+
def test_live_conversation():
13+
"""Test a live conversation with actual audio I/O"""
14+
15+
api_key = os.getenv("NEURALAUDIO_API_KEY")
16+
if not api_key:
17+
raise ValueError("NEURALAUDIO_API_KEY environment variable missing.")
18+
19+
agent_id = os.getenv("AGENT_ID")
20+
if not api_key or not agent_id:
21+
raise ValueError("AGENT_ID environment variable missing.")
22+
23+
client = NeuralAudio(api_key=api_key)
24+
25+
# Create conversation handlers
26+
def on_agent_response(text: str):
27+
print(f"Agent: {text}")
28+
29+
def on_user_transcript(text: str):
30+
print(f"You: {text}")
31+
32+
def on_latency(ms: int):
33+
print(f"Latency: {ms}ms")
34+
35+
# Initialize client tools
36+
client_tools = ClientTools()
37+
38+
def test(parameters):
39+
print("Sync tool called with parameters:", parameters)
40+
return "Tool called successfully"
41+
42+
async def test_async(parameters):
43+
# Simulate some async work
44+
await asyncio.sleep(10)
45+
print("Async tool called with parameters:", parameters)
46+
return "Tool called successfully"
47+
48+
client_tools.register("test", test)
49+
client_tools.register("test_async", test_async, is_async=True)
50+
51+
# Initialize conversation
52+
conversation = Conversation(
53+
client=client,
54+
agent_id=agent_id,
55+
requires_auth=False,
56+
audio_interface=DefaultAudioInterface(),
57+
callback_agent_response=on_agent_response,
58+
callback_user_transcript=on_user_transcript,
59+
callback_latency_measurement=on_latency,
60+
client_tools=client_tools,
61+
)
62+
63+
# Start the conversation
64+
conversation.start_session()
65+
66+
# Let it run for 100 seconds
67+
time.sleep(100)
68+
69+
# End the conversation
70+
conversation.end_session()
71+
conversation.wait_for_session_end()
72+
73+
# Get the conversation ID for reference
74+
conversation_id = conversation._conversation_id
75+
print(f"Conversation ID: {conversation_id}")
76+
77+
78+
if __name__ == "__main__":
79+
test_live_conversation()

tests/fixtures/voice_sample.mp3

24.5 KB
Binary file not shown.

tests/test_audio_isolation.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from neuralaudio import play
2+
from neuralaudio.client import NeuralAudio
3+
4+
from .utils import IN_GITHUB, DEFAULT_VOICE_FILE
5+
6+
7+
def test_audio_isolation() -> None:
8+
"""Test basic audio isolation."""
9+
client = NeuralAudio()
10+
audio_file = open(DEFAULT_VOICE_FILE, "rb")
11+
try:
12+
audio_stream = client.audio_isolation.audio_isolation(audio=audio_file)
13+
audio = b"".join(chunk for chunk in audio_stream)
14+
assert isinstance(audio, bytes), "Combined audio should be bytes"
15+
if not IN_GITHUB:
16+
play(audio)
17+
finally:
18+
audio_file.close()
19+
20+
21+
def test_audio_isolation_as_stream():
22+
"""Test audio isolation with streaming."""
23+
client = NeuralAudio()
24+
audio_file = open(DEFAULT_VOICE_FILE, "rb")
25+
try:
26+
audio_stream = client.audio_isolation.audio_isolation_stream(audio=audio_file)
27+
audio = b"".join(chunk for chunk in audio_stream)
28+
assert isinstance(audio, bytes), "Combined audio should be bytes"
29+
if not IN_GITHUB:
30+
play(audio)
31+
finally:
32+
audio_file.close()

tests/test_convai.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
from unittest.mock import MagicMock, patch
2+
from neuralaudio.conversational_ai.conversation import Conversation, AudioInterface, ConversationInitiationData
3+
import json
4+
import time
5+
6+
7+
class MockAudioInterface(AudioInterface):
8+
def start(self, input_callback):
9+
print("Audio interface started")
10+
self.input_callback = input_callback
11+
12+
def stop(self):
13+
print("Audio interface stopped")
14+
15+
def output(self, audio):
16+
print(f"Would play audio of length: {len(audio)} bytes")
17+
18+
def interrupt(self):
19+
print("Audio interrupted")
20+
21+
22+
# Add test constants and helpers at module level
23+
TEST_CONVERSATION_ID = "test123"
24+
TEST_AGENT_ID = "test_agent"
25+
26+
27+
def create_mock_websocket(messages=None):
28+
"""Helper to create a mock websocket with predefined responses"""
29+
mock_ws = MagicMock()
30+
31+
if messages is None:
32+
messages = [
33+
{
34+
"type": "conversation_initiation_metadata",
35+
"conversation_initiation_metadata_event": {"conversation_id": TEST_CONVERSATION_ID},
36+
},
37+
{"type": "agent_response", "agent_response_event": {"agent_response": "Hello there!"}},
38+
]
39+
40+
def response_generator():
41+
for msg in messages:
42+
yield json.dumps(msg)
43+
while True:
44+
yield '{"type": "keep_alive"}'
45+
46+
mock_ws.recv = MagicMock(side_effect=response_generator())
47+
return mock_ws
48+
49+
50+
def test_conversation_basic_flow():
51+
# Mock setup
52+
mock_ws = create_mock_websocket()
53+
mock_client = MagicMock()
54+
agent_response_callback = MagicMock()
55+
56+
# Setup the conversation
57+
conversation = Conversation(
58+
client=mock_client,
59+
agent_id=TEST_AGENT_ID,
60+
requires_auth=False,
61+
audio_interface=MockAudioInterface(),
62+
callback_agent_response=agent_response_callback,
63+
)
64+
65+
# Run the test
66+
with patch("neuralaudio.conversational_ai.conversation.connect") as mock_connect:
67+
mock_connect.return_value.__enter__.return_value = mock_ws
68+
conversation.start_session()
69+
70+
# Add a wait for the callback to be called
71+
timeout = 5 # 5 seconds timeout
72+
start_time = time.time()
73+
while not agent_response_callback.called and time.time() - start_time < timeout:
74+
time.sleep(0.1)
75+
76+
conversation.end_session()
77+
conversation.wait_for_session_end()
78+
79+
# Assertions
80+
expected_init_message = {
81+
"type": "conversation_initiation_client_data",
82+
"custom_llm_extra_body": {},
83+
"conversation_config_override": {},
84+
"dynamic_variables": {},
85+
}
86+
mock_ws.send.assert_any_call(json.dumps(expected_init_message))
87+
agent_response_callback.assert_called_once_with("Hello there!")
88+
assert conversation._conversation_id == TEST_CONVERSATION_ID
89+
90+
91+
def test_conversation_with_auth():
92+
# Mock setup
93+
mock_client = MagicMock()
94+
mock_client.conversational_ai.get_signed_url.return_value.signed_url = "wss://signed.url"
95+
mock_ws = create_mock_websocket(
96+
[
97+
{
98+
"type": "conversation_initiation_metadata",
99+
"conversation_initiation_metadata_event": {"conversation_id": TEST_CONVERSATION_ID},
100+
}
101+
]
102+
)
103+
104+
conversation = Conversation(
105+
client=mock_client,
106+
agent_id=TEST_AGENT_ID,
107+
requires_auth=True,
108+
audio_interface=MockAudioInterface(),
109+
)
110+
111+
# Run the test
112+
with patch("neuralaudio.conversational_ai.conversation.connect") as mock_connect:
113+
mock_connect.return_value.__enter__.return_value = mock_ws
114+
conversation.start_session()
115+
conversation.end_session()
116+
conversation.wait_for_session_end()
117+
118+
# Assertions
119+
mock_client.conversational_ai.get_signed_url.assert_called_once_with(agent_id=TEST_AGENT_ID)
120+
121+
def test_conversation_with_dynamic_variables():
122+
# Mock setup
123+
mock_ws = create_mock_websocket()
124+
mock_client = MagicMock()
125+
agent_response_callback = MagicMock()
126+
127+
dynamic_variables = {"name": "angelo"}
128+
config = ConversationInitiationData(dynamic_variables=dynamic_variables)
129+
130+
# Setup the conversation
131+
conversation = Conversation(
132+
client=mock_client,
133+
config=config,
134+
agent_id=TEST_AGENT_ID,
135+
requires_auth=False,
136+
audio_interface=MockAudioInterface(),
137+
callback_agent_response=agent_response_callback,
138+
)
139+
140+
# Run the test
141+
with patch("neuralaudio.conversational_ai.conversation.connect") as mock_connect:
142+
mock_connect.return_value.__enter__.return_value = mock_ws
143+
conversation.start_session()
144+
145+
# Add a wait for the callback to be called
146+
timeout = 5 # 5 seconds timeout
147+
start_time = time.time()
148+
while not agent_response_callback.called and time.time() - start_time < timeout:
149+
time.sleep(0.1)
150+
151+
conversation.end_session()
152+
conversation.wait_for_session_end()
153+
154+
# Assertions
155+
expected_init_message = {
156+
"type": "conversation_initiation_client_data",
157+
"custom_llm_extra_body": {},
158+
"conversation_config_override": {},
159+
"dynamic_variables": {
160+
"name": "angelo"
161+
},
162+
}
163+
mock_ws.send.assert_any_call(json.dumps(expected_init_message))
164+
agent_response_callback.assert_called_once_with("Hello there!")
165+
assert conversation._conversation_id == TEST_CONVERSATION_ID

tests/test_history.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from neuralaudio import GetSpeechHistoryResponse, NeuralAudio
2+
3+
4+
def test_history():
5+
client = NeuralAudio()
6+
page_size = 5
7+
history = client.history.get_all(page_size=page_size)
8+
assert isinstance(history, GetSpeechHistoryResponse)

tests/test_models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from neuralaudio import Model
2+
from neuralaudio.client import NeuralAudio
3+
4+
5+
def test_models_get_all():
6+
client = NeuralAudio()
7+
models = client.models.get_all()
8+
assert len(models) > 0
9+
assert isinstance(models[0], Model)

tests/test_sts.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from neuralaudio import play
2+
from neuralaudio.client import NeuralAudio
3+
4+
from .utils import IN_GITHUB, DEFAULT_VOICE, DEFAULT_VOICE_FILE
5+
6+
7+
def test_sts() -> None:
8+
"""Test basic speech-to-speech generation."""
9+
client = NeuralAudio()
10+
audio_file = open(DEFAULT_VOICE_FILE, "rb")
11+
try:
12+
audio_stream = client.speech_to_speech.convert(voice_id=DEFAULT_VOICE, audio=audio_file)
13+
audio = b"".join(chunk for chunk in audio_stream)
14+
assert isinstance(audio, bytes), "Combined audio should be bytes"
15+
if not IN_GITHUB:
16+
play(audio)
17+
finally:
18+
audio_file.close()
19+
20+
21+
def test_sts_as_stream():
22+
client = NeuralAudio()
23+
audio_file = open(DEFAULT_VOICE_FILE, "rb")
24+
try:
25+
audio_stream = client.speech_to_speech.convert_as_stream(voice_id=DEFAULT_VOICE, audio=audio_file)
26+
audio = b"".join(chunk for chunk in audio_stream)
27+
assert isinstance(audio, bytes), "Combined audio should be bytes"
28+
if not IN_GITHUB:
29+
play(audio)
30+
finally:
31+
audio_file.close()

tests/test_stt.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pytest
2+
from neuralaudio.client import AsyncNeuralAudio, NeuralAudio
3+
4+
from .utils import DEFAULT_VOICE_FILE
5+
6+
DEFAULT_EXT_AUDIO = "https://storage.googleapis.com/neuralaudio-public-cdn/audio/marketing/nicole.mp3"
7+
8+
9+
10+
@pytest.mark.asyncio
11+
async def test_stt_convert():
12+
"""Test basic speech-to-text conversion."""
13+
client = NeuralAudio()
14+
15+
audio_file = open(DEFAULT_VOICE_FILE, "rb")
16+
17+
transcription = client.speech_to_text.convert(
18+
file=audio_file,
19+
model_id="scribe_v1"
20+
)
21+
22+
assert isinstance(transcription.text, str)
23+
assert len(transcription.text) > 0
24+
assert isinstance(transcription.words, list)
25+
assert len(transcription.words) > 0
26+
27+
@pytest.mark.asyncio
28+
async def test_stt_convert_as_stream():
29+
"""Test speech-to-text conversion as stream."""
30+
client = AsyncNeuralAudio()
31+
32+
audio_file = open(DEFAULT_VOICE_FILE, "rb")
33+
34+
stream = client.speech_to_text.convert_as_stream(
35+
file=audio_file,
36+
model_id="scribe_v1"
37+
)
38+
39+
transcription_text = ""
40+
async for chunk in stream:
41+
assert isinstance(chunk.text, str)
42+
transcription_text += chunk.text
43+
44+
assert len(transcription_text) > 0

0 commit comments

Comments
 (0)