diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 0ad38819e..625dcd631 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -1,13 +1,15 @@ import asyncio +import base64 import json import os import time from typing import Optional import aiohttp +import anyio import websockets from astrbot import logger -from astrbot.api.message_components import Plain, Image, At +from astrbot.api.message_components import Plain, Image, At, Record from astrbot.api.platform import Platform, PlatformMetadata from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform.astrbot_message import ( @@ -581,6 +583,32 @@ class WeChatPadProAdapter(Platform): logger.error(f"下载图片时发生错误: {e}") return None + async def download_voice( + self, to_user_name: str, new_msg_id: str, bufid: str, length: int + ): + """下载原始音频。""" + url = f"{self.base_url}/message/GetMsgVoice" + params = {"key": self.auth_key} + payload = { + "Bufid": bufid, + "ToUserName": to_user_name, + "NewMsgId": new_msg_id, + "Length": length, + } + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, params=params, json=payload) as response: + if response.status == 200: + return await response.json() + logger.error(f"下载音频失败: {response.status}") + return None + except aiohttp.ClientConnectorError as e: + logger.error(f"连接到 WeChatPadPro 服务失败: {e}") + return None + except Exception as e: + logger.error(f"下载音频时发生错误: {e}") + return None + async def _process_message_content( self, abm: AstrBotMessage, raw_message: dict, msg_type: int, content: str ): @@ -659,8 +687,38 @@ class WeChatPadProAdapter(Platform): if emoji_message is not None: abm.message.append(emoji_message) elif msg_type == 50: - # 语音/视频 logger.warning("收到语音/视频消息,待实现。") + elif msg_type == 34: + # 语音消息 + bufid = 0 + to_user_name = raw_message.get("to_user_name", {}).get("str", "") + new_msg_id = raw_message.get("new_msg_id") + data_parser = GeweDataParser( + content=content, + is_private_chat=(abm.type != MessageType.GROUP_MESSAGE), + raw_message=raw_message, + ) + + voicemsg = data_parser._format_to_xml().find("voicemsg") + bufid = voicemsg.get("bufid") or "0" + length = int(voicemsg.get("length") or 0) + voice_resp = await self.download_voice( + to_user_name=to_user_name, + new_msg_id=new_msg_id, + bufid=bufid, + length=length, + ) + voice_bs64_data = voice_resp.get("Data", {}).get("Base64", None) + if voice_bs64_data: + voice_bs64_data = base64.b64decode(voice_bs64_data) + temp_dir = os.path.join(get_astrbot_data_path(), "temp") + file_path = os.path.join( + temp_dir, f"wechatpadpro_voice_{abm.message_id}.silk" + ) + + async with await anyio.open_file(file_path, "wb") as f: + await f.write(voice_bs64_data) + abm.message.append(Record(file=file_path, url=file_path)) elif msg_type == 49: try: parser = GeweDataParser( diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py index 3c37be345..ab836ad28 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py @@ -7,11 +7,17 @@ import aiohttp from PIL import Image as PILImage # 使用别名避免冲突 from astrbot import logger -from astrbot.core.message.components import Image, Plain, WechatEmoji # Import Image +from astrbot.core.message.components import ( + Image, + Plain, + WechatEmoji, + Record, +) # Import Image from astrbot.core.message.message_event_result import MessageChain from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType from astrbot.core.platform.platform_metadata import PlatformMetadata +from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk_base64 if TYPE_CHECKING: from .wechatpadpro_adapter import WeChatPadProAdapter @@ -40,6 +46,8 @@ class WeChatPadProMessageEvent(AstrMessageEvent): await self._send_image(session, comp) elif isinstance(comp, WechatEmoji): await self._send_emoji(session, comp) + elif isinstance(comp, Record): + await self._send_voice(session, comp) await super().send(message) async def _send_image(self, session: aiohttp.ClientSession, comp: Image): @@ -98,6 +106,19 @@ class WeChatPadProMessageEvent(AstrMessageEvent): url = f"{self.adapter.base_url}/message/SendEmojiMessage" await self._post(session, url, payload) + async def _send_voice(self, session: aiohttp.ClientSession, comp: Record): + record_path = await comp.convert_to_file_path() + # 默认已经存在 data/temp 中 + b64, duration = await wav_to_tencent_silk_base64(record_path) + payload = { + "ToUserName": self.session_id, + "VoiceData": b64, + "VoiceFormat": 4, + "VoiceSecond": duration, + } + url = f"{self.adapter.base_url}/message/SendVoice" + await self._post(session, url, payload) + @staticmethod def _validate_base64(b64: str) -> bytes: return base64.b64decode(b64, validate=True) diff --git a/astrbot/core/utils/tencent_record_helper.py b/astrbot/core/utils/tencent_record_helper.py index f7b2eb5a4..00886bbf7 100644 --- a/astrbot/core/utils/tencent_record_helper.py +++ b/astrbot/core/utils/tencent_record_helper.py @@ -1,5 +1,10 @@ +import base64 import wave +import os from io import BytesIO +import asyncio +import tempfile +from astrbot.core.utils.astrbot_path import get_astrbot_data_path async def tencent_silk_to_wav(silk_path: str, output_path: str) -> str: @@ -50,3 +55,46 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> int: rate = wav.getframerate() duration = pilk.encode(wav_path, output_path, pcm_rate=rate, tencent=True) return duration + + +async def wav_to_tencent_silk_base64(wav_path: str) -> str: + """ + 将 WAV 文件转为 Silk,并返回 Base64 字符串。 + 默认采样率为 24000,输出临时文件为 temp/output.silk。 + + 参数: + - wav_path: 输入 .wav 文件路径(需为 PCM 16bit) + + 返回: + - Base64 编码的 Silk 字符串 + - duration: 音频时长(秒) + """ + try: + import pilk + except ImportError as e: + raise Exception("pysilk 模块未安装,请安装 pysilk") from e + + temp_dir = os.path.join(get_astrbot_data_path(), "temp") + os.makedirs(temp_dir, exist_ok=True) + + with wave.open(wav_path, "rb") as wav: + rate = wav.getframerate() + + with tempfile.NamedTemporaryFile( + suffix=".silk", delete=False, dir=temp_dir + ) as tmp_file: + silk_path = tmp_file.name + + try: + duration = await asyncio.to_thread( + pilk.encode, wav_path, silk_path, pcm_rate=rate, tencent=True + ) + + with open(silk_path, "rb") as f: + silk_bytes = await asyncio.to_thread(f.read) + silk_b64 = base64.b64encode(silk_bytes).decode("utf-8") + + return silk_b64, duration # 已是秒 + finally: + if os.path.exists(silk_path): + os.remove(silk_path)