Files

200 lines
7.0 KiB
Python

"""
ComfyUI Text-to-Speech Node
GeekatPlay Studio
GitHub: https://github.com/GeekatPlayStudio
YouTube: @geekatplay | @geekatplay-ru (Russian)
Patreon: https://patreon.com/geekatplay
Provides high-quality text-to-speech using Microsoft Edge TTS with pyttsx3 fallback.
Requires a local Flask server (tts_server.py) running on port 5002.
"""
import os
import requests
import folder_paths
class HttpTTSToAudio:
"""
Text-to-Speech node that connects to a local TTS server.
Features:
- High-quality neural voices via Microsoft Edge TTS
- 13 voice options across US, GB, AU, CA, and IN English variants
- Text file input support
- Custom output directory selection
- Adjustable speech rate and volume
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {
"multiline": True,
"default": "Hello from ComfyUI."
}),
"language": ("STRING", {
"default": "en"
}),
"server_url": ("STRING", {
"default": "http://127.0.0.1:5002/tts"
})
},
"optional": {
"text_file_path": ("STRING", {
"default": "",
"file_picker": "file"
}),
"output_directory": ("STRING", {
"default": "",
"file_picker": "directory"
}),
"voice": ([
"en-US-AriaNeural",
"en-US-ZiraNeural",
"en-US-JennyNeural",
"en-US-GuyNeural",
"en-US-ChristopherNeural",
"en-US-EricNeural",
"en-US-MichelleNeural",
"en-US-RogerNeural",
"en-GB-SoniaNeural",
"en-GB-RyanNeural",
"en-GB-LibbyNeural",
"en-AU-NatashaNeural",
"en-AU-WilliamNeural",
"en-CA-ClaraNeural",
"en-CA-LiamNeural",
"en-IN-NeerjaNeural",
"en-IN-PrabhatNeural"
],),
"rate": ("INT", {
"default": 180,
"min": 50,
"max": 400
}),
"volume": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0
})
,
"timeout_seconds": ("INT", {
"default": 300,
"min": 60,
"max": 3600,
"step": 10
})
,
"auto_timeout": ("BOOLEAN", {
"default": True
})
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("audio_path",)
FUNCTION = "do_tts"
CATEGORY = "geekatplay/TTS"
def do_tts(self, text, language, server_url, text_file_path="", output_directory="", voice="en-US-AriaNeural", rate=180, volume=1.0, timeout_seconds=300, auto_timeout=True):
# Load text from file if provided
source_filename = "tts_output"
if text_file_path:
if os.path.isfile(text_file_path):
with open(text_file_path, 'r', encoding='utf-8') as f:
text = f.read().strip()
# Extract filename without extension for output naming
source_filename = os.path.splitext(os.path.basename(text_file_path))[0]
else:
raise RuntimeError(f"Text file not found: {text_file_path}")
# Prepare request payload for TTS server
payload = {
"text": text,
"language": language,
"voice": voice,
"rate": rate,
"volume": volume
}
try:
# Determine effective timeout
if auto_timeout:
words = len(text.split())
estimated_seconds = int(((words / 2.5) * 1.3) + 10) # words/sec ~2.5; add 30% buffer + setup
estimated_seconds = max(60, min(3600, estimated_seconds))
effective_timeout = max(timeout_seconds, estimated_seconds)
else:
effective_timeout = timeout_seconds
# Send TTS request to local server
resp = requests.post(server_url, json=payload, timeout=effective_timeout)
resp.raise_for_status()
data = resp.json()
path = data.get("path")
if not path or not os.path.isfile(path):
raise RuntimeError(f"No file returned from TTS server: {data}")
# Copy audio file to output directory
with open(path, "rb") as f:
audio_bytes = f.read()
# Determine output directory
out_dir = output_directory.strip() if output_directory.strip() else folder_paths.get_output_directory()
os.makedirs(out_dir, exist_ok=True)
# Generate descriptive filename: originalname_voice_YYYYMMDD_HHMMSS.wav
from datetime import datetime
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
voice_short = voice.replace("en-", "").replace("-Neural", "") # e.g., "US-Aria"
out_name = f"{source_filename}_{voice_short}_{timestamp}.wav"
out_path = os.path.join(out_dir, out_name)
with open(out_path, "wb") as f:
f.write(audio_bytes)
return (out_path,)
except requests.exceptions.ConnectionError:
raise RuntimeError(f"Could not connect to TTS server at {server_url}. Please ensure 'tts_server.py' is running (use run_tts.bat).")
except Exception as e:
raise RuntimeError(f"TTS request failed: {str(e)}")
class TTSServerStatus:
"""
Health check node for the TTS server.
Verifies that the Flask TTS server is running and accessible.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"server_url": ("STRING", {
"default": "http://127.0.0.1:5002/status"
})
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("status",)
FUNCTION = "check_status"
CATEGORY = "geekatplay/TTS"
def check_status(self, server_url):
try:
resp = requests.get(server_url, timeout=5)
if resp.status_code == 200:
return ("Server is running",)
else:
return (f"Server responded with status {resp.status_code}",)
except requests.exceptions.RequestException as e:
return (f"Server not running or unreachable: {str(e)}",)
NODE_CLASS_MAPPINGS = {
"HttpTTSToAudio": HttpTTSToAudio,
"TTSServerStatus": TTSServerStatus,
}
print("\033[96m[GeekatPlay Studio]\033[0m TTS Node loaded")