Files

354 lines
14 KiB
Python

import asyncio
import logging
import os
import time
import concurrent.futures
from typing import Dict, Optional
import aiohttp
from tabulate import tabulate
from core.utils.asr import create_instance as create_stt_instance
# 设置全局日志级别为WARNING,抑制INFO级别日志
logging.basicConfig(level=logging.WARNING)
description = "语音识别模型性能测试"
class ASRPerformanceTester:
def __init__(self):
self.config = self._load_config_from_data_dir()
self.test_wav_list = self._load_test_wav_files()
self.results = {"stt": {}}
# 调试日志
print(f"[DEBUG] 加载的ASR配置: {self.config.get('ASR', {})}")
print(f"[DEBUG] 音频文件数量: {len(self.test_wav_list)}")
def _load_config_from_data_dir(self) -> Dict:
"""从 data 目录加载所有 .config.yaml 文件的配置"""
config = {"ASR": {}}
data_dir = os.path.join(os.getcwd(), "data")
print(f"[DEBUG] 扫描配置文件目录: {data_dir}")
for root, _, files in os.walk(data_dir):
for file in files:
if file.endswith(".config.yaml"):
file_path = os.path.join(root, file)
try:
with open(file_path, "r", encoding="utf-8") as f:
import yaml
file_config = yaml.safe_load(f)
# 兼容大小写的 ASR/asr 配置
asr_config = file_config.get("ASR") or file_config.get("asr")
if asr_config:
config["ASR"].update(asr_config)
print(f"[DEBUG] 从 {file_path} 加载 ASR 配置成功")
except Exception as e:
print(f" 加载配置文件 {file_path} 失败: {str(e)}")
return config
def _load_test_wav_files(self) -> list:
"""加载测试用的音频文件(添加路径调试)"""
wav_root = os.path.join(os.getcwd(), "config", "assets")
print(f"[DEBUG] 音频文件目录: {wav_root}")
test_wav_list = []
if os.path.exists(wav_root):
file_list = os.listdir(wav_root)
print(f"[DEBUG] 找到音频文件: {file_list}")
for file_name in file_list:
file_path = os.path.join(wav_root, file_name)
if os.path.getsize(file_path) > 300 * 1024: # 300KB
with open(file_path, "rb") as f:
test_wav_list.append(f.read())
else:
print(f" 目录不存在: {wav_root}")
return test_wav_list
async def _test_single_audio(self, stt_name: str, stt, audio_data: bytes) -> Optional[float]:
"""测试单个音频文件的性能"""
try:
start_time = time.time()
text, _ = await stt.speech_to_text([audio_data], "1", stt.audio_format)
if text is None:
return None
duration = time.time() - start_time
# 检测0.000s的异常时间
if abs(duration) < 0.001: # 小于1毫秒视为异常
print(f"{stt_name} 检测到异常时间: {duration:.6f}s (视为错误)")
return None
return duration
except Exception as e:
error_msg = str(e).lower()
if "502" in error_msg or "bad gateway" in error_msg:
print(f"{stt_name} 遇到502错误")
return None
return None
async def _test_stt_with_timeout(self, stt_name: str, config: Dict) -> Dict:
"""异步测试单个STT性能,带超时控制"""
try:
# 检查配置有效性
token_fields = ["access_token", "api_key", "token"]
if any(
field in config
and str(config[field]).lower() in ["你的", "placeholder", "none", "null", ""]
for field in token_fields
):
print(f" STT {stt_name} 未配置有效access_token/api_key,已跳过")
return {
"name": stt_name,
"type": "stt",
"errors": 1,
"error_type": "配置错误"
}
module_type = config.get("type", stt_name)
stt = create_stt_instance(module_type, config, delete_audio_file=True)
stt.audio_format = "pcm"
print(f" 测试 STT: {stt_name}")
# 使用线程池和超时控制
loop = asyncio.get_event_loop()
# 测试第一个音频文件作为连通性检查
try:
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(
lambda: asyncio.run(self._test_single_audio(stt_name, stt, self.test_wav_list[0]))
)
first_result = await asyncio.wait_for(
asyncio.wrap_future(future), timeout=10.0
)
if first_result is None:
print(f" {stt_name} 连接失败")
return {
"name": stt_name,
"type": "stt",
"errors": 1,
"error_type": "网络错误"
}
except asyncio.TimeoutError:
print(f" {stt_name} 连接超时(10秒),跳过")
return {
"name": stt_name,
"type": "stt",
"errors": 1,
"error_type": "超时连接"
}
except Exception as e:
error_msg = str(e).lower()
if "502" in error_msg or "bad gateway" in error_msg:
print(f" {stt_name} 遇到502错误,跳过")
return {
"name": stt_name,
"type": "stt",
"errors": 1,
"error_type": "502网络错误"
}
print(f" {stt_name} 连接异常: {str(e)}")
return {
"name": stt_name,
"type": "stt",
"errors": 1,
"error_type": "网络错误"
}
# 全量测试,带超时控制
total_time = 0
valid_tests = 0
test_count = len(self.test_wav_list)
for i, audio_data in enumerate(self.test_wav_list, 1):
try:
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(
lambda: asyncio.run(self._test_single_audio(stt_name, stt, audio_data))
)
duration = await asyncio.wait_for(
asyncio.wrap_future(future), timeout=10.0
)
if duration is not None and duration > 0.001:
total_time += duration
valid_tests += 1
print(f" {stt_name} [{i}/{test_count}] 耗时: {duration:.2f}s")
else:
print(f" {stt_name} [{i}/{test_count}] 测试失败(含0.000s异常)")
except asyncio.TimeoutError:
print(f" {stt_name} [{i}/{test_count}] 超时(10秒),跳过")
continue
except Exception as e:
error_msg = str(e).lower()
if "502" in error_msg or "bad gateway" in error_msg:
print(f" {stt_name} [{i}/{test_count}] 502错误,跳过")
return {
"name": stt_name,
"type": "stt",
"errors": 1,
"error_type": "502网络错误"
}
print(f" {stt_name} [{i}/{test_count}] 异常: {str(e)}")
continue
# 检查有效测试数量
if valid_tests < test_count * 0.3: # 至少30%成功率
print(f" {stt_name} 成功测试过少({valid_tests}/{test_count}),可能网络不稳定")
return {
"name": stt_name,
"type": "stt",
"errors": 1,
"error_type": "网络错误"
}
if valid_tests == 0:
return {
"name": stt_name,
"type": "stt",
"errors": 1,
"error_type": "网络错误"
}
avg_time = total_time / valid_tests
return {
"name": stt_name,
"type": "stt",
"avg_time": avg_time,
"success_rate": f"{valid_tests}/{test_count}",
"errors": 0,
}
except Exception as e:
error_msg = str(e).lower()
if "502" in error_msg or "bad gateway" in error_msg:
error_type = "502网络错误"
elif "timeout" in error_msg:
error_type = "超时连接"
else:
error_type = "网络错误"
print(f"⚠️ {stt_name} 测试失败: {str(e)}")
return {
"name": stt_name,
"type": "stt",
"errors": 1,
"error_type": error_type
}
def _print_results(self):
"""打印测试结果,按响应时间排序"""
print("\n" + "=" * 50)
print("ASR 性能测试结果")
print("=" * 50)
if not self.results.get("stt"):
print("没有可用的测试结果")
return
headers = ["模型名称", "平均耗时(s)", "成功率", "状态"]
table_data = []
# 收集所有数据并分类
valid_results = []
error_results = []
for name, data in self.results["stt"].items():
if data["errors"] == 0:
# 正常结果
avg_time = f"{data['avg_time']:.3f}"
success_rate = data.get("success_rate", "N/A")
status = "✅ 正常"
# 保存用于排序的值
sort_key = data["avg_time"]
valid_results.append({
"name": name,
"avg_time": avg_time,
"success_rate": success_rate,
"status": status,
"sort_key": sort_key,
})
else:
# 错误结果
avg_time = "-"
success_rate = "0/N"
# 获取具体错误类型
error_type = data.get("error_type", "网络错误")
status = f"❌ {error_type}"
error_results.append([name, avg_time, success_rate, status])
# 按响应时间升序排序(从快到慢)
valid_results.sort(key=lambda x: x["sort_key"])
# 将排序后的有效结果转换为表格数据
for result in valid_results:
table_data.append([
result["name"],
result["avg_time"],
result["success_rate"],
result["status"],
])
# 将错误结果添加到表格数据末尾
table_data.extend(error_results)
print(tabulate(table_data, headers=headers, tablefmt="grid"))
print("\n测试说明:")
print("- 超时控制:单个音频最大等待时间为10秒")
print("- 错误处理:自动跳过502错误、超时和网络异常的模型")
print("- 成功率:成功识别的音频数量/总测试音频数量")
print("- 排序规则:按平均耗时从快到慢排序,错误模型排最后")
print("\n测试完成!")
async def run(self):
"""执行全量异步测试"""
print("开始筛选可用ASR模块...")
if not self.config.get("ASR"):
print("配置中未找到 ASR 模块")
return
all_tasks = []
for stt_name, config in self.config["ASR"].items():
# 检查配置有效性
token_fields = ["access_token", "api_key", "token"]
if any(
field in config
and str(config[field]).lower() in ["你的", "placeholder", "none", "null", ""]
for field in token_fields
):
print(f"ASR {stt_name} 未配置有效access_token/api_key,已跳过")
continue
print(f"添加 ASR 测试任务: {stt_name}")
all_tasks.append(self._test_stt_with_timeout(stt_name, config))
if not all_tasks:
print("没有可用的ASR模块进行测试。")
return
print(f"\n找到 {len(all_tasks)} 个可用ASR模块")
print("\n开始并发测试所有ASR模块...")
all_results = await asyncio.gather(*all_tasks, return_exceptions=True)
# 处理结果
for result in all_results:
if isinstance(result, dict) and result.get("type") == "stt":
self.results["stt"][result["name"]] = result
# 打印结果
self._print_results()
async def main():
tester = ASRPerformanceTester()
await tester.run()
if __name__ == "__main__":
asyncio.run(main())