Download src/agentcade/audio_stream.py from left0ver/WatcherRobot-com.left0ver.agentcade: direct link, hf CLI and curl.
- Browser
- Download file 3.75 kB
-
https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/audio_stream.py
- Command line
-
hf download hf://spaces/left0ver/WatcherRobot-com.left0ver.agentcade/src/agentcade/audio_stream.py
-
curl -L -o audio_stream.py https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/audio_stream.py
3.75 kB
| import asyncio | |
| import time | |
| from collections.abc import Awaitable, Callable | |
| from dataclasses import dataclass | |
| from fractions import Fraction | |
| from av import AudioFrame | |
| from av.filter import Graph | |
| from watcherobot.audio import PCMAudio | |
| from watcherobot.robot import AudioDomain | |
| PlaybackCallback = Callable[[float], Awaitable[None]] | |
| class CapturedPcm: | |
| data: bytes | |
| captured_at: float | |
| muted: bool = False | |
| class PcmInputGain: | |
| def __init__(self, gain): | |
| if not 1 <= gain <= 8: | |
| raise ValueError("麦克风输入增益必须在 1 至 8 之间") | |
| self.graph = Graph() | |
| source = self.graph.add_abuffer(sample_rate=16000, format="s16", layout="mono") | |
| volume = self.graph.add("volume", f"volume={gain}:precision=fixed") | |
| sink = self.graph.add("abuffersink") | |
| source.link_to(volume) | |
| volume.link_to(sink) | |
| self.graph.configure() | |
| self.samples = 0 | |
| def process(self, pcm): | |
| if not pcm or len(pcm) % 2: | |
| raise ValueError("输入增益处理需要完整的 PCM 采样") | |
| frame = AudioFrame(format="s16", layout="mono", samples=len(pcm) // 2) | |
| frame.sample_rate = 16000 | |
| frame.time_base = Fraction(1, 16000) | |
| frame.pts = self.samples | |
| frame.planes[0].update(pcm) | |
| self.samples += frame.samples | |
| self.graph.push(frame) | |
| output = self.graph.pull() | |
| if output.samples != frame.samples or output.format.name != "s16": | |
| raise ValueError("输入增益处理改变了 PCM 格式或采样数量") | |
| return bytes(output.planes[0])[:output.samples * 2] | |
| class PlaybackMetrics: | |
| pcm_bytes: int | |
| playback_id: int | |
| started_seconds: float | |
| completed_seconds: float | |
| async def sdk_command(function, *args, **kwargs): | |
| # SDK 指令在工作线程等待确认,取消时也要等待该指令结束。 | |
| task = asyncio.create_task(asyncio.to_thread(function, *args, **kwargs)) | |
| try: | |
| return await asyncio.shield(task) | |
| except asyncio.CancelledError: | |
| await task | |
| raise | |
| class PcmAudioPlayer: | |
| def __init__(self, audio: AudioDomain): | |
| self.audio = audio | |
| self.playing = False | |
| async def play( | |
| self, pcm: bytes, *, on_start: PlaybackCallback | None = None, | |
| ) -> PlaybackMetrics: | |
| if self.playing: | |
| raise RuntimeError("请按顺序提交语音播报") | |
| source = PCMAudio(pcm) | |
| self.playing = True | |
| playback = None | |
| epoch = time.monotonic() | |
| try: | |
| opening = asyncio.create_task(asyncio.to_thread(self.audio.play_pcm, source.data)) | |
| try: | |
| playback = await asyncio.shield(opening) | |
| except asyncio.CancelledError: | |
| # 取得已启动的播放任务,确保取消期间仍能停止设备播放。 | |
| playback = await opening | |
| raise | |
| started = time.monotonic() - epoch | |
| async with asyncio.timeout(playback.expected_duration_seconds + 15): | |
| if on_start is not None: | |
| await on_start(playback.expected_duration_seconds) | |
| while not playback.state.terminal: | |
| await asyncio.sleep(0.02) | |
| playback.wait(0) | |
| return PlaybackMetrics( | |
| pcm_bytes=len(source.data), playback_id=playback.id, | |
| started_seconds=started, completed_seconds=time.monotonic() - epoch, | |
| ) | |
| finally: | |
| try: | |
| if playback is not None and not playback.state.terminal: | |
| await sdk_command(playback.cancel) | |
| finally: | |
| self.playing = False | |