197 lines
6.2 KiB
Python
197 lines
6.2 KiB
Python
"""Tests for Inworld TTS plugin configuration options and wire payloads."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from livekit.agents import APIConnectionError
|
|
from livekit.agents.types import NOT_GIVEN
|
|
from livekit.agents.utils import aio
|
|
|
|
pytestmark = pytest.mark.plugin("inworld")
|
|
|
|
|
|
async def test_delivery_mode_default_not_given():
|
|
from livekit.plugins.inworld import TTS
|
|
|
|
tts = TTS(api_key="test-key", model="inworld-tts-2")
|
|
assert tts._opts.delivery_mode is NOT_GIVEN
|
|
|
|
|
|
async def test_delivery_mode_set_on_init():
|
|
from livekit.plugins.inworld import TTS
|
|
|
|
tts = TTS(api_key="test-key", model="inworld-tts-2", delivery_mode="STABLE")
|
|
assert tts._opts.delivery_mode == "STABLE"
|
|
|
|
|
|
async def test_delivery_mode_accepts_all_documented_values():
|
|
from livekit.plugins.inworld import TTS
|
|
|
|
for value in ("DELIVERY_MODE_UNSPECIFIED", "STABLE", "BALANCED", "CREATIVE"):
|
|
tts = TTS(api_key="test-key", model="inworld-tts-2", delivery_mode=value)
|
|
assert tts._opts.delivery_mode == value
|
|
|
|
|
|
async def test_delivery_mode_rejects_unknown_value():
|
|
from livekit.plugins.inworld import TTS
|
|
|
|
with pytest.raises(ValueError):
|
|
TTS(api_key="test-key", model="inworld-tts-2", delivery_mode="EXPRESSIVE")
|
|
|
|
|
|
async def test_delivery_mode_update_options():
|
|
from livekit.plugins.inworld import TTS
|
|
|
|
tts = TTS(api_key="test-key", model="inworld-tts-2")
|
|
assert tts._opts.delivery_mode is NOT_GIVEN
|
|
tts.update_options(delivery_mode="CREATIVE")
|
|
assert tts._opts.delivery_mode == "CREATIVE"
|
|
|
|
with pytest.raises(ValueError):
|
|
tts.update_options(delivery_mode="LOUD")
|
|
|
|
|
|
async def _capture_first_ws_create_packet(opts) -> dict:
|
|
"""Drive `_InworldConnection._send_loop` against a fake websocket and
|
|
return the first `create` packet it sends, as a parsed dict.
|
|
|
|
The fake `send_str` signals an `asyncio.Event` once it has captured a
|
|
payload, so the test wakes immediately rather than polling.
|
|
"""
|
|
from livekit.plugins.inworld.tts import _CreateContextMsg, _InworldConnection
|
|
|
|
sent_payloads: list[str] = []
|
|
captured = asyncio.Event()
|
|
|
|
def _on_send(payload: str) -> None:
|
|
sent_payloads.append(payload)
|
|
captured.set()
|
|
|
|
fake_ws = MagicMock()
|
|
fake_ws.send_str = AsyncMock(side_effect=_on_send)
|
|
|
|
conn = _InworldConnection(
|
|
session=MagicMock(),
|
|
ws_url="wss://example.invalid/",
|
|
authorization="Basic test",
|
|
)
|
|
# Skip real connect(); inject the fake websocket directly so `_send_loop`
|
|
# reads from it as if a connection had been established.
|
|
conn._ws = fake_ws
|
|
|
|
await conn._outbound_queue.put(_CreateContextMsg(context_id="ctx-1", opts=opts))
|
|
|
|
send_task = asyncio.create_task(conn._send_loop())
|
|
try:
|
|
await asyncio.wait_for(captured.wait(), timeout=2.0)
|
|
finally:
|
|
conn._closed = True
|
|
await aio.cancel_and_wait(send_task)
|
|
|
|
return json.loads(sent_payloads[0])
|
|
|
|
|
|
async def test_ws_create_packet_includes_delivery_mode():
|
|
"""The WebSocket `create` packet sent by `_send_loop` includes
|
|
`deliveryMode` at the top level of the `create` object when
|
|
`delivery_mode` is set on the TTS."""
|
|
from livekit.plugins.inworld.tts import _TTSOptions
|
|
|
|
opts = _TTSOptions(
|
|
model="inworld-tts-2",
|
|
encoding="PCM",
|
|
voice="Ashley",
|
|
sample_rate=24000,
|
|
bit_rate=64000,
|
|
speaking_rate=1.0,
|
|
temperature=1.0,
|
|
delivery_mode="STABLE",
|
|
)
|
|
|
|
pkt = await _capture_first_ws_create_packet(opts)
|
|
assert pkt["create"]["deliveryMode"] == "STABLE"
|
|
assert pkt["create"]["modelId"] == "inworld-tts-2"
|
|
|
|
|
|
async def test_ws_create_packet_omits_delivery_mode_when_not_given():
|
|
"""When `delivery_mode` is not set, the WS `create` packet must not
|
|
include the `deliveryMode` key at all (Inworld treats absence as the
|
|
server default)."""
|
|
from livekit.plugins.inworld.tts import _TTSOptions
|
|
|
|
opts = _TTSOptions(
|
|
model="inworld-tts-2",
|
|
encoding="PCM",
|
|
voice="Ashley",
|
|
sample_rate=24000,
|
|
bit_rate=64000,
|
|
speaking_rate=1.0,
|
|
temperature=1.0,
|
|
)
|
|
|
|
pkt = await _capture_first_ws_create_packet(opts)
|
|
assert "deliveryMode" not in pkt["create"]
|
|
|
|
|
|
def _patch_session_to_capture_post(tts, captured: dict[str, object]) -> None:
|
|
"""Replace the TTS's aiohttp session with a stub that captures the body of
|
|
the first ``post()`` call and short-circuits the rest of the request."""
|
|
|
|
class _FakePostCM:
|
|
async def __aenter__(self):
|
|
# Raise after capture so ChunkedStream._run unwinds quickly; the
|
|
# body has already been recorded by _fake_post.
|
|
raise RuntimeError("short-circuit")
|
|
|
|
async def __aexit__(self, *exc):
|
|
return None
|
|
|
|
def _fake_post(url, *, json=None, **kwargs):
|
|
captured["url"] = url
|
|
captured["json"] = json
|
|
return _FakePostCM()
|
|
|
|
fake_session = MagicMock()
|
|
fake_session.post = _fake_post
|
|
tts._session = fake_session
|
|
|
|
|
|
async def test_http_body_includes_delivery_mode():
|
|
"""The HTTP `synthesize` request body includes `deliveryMode` at the
|
|
top level when `delivery_mode` is set on the TTS."""
|
|
from livekit.plugins.inworld import TTS
|
|
|
|
tts = TTS(api_key="test-key", model="inworld-tts-2", delivery_mode="BALANCED")
|
|
captured: dict[str, object] = {}
|
|
_patch_session_to_capture_post(tts, captured)
|
|
|
|
with pytest.raises(APIConnectionError):
|
|
async for _ in tts.synthesize("hello"):
|
|
pass
|
|
|
|
body = captured.get("json")
|
|
assert isinstance(body, dict), f"expected JSON dict, got {body!r}"
|
|
assert body["deliveryMode"] == "BALANCED"
|
|
assert body["modelId"] == "inworld-tts-2"
|
|
|
|
|
|
async def test_http_body_omits_delivery_mode_when_not_given():
|
|
from livekit.plugins.inworld import TTS
|
|
|
|
tts = TTS(api_key="test-key", model="inworld-tts-2")
|
|
captured: dict[str, object] = {}
|
|
_patch_session_to_capture_post(tts, captured)
|
|
|
|
with pytest.raises(APIConnectionError):
|
|
async for _ in tts.synthesize("hello"):
|
|
pass
|
|
|
|
body = captured.get("json")
|
|
assert isinstance(body, dict)
|
|
assert "deliveryMode" not in body
|