Files
MoviePilot/app/agent/tools/impl/send_voice_message.py
freeman f3ad81b39e style: apply safe ruff auto-fixes and exempt startup late imports
Run `ruff check app tests scripts --fix` with the configured rule set
(E4/E7/E9/F/I): import sorting (I001), f-strings without placeholders
(F541), lambda/variable/unused-import cleanups that ruff marks safe.
Nine files whose only ruff debt sits next to pre-existing pylint
import-resolution errors (lazy exports, sys.path script imports) or
whose "unused" imports are reached through attribute access by tests are
left untouched so the changed-file pylint gate and the benchmark tests
stay green; their diagnostics remain in the baseline.

app/startup/lifecycle/__init__.py and app/main.py import business
modules after cache composition, import-path and stdio setup on purpose
(the source comments document the contract), so declare E402
per-file-ignores for them instead of adding per-line noqa.

Ruff ratchet watermark drops from 498 to 100; sync the baseline fixture
and the optimization checklist figure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 13:29:09 +08:00

108 lines
3.9 KiB
Python

"""发送语音消息工具。"""
from typing import Optional, Type
from pydantic import BaseModel, Field
from app.agent.llm.capability import AgentCapabilityManager
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
from app.schemas.message import Message, MessageType
class SendVoiceMessageInput(BaseModel):
"""发送语音消息工具输入。"""
message: str = Field(
...,
description="The spoken content to send back to the user",
)
class SendVoiceMessageTool(MoviePilotTool):
"""发送 Agent 语音回复的工具。"""
name: str = "send_voice_message"
tags: list[str] = [
ToolTag.Write,
ToolTag.Message,
ToolTag.TerminalResponse,
]
sends_message: bool = True
return_direct: bool = True
description: str = (
"Send a voice reply to the current user. Use this only when the user explicitly asks for "
"a voice reply or when spoken playback is clearly better than plain text. On channels "
"without voice support or when TTS is unavailable, it automatically falls back to sending "
"the same content as plain text. This is a terminal response tool: put the complete "
"user-facing reply in `message`; after this tool runs, do not send another text reply "
"or call `send_message` with the same content."
)
args_schema: Type[BaseModel] = SendVoiceMessageInput
def get_tool_message(self, **kwargs) -> Optional[str]:
"""生成语音回复工具的执行提示。"""
message = kwargs.get("message") or ""
if len(message) > 40:
message = message[:40] + "..."
return f"发送语音回复: {message}"
async def run(self, message: str, **kwargs) -> str:
"""合成语音并发送到当前对话渠道,不支持时回退为文字。"""
if not message:
return "语音回复内容不能为空"
voice_path = None
used_voice = False
channel = self._channel or ""
reply_mode = AgentCapabilityManager.resolve_reply_mode(
channel=channel,
source=self._source,
)
fallback_reason = "当前渠道不支持语音回复"
if not AgentCapabilityManager.supports_audio_output():
fallback_reason = "当前未启用音频输出"
if (
reply_mode == AgentCapabilityManager.REPLY_MODE_NATIVE
and AgentCapabilityManager.is_audio_output_available()
):
voice_file = await self.run_blocking(
"default",
AgentCapabilityManager.synthesize_speech, message
)
if voice_file:
voice_path = str(voice_file)
used_voice = True
elif reply_mode == AgentCapabilityManager.REPLY_MODE_NATIVE:
fallback_reason = "当前未配置可用的语音合成能力"
logger.info(
f"执行工具: {self.name}, channel={channel}, "
f"use_voice={used_voice}, text_len={len(message)}"
)
await self.send_message(
Message(
channel=self._channel,
source=self._source,
mtype=MessageType.Agent,
userid=self._user_id,
username=self._username,
text=message,
voice_path=voice_path,
voice_caption=(
message
if voice_path and get_runtime_setting('AUDIO_OUTPUT_INCLUDE_TEXT')
else None
),
save_history=False,
)
)
self._agent_context["user_reply_sent"] = True
self._agent_context["reply_mode"] = "voice" if used_voice else "text_fallback"
if used_voice:
return "语音回复已发送"
return f"{fallback_reason},已自动回退为文字回复"