200 lines
7.0 KiB
Python
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") |