Skip to content

Instantly share code, notes, and snippets.

@seszele64
Last active June 24, 2026 08:07
Show Gist options
  • Select an option

  • Save seszele64/747a74ab5545e51bb5eaf7e50360960c to your computer and use it in GitHub Desktop.

Select an option

Save seszele64/747a74ab5545e51bb5eaf7e50360960c to your computer and use it in GitHub Desktop.
OpenCode AI MCP Text-to-Speech Tool - Edge TTS Plugin for AI Agents

OpenCode TTS Tool Setup

Microsoft Edge Text-to-Speech plugin for OpenCode.

Prerequisites

  1. Python 3 with edge-tts installed:

    pip install edge-tts
  2. An audio player (one of): mpg123, ffplay, cvlc, paplay, or aplay

    # Ubuntu/Debian
    sudo apt install mpg123
    # or
    sudo apt install ffmpeg  # for ffplay

Installation

  1. Copy all three files to your OpenCode tools directory:

    # Default location: ~/.config/opencode/tools/
    cp speak.ts speak.json speak.py ~/.config/opencode/tools/
  2. Make sure the Python script is executable (optional):

    chmod +x ~/.config/opencode/tools/speak.py
  3. Restart OpenCode or reload tools

Usage

Once installed, you can use the speak tool in OpenCode:

speak({
  text: "Hello world",
  voice: "en-US-AriaNeural",  // optional
  rate: "+0%",                // optional
  volume: "+0%"               // optional
})

Available Voices

List available voices with:

