oxi-agent 0.61.0

Agent runtime with tool-calling loop for AI coding assistants
Documentation
/// TTS tool — text-to-speech synthesis.
///
/// Converts text to spoken audio using available TTS engines.
/// Supports local (kokoro-js/ONNX) and remote (xAI Grok Voice) backends.
/// The omp implementation supports both local neural TTS and xAI's API.
/// This is a practical stub that documents available options.
use super::{AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode, ToolTier};
use async_trait::async_trait;
use serde_json::{Value, json};
use tokio::sync::oneshot;

/// TtsTool — text-to-speech synthesis.
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");

        // Full implementation would use:
        // - Local: kokoro-js (ONNX neural TTS)
        // - Remote: xAI Grok Voice API, OpenAI TTS API
        // For now, provide documentation.

        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"));
    }
}