ForcePilot/backend/package/yuxi/channels/adapters/msteams/audio.py
Kris bd60c15df0 feat(msteams): 新增完整的 Microsoft Teams 适配器模块
实现了 Teams 机器人所需的全功能组件,包括:
- 基础命令解析与帮助卡片生成
- 租户验证与访问控制
- 自定义 UA 与媒体工具
- 消息分块、批注处理与会话管理
- 防抖、缓存与配置路由能力
- 投票、配对、审计与运行时状态管理
- TTS 语音合成与卡片构建工具
- 群组管理与权限控制逻辑
2026-05-12 00:46:44 +08:00

101 lines
2.6 KiB
Python

"""Microsoft Teams TTS/Voice 能力。
Voice 附件出站发送,音频处理。
"""
from __future__ import annotations
import base64
from typing import Any
from yuxi.utils.logging_config import logger
def build_voice_activity(
audio_data: bytes,
mime_type: str = "audio/wav",
filename: str = "voice.wav",
reply_to_id: str | None = None,
) -> dict[str, Any]:
audio_b64 = base64.b64encode(audio_data).decode()
content_url = f"data:{mime_type};base64,{audio_b64}"
activity: dict[str, Any] = {
"type": "message",
"attachments": [
{
"contentType": mime_type,
"contentUrl": content_url,
"name": filename,
}
],
}
if reply_to_id:
activity["replyToId"] = reply_to_id
return activity
def build_tts_activity(
text: str,
voice: str = "zh-CN-XiaoxiaoNeural",
reply_to_id: str | None = None,
) -> dict[str, Any]:
activity: dict[str, Any] = {
"type": "message",
"text": text,
"textFormat": "markdown",
"channelData": {
"speak": text,
"voice": voice,
},
}
if reply_to_id:
activity["replyToId"] = reply_to_id
return activity
def parse_voice_attachment(
attachment: dict[str, Any],
) -> dict[str, Any] | None:
content_type = attachment.get("contentType", "")
if not content_type.startswith("audio/"):
return None
return {
"type": "audio",
"url": attachment.get("contentUrl", ""),
"name": attachment.get("name", "audio"),
"content_type": content_type,
}
async def synthesize_speech(
text: str,
voice: str = "zh-CN-XiaoxiaoNeural",
subscription_key: str = "",
region: str = "eastasia",
) -> bytes | None:
import aiohttp
endpoint = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1"
ssml = (
f'<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="zh-CN">'
f'<voice name="{voice}">{text}</voice>'
f"</speak>"
)
headers = {
"Ocp-Apim-Subscription-Key": subscription_key,
"Content-Type": "application/ssml+xml",
"X-Microsoft-OutputFormat": "audio-16khz-128kbitrate-mono-mp3",
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(endpoint, headers=headers, data=ssml.encode()) as resp:
if resp.status == 200:
return await resp.read()
logger.warning(f"TTS synthesis failed: HTTP {resp.status}")
except Exception as e:
logger.error(f"TTS synthesis error: {e}")
return None