oxi_agent/tools/
tts_tool.rs1use super::{AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode, ToolTier};
8use async_trait::async_trait;
9use serde_json::{Value, json};
10use tokio::sync::oneshot;
11
12pub struct TtsTool;
14
15#[async_trait]
16impl AgentTool for TtsTool {
17 fn name(&self) -> &str {
18 "tts"
19 }
20
21 fn label(&self) -> &str {
22 "TTS"
23 }
24
25 fn description(&self) -> &str {
26 concat!(
27 "Convert text to spoken audio using text-to-speech synthesis. ",
28 "Specify the text to speak and optionally the voice and format. ",
29 "Produces an audio file (MP3 or WAV) at the specified path. ",
30 "Supports multiple voices and languages."
31 )
32 }
33
34 fn essential(&self) -> bool {
35 false
36 }
37
38 fn parameters_schema(&self) -> Value {
39 json!({
40 "type": "object",
41 "properties": {
42 "text": {
43 "type": "string",
44 "description": "Text to convert to speech."
45 },
46 "voice": {
47 "type": "string",
48 "description": "Voice to use (e.g., 'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer')."
49 },
50 "format": {
51 "type": "string",
52 "enum": ["mp3", "wav"],
53 "description": "Output audio format."
54 },
55 "output_path": {
56 "type": "string",
57 "description": "Path to save the audio file."
58 }
59 },
60 "required": ["text"]
61 })
62 }
63
64 fn intent(&self) -> Option<&str> {
65 Some("Convert text to speech")
66 }
67
68 fn execution_mode(&self) -> ToolExecutionMode {
69 ToolExecutionMode::SequentialOnly
70 }
71
72 fn tool_tier(&self) -> ToolTier {
73 ToolTier::Read
74 }
75
76 async fn execute(
77 &self,
78 _tool_call_id: &str,
79 params: Value,
80 _signal: Option<oneshot::Receiver<()>>,
81 _ctx: &ToolContext,
82 ) -> Result<AgentToolResult, ToolError> {
83 let text = params
84 .get("text")
85 .and_then(|v| v.as_str())
86 .ok_or_else(|| "Missing required parameter: text".to_string())?;
87
88 let voice = params
89 .get("voice")
90 .and_then(|v| v.as_str())
91 .unwrap_or("alloy");
92
93 let format = params
94 .get("format")
95 .and_then(|v| v.as_str())
96 .unwrap_or("mp3");
97
98 let output_path = params
99 .get("output_path")
100 .and_then(|v| v.as_str())
101 .unwrap_or("/tmp/tts_output.mp3");
102
103 Ok(AgentToolResult::success(format!(
109 concat!(
110 "TTS synthesis requested.\n",
111 "Text: {}\n",
112 "Voice: {}\n",
113 "Format: {}\n",
114 "Output: {}\n\n",
115 "TTS engine not yet connected. To generate speech, use shell tools:\n",
116 " macOS: `say \"{}\" -o {}.{}`\n",
117 " Linux: `espeak \"{}\" -w {}.wav`\n\n",
118 "Full TTS support requires a TTS backend (kokoro-js local or API key)."
119 ),
120 text, voice, format, output_path, text, output_path, format, text, output_path
121 )))
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[tokio::test]
130 async fn test_tts_basic() {
131 let tool = TtsTool;
132 let params = json!({
133 "text": "Hello, this is a test.",
134 "voice": "alloy"
135 });
136 let result = tool
137 .execute("id", params, None, &ToolContext::default())
138 .await
139 .unwrap();
140 assert!(result.success);
141 assert!(result.output.contains("TTS synthesis requested"));
142 }
143}