use super::{AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode, ToolTier};
use async_trait::async_trait;
use serde_json::{Value, json};
use tokio::sync::oneshot;
pub struct TtsTool;
#[async_trait]
impl AgentTool for TtsTool {
fn name(&self) -> &str {
"tts"
}
fn label(&self) -> &str {
"TTS"
}
fn description(&self) -> &str {
concat!(
"Convert text to spoken audio using text-to-speech synthesis. ",
"Specify the text to speak and optionally the voice and format. ",
"Produces an audio file (MP3 or WAV) at the specified path. ",
"Supports multiple voices and languages."
)
}
fn essential(&self) -> bool {
false
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to convert to speech."
},
"voice": {
"type": "string",
"description": "Voice to use (e.g., 'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer')."
},
"format": {
"type": "string",
"enum": ["mp3", "wav"],
"description": "Output audio format."
},
"output_path": {
"type": "string",
"description": "Path to save the audio file."
}
},
"required": ["text"]
})
}
fn intent(&self) -> Option<&str> {
Some("Convert text to speech")
}
fn execution_mode(&self) -> ToolExecutionMode {
ToolExecutionMode::SequentialOnly
}
fn tool_tier(&self) -> ToolTier {
ToolTier::Read
}
async fn execute(
&self,
_tool_call_id: &str,
params: Value,
_signal: Option<oneshot::Receiver<()>>,
_ctx: &ToolContext,
) -> Result<AgentToolResult, ToolError> {
let text = params
.get("text")
.and_then(|v| v.as_str())
.ok_or_else(|| "Missing required parameter: text".to_string())?;
let voice = params
.get("voice")
.and_then(|v| v.as_str())
.unwrap_or("alloy");
let format = params
.get("format")
.and_then(|v| v.as_str())
.unwrap_or("mp3");
let output_path = params
.get("output_path")
.and_then(|v| v.as_str())
.unwrap_or("/tmp/tts_output.mp3");
Ok(AgentToolResult::success(format!(
concat!(
"TTS synthesis requested.\n",
"Text: {}\n",
"Voice: {}\n",
"Format: {}\n",
"Output: {}\n\n",
"TTS engine not yet connected. To generate speech, use shell tools:\n",
" macOS: `say \"{}\" -o {}.{}`\n",
" Linux: `espeak \"{}\" -w {}.wav`\n\n",
"Full TTS support requires a TTS backend (kokoro-js local or API key)."
),
text, voice, format, output_path, text, output_path, format, text, output_path
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_tts_basic() {
let tool = TtsTool;
let params = json!({
"text": "Hello, this is a test.",
"voice": "alloy"
});
let result = tool
.execute("id", params, None, &ToolContext::default())
.await
.unwrap();
assert!(result.success);
assert!(result.output.contains("TTS synthesis requested"));
}
}