Files
gabe-init a72c60cf0c Initial commit - ComfyUI ElevenLabs TTS Node
- Text-to-speech integration for ComfyUI
- Support for all ElevenLabs voices and models
- Advanced voice controls
- MIT License
2025-05-25 13:19:12 -05:00

197 lines
7.4 KiB
Python

import requests
import json
import time
import io
import torch
import torchaudio
class ElevenLabsNode:
voices_cache = None
models_cache = None
last_fetch_time = 0
cache_duration = 3600 # Cache duration in seconds (1 hour)
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
voices = cls.fetch_elevenlabs_voices()
models = cls.fetch_elevenlabs_models()
return {
"required": {
"api_key": ("STRING", {
"multiline": False,
"default": ""
}),
"text": ("STRING", {
"multiline": True,
"default": "Hello, how are you?"
}),
"voice": (voices,),
"model": (models,),
"stability": ("FLOAT", {
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.1
}),
"similarity_boost": ("FLOAT", {
"default": 0.8,
"min": 0.0,
"max": 1.0,
"step": 0.1
}),
"style": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.1
}),
"use_speaker_boost": ("BOOLEAN", {
"default": True,
}),
},
"optional": {
"input_text": ("STRING", {"forceInput": True}),
}
}
RETURN_TYPES = ("AUDIO",)
FUNCTION = "generate_speech"
CATEGORY = "ElevenLabs"
@classmethod
def fetch_elevenlabs_voices(cls):
current_time = time.time()
if cls.voices_cache is None or (current_time - cls.last_fetch_time > cls.cache_duration):
voice_list = []
# Fetch voices from the API
url = "https://api.elevenlabs.io/v1/voices"
try:
response = requests.get(url)
response.raise_for_status()
voices = response.json()["voices"]
voice_list = [f"{voice['name']} ({voice['voice_id']})" for voice in voices]
cls.voices_cache = voice_list
cls.last_fetch_time = current_time
except requests.exceptions.RequestException as e:
print(f"Error fetching voices: {e}")
if cls.voices_cache is None:
cls.voices_cache = ["error_fetching_voices"]
return cls.voices_cache
@classmethod
def fetch_elevenlabs_models(cls):
current_time = time.time()
if cls.models_cache is None or (current_time - cls.last_fetch_time > cls.cache_duration):
cls.models_cache = ["eleven_multilingual_v2", "eleven_english_sts_v2", "eleven_turbo_v2"]
cls.last_fetch_time = current_time
return cls.models_cache
def ensure_3d_tensor(self, tensor):
if tensor.dim() == 1:
return tensor.unsqueeze(0).unsqueeze(0)
elif tensor.dim() == 2:
return tensor.unsqueeze(0)
elif tensor.dim() > 3:
return tensor.squeeze().unsqueeze(0)
return tensor
def generate_speech(self, api_key, text, voice, model, stability, similarity_boost, style, use_speaker_boost, input_text=None, input_audio=None):
# Use input_text if provided, otherwise use the text from the textbox
final_text = input_text if input_text is not None else text
voice_id = voice.split("(")[-1].strip(")")
headers = {
"Accept": "application/json",
"xi-api-key": api_key,
"Content-Type": "application/json"
}
if input_audio is not None:
# Speech-to-Speech
url = f"https://api.elevenlabs.io/v1/speech-to-speech/{voice_id}"
payload = {
"model_id": model,
"voice_settings": {
"stability": stability,
"similarity_boost": similarity_boost,
"style": style,
"use_speaker_boost": use_speaker_boost
}
}
input_waveform = self.ensure_3d_tensor(input_audio["waveform"])
wav_buffer = io.BytesIO()
torchaudio.save(wav_buffer, input_waveform.squeeze(0), input_audio["sample_rate"], format="wav")
wav_buffer.seek(0)
files = {"audio": ("input.wav", wav_buffer, "audio/wav")}
try:
response = requests.post(url, headers=headers, data=payload, files=files)
except requests.exceptions.RequestException as e:
print(f"Error in speech-to-speech: {str(e)}")
return ({"waveform": torch.zeros(1, 1, 1).float(), "sample_rate": input_audio["sample_rate"]},)
else:
# Text-to-Speech
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
payload = {
"text": final_text,
"model_id": model,
"voice_settings": {
"stability": stability,
"similarity_boost": similarity_boost,
"style": style,
"use_speaker_boost": use_speaker_boost
}
}
try:
response = requests.post(url, headers=headers, json=payload)
except requests.exceptions.RequestException as e:
print(f"Error in text-to-speech: {str(e)}")
return ({"waveform": torch.zeros(1, 1, 1).float(), "sample_rate": 44100},)
if response.status_code == 200:
audio_content = io.BytesIO(response.content)
try:
waveform, sample_rate = torchaudio.load(audio_content)
except Exception as e:
print(f"Error loading audio content: {str(e)}")
return ({"waveform": torch.zeros(1, 1, 1).float(), "sample_rate": 44100},)
waveform = self.ensure_3d_tensor(waveform)
if waveform.dtype != torch.float32:
waveform = waveform.float() / torch.iinfo(waveform.dtype).max
# Ensure the waveform is 3D
if waveform.dim() != 3:
print(f"Warning: Unexpected tensor dimension {waveform.dim()}. Reshaping to 3D.")
waveform = waveform.view(1, 1, -1)
return ({"waveform": waveform, "sample_rate": sample_rate},)
else:
print(f"API Error: {response.status_code} - {response.text}")
return ({"waveform": torch.zeros(1, 1, 1).float(), "sample_rate": 44100},)
@classmethod
def IS_CHANGED(cls, api_key, text, voice, model, stability, similarity_boost, style, use_speaker_boost, input_text=None, input_audio=None):
return (api_key, text, voice, model, stability, similarity_boost, style, use_speaker_boost, input_text, input_audio)
# Node class mappings
NODE_CLASS_MAPPINGS = {
"ElevenLabsNode": ElevenLabsNode
}
# Node display name mappings
NODE_DISPLAY_NAME_MAPPINGS = {
"ElevenLabsNode": "ElevenLabs TTS Node"
}