ForcePilot/backend/package/yuxi/channels/adapters/slack/voice/tts.py
Kris a2aa782b86 feat(slack adapter): 实现完整的Slack频道适配器基础功能
新增Slack适配器全套核心模块,包括消息处理流水线、会话管理、配置适配、权限控制等完整功能:
1. 新增语音、视觉相关的TTS和图像分析导出接口
2. 实现消息预处理、路由、线程上下文处理的完整流水线
3. 新增账号管理、缓存机制、房间上下文提取功能
4. 支持Webhook和Socket Mode两种事件接收方式
5. 实现权限白名单、审批配对、自动状态管理功能
6. 新增配置迁移、作用域校验、重连策略等辅助模块
2026-05-12 00:48:57 +08:00

145 lines
4.9 KiB
Python

from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
@dataclass
class SlackTTSConfig:
enabled: bool = False
provider: str = "openai"
voice: str = "alloy"
speed: float = 1.0
api_url: str = ""
api_key: str = ""
model: str = "tts-1"
response_format: str = "mp3"
visible_text_after_voice: bool = True
@classmethod
def from_config(cls, config: dict[str, Any] | None) -> SlackTTSConfig:
if not config:
return cls()
tts_cfg = config.get("tts", {}) or {}
return cls(
enabled=tts_cfg.get("enabled", False),
provider=tts_cfg.get("provider", "openai"),
voice=tts_cfg.get("voice", "alloy"),
speed=float(tts_cfg.get("speed", 1.0)),
api_url=tts_cfg.get("api_url", ""),
api_key=tts_cfg.get("api_key", ""),
model=tts_cfg.get("model", "tts-1"),
response_format=tts_cfg.get("response_format", "mp3"),
visible_text_after_voice=tts_cfg.get("visible_text_after_voice", True),
)
async def synthesize_slack_tts(text: str, tts_config: SlackTTSConfig) -> bytes | None:
if tts_config.provider == "openai":
return await _synthesize_openai(text, tts_config)
elif tts_config.provider == "azure":
return await _synthesize_azure(text, tts_config)
elif tts_config.provider == "custom":
return await _synthesize_custom(text, tts_config)
else:
logger.warning(f"[SlackTTS] Unknown TTS provider: {tts_config.provider}")
return None
async def _synthesize_openai(text: str, cfg: SlackTTSConfig) -> bytes | None:
import aiohttp
api_url = cfg.api_url or "https://api.openai.com/v1/audio/speech"
api_key = cfg.api_key
if not api_key:
logger.warning("[SlackTTS] OpenAI API key not configured")
return None
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {
"model": cfg.model,
"input": text,
"voice": cfg.voice,
"speed": cfg.speed,
"response_format": cfg.response_format,
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(
api_url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=60)
) as resp:
if resp.status == 200:
return await resp.read()
logger.warning(f"[SlackTTS] OpenAI TTS returned status {resp.status}: {await resp.text()}")
return None
except Exception as e:
logger.warning(f"[SlackTTS] OpenAI TTS call failed: {e}")
return None
async def _synthesize_azure(text: str, cfg: SlackTTSConfig) -> bytes | None:
import aiohttp
region = cfg.api_url or "eastasia"
subscription_key = cfg.api_key
if not subscription_key:
logger.warning("[SlackTTS] Azure subscription key not configured")
return None
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="{cfg.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(), timeout=aiohttp.ClientTimeout(total=60)
) as resp:
if resp.status == 200:
return await resp.read()
logger.warning(f"[SlackTTS] Azure TTS returned status {resp.status}")
return None
except Exception as e:
logger.warning(f"[SlackTTS] Azure TTS call failed: {e}")
return None
async def _synthesize_custom(text: str, cfg: SlackTTSConfig) -> bytes | None:
import aiohttp
if not cfg.api_url:
logger.warning("[SlackTTS] Custom TTS API URL not configured")
return None
headers = {"Content-Type": "application/json"}
if cfg.api_key:
headers["Authorization"] = f"Bearer {cfg.api_key}"
payload = {"text": text, "voice": cfg.voice, "format": cfg.response_format}
try:
async with aiohttp.ClientSession() as session:
async with session.post(
cfg.api_url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=60)
) as resp:
if resp.status == 200:
return await resp.read()
logger.warning(f"[SlackTTS] Custom TTS returned status {resp.status}")
return None
except Exception as e:
logger.warning(f"[SlackTTS] Custom TTS call failed: {e}")
return None