edge-tts --list-voices
{
"name": "speak",
"description": "Convert text to speech using Microsoft Edge TTS",
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The text to convert to speech"
},
"voice": {
"type": "string",
"description": "The voice to use for speech synthesis (default: en-US-AriaNeural)",
"default": "en-US-AriaNeural"
},
"rate": {
"type": "string",
"description": "The speech rate adjustment (e.g., '+10%' for faster, '-10%' for slower, default: '+0%')",
"default": "+0%"
},
"volume": {
"type": "string",
"description": "The speech volume adjustment (e.g., '+10%' for louder, '-10%' for quieter, default: '+0%')",
"default": "+0%"
}
},
"required": ["text"]
}
}
#!/usr/bin/env python3
"""
Text-to-Speech tool using Microsoft Edge TTS.
This script reads JSON input from stdin, converts text to speech using edge-tts,
plays the audio using system audio players, and outputs JSON status to stdout.
Input format (JSON):
{
"text": "Hello world", # Required - text to speak
"voice": "en-US-AriaNeural", # Optional - voice to use
"rate": "+0%", # Optional - speech rate
"volume": "+0%" # Optional - speech volume
}
Output format (JSON):
{
"success": true/false,
"message": "Status message"
}
"""
import asyncio
import json
import os
import sys
import tempfile
from typing import Optional
import edge_tts
# Default values
DEFAULT_VOICE = "en-US-AriaNeural"
DEFAULT_RATE = "+0%"
DEFAULT_VOLUME = "+0%"
# Audio players to try, in order of preference
AUDIO_PLAYERS = ["mpg123", "ffplay", "cvlc", "paplay", "aplay"]
async def generate_speech(
text: str,
voice: str = DEFAULT_VOICE,
rate: str = DEFAULT_RATE,
volume: str = DEFAULT_VOLUME,
output_file: Optional[str] = None,
) -> str:
"""
Generate speech audio file from text using edge-tts.
Args:
text: The text to convert to speech
voice: The voice to use (default: en-US-AriaNeural)
rate: The speech rate (default: +0%)
volume: The speech volume (default: +0%)
output_file: Optional output file path (created if not provided)
Returns:
Path to the generated audio file
Raises:
Exception: If speech generation fails
"""
# Create output file if not specified
if output_file is None:
fd, output_file = tempfile.mkstemp(suffix=".mp3")
os.close(fd)
# Create communicate object with specified parameters
communicate = edge_tts.Communicate(text, voice, rate=rate, volume=volume)
# Save audio to file
await communicate.save(output_file)
return output_file
def find_available_player() -> Optional[str]:
"""
Find the first available audio player from the preference list.
Returns:
Path to the available player executable, or None if no player found
"""
for player in AUDIO_PLAYERS:
# Check if the player exists in PATH
if os.system(f"which {player} > /dev/null 2>&1") == 0:
return player
return None
def play_audio(file_path: str, player: str) -> bool:
"""
Play an audio file using the specified player.
Args:
file_path: Path to the audio file
player: The audio player executable name
Returns:
True if playback succeeded, False otherwise
"""
try:
if player == "mpg123":
return os.system(f'mpg123 -q "{file_path}"') == 0
elif player == "ffplay":
# ffplay with no window and auto-exit
return (
os.system(f'ffplay -nodisp -autoexit -loglevel quiet "{file_path}"')
== 0
)
elif player == "cvlc":
# cvlc (console vlc) with no GUI
return os.system(f'cvlc --play-and-exit --quiet "{file_path}"') == 0
elif player == "paplay":
# paplay for PulseAudio
return os.system(f'paplay "{file_path}"') == 0
elif player == "aplay":
# aplay for ALSA
return os.system(f'aplay -q "{file_path}"') == 0
else:
return False
except Exception:
return False
def cleanup_file(file_path: str) -> None:
"""
Safely remove a temporary file.
Args:
file_path: Path to the file to remove
"""
try:
if file_path and os.path.exists(file_path):
os.remove(file_path)
except Exception:
pass # Ignore cleanup errors
def main() -> int:
"""
Main entry point for the TTS tool.
Returns:
0 on success, 1 on failure
"""
audio_file: Optional[str] = None
try:
# Read JSON input from stdin
input_data = sys.stdin.read().strip()
if not input_data:
output = {
"success": False,
"message": "No input provided. Expected JSON with 'text' field.",
}
print(json.dumps(output))
return 1
# Parse JSON input
try:
data = json.loads(input_data)
except json.JSONDecodeError as e:
output = {"success": False, "message": f"Invalid JSON input: {str(e)}"}
print(json.dumps(output))
return 1
# Extract parameters with defaults
text = data.get("text", "").strip()
if not text:
output = {
"success": False,
"message": "No text provided. The 'text' field is required.",
}
print(json.dumps(output))
return 1
voice = data.get("voice", DEFAULT_VOICE)
rate = data.get("rate", DEFAULT_RATE)
volume = data.get("volume", DEFAULT_VOLUME)
# Generate speech audio
try:
audio_file = asyncio.run(generate_speech(text, voice, rate, volume))
except Exception as e:
output = {
"success": False,
"message": f"Failed to generate speech: {str(e)}",
}
print(json.dumps(output))
return 1
# Find available audio player
player = find_available_player()
if player is None:
output = {
"success": False,
"message": "No audio player found. Install one of: mpg123, ffmpeg, vlc, pulseaudio-utils, or alsa-utils.",
}
print(json.dumps(output))
return 1
# Play the audio
if not play_audio(audio_file, player):
output = {
"success": False,
"message": f"Failed to play audio using {player}.",
}
print(json.dumps(output))
return 1
# Success
output = {
"success": True,
"message": f"Speech played successfully using {player}.",
}
print(json.dumps(output))
return 0
except KeyboardInterrupt:
output = {"success": False, "message": "Interrupted by user."}
print(json.dumps(output))
return 1
except Exception as e:
output = {"success": False, "message": f"Unexpected error: {str(e)}"}
print(json.dumps(output))
return 1
finally:
# Cleanup temporary audio file
if audio_file:
cleanup_file(audio_file)
if __name__ == "__main__":
sys.exit(main())
import { tool } from "@opencode-ai/plugin";
import { z } from "zod";
import { spawn } from "child_process";
import path from "path";
const speakTool = tool({
description: "Convert text to speech using Microsoft Edge TTS",
args: {
text: z.string().describe("The text to convert to speech"),
voice: z
.string()
.default("en-US-AriaNeural")
.describe("The voice to use for speech synthesis"),
rate: z
.string()
.default("+0%")
.describe("The speech rate adjustment"),
volume: z
.string()
.default("+0%")
.describe("The speech volume adjustment"),
},
async execute(args, context) {
const { text, voice, rate, volume } = args;
// Get the path to the Python script
const pythonScriptPath = "/home/tr1x/.config/opencode/tools/speak.py";
// Prepare the input JSON for the Python script
const inputJson = JSON.stringify({
text,
voice,
rate,
volume,
});
// Execute the Python script with JSON as stdin
const result = await new Promise<{ success: boolean; message: string }>(
(resolve, reject) => {
const pythonProcess = spawn("python3", [pythonScriptPath], {
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
pythonProcess.stdout.on("data", (data) => {
stdout += data.toString();
});
pythonProcess.stderr.on("data", (data) => {
stderr += data.toString();
});
pythonProcess.on("close", (code) => {
if (code === 0) {
try {
const result = JSON.parse(stdout.trim());
resolve(result);
} catch {
resolve({
success: false,
message: `Failed to parse Python output: ${stdout}`,
});
}
} else {
resolve({
success: false,
message: stderr || `Python script exited with code ${code}`,
});
}
});
pythonProcess.on("error", (error) => {
resolve({
success: false,
message: `Failed to execute Python script: ${error.message}`,
});
});
// Write the input JSON to stdin
pythonProcess.stdin.write(inputJson);
pythonProcess.stdin.end();
}
);
if (!result.success) {
throw new Error(result.message);
}
return result.message;
},
});
export default speakTool;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment