Files
ai-video-fullstack/backend/tests/test_dashscope_tts.py
T

131 lines
5.9 KiB
Python
Raw Normal View History

import asyncio
import json
import unittest
from unittest.mock import AsyncMock, patch
from pipecat.frames.frames import TTSAudioRawFrame, TTSStoppedFrame
from services.pipecat.dashscope_tts import DashScopeTTSService, SynthesisSession, SUPPORTED_MODELS
from services.model_resource_tester import test_model_resource
URL = 'wss://workspace.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference'
class Socket:
def __init__(self):
self.sent = []
self.queue = asyncio.Queue()
self.close = AsyncMock()
async def send(self, raw):
msg = json.loads(raw)
self.sent.append(msg)
if msg['header']['action'] == 'run-task':
self.task_id = msg['header']['task_id']
self.event('task-started')
def event(self, name):
self.queue.put_nowait(json.dumps({'header': {'event': name, 'task_id': self.task_id}}))
async def recv(self):
return await self.queue.get()
class DashScopeTests(unittest.IsolatedAsyncioTestCase):
async def test_protocol_streams_before_finish_and_preserves_spaces(self):
socket = Socket()
session = SynthesisSession(URL, 'secret', 'cosyvoice-v1', 'longxiaochun')
with patch('services.pipecat.dashscope_tts.connect', AsyncMock(return_value=socket)):
await session.start()
await session.text('Hello')
await session.text(' world')
socket.queue.put_nowait(b'\x01\x00' * 20)
audio = session.audio()
self.assertEqual(await anext(audio), b'\x01\x00' * 20)
self.assertFalse(session.finished_input)
await session.finish()
await session.finish()
socket.event('task-finished')
with self.assertRaises(StopAsyncIteration):
await anext(audio)
await session.close()
self.assertEqual([m['header']['action'] for m in socket.sent],
['run-task', 'continue-task', 'continue-task', 'finish-task'])
self.assertEqual(socket.sent[2]['payload']['input']['text'], ' world')
self.assertEqual(socket.sent[0]['payload']['parameters']['format'], 'pcm')
self.assertEqual(len({m['header']['task_id'] for m in socket.sent}), 1)
async def test_empty_audio_and_provider_failure(self):
for failure in (False, True):
socket = Socket()
session = SynthesisSession(URL, 'secret', 'cosyvoice-v2', 'voice')
with patch('services.pipecat.dashscope_tts.connect', AsyncMock(return_value=socket)):
await session.start()
socket.event('task-failed' if failure else 'task-finished')
with self.assertRaises(RuntimeError):
await anext(session.audio())
await session.close()
async def test_pipecat_audio_completion_and_interruption(self):
socket = Socket()
service = DashScopeTTSService(api_key='secret', model='qwen-audio-3.1-tts-flash',
voice='voice', url=URL)
service._sample_rate = 24000
service.create_task = asyncio.create_task
service.cancel_task = AsyncMock(side_effect=self.cancel_task)
service.audio_context_available = lambda _: True
service.append_to_audio_context = AsyncMock()
service.remove_audio_context = AsyncMock()
with patch('services.pipecat.dashscope_tts.connect', AsyncMock(return_value=socket)):
async for _ in service.run_tts('你好', 'turn1'):
pass
receiver = service._streams['turn1'].receiver
socket.queue.put_nowait(b'\0\0' * 20)
await service.flush_audio('turn1')
socket.event('task-finished')
await receiver
frames = [call.args[1] for call in service.append_to_audio_context.call_args_list]
self.assertIsInstance(frames[0], TTSAudioRawFrame)
self.assertIsInstance(frames[-1], TTSStoppedFrame)
async for _ in service.run_tts('下一轮', 'turn2'):
pass
await service.on_audio_context_interrupted('turn2')
self.assertFalse(service._streams)
self.assertGreaterEqual(socket.close.await_count, 2)
async def cancel_task(self, task):
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
async def test_tester_requires_audio_and_completion(self):
with patch.object(SynthesisSession, 'start', AsyncMock()), \
patch.object(SynthesisSession, 'text', AsyncMock()), \
patch.object(SynthesisSession, 'finish', AsyncMock()), \
patch.object(SynthesisSession, 'close', AsyncMock()):
async def no_audio(self):
raise RuntimeError('no audio secret')
yield b''
with patch.object(SynthesisSession, 'audio', no_audio):
result = await test_model_resource('dashscope-tts', 'TTS',
{'apiUrl': URL, 'modelId': 'cosyvoice-v2', 'voice': 'voice'}, {'apiKey': 'secret'})
self.assertFalse(result.ok)
self.assertNotIn('secret', result.detail)
def test_factory_routes_dashscope_to_token_streaming(self):
from models import AssistantConfig
from services.pipecat.service_factory import create_tts
service = create_tts(AssistantConfig(
tts_interface_type="dashscope-tts", tts_model="cosyvoice-v1",
voice="longxiaochun", tts_api_key="secret", tts_base_url=URL))
self.assertIsInstance(service, DashScopeTTSService)
self.assertTrue(service._is_streaming_tokens)
def test_models_and_old_http_url(self):
for model in SUPPORTED_MODELS:
SynthesisSession(URL, 'secret', model, 'voice')
with self.assertRaisesRegex(ValueError, '旧 HTTP'):
SynthesisSession('https://dashscope.aliyuncs.com/api/v1/services/audio/tts',
'secret', 'cosyvoice-v1', 'voice')