Files
ai-video-fullstack/backend/tests/test_qwen_audio_realtime.py
Xin Wang 774825593d feat(qwen-audio): add Alibaba Cloud Qwen-Audio Realtime service integration
- Introduced Qwen-Audio Realtime service for speech-to-speech processing in Pipecat.
- Updated interface catalog to include Qwen-Audio Realtime capabilities.
- Enhanced model resource testing to support new service.
- Added configuration options for audio sample rates and turn detection modes.
- Updated documentation to reflect integration details and usage instructions.
2026-07-21 15:57:43 +08:00

235 lines
8.6 KiB
Python

from __future__ import annotations
import base64
import json
import unittest
from unittest.mock import AsyncMock, patch
from models import AssistantConfig
from pipecat.frames.frames import OutputTransportMessageUrgentFrame, TTSAudioRawFrame
from services import model_resource_tester
from services.interface_catalog import INTERFACE_DEFINITIONS
from services.pipecat.qwen_audio_realtime import QwenAudioRealtimeService
from services.pipecat.service_factory import (
create_realtime_service,
realtime_audio_sample_rates,
)
from websockets.protocol import State
class _OpenWebSocket:
state = State.OPEN
def __init__(self) -> None:
self.messages: list[dict] = []
async def send(self, raw_message: str) -> None:
self.messages.append(json.loads(raw_message))
def _service(**overrides) -> QwenAudioRealtimeService:
settings = {
"api_key": "test-key",
"model": "qwen-audio-3.0-realtime-flash",
"base_url": (
"wss://workspace.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime"
),
"instructions": "You are helpful.",
}
settings.update(overrides)
return QwenAudioRealtimeService(**settings)
class QwenAudioRealtimeServiceTest(unittest.IsolatedAsyncioTestCase):
def test_provider_defaults_and_session_config(self):
service = _service()
self.assertEqual(
service._connection_url(),
"wss://workspace.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime"
"?model=qwen-audio-3.0-realtime-flash",
)
self.assertEqual(
service._initial_session_config(),
{
"modalities": ["text", "audio"],
"instructions": "You are helpful.",
"voice": "longanqian",
"input_audio_format": "pcm",
"output_audio_format": "pcm",
"turn_detection": {
"type": "server_vad",
"threshold": 0.5,
"silence_duration_ms": 800,
},
"max_history_turns": 20,
},
)
def test_smart_turn_and_value_ranges_are_explicit(self):
service = _service(turn_detection_mode="smart_turn")
self.assertEqual(service._turn_detection_config(), {"type": "smart_turn"})
with self.assertRaisesRegex(ValueError, "turn detection mode"):
_service(turn_detection_mode="manual")
with self.assertRaisesRegex(ValueError, "threshold"):
_service(vad_threshold=1.1)
with self.assertRaisesRegex(ValueError, "history"):
_service(max_history_turns=51)
async def test_session_ready_flushes_queued_events_in_order(self):
service = _service()
websocket = _OpenWebSocket()
service._websocket = websocket
await service.send_text("hello")
self.assertEqual(websocket.messages, [])
await service._handle_server_event({"type": "session.created"})
self.assertEqual(websocket.messages[0]["type"], "session.update")
self.assertEqual(
websocket.messages[0]["session"]["input_audio_format"], "pcm"
)
await service._handle_server_event({"type": "session.updated"})
self.assertEqual(
[message["type"] for message in websocket.messages],
["session.update", "conversation.item.create", "response.create"],
)
async def test_dynamic_instructions_do_not_resend_locked_settings(self):
service = _service()
websocket = _OpenWebSocket()
service._websocket = websocket
service._session_ready.set()
await service.update_instructions("Call the user Alice.")
self.assertEqual(websocket.messages[-1]["type"], "session.update")
self.assertEqual(
websocket.messages[-1]["session"],
{"instructions": "Call the user Alice."},
)
async def test_interruption_cancels_and_suppresses_residual_audio(self):
service = _service()
websocket = _OpenWebSocket()
service._websocket = websocket
service._session_ready.set()
service.push_frame = AsyncMock()
service.broadcast_interruption = AsyncMock()
audio = base64.b64encode(b"\x01\x02").decode("ascii")
await service._handle_server_event({"type": "response.created"})
await service._handle_server_event(
{"type": "response.audio_transcript.delta", "delta": "Hi"}
)
await service._handle_server_event(
{"type": "response.audio.delta", "delta": audio}
)
await service._handle_server_event(
{"type": "input_audio_buffer.speech_started"}
)
calls_before_residual_audio = service.push_frame.await_count
await service._handle_server_event(
{"type": "response.audio.delta", "delta": audio}
)
self.assertEqual(service.push_frame.await_count, calls_before_residual_audio)
self.assertIn("response.cancel", [item["type"] for item in websocket.messages])
service.broadcast_interruption.assert_awaited_once()
frames = [call.args[0] for call in service.push_frame.await_args_list]
self.assertTrue(any(isinstance(frame, TTSAudioRawFrame) for frame in frames))
end_messages = [
frame.message
for frame in frames
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "assistant-text-end"
]
self.assertEqual(end_messages[0]["content"], "Hi")
self.assertTrue(end_messages[0]["interrupted"])
async def test_greeting_request_is_removed_after_response(self):
service = _service()
websocket = _OpenWebSocket()
service._websocket = websocket
service._session_ready.set()
service.push_frame = AsyncMock()
await service.speak("Welcome")
greeting_item_id = websocket.messages[0]["item"]["id"]
await service._handle_server_event({"type": "response.created"})
await service._handle_server_event(
{"type": "response.audio_transcript.done", "transcript": "Welcome"}
)
await service._handle_server_event(
{"type": "response.done", "response": {"status": "completed"}}
)
self.assertEqual(websocket.messages[-1]["type"], "conversation.item.delete")
self.assertEqual(websocket.messages[-1]["item_id"], greeting_item_id)
def test_pipeline_sample_rates_use_qwen_defaults(self):
cfg = AssistantConfig(
realtime_interface_type="qwen-audio-realtime",
realtime_values={},
)
self.assertEqual(realtime_audio_sample_rates(cfg), (16_000, 24_000))
def test_factory_and_interface_catalog_register_qwen(self):
definition = next(
item
for item in INTERFACE_DEFINITIONS
if item["interface_type"] == "qwen-audio-realtime"
)
self.assertEqual(definition["capability"], "Realtime")
self.assertIn(
"smart_turn",
next(
field
for field in definition["fields"]
if field["key"] == "turnDetection"
)["options"],
)
cfg = AssistantConfig(
realtime_interface_type="qwen-audio-realtime",
realtimeModel="qwen-audio-3.0-realtime-plus",
realtime_api_key="test-key",
realtime_base_url=(
"wss://workspace.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime"
),
realtime_values={"turnDetection": "smart_turn"},
)
service = create_realtime_service(cfg, instructions="Be concise.")
self.assertIsInstance(service, QwenAudioRealtimeService)
self.assertEqual(service._turn_detection_config(), {"type": "smart_turn"})
async def test_resource_tester_routes_qwen_with_dashscope_header(self):
expected = object()
probe = AsyncMock(return_value=expected)
with patch.object(
model_resource_tester,
"_test_realtime_websocket",
probe,
):
result = await model_resource_tester.test_model_resource(
"qwen-audio-realtime",
"Realtime",
{"apiUrl": "wss://example.test/realtime", "modelId": "model"},
{"apiKey": "secret"},
)
self.assertIs(result, expected)
probe.assert_awaited_once_with(
{"apiUrl": "wss://example.test/realtime", "modelId": "model"},
{"apiKey": "secret"},
provider="Qwen-Audio",
extra_headers={"x-dashscope-dataInspection": "disable"},
)
if __name__ == "__main__":
unittest.main()