"""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'' f'{text}' f"" ) 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