ForcePilot/backend/package/yuxi/channel/extensions/qqbot/audio.py
Kris 2ab65f153f feat(channel): 添加 QQ Bot 渠道扩展
新增 QQ Bot 渠道扩展,支持在 Yuxi 平台中集成 QQ 机器人渠道。

包含以下功能模块:
- api_client: QQ API 客户端封装
- api_routes: API 路由管理
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- websocket: WebSocket 实时连接
- credentials: 凭证管理
- token: Token 管理
- outbound: 外发消息管理
- outbound_media: 媒体外发
- streaming: 流式消息处理
- streaming_media: 媒体流处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- pipeline: 消息管道
- pipeline_stages: 管道阶段
- commands: 指令处理
- commands_builtin: 内置指令
- interaction: 交互处理
- approval: 审批流程
- ark: ARK 消息
- audio: 音频处理
- media: 媒体资源
- media_chunked: 分块媒体
- media_tags: 媒体标签
- message_queue: 消息队列
- delivery: 消息送达确认
- reconnect: 重连机制
- typing_keepalive: 输入状态保活
- group_activation: 群激活
- group_gating: 群门控
- group_history: 群历史
- known_users: 已知用户
- ref_index: 引用索引
- tools: Agent 工具集成
- types: 类型定义
2026-05-21 11:35:12 +08:00

111 lines
3.4 KiB
Python

from __future__ import annotations
import asyncio
import logging
import subprocess
import tempfile
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
class QQBotAudio:
def __init__(self, ffmpeg_path: str = "ffmpeg"):
self._ffmpeg = ffmpeg_path
self._audio_format_policy = {
"stt_direct_formats": ["wav", "pcm"],
"upload_direct_formats": ["silk", "amr"],
"transcode_enabled": True,
}
async def silk_to_wav(self, silk_data: bytes) -> bytes:
with tempfile.NamedTemporaryFile(suffix=".silk", delete=False) as silk_file:
silk_file.write(silk_data)
silk_path = silk_file.name
wav_path = silk_path + ".wav"
try:
proc = await asyncio.create_subprocess_exec(
self._ffmpeg,
"-y",
"-f",
"silk",
"-i",
silk_path,
"-acodec",
"pcm_s16le",
"-ar",
"24000",
"-ac",
"1",
wav_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await proc.communicate()
if proc.returncode != 0:
logger.warning("ffmpeg SILK→WAV conversion failed, returncode=%d", proc.returncode)
return silk_data
wav_data = Path(wav_path).read_bytes()
return wav_data
except FileNotFoundError:
logger.warning("ffmpeg not found, returning raw data")
return silk_data
finally:
Path(silk_path).unlink(missing_ok=True)
Path(wav_path).unlink(missing_ok=True)
async def wav_to_silk(self, wav_data: bytes) -> bytes:
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wav_file:
wav_file.write(wav_data)
wav_path = wav_file.name
silk_path = wav_path + ".silk"
try:
proc = await asyncio.create_subprocess_exec(
self._ffmpeg,
"-y",
"-i",
wav_path,
"-acodec",
"silk",
"-ar",
"24000",
"-ac",
"1",
silk_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await proc.communicate()
if proc.returncode != 0:
logger.warning("ffmpeg WAV→SILK conversion failed, returncode=%d", proc.returncode)
return wav_data
silk_data = Path(silk_path).read_bytes()
return silk_data
except FileNotFoundError:
logger.warning("ffmpeg not found, returning raw data")
return wav_data
finally:
Path(wav_path).unlink(missing_ok=True)
Path(silk_path).unlink(missing_ok=True)
def is_silk(self, data: bytes) -> bool:
return data[:2] == b"#!"
def is_direct_upload_format(self, ext: str) -> bool:
return ext.lower().lstrip(".") in self._audio_format_policy["upload_direct_formats"]
def can_transcode(self) -> bool:
try:
subprocess.run([self._ffmpeg, "-version"], capture_output=True, check=False)
return True
except FileNotFoundError:
return False