atom-audio-engine 0.1.2__py3-none-any.whl → 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {atom_audio_engine-0.1.2.dist-info → atom_audio_engine-0.1.5.dist-info}/METADATA +1 -1
  2. atom_audio_engine-0.1.5.dist-info/RECORD +32 -0
  3. audio_engine/__init__.py +1 -1
  4. audio_engine/asr/__init__.py +2 -3
  5. audio_engine/asr/base.py +1 -1
  6. audio_engine/asr/cartesia.py +4 -10
  7. audio_engine/asr/deepgram.py +1 -1
  8. audio_engine/core/__init__.py +3 -3
  9. audio_engine/core/config.py +4 -4
  10. audio_engine/core/pipeline.py +6 -10
  11. audio_engine/integrations/__init__.py +1 -1
  12. audio_engine/integrations/geneface.py +1 -1
  13. audio_engine/llm/__init__.py +2 -4
  14. audio_engine/llm/base.py +3 -5
  15. audio_engine/llm/groq.py +2 -4
  16. audio_engine/streaming/__init__.py +1 -1
  17. audio_engine/streaming/websocket_server.py +7 -15
  18. audio_engine/tts/__init__.py +2 -4
  19. audio_engine/tts/base.py +3 -5
  20. audio_engine/tts/cartesia.py +12 -34
  21. audio_engine/utils/__init__.py +1 -1
  22. audio_engine/utils/audio.py +1 -3
  23. atom_audio_engine-0.1.2.dist-info/RECORD +0 -57
  24. audio_engine/examples/__init__.py +0 -1
  25. audio_engine/examples/basic_stt_llm_tts.py +0 -200
  26. audio_engine/examples/geneface_animation.py +0 -99
  27. audio_engine/examples/personaplex_pipeline.py +0 -116
  28. audio_engine/examples/websocket_server.py +0 -86
  29. audio_engine/scripts/debug_pipeline.py +0 -79
  30. audio_engine/scripts/debug_tts.py +0 -162
  31. audio_engine/scripts/test_cartesia_connect.py +0 -57
  32. audio_engine/tests/__init__.py +0 -1
  33. audio_engine/tests/test_personaplex/__init__.py +0 -1
  34. audio_engine/tests/test_personaplex/test_personaplex.py +0 -10
  35. audio_engine/tests/test_personaplex/test_personaplex_client.py +0 -259
  36. audio_engine/tests/test_personaplex/test_personaplex_config.py +0 -71
  37. audio_engine/tests/test_personaplex/test_personaplex_message.py +0 -80
  38. audio_engine/tests/test_personaplex/test_personaplex_pipeline.py +0 -226
  39. audio_engine/tests/test_personaplex/test_personaplex_session.py +0 -184
  40. audio_engine/tests/test_personaplex/test_personaplex_transcript.py +0 -184
  41. audio_engine/tests/test_traditional_pipeline/__init__.py +0 -1
  42. audio_engine/tests/test_traditional_pipeline/test_cartesia_asr.py +0 -474
  43. audio_engine/tests/test_traditional_pipeline/test_config_env.py +0 -97
  44. audio_engine/tests/test_traditional_pipeline/test_conversation_context.py +0 -115
  45. audio_engine/tests/test_traditional_pipeline/test_pipeline_creation.py +0 -64
  46. audio_engine/tests/test_traditional_pipeline/test_pipeline_with_mocks.py +0 -173
  47. audio_engine/tests/test_traditional_pipeline/test_provider_factories.py +0 -61
  48. audio_engine/tests/test_traditional_pipeline/test_websocket_server.py +0 -58
  49. {atom_audio_engine-0.1.2.dist-info → atom_audio_engine-0.1.5.dist-info}/WHEEL +0 -0
  50. {atom_audio_engine-0.1.2.dist-info → atom_audio_engine-0.1.5.dist-info}/top_level.txt +0 -0
