事情的开始
最近,我在完成一项与同声传译相关的工作,其中需要用到语音识别(ASR, Automatic Speech Recognition)功能。研究了一圈市面上的ASR服务,包括字节、百度、阿里和讯飞,最后发现阿里的价格最便宜,于是选择了他们的服务。
然而,在查看阿里ASR的使用文档时,我发现只有SDK接口,没有提供OpenAPI。我想,既然没有OpenAPI,那就用SDK吧。于是我参考文档,尝试了一下他们的SDK,代码如下:
class Callback(RecognitionCallback):
def on_complete(self) -> None:
# 识别完成
def on_error(self, result: RecognitionResult) -> None:
# 错误处理
def on_event(self, result: RecognitionResult) -> None:
# 处理识别结果
callback = Callback()
recognition = Recognition(model='paraformer-realtime-v2',
format='pcm',
sample_rate=16000,
callback=callback)
recognition.start()
一看,这SDK竟然不支持异步!而且写法非常的古怪,明显是根据java翻译而来的。我当前使用的是FastAPI框架,需要处理大量I/O操作,所以异步能力对我来说非常重要。进一步研究后发现,这个流式ASR通过WebSocket实现通信,底层启动了一个线程来处理逻辑。虽然可以用asyncio.to_thread将同步代码包装成异步,但这显然并不理想。
性能问题的暴露
当我开始开发同声传译的核心逻辑时,需要用到声音可用性检测(VAD, Voice Activity Detection),以判断声音是否中断。我使用的是基于机器学习的库 Silero VAD,这个操作相当耗费CPU资源。为了评估其性能,我用py-spy生成了火焰图,结果大吃一惊:
火焰图显示,ASR中的几行代码消耗了极高的CPU资源。仔细检查调用栈后,发现以下代码是罪魁祸首:
这段代码实现了一个生成器,通过单独线程监听WebSocket的返回结果。问题在于,当输入数据为空时,代码会空转,占用大量CPU资源。显然,这里缺少一个time.sleep()来释放CPU。
视频里的原始代码,由于没有开源,找了个存档,大家可以在这里品鉴。 github.com/ringsaturn/…
官方文档 help.aliyun.com/zh/dashscop…
优化实现
虽然加一个sleep能解决问题,但实在不够优雅。我改用asyncio.to_thread实现同步与异步之间的平滑转换,最终优化代码如下:
async def a_asr(
self, input_q: queue.Queue[bytes]
) -> AsyncGenerator[RecognitionResponse, None]:
out_q = asyncio.Queue()
asr_task = asyncio.create_task(asyncio.to_thread(self.asr, input_q, out_q))
while data := await out_q.get():
yield data
await asr_task
def asr(
self,
input_q: queue.Queue[bytes],
out_q: asyncio.Queue,
):
def input_generator() -> Generator[bytes, None, None]:
while data := input_q.get():
yield data
response_stream = self.call(
model=self.model,
task_group="audio",
task="asr",
function="recognition",
input=input_generator(),
api_protocol=ApiProtocol.WEBSOCKET,
ws_stream_mode=WebsocketStreamingMode.DUPLEX,
is_binary_input=True,
sample_rate=16000,
format="wav",
stream=True,
)
for part in response_stream:
if part.status_code != HTTPStatus.OK:
raise Exception(part.message)
out_q.put_nowait(RecognitionResponse.from_api_response(part))
out_q.put_nowait(None)
优化后的代码运行效果明显改善,火焰图显示CPU时间主要用于VAD处理,线程切换的开销显著减少:
最后
不知道阿里的DashScope水平为什么是这样。2024年了,依然没有提供OpenAPI,Python SDK缺乏异步支持(实际上内部是将异步功能转成了同步调用 😷)。更令人费解的是,同步代码实现水平令人堪忧,显然是并非python开发者实现的,且并有进行过详细的代码评审和单元测试,这种问题在现代开发中堪称“下毒”。使用这样的SDK,不禁让人怀疑,这真的是面向开发者的吗?