left0ver's picture
Publish com.left0ver.agentcade 0.1.0
ad88834 verified
Raw History Blame Contribute Delete
4.25 kB
import asyncio
import time
from collections import deque
from contextlib import aclosing
import observability
from agentcade.audio_stream import CapturedPcm, PcmInputGain
from agentcade.speech import QwenSpeech
MAX_RECONNECT_DELAY_SECONDS = 10
STABLE_SESSION_SECONDS = 60
class SpeechInput:
def __init__(self, settings, agent, microphone, logger):
self.speech = QwenSpeech(settings)
self.agent, self.microphone, self.logger = agent, microphone, logger
self.busy = False
self.input_speaking = False
self.heard = ""
self.input_gain = PcmInputGain(settings.input_gain)
self.events = deque(maxlen=512)
def activity(self, speaking, text=""):
self.input_speaking = speaking and not self.busy
# 页面只在识别出文字时显示正在听,环境声音产生的空结果不算。
heard = text.strip() if self.input_speaking else ""
if heard != self.heard:
self.heard = heard
observability.user_speaking(heard)
async def run(self, ready):
messages = asyncio.Queue(32)
def receive(message):
if self.busy:
return
self.busy = True
self.activity(False)
self.events.append({
"type": "recognized", "time": time.monotonic(),
"started_at": message.started_at, "text": message.text,
})
self.logger.info("ASR 用户原话:%s", message.text)
turn_id = observability.user_input(
message.text, message.started_at, message.ended_at, message.sent_until,
)
messages.put_nowait((message, turn_id))
async def frames():
async with aclosing(self.microphone.audio()) as audio:
async for frame in audio:
muted = frame.muted or self.busy
pcm = bytes(len(frame.data)) if muted else self.input_gain.process(frame.data)
yield CapturedPcm(pcm, frame.captured_at, muted)
async def process():
while True:
message, turn_id = await messages.get()
try:
await self.microphone.pause(owner=self)
with observability.turn(turn_id):
completion = await self.agent.receive(message.text)
if completion is not None:
await asyncio.shield(completion)
finally:
self.microphone.resume(owner=self)
self.busy = False
messages.task_done()
async def recognize():
failures = 0
while True:
started = time.monotonic()
try:
# 每次连接使用新的音频流,上一次连接中断时旧的流已经关闭。
async with aclosing(frames()) as source:
await self.speech.transcribe(
source, receive, on_activity=self.activity, ready=ready,
hotwords=tuple(game.descriptor.name
for game in self.agent.games.values()),
)
return
except Exception as error:
# 启动时连不上识别服务属于配置或网络问题,直接报错;运行中断开则重新连接。
if not ready.is_set():
raise
stable = time.monotonic() - started >= STABLE_SESSION_SECONDS
failures = 1 if stable else failures + 1
delay = min(2 ** (failures - 1), MAX_RECONNECT_DELAY_SECONDS)
self.activity(False)
self.logger.warning("语音识别中断,%s 秒后重新连接(连续第 %s 次):%s",
delay, failures, observability.describe(error))
await asyncio.sleep(delay)
async with asyncio.TaskGroup() as group:
consumer = group.create_task(process())
await recognize()
await messages.join()
consumer.cancel()