@@ -1,162 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Debug script: Test CartesiaTTS in isolation
4
- """
5
-
6
- import asyncio
7
- import sys
8
- import logging
9
- from pathlib import Path
10
-
11
- sys.path.insert(0, str(Path(__file__).parent))
12
-
13
- from dotenv import load_dotenv
14
- from tts.cartesia import CartesiaTTS
15
-
16
- # Load env variables
17
- load_dotenv()
18
-
19
- # Setup logging
20
- logging.basicConfig(
21
- level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
22
- )
23
- logger = logging.getLogger(__name__)
24
-
25
-
26
- async def test_simple_text():
27
- """Test simple text-to-speech."""
28
- logger.info("=" * 70)
29
- logger.info("TEST: Simple Text-to-Speech")
30
- logger.info("=" * 70)
31
-
32
- tts = CartesiaTTS(
33
- api_key=None, # Will use env var
34
- voice_id=None, # Will use default
35
- model="sonic-3",
36
- sample_rate=16000,
37
- )
38
-
39
- logger.info(f"TTS Config:")
40
- logger.info(f" API Key: {tts.api_key}")
41
- logger.info(f" Voice ID: {tts.voice_id}")
42
- logger.info(f" Model: {tts.model}")
43
- logger.info(f" Sample Rate: {tts.sample_rate}")
44
-
45
- try:
46
- logger.info("\nCalling synthesize_stream_text with simple text...")
47
-
48
- async def text_gen():
49
- yield "Hello "
50
- yield "world"
51
-
52
- chunk_count = 0
53
- total_bytes = 0
54
-
55
- async for chunk in tts.synthesize_stream_text(text_gen()):
56
- chunk_count += 1
57
- if chunk.data:
58
- total_bytes += len(chunk.data)
59
- logger.info(
60
- f"✓ Got audio chunk {chunk_count}: {len(chunk.data)} bytes, is_final={chunk.is_final}"
61
- )
62
- else:
63
- logger.info(f"✓ Got final marker: is_final={chunk.is_final}")
64
-
65
- logger.info(f"\n✓ SUCCESS: Got {chunk_count} chunks, {total_bytes} bytes total")
66
-
67
- except Exception as e:
68
- logger.error(f"✗ FAILED: {type(e).__name__}: {e}", exc_info=True)
69
- return False
70
-
71
- return True
72
-
73
-
74
- async def test_with_queue():
75
- """Test using asyncio.Queue like the original example."""
76
- logger.info("\n" + "=" * 70)
77
- logger.info("TEST: With asyncio.Queue (like StreamingService)")
78
- logger.info("=" * 70)
79
-
80
- tts = CartesiaTTS(
81
- api_key=None,
82
- voice_id=None,
83
- model="sonic-3",
84
- sample_rate=16000,
85
- )
86
-
87
- queue = asyncio.Queue()
88
-
89
- async def text_producer():
90
- """Simulate LLM producing text tokens."""
91
- tokens = ["Hello ", "world", "!"]
92
- for token in tokens:
93
- logger.info(f"📤 Producing: {token!r}")
94
- await queue.put(token)
95
- logger.info("📤 Putting None to signal end")
96
- await queue.put(None)
97
-
98
- async def queue_to_async_iter():
99
- """Convert queue to async iterator."""
100
- while True:
101
- token = await queue.get()
102
- if token is None:
103
- break
104
- yield token
105
-
106
- try:
107
- logger.info("\nStarting text producer and TTS consumer...")
108
-
109
- producer_task = asyncio.create_task(text_producer())
110
-
111
- chunk_count = 0
112
- total_bytes = 0
113
-
114
- async for chunk in tts.synthesize_stream_text(queue_to_async_iter()):
115
- chunk_count += 1
116
- if chunk.data:
117
- total_bytes += len(chunk.data)
118
- logger.info(f"✓ Got audio chunk {chunk_count}: {len(chunk.data)} bytes")
119
- else:
120
- logger.info(f"✓ Got final marker")
121
-
122
- await producer_task
123
-
124
- logger.info(f"\n✓ SUCCESS: Got {chunk_count} chunks, {total_bytes} bytes total")
125
- return True
126
-
127
- except Exception as e:
128
- logger.error(f"✗ FAILED: {type(e).__name__}: {e}", exc_info=True)
129
- return False
130
-
131
-
132
- async def main():
133
- logger.info("\n")
134
- logger.info("╔" + "=" * 68 + "╗")
135
- logger.info("║" + " " * 15 + "CARTESIA TTS DEBUG TEST" + " " * 31 + "║")
136
- logger.info("╚" + "=" * 68 + "╝")
137
-
138
- results = []
139
-
140
- # Test 1: Simple text
141
- results.append(("Simple text-to-speech", await test_simple_text()))
142
-
143
- # Test 2: Queue-based
144
- results.append(("Queue-based streaming", await test_with_queue()))
145
-
146
- # Summary
147
- logger.info("\n" + "=" * 70)
148
- logger.info("SUMMARY")
149
- logger.info("=" * 70)
150
- for test_name, passed in results:
151
- status = "✓ PASS" if passed else "✗ FAIL"
152
- logger.info(f"{status}: {test_name}")
153
-
154
- all_passed = all(result for _, result in results)
155
- logger.info("=" * 70)
156
-
157
- return 0 if all_passed else 1
158
-
159
-
160
- if __name__ == "__main__":
161
- exit_code = asyncio.run(main())
162
- sys.exit(exit_code)
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Test Cartesia WebSocket connection and capture error message."""
3
-
4
- import asyncio
5
- import logging
6
- from pathlib import Path
7
- from urllib.parse import quote
8
-
9
- import websockets
10
- from websockets.exceptions import InvalidStatus
11
-
12
- # Setup logging
13
- logging.basicConfig(level=logging.DEBUG)
14
-
15
- # Load env
16
- from dotenv import load_dotenv
17
- import os
18
-
19
- load_dotenv(Path(__file__).parent.parent / ".env")
20
-
21
- CARTESIA_API_KEY = os.getenv("CARTESIA_API_KEY")
22
-
23
-
24
- async def test_connection():
25
- """Test Cartesia WebSocket connection."""
26
- url = (
27
- f"wss://api.cartesia.ai/stt/websocket?"
28
- f"model={quote('ink-whisper')}"
29
- f"&language={quote('en')}"
30
- f"&encoding={quote('pcm_s16le')}"
31
- f"&sample_rate={quote('16000')}"
32
- f"&min_volume={quote('0.0')}"
33
- f"&max_silence_duration_secs={quote('30.0')}"
34
- f"&api_key={quote(CARTESIA_API_KEY)}"
35
- )
36
-
37
- print(f"Connecting to: {url}\n")
38
-
39
- try:
40
- async with websockets.connect(url, open_timeout=30) as ws:
41
- print("✓ Connected!")
42
- except InvalidStatus as e:
43
- print(f"✗ Invalid Status Error")
44
- print(f" Response object: {e.response}")
45
- print(
46
- f" Response dir: {[attr for attr in dir(e.response) if not attr.startswith('_')]}"
47
- )
48
- print(f" Exception str: {str(e)}")
49
- except Exception as e:
50
- print(f"✗ Connection failed: {e}")
51
- import traceback
52
-
53
- traceback.print_exc()
54
-
55
-
56
- if __name__ == "__main__":
57
- asyncio.run(test_connection())
@@ -1 +0,0 @@
1
- """Tests for the audio engine."""
@@ -1 +0,0 @@
1
- """Tests for the audio engine."""
@@ -1,10 +0,0 @@
1
- """PersonaPlex test suite.
2
-
3
- Organized by step for maintainability:
4
- - test_personaplex_config.py (Step 1: Config)
5
- - test_personaplex_message.py (Step 2: Message encoding/decoding)
6
- - test_personaplex_transcript.py (Step 3: Transcript save/load)
7
- - test_personaplex_session.py (Step 4: Session management)
8
- - test_personaplex_client.py (Step 5: Client connection)
9
- - test_personaplex_pipeline.py (Step 6-7: Pipeline lifecycle + mock messages)
10
- """
@@ -1,259 +0,0 @@
1
- """Tests for PersonaPlexClient WebSocket connection (Step 5)."""
2
-
3
- import pytest
4
- from unittest.mock import AsyncMock, patch
5
- import asyncio
6
-
7
- from pipelines.personaplex import PersonaPlexClient, PersonaPlexConfig
8
-
9
-
10
- class TestPersonaPlexClientInit:
11
- """Test client initialization and URL building."""
12
-
13
- def test_client_init_with_defaults(self):
14
- """Can we create a client with default config?"""
15
- config = PersonaPlexConfig()
16
- client = PersonaPlexClient(config)
17
-
18
- assert client.config == config
19
- assert client.connection is None
20
- assert not client._is_connected
21
-
22
- def test_client_init_with_custom_config(self):
23
- """Can we create a client with custom config?"""
24
- config = PersonaPlexConfig(
25
- server_url="wss://custom.example.com",
26
- voice_prompt="NATM0.pt",
27
- )
28
- client = PersonaPlexClient(config)
29
-
30
- assert client.config.server_url == "wss://custom.example.com"
31
- assert client.config.voice_prompt == "NATM0.pt"
32
-
33
- def test_url_building_includes_voice_prompt(self):
34
- """Does URL building include voice_prompt parameter?"""
35
- config = PersonaPlexConfig(voice_prompt="NATF0.pt")
36
- client = PersonaPlexClient(config)
37
-
38
- url = client._build_url("Test prompt")
39
-
40
- assert "voice_prompt=NATF0.pt" in url
41
- assert "text_prompt=" in url
42
-
43
- def test_url_building_includes_temperatures(self):
44
- """Does URL include temperature parameters?"""
45
- config = PersonaPlexConfig(
46
- text_temperature=0.5,
47
- audio_temperature=0.9,
48
- )
49
- client = PersonaPlexClient(config)
50
-
51
- url = client._build_url("Test prompt")
52
-
53
- assert "text_temperature=0.5" in url
54
- assert "audio_temperature=0.9" in url
55
-
56
- def test_url_building_encodes_system_prompt(self):
57
- """Is system prompt included in URL?"""
58
- config = PersonaPlexConfig()
59
- client = PersonaPlexClient(config)
60
-
61
- url = client._build_url("Hello world!")
62
-
63
- assert "text_prompt=" in url
64
-
65
-
66
- @pytest.mark.asyncio
67
- class TestPersonaPlexClientConnection:
68
- """Test client WebSocket connection lifecycle."""
69
-
70
- async def test_connect_opens_websocket(self):
71
- """Does connect establish a WebSocket?"""
72
- config = PersonaPlexConfig()
73
- client = PersonaPlexClient(config)
74
-
75
- with patch("websockets.connect", new_callable=AsyncMock) as mock_connect:
76
- mock_conn = AsyncMock()
77
- mock_connect.return_value = mock_conn
78
-
79
- await client.connect("Hello assistant")
80
-
81
- assert client._is_connected
82
- mock_connect.assert_called_once()
83
-
84
- async def test_disconnect_closes_websocket(self):
85
- """Does disconnect close the WebSocket?"""
86
- config = PersonaPlexConfig()
87
- client = PersonaPlexClient(config)
88
-
89
- mock_conn = AsyncMock()
90
- client.connection = mock_conn
91
- client._is_connected = True
92
-
93
- await client.disconnect()
94
-
95
- mock_conn.close.assert_called_once()
96
- assert not client._is_connected
97
- assert client.connection is None
98
-
99
- async def test_context_manager_exits_cleanly(self):
100
- """Does context manager call disconnect on exit?"""
101
- config = PersonaPlexConfig()
102
- client = PersonaPlexClient(config)
103
-
104
- # Manually set up connection
105
- mock_conn = AsyncMock()
106
- client.connection = mock_conn
107
- client._is_connected = True
108
-
109
- async with client:
110
- # Still connected during the context
111
- assert client._is_connected
112
-
113
- # Should be disconnected after exiting
114
- mock_conn.close.assert_called_once()
115
- assert not client._is_connected
116
-
117
-
118
- @pytest.mark.asyncio
119
- class TestPersonaPlexClientSendReceive:
120
- """Test sending and receiving data."""
121
-
122
- async def test_send_audio_creates_message(self):
123
- """Does send_audio create a binary message?"""
124
- config = PersonaPlexConfig()
125
- client = PersonaPlexClient(config)
126
-
127
- mock_conn = AsyncMock()
128
- client.connection = mock_conn
129
- client._is_connected = True
130
-
131
- audio_data = b"fake_opus_data"
132
- await client.send_audio(audio_data)
133
-
134
- mock_conn.send.assert_called_once()
135
- sent_data = mock_conn.send.call_args[0][0]
136
-
137
- # Should start with 0x01 (audio type)
138
- assert sent_data[0] == 0x01
139
- assert sent_data[1:] == audio_data
140
-
141
- async def test_receive_audio_parses_message(self):
142
- """Does receive_audio return AudioChunk?"""
143
- config = PersonaPlexConfig()
144
- client = PersonaPlexClient(config)
145
-
146
- # Create a raw audio message (0x01 + audio data)
147
- audio_data = b"received_opus"
148
- raw_message = bytes([0x01]) + audio_data
149
-
150
- mock_conn = AsyncMock()
151
- mock_conn.recv.return_value = raw_message
152
- client.connection = mock_conn
153
- client._is_connected = True
154
-
155
- from pipelines.personaplex import AudioChunk
156
-
157
- chunk = await client.receive_audio()
158
-
159
- assert isinstance(chunk, AudioChunk)
160
- assert chunk.data == audio_data
161
- assert chunk.sample_rate == 48000
162
-
163
- async def test_receive_text_parses_message(self):
164
- """Does receive_text return TextChunk?"""
165
- config = PersonaPlexConfig()
166
- client = PersonaPlexClient(config)
167
-
168
- # Create a raw text message (0x02 + UTF-8 text)
169
- text = "Hello from server"
170
- raw_message = bytes([0x02]) + text.encode("utf-8")
171
-
172
- mock_conn = AsyncMock()
173
- mock_conn.recv.return_value = raw_message
174
- client.connection = mock_conn
175
- client._is_connected = True
176
-
177
- from pipelines.personaplex import TextChunk
178
-
179
- chunk = await client.receive_text()
180
-
181
- assert isinstance(chunk, TextChunk)
182
- assert chunk.text == text
183
-
184
- async def test_receive_error_returns_none(self):
185
- """Does receive_audio handle error messages gracefully?"""
186
- config = PersonaPlexConfig()
187
- client = PersonaPlexClient(config)
188
-
189
- # Create an error message (0x05 + error text)
190
- error_text = "Connection failed"
191
- raw_message = bytes([0x05]) + error_text.encode("utf-8")
192
-
193
- mock_conn = AsyncMock()
194
- mock_conn.recv.return_value = raw_message
195
- client.connection = mock_conn
196
- client._is_connected = True
197
-
198
- result = await client.receive_audio()
199
-
200
- # Should return None, not raise
201
- assert result is None
202
-
203
-
204
- @pytest.mark.asyncio
205
- class TestPersonaPlexClientStreaming:
206
- """Test streaming message handling."""
207
-
208
- async def test_stream_messages_yields_raw_messages(self):
209
- """Does stream_messages yield PersonaPlexMessage objects?"""
210
- from pipelines.personaplex import PersonaPlexMessage, MessageType
211
-
212
- config = PersonaPlexConfig()
213
- client = PersonaPlexClient(config)
214
-
215
- # Create mock messages - raw bytes
216
- audio_msg = bytes([0x01]) + b"audio_data"
217
- text_msg = bytes([0x02]) + "Hello".encode("utf-8")
218
-
219
- # Mock connection returns bytes
220
- mock_conn = AsyncMock()
221
- mock_conn.__aiter__.return_value = [audio_msg, text_msg]
222
- client.connection = mock_conn
223
- client._is_connected = True
224
-
225
- messages = []
226
- try:
227
- async for msg in client.stream_messages():
228
- messages.append(msg)
229
- if len(messages) >= 2:
230
- break
231
- except (StopAsyncIteration, asyncio.CancelledError):
232
- pass
233
-
234
- # Should get 2 PersonaPlexMessage objects
235
- assert len(messages) == 2
236
- assert messages[0].type == MessageType.AUDIO
237
- assert messages[1].type == MessageType.TEXT
238
-
239
- async def test_stream_messages_skips_errors(self):
240
- """Does stream_messages skip error messages?"""
241
- config = PersonaPlexConfig()
242
- client = PersonaPlexClient(config)
243
-
244
- # Error message type (0x05)
245
- error_msg = bytes([0x05]) + b"Server error"
246
-
247
- mock_conn = AsyncMock()
248
- mock_conn.recv.return_value = error_msg
249
- client.connection = mock_conn
250
- client._is_connected = True
251
-
252
- chunks = []
253
- async for chunk in client.stream_messages():
254
- chunks.append(chunk)
255
- if len(chunks) > 1: # Prevent infinite loop
256
- break
257
-
258
- # Error should be skipped (logged), not returned as chunk
259
- assert len(chunks) == 0
@@ -1,71 +0,0 @@
1
- """Tests for PersonaPlexConfig (Step 1)."""
2
-
3
- import pytest
4
- from pathlib import Path
5
- from pipelines.personaplex import PersonaPlexConfig
6
-
7
-
8
- class TestPersonaPlexConfig:
9
- """Test PersonaPlexConfig creation and validation."""
10
-
11
- def test_default_creation(self):
12
- """Can we create PersonaPlexConfig with defaults?"""
13
- config = PersonaPlexConfig()
14
- assert config.voice_prompt == "NATF0.pt"
15
- assert config.text_temperature == 0.7
16
- assert config.audio_temperature == 0.8
17
- assert config.sample_rate == 48000
18
-
19
- def test_custom_values(self):
20
- """Can we create config with custom values?"""
21
- config = PersonaPlexConfig(
22
- voice_prompt="NATM1.pt",
23
- text_temperature=0.5,
24
- audio_temperature=0.9,
25
- )
26
- assert config.voice_prompt == "NATM1.pt"
27
- assert config.text_temperature == 0.5
28
- assert config.audio_temperature == 0.9
29
-
30
- def test_reject_invalid_text_temp(self):
31
- """Does it reject text_temperature > 2.0?"""
32
- with pytest.raises(ValueError):
33
- PersonaPlexConfig(text_temperature=2.5)
34
-
35
- def test_reject_invalid_audio_temp(self):
36
- """Does it reject audio_temperature < 0.0?"""
37
- with pytest.raises(ValueError):
38
- PersonaPlexConfig(audio_temperature=-0.1)
39
-
40
- def test_reject_invalid_topk(self):
41
- """Does it reject invalid top-K values?"""
42
- with pytest.raises(ValueError):
43
- PersonaPlexConfig(text_topk=0)
44
-
45
- def test_reject_invalid_sample_rate(self):
46
- """Does it reject invalid sample rates?"""
47
- with pytest.raises(ValueError):
48
- PersonaPlexConfig(sample_rate=22050)
49
-
50
- def test_from_dict(self):
51
- """Can we load config from a dictionary?"""
52
- data = {
53
- "voice_prompt": "NATF2.pt",
54
- "text_temperature": 0.8,
55
- "audio_temperature": 0.9,
56
- }
57
- config = PersonaPlexConfig.from_dict(data)
58
- assert config.voice_prompt == "NATF2.pt"
59
- assert config.text_temperature == 0.8
60
- assert config.audio_temperature == 0.9
61
-
62
- def test_transcript_dir_created(self):
63
- """Does it create transcript directory on init?"""
64
- import tempfile
65
-
66
- with tempfile.TemporaryDirectory() as tmpdir:
67
- config = PersonaPlexConfig(
68
- transcript_path=tmpdir + "/transcripts/",
69
- save_transcripts=True,
70
- )
71
- assert Path(config.transcript_path).exists()
@@ -1,80 +0,0 @@
1
- """Tests for PersonaPlexMessage (Step 2)."""
2
-
3
- import pytest
4
- from pipelines.personaplex import PersonaPlexMessage, MessageType
5
-
6
-
7
- class TestPersonaPlexMessage:
8
- """Test WebSocket message encoding and decoding."""
9
-
10
- def test_encode_audio(self):
11
- """Can we encode an audio message?"""
12
- audio_data = b"fake_opus_audio"
13
- msg = PersonaPlexMessage(type=MessageType.AUDIO, data=audio_data)
14
- encoded = msg.encode()
15
-
16
- assert encoded[0] == 0x01
17
- assert encoded[1:] == audio_data
18
-
19
- def test_encode_text(self):
20
- """Can we encode a text message?"""
21
- text = "Hello, world!"
22
- msg = PersonaPlexMessage(type=MessageType.TEXT, data=text)
23
- encoded = msg.encode()
24
-
25
- assert encoded[0] == 0x02
26
- assert encoded[1:] == text.encode("utf-8")
27
-
28
- def test_decode_audio(self):
29
- """Can we decode an audio message?"""
30
- audio_data = b"fake_opus_audio"
31
- raw = bytes([0x01]) + audio_data
32
-
33
- msg = PersonaPlexMessage.decode(raw)
34
- assert msg.type == MessageType.AUDIO
35
- assert msg.data == audio_data
36
-
37
- def test_decode_text(self):
38
- """Can we decode a text message?"""
39
- text = "Hello, assistant!"
40
- raw = bytes([0x02]) + text.encode("utf-8")
41
-
42
- msg = PersonaPlexMessage.decode(raw)
43
- assert msg.type == MessageType.TEXT
44
- assert msg.data == text
45
-
46
- def test_decode_error(self):
47
- """Can we decode an error message?"""
48
- error_text = b"Connection timeout"
49
- raw = bytes([0x05]) + error_text
50
-
51
- msg = PersonaPlexMessage.decode(raw)
52
- assert msg.type == MessageType.ERROR
53
- assert msg.data == error_text
54
-
55
- def test_roundtrip_audio(self):
56
- """Audio message roundtrip: encode then decode?"""
57
- original_data = b"\x00\x01\x02\x03\x04\x05"
58
- msg1 = PersonaPlexMessage(type=MessageType.AUDIO, data=original_data)
59
-
60
- encoded = msg1.encode()
61
- msg2 = PersonaPlexMessage.decode(encoded)
62
-
63
- assert msg2.type == msg1.type
64
- assert msg2.data == original_data
65
-
66
- def test_roundtrip_text(self):
67
- """Text message roundtrip: encode then decode?"""
68
- original_text = "This is a test message with émojis 🎉"
69
- msg1 = PersonaPlexMessage(type=MessageType.TEXT, data=original_text)
70
-
71
- encoded = msg1.encode()
72
- msg2 = PersonaPlexMessage.decode(encoded)
73
-
74
- assert msg2.type == msg1.type
75
- assert msg2.data == original_text
76
-
77
- def test_decode_too_short(self):
78
- """Does decode reject empty message?"""
79
- with pytest.raises(ValueError):
80
- PersonaPlexMessage.decode(b"")