Download src/agentcade/speech_input.py from left0ver/WatcherRobot-com.left0ver.agentcade: direct link, hf CLI and curl.
- Browser
- Download file 4.25 kB
-
https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/speech_input.py
- Command line
-
hf download hf://spaces/left0ver/WatcherRobot-com.left0ver.agentcade/src/agentcade/speech_input.py
-
curl -L -o speech_input.py https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/speech_input.py
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() | |