新增飞书机器人适配器全套功能,包括: - 基础适配器入口与工具导出 - 消息格式化、卡片渲染、回复调度逻辑 - 会话ID生成、模型覆盖策略 - 消息发送缓存、顺序队列管理 - 飞书签名验证、加解密webhook请求 - 审批权限校验、机器人菜单事件处理 - 文档评论、钉消息、语音转码处理 - 静态/动态目录管理、子代理生命周期管理 - 各类工具集:聊天、云盘、文档、知识库API封装
75 lines
2.0 KiB
Python
75 lines
2.0 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TARGET_SAMPLE_RATE = 48000
|
|
TARGET_BITRATE = "64k"
|
|
TARGET_FORMAT = "ogg"
|
|
TARGET_CODEC = "libopus"
|
|
|
|
|
|
def check_ffmpeg_available() -> bool:
|
|
try:
|
|
result = subprocess.run(["ffmpeg", "-version"], capture_output=True, timeout=5)
|
|
return result.returncode == 0
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return False
|
|
|
|
|
|
def transcode_to_ogg_opus(
|
|
input_data: bytes,
|
|
input_format: str = "mp3",
|
|
sample_rate: int = TARGET_SAMPLE_RATE,
|
|
bitrate: str = TARGET_BITRATE,
|
|
) -> bytes | None:
|
|
if not check_ffmpeg_available():
|
|
logger.warning("[FeishuAudio] FFmpeg not available, skipping transcode")
|
|
return None
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=f".{input_format}", delete=False) as infile:
|
|
infile.write(input_data)
|
|
input_path = infile.name
|
|
|
|
output_path = input_path + f".{TARGET_FORMAT}"
|
|
|
|
try:
|
|
cmd = [
|
|
"ffmpeg",
|
|
"-y",
|
|
"-i",
|
|
input_path,
|
|
"-ar",
|
|
str(sample_rate),
|
|
"-b:a",
|
|
bitrate,
|
|
"-c:a",
|
|
TARGET_CODEC,
|
|
output_path,
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, timeout=60)
|
|
if result.returncode != 0:
|
|
logger.error(
|
|
"[FeishuAudio] FFmpeg transcode failed: %s",
|
|
result.stderr.decode(errors="replace")[:300],
|
|
)
|
|
return None
|
|
|
|
output_bytes = Path(output_path).read_bytes()
|
|
logger.info(
|
|
"[FeishuAudio] Transcode complete: %d -> %d bytes",
|
|
len(input_data),
|
|
len(output_bytes),
|
|
)
|
|
return output_bytes
|
|
except Exception as e:
|
|
logger.error("[FeishuAudio] Transcode error: %s", e)
|
|
return None
|
|
finally:
|
|
Path(input_path).unlink(missing_ok=True)
|
|
Path(output_path).unlink(missing_ok=True)
|