"""otto 活字印刷 —— 触发器(Trigger)层。""" from __future__ import annotations import asyncio import base64 import os import re import socket import time from pathlib import Path from nonebot import get_driver, on_startswith, require from nonebot.adapters.onebot.v11 import MessageEvent from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import UniMessage from .. import _TEMP_ROOT from ..services.synthesis import synthesize require("nonebot_plugin_alconna") __plugin_meta__ = PluginMetadata( name="otto活字印刷", description="鬼畜音源活字印刷(otto语音合成)", usage="otto 一些内容 生成otto语音\notto 一些内容 -reverse 生成并倒放", type="application", ) otto = on_startswith("otto") async def _remove_later(path: Path) -> None: """发送完成后延迟清理临时音频,避免 NapCat 还在下载/转码时被删。""" await asyncio.sleep(300) path.unlink(missing_ok=True) def _public_base() -> str: """构造对外可达的 bot HTTP 地址,供协议端跨机器下载音频。 优先读 .env 的 OTTO_PUBLIC_BASE_URL(例如 http://192.168.2.100:39697 或公网地址);未配置时回退为 HOST:PORT,且 0.0.0.0 自动替换成本机可路由 IP。 """ base = os.getenv("OTTO_PUBLIC_BASE_URL", "").strip().rstrip("/") if base: return base cfg = get_driver().config host = str(getattr(cfg, "host", None) or os.getenv("HOST", "0.0.0.0")) port = str(getattr(cfg, "port", None) or os.getenv("PORT", "39697")) if host in ("0.0.0.0", "::", ""): try: # 用 UDP connect 探测本机对外(路由) IP,不真正发包 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) host = s.getsockname()[0] s.close() except OSError: host = "127.0.0.1" return f"http://{host}:{port}" @otto.handle() async def otto_handle(ev: MessageEvent): txt = ev.message.extract_plain_text().removeprefix("otto") pattern_reverse = re.compile(r"-reverse\s*$") if re.search(pattern_reverse, txt): reverse = True txt = re.sub(pattern_reverse, "", txt) txt = ",,," + txt else: reverse = False txt = txt + ",,," b64audio = await synthesize(txt, reverse) audio_bytes = base64.b64decode(b64audio) _TEMP_ROOT.mkdir(parents=True, exist_ok=True) mp3_path = _TEMP_ROOT / f"otto-{time.time_ns()}.mp3" mp3_path.write_bytes(audio_bytes) url = f"{_public_base()}/api/otto/{mp3_path.name}" try: await UniMessage.voice(url=url).send() finally: asyncio.create_task(_remove_later(mp3_path))