|
#!/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()) |