306 lines
9.6 KiB
Python
306 lines
9.6 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
TTS/ASR模块集成测试 — MLX/Qwen3-ASR 版本
|
|
测试API端点和完整流程(需要运行后端服务)
|
|
|
|
运行方式:
|
|
pytest backend/tests/test_tts_asr_integration.py -v -s
|
|
python backend/tests/test_tts_asr_integration.py --test asr
|
|
|
|
MLX 模型通过 ModelScope (aufklarer/Qwen3-ASR) + ForcedAligner
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import io
|
|
import os
|
|
import sys
|
|
import time
|
|
import unittest
|
|
from typing import Optional
|
|
|
|
try:
|
|
import httpx # type: ignore
|
|
except ImportError:
|
|
print("httpx 未安装,跳过集成测试")
|
|
sys.exit(1)
|
|
|
|
import numpy as np
|
|
|
|
API_BASE_URL = os.environ.get('API_BASE_URL', 'http://localhost:8001')
|
|
API_KEY = os.environ.get('API_KEY', 'your-secret-key-here')
|
|
TEST_TIMEOUT = 120.0
|
|
|
|
|
|
class TTSASRIntegrationTest(unittest.TestCase):
|
|
"""TTS/ASR集成测试"""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.client = httpx.Client(timeout=TEST_TIMEOUT)
|
|
cls.headers = {'X-API-Key': API_KEY}
|
|
|
|
try:
|
|
response = cls.client.get(f'{API_BASE_URL}/v1/tts-asr/status', headers=cls.headers)
|
|
if response.status_code == 200:
|
|
cls.service_available = True
|
|
print(f"\n✓ 服务可用: {API_BASE_URL}")
|
|
else:
|
|
cls.service_available = False
|
|
print(f"\n✗ 服务返回非200状态码: {response.status_code}")
|
|
except Exception as e: # noqa: ANN001
|
|
cls.service_available = False
|
|
print(f"\n✗ 无法连接到服务: {e}")
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.client.close()
|
|
|
|
def setUp(self):
|
|
if not self.service_available:
|
|
self.skipTest("后端服务不可用")
|
|
|
|
def test_01_config_endpoint(self):
|
|
"""测试配置端点"""
|
|
response = self.client.get(
|
|
f'{API_BASE_URL}/v1/tts-asr/config',
|
|
headers=self.headers
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
config = response.json()
|
|
|
|
self.assertIn('device', config)
|
|
self.assertIn('model', config)
|
|
self.assertIn('status', config)
|
|
|
|
model = config['model']
|
|
status = config['status']
|
|
self.assertIn('tts', model)
|
|
self.assertIn('asr', model)
|
|
|
|
print(f"\n配置信息:")
|
|
print(f" TTS模型: {model['tts']}")
|
|
print(f" ASR模型: {model.get('asr', 'N/A')}")
|
|
print(f" TTS已加载: {status['tts_loaded']}")
|
|
print(f" ASR已加载: {status['asr_loaded']}")
|
|
|
|
def test_02_status_endpoint(self):
|
|
"""测试状态端点"""
|
|
response = self.client.get(
|
|
f'{API_BASE_URL}/v1/tts-asr/status',
|
|
headers=self.headers
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
status = response.json()
|
|
|
|
self.assertIn('tts_loaded', status)
|
|
self.assertIn('asr_loaded', status)
|
|
self.assertIn('device', status)
|
|
|
|
print(f"\n状态信息:")
|
|
print(f" TTS已加载: {status['tts_loaded']}")
|
|
print(f" ASR已加载: {status['asr_loaded']}")
|
|
print(f" 设备: {status['device']}")
|
|
|
|
def test_03_warmup_endpoint(self):
|
|
"""测试预热端点"""
|
|
print("\n开始模型预热(可能需要几分钟)...")
|
|
start_time = time.time()
|
|
|
|
response = self.client.post(
|
|
f'{API_BASE_URL}/v1/tts-asr/warmup',
|
|
headers=self.headers,
|
|
)
|
|
|
|
elapsed = time.time() - start_time
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
result = response.json()
|
|
|
|
self.assertIn('tts_warmup', result)
|
|
self.assertIn('asr_warmup', result)
|
|
|
|
print(f"\n预热完成 (耗时: {elapsed:.2f}秒):")
|
|
print(f" TTS预热: {'成功' if result['tts_warmup'] else '失败'}")
|
|
print(f" ASR预热: {'成功' if result.get('asr_warmup') else '失败/跳过'}")
|
|
|
|
if not result['tts_warmup'] or not result.get('asr_warmup'):
|
|
print("\n⚠ 警告: 预热失败可能是因为模型未下载")
|
|
|
|
def test_04_tts_endpoint_basic(self):
|
|
"""测试TTS基本功能"""
|
|
test_text = "这是一个测试"
|
|
|
|
response = self.client.post(
|
|
f'{API_BASE_URL}/v1/tts-asr/tts',
|
|
headers=self.headers,
|
|
json={'text': test_text}
|
|
)
|
|
|
|
if response.status_code == 500:
|
|
error = response.json()
|
|
print(f"\n⚠ TTS失败(可能是模型未加载): {error.get('detail', 'Unknown error')}")
|
|
self.skipTest("TTS模型未加载或不可用")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
result = response.json()
|
|
|
|
self.assertIn('audio_base64', result)
|
|
self.assertIn('format', result)
|
|
self.assertIn('duration_ms', result)
|
|
|
|
audio_data = base64.b64decode(result['audio_base64'])
|
|
self.assertGreater(len(audio_data), 0)
|
|
|
|
print(f"\nTTS测试成功:")
|
|
print(f" 输入文本: {test_text}")
|
|
print(f" 音频大小: {len(audio_data)} bytes")
|
|
|
|
def test_05_asr_endpoint_basic(self):
|
|
"""测试ASR基本功能"""
|
|
sample_rate = 16000
|
|
duration = 1.0
|
|
samples = int(sample_rate * duration)
|
|
|
|
silence = np.zeros(samples, dtype=np.int16)
|
|
|
|
wav_buffer = io.BytesIO()
|
|
with wave.open(wav_buffer, 'wb') as wf: # noqa: SIM115
|
|
wf.setnchannels(1)
|
|
wf.setsampwidth(2)
|
|
wf.setframerate(sample_rate)
|
|
wf.writeframes(silence.tobytes())
|
|
|
|
audio_bytes = wav_buffer.getvalue()
|
|
audio_base64 = base64.b64encode(audio_bytes).decode()
|
|
|
|
response = self.client.post(
|
|
f'{API_BASE_URL}/v1/tts-asr/asr',
|
|
headers=self.headers,
|
|
json={
|
|
'audio_base64': audio_base64,
|
|
'language': 'zh-CN'
|
|
}
|
|
)
|
|
|
|
if response.status_code in (500, 501):
|
|
detail = response.json().get('detail', 'Unknown')
|
|
print(f"\n⚠ ASR失败: {detail}")
|
|
self.skipTest("ASR模型未加载或不可用")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
result = response.json()
|
|
|
|
self.assertIn('text', result)
|
|
self.assertIn('language', result)
|
|
|
|
print(f"\nASR测试成功:")
|
|
print(f" 识别文本: '{result['text']}'")
|
|
print(f" 语言: {result['language']}")
|
|
|
|
def test_06_api_key_validation(self):
|
|
"""测试API密钥验证"""
|
|
wrong_headers = {'X-API-Key': 'wrong-api-key'}
|
|
|
|
response = self.client.get(
|
|
f'{API_BASE_URL}/v1/tts-asr/status',
|
|
headers=wrong_headers,
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
|
|
class PerformanceTest(unittest.TestCase):
|
|
"""性能测试"""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.client = httpx.Client(timeout=TEST_TIMEOUT)
|
|
cls.headers = {'X-API-Key': API_KEY}
|
|
|
|
try:
|
|
response = cls.client.get(f'{API_BASE_URL}/v1/tts-asr/status', headers=cls.headers)
|
|
cls.service_available = response.status_code == 200
|
|
except Exception: # noqa: ANN001, S110
|
|
cls.service_available = False
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.client.close()
|
|
|
|
def setUp(self):
|
|
if not self.service_available:
|
|
self.skipTest("后端服务不可用")
|
|
|
|
def test_tts_latency(self):
|
|
"""测试TTS延迟"""
|
|
latencies = []
|
|
for i in range(3):
|
|
start = time.time()
|
|
response = self.client.post(
|
|
f'{API_BASE_URL}/v1/tts-asr/tts',
|
|
headers=self.headers,
|
|
json={'text': '测试延迟'}
|
|
)
|
|
elapsed = time.time() - start
|
|
|
|
if response.status_code == 200:
|
|
latencies.append(elapsed)
|
|
|
|
if latencies:
|
|
print(f"\nTTS延迟测试:")
|
|
print(f" 平均: {sum(latencies)/len(latencies):.3f}s")
|
|
print(f" 最小: {min(latencies):.3f}s / 最大: {max(latencies):.3f}s")
|
|
|
|
|
|
def run_tests(test_type: Optional[str] = None) -> bool:
|
|
"""运行测试"""
|
|
loader = unittest.TestLoader()
|
|
suite = unittest.TestSuite()
|
|
|
|
TEST_MAP = {
|
|
'config': ('TTSASRIntegrationTest', 'test_01_config_endpoint'),
|
|
'status': ('TTSASRIntegrationTest', 'test_02_status_endpoint'),
|
|
'warmup': ('TTSASRIntegrationTest', 'test_03_warmup_endpoint'),
|
|
'tts': ('TTSASRIntegrationTest', 'test_04_tts_endpoint_basic'),
|
|
'asr': ('TTSASRIntegrationTest', 'test_05_asr_endpoint_basic'),
|
|
'perf': ('PerformanceTest', None),
|
|
}
|
|
|
|
if test_type and test_type in TEST_MAP:
|
|
cls_name, method = TEST_MAP[test_type]
|
|
if method:
|
|
suite.addTest(globals()[cls_name](method))
|
|
else:
|
|
suite.addTests(loader.loadTestsFromTestCase(globals()[cls_name]))
|
|
elif test_type == 'api_key':
|
|
suite.addTest(TTSASRIntegrationTest('test_06_api_key_validation'))
|
|
else:
|
|
suite.addTests(loader.loadTestsFromTestCase(TTSASRIntegrationTest))
|
|
suite.addTests(loader.loadTestsFromTestCase(PerformanceTest))
|
|
|
|
runner = unittest.TextTestRunner(verbosity=2)
|
|
result = runner.run(suite)
|
|
return result.wasSuccessful()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description='TTS/ASR 集成测试')
|
|
parser.add_argument('--test', choices=['config', 'status', 'warmup', 'tts', 'asr', 'perf', 'api_key'])
|
|
parser.add_argument('--url', default=API_BASE_URL)
|
|
parser.add_argument('--key', default=API_KEY)
|
|
|
|
args = parser.parse_args()
|
|
API_BASE_URL = args.url
|
|
API_KEY = args.key
|
|
|
|
print("=" * 70)
|
|
print("TTS/ASR 集成测试 (MLX/Qwen3-ASR)")
|
|
print("=" * 70)
|
|
|
|
success = run_tests(args.test)
|
|
sys.exit(0 if success else 1)
|