137 lines
5.4 KiB
Python
137 lines
5.4 KiB
Python
import os
|
|
import io
|
|
import torch
|
|
import requests
|
|
import soundfile as sf
|
|
|
|
class ElevenLabsTTSNode:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"text": ("STRING", {"multiline": True, "default": ""}),
|
|
"model_id": ([
|
|
"eleven_multilingual_v2",
|
|
"eleven_turbo_v2_5",
|
|
"eleven_flash_v2_5",
|
|
"eleven_flash_v2",
|
|
"eleven_turbo_v2",
|
|
"eleven_multilingual_v1",
|
|
"eleven_v3"
|
|
],),
|
|
"output_format": ([
|
|
"mp3_44100_128",
|
|
"mp3_22050_32",
|
|
"mp3_44100_32",
|
|
"mp3_44100_64",
|
|
"mp3_44100_96",
|
|
"mp3_44100_192",
|
|
"pcm_8000",
|
|
"pcm_16000",
|
|
"pcm_22050",
|
|
"pcm_24000",
|
|
"pcm_44100",
|
|
"pcm_48000",
|
|
"ulaw_8000",
|
|
"alaw_8000",
|
|
"opus_48000_32",
|
|
"opus_48000_64",
|
|
"opus_48000_96",
|
|
"opus_48000_128",
|
|
"opus_48000_192"
|
|
],),
|
|
"voice_id": ("STRING", {"multiline": False, "default": "oPM3trUCF4e0vTcsrMQr"}),
|
|
"stability": ("FLOAT", {"default": 0.50, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"similarity_boost": ("FLOAT", {"default": 0.50, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"speed": ("FLOAT", {"default": 1.0, "min": 0.25, "max": 2.0, "step": 0.01}),
|
|
"style": ("FLOAT", {"default": 0.50, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"use_speaker_boost": ("BOOLEAN", {"default": True}),
|
|
"seed": ("INT", {"default": 40, "min": 0, "max": 4294967294}),
|
|
"api_key": ("STRING", {"multiline": False, "default": "", "tooltip": "Directly put ElevenLabs API key or .env variable name (XI_API_KEY)"}),
|
|
},
|
|
"optional": {
|
|
"previous_text": ("STRING", {"multiline": True, "default": ""}),
|
|
"next_text": ("STRING", {"multiline": True, "default": ""}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("AUDIO",)
|
|
RETURN_NAMES = ("audio",)
|
|
FUNCTION = "generate_speech"
|
|
CATEGORY = "audio/generation"
|
|
|
|
def generate_speech(self, text, api_key, voice_id, model_id, output_format,
|
|
stability, similarity_boost, speed, style, use_speaker_boost, seed,
|
|
previous_text="", next_text=""):
|
|
|
|
if not text.strip():
|
|
raise ValueError("Text input cannot be empty.")
|
|
|
|
key = os.environ.get(api_key.strip(), api_key.strip()) or os.environ.get("XI_API_KEY")
|
|
if not key:
|
|
raise ValueError("No API key provided.")
|
|
|
|
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
|
|
|
|
headers = {
|
|
"xi-api-key": key,
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
if model_id == "eleven_v3":
|
|
allowed_stabilities = [0.0, 0.5, 1.0]
|
|
original_stability = stability
|
|
stability = min(allowed_stabilities, key=lambda x: abs(x - original_stability))
|
|
if stability != original_stability:
|
|
print(f"For 'eleven_v3' model, stability value must be one of: [0.0, 0.5, 1.0] (0.0 = Creative, 0.5 = Natural, 1.0 = Robust). Rounding stability to {stability}.")
|
|
|
|
voice_settings = {
|
|
"stability": stability,
|
|
"similarity_boost": similarity_boost,
|
|
"speed": speed,
|
|
"style": style,
|
|
"use_speaker_boost": use_speaker_boost
|
|
}
|
|
|
|
data = {
|
|
"text": text,
|
|
"voice_settings": voice_settings,
|
|
"model_id": model_id,
|
|
"seed": seed,
|
|
"output_format": output_format
|
|
}
|
|
|
|
if model_id == "eleven_v3":
|
|
if previous_text.strip() or next_text.strip():
|
|
print("Providing previous_text or next_text is not yet supported with the 'eleven_v3' model. Ignoring these inputs.")
|
|
else:
|
|
if previous_text.strip():
|
|
data["previous_text"] = previous_text
|
|
if next_text.strip():
|
|
data["next_text"] = next_text
|
|
|
|
response = requests.post(url, json=data, headers=headers)
|
|
|
|
if response.status_code != 200:
|
|
raise Exception(f"ElevenLabs API Error: {response.status_code}, {response.text}")
|
|
|
|
# Decode audio with soundfile
|
|
audio_buffer = io.BytesIO(response.content)
|
|
waveform, sample_rate = sf.read(audio_buffer, dtype='float32')
|
|
waveform = torch.from_numpy(waveform)
|
|
|
|
# Ensure correct shape [channels, samples]
|
|
if waveform.dim() == 1:
|
|
waveform = waveform.unsqueeze(0)
|
|
else:
|
|
waveform = waveform.t()
|
|
|
|
return ({"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate},)
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
return f"{kwargs.get('text', '')}-{kwargs.get('voice_id', '')}-{kwargs.get('seed', 40)}"
|
|
|
|
NODE_CLASS_MAPPINGS = {"ElevenLabsTTSNode": ElevenLabsTTSNode}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {"ElevenLabsTTSNode": "ElevenLabs TTS"} |