first commit

This commit is contained in:
Jeril Sebastian
2026-01-06 11:00:31 +05:30
commit 2246ab9b2d
6 changed files with 395 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Environment
.env
.venv
env/
venv/
ENV/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Secrets
api_keys.json
secrets.json
*.key
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 gabe-init
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+97
View File
@@ -0,0 +1,97 @@
# ComfyUI ElevenLabs TTS Node
A custom node for ComfyUI that integrates ElevenLabs' text-to-speech API, enabling high-quality voice synthesis within your ComfyUI workflows.
## Features
- Pulls your ElevenLabs voices on demand (cached for 1 hour) and maps names → IDs automatically
- Built-in support for the shipped models: `eleven_multilingual_v2`, `eleven_english_sts_v2`, `eleven_turbo_v2`
- Voice tuning controls: stability, similarity boost, style, and speaker boost
- Text input flexibility: type in the node UI or feed text from another node (`input_text` override)
- Returns a ComfyUI `AUDIO` dict with waveform tensor + sample rate decoded from ElevenLabs MP3 responses
## Installation
1. Clone this repository into your ComfyUI `custom_nodes` folder:
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/yourusername/ComfyUI-ElevenLabs
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Restart ComfyUI.
## Setup
You'll need an ElevenLabs API key (`ELEVENLABS_API_KEY`) set in your environment. Example for a shell session:
```bash
export ELEVENLABS_API_KEY=your_key
```
The node reads this variable automatically; there is no API key input inside the UI.
## Usage
### Basic Text-to-Speech
1. Add the "ElevenLabs TTS Node" to your workflow.
2. Type text in the node or wire a string input into `input_text` (wired input takes priority).
3. Pick a voice from the dropdown (list is fetched from your ElevenLabs account and cached).
4. Choose a model (defaults to multilingual v2).
5. Adjust stability, similarity boost, style, and speaker boost as desired.
6. Connect the audio output to preview or save nodes.
### Inputs
- **text**: Text box content to synthesize.
- **input_text** (optional): Upstream string input; overrides the text box when connected.
- **voice**: Dropdown of your ElevenLabs voices (deduped names, numbered when repeated).
- **model**: One of `eleven_multilingual_v2`, `eleven_english_sts_v2`, `eleven_turbo_v2`.
- **stability**: 0.0–1.0.
- **similarity_boost**: 0.0–1.0.
- **style**: 0.0–1.0.
- **use_speaker_boost**: Boolean toggle.
### Output
- **AUDIO**: `{"waveform": torch.Tensor(shape: 1 x channels x samples), "sample_rate": int}`
## Notes & Troubleshooting
- Voices are fetched from `https://api.elevenlabs.io/v1/voices` and cached for 1 hour. Missing/expired API keys log an error and return a silent placeholder waveform.
- Failures in TTS requests or MP3 decoding also return a 1-sample silent waveform and log the error.
- Some voices or models may depend on your ElevenLabs subscription tier.
## Requirements
- ComfyUI
- Python packages: requests, torch, numpy
- ElevenLabs API key
- Internet connection for API calls
## License
MIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3
View File
@@ -0,0 +1,3 @@
from .node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+226
View File
@@ -0,0 +1,226 @@
import io
import os
import time
import numpy as np
import requests
import torch
from pydub import AudioSegment
import logging
class ElevenLabsNode:
voices_cache = None
voices_map = {}
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": {
"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 _get_api_key(cls):
api_key = os.getenv("ELEVENLABS_API_KEY", "").strip()
if not api_key:
logging.error("ELEVENLABS_API_KEY is not set. Please configure it in your environment.")
return None
return api_key
@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 = []
voice_map = {}
api_key = cls._get_api_key()
if api_key is None:
cls.voices_cache = ["missing_api_key"]
cls.voices_map = {}
return cls.voices_cache
# Fetch voices from the API
url = "https://api.elevenlabs.io/v1/voices"
try:
response = requests.get(url, headers={"xi-api-key": api_key})
response.raise_for_status()
voices = response.json()["voices"]
name_counts = {}
for voice in voices:
base_name = voice["name"]
count = name_counts.get(base_name, 0) + 1
name_counts[base_name] = count
display_name = base_name if count == 1 else f"{base_name} ({count})"
voice_list.append(display_name)
voice_map[display_name] = voice["voice_id"]
cls.voices_cache = voice_list
cls.voices_map = voice_map
cls.last_fetch_time = current_time
except requests.exceptions.RequestException as e:
logging.error(f"Error fetching voices: {e}")
if cls.voices_cache is None:
cls.voices_cache = ["error_fetching_voices"]
cls.voices_map = {}
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 _load_audio_from_bytes(self, audio_bytes: bytes):
"""Decode ElevenLabs MP3 response into torch waveform."""
if not audio_bytes:
logging.error("Empty audio response received.")
return None, None
try:
audio = AudioSegment.from_file(io.BytesIO(audio_bytes), format="mp3")
except Exception as e:
preview = audio_bytes[:80]
try:
preview_text = preview.decode("utf-8", errors="replace")
except Exception:
preview_text = str(preview)
logging.error(f"Error decoding MP3 audio: {e}. Preview: {preview_text}")
return None, None
sample_rate = audio.frame_rate
sample_width = audio.sample_width
channels = audio.channels
samples = np.array(audio.get_array_of_samples())
if channels > 1:
samples = samples.reshape(-1, channels).T # channels x samples
else:
samples = samples.reshape(1, -1)
max_val = float(2 ** (8 * sample_width - 1))
waveform = torch.from_numpy(samples).unsqueeze(0).float() / max_val
waveform = self.ensure_3d_tensor(waveform)
return waveform, sample_rate
def generate_speech(self, text, voice, model, stability, similarity_boost, style, use_speaker_boost, input_text=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 = self.voices_map.get(voice)
if not voice_id:
logging.error(f"Voice ID not found for selection '{voice}'. Refreshing voices cache.")
self.fetch_elevenlabs_voices()
voice_id = self.voices_map.get(voice)
if not voice_id:
return ({"waveform": torch.zeros(1, 1, 1).float(), "sample_rate": 44100},)
api_key = self._get_api_key()
if api_key is None:
return ({"waveform": torch.zeros(1, 1, 1).float(), "sample_rate": 44100},)
headers = {
"Accept": "audio/mpeg",
"xi-api-key": api_key,
"Content-Type": "application/json"
}
# 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:
logging.error(f"Error in text-to-speech: {str(e)}")
return ({"waveform": torch.zeros(1, 1, 1).float(), "sample_rate": 44100},)
if response.status_code == 200:
waveform, sample_rate = self._load_audio_from_bytes(response.content)
if waveform is None or sample_rate is None:
return ({"waveform": torch.zeros(1, 1, 1).float(), "sample_rate": 44100},)
# Ensure the waveform is 3D
if waveform.dim() != 3:
logging.info(f"Unexpected tensor dimension {waveform.dim()}. Reshaping to 3D.")
waveform = waveform.view(1, 1, -1)
return ({"waveform": waveform, "sample_rate": sample_rate},)
else:
logging.error(f"API Error: {response.status_code} - {response.text}")
return ({"waveform": torch.zeros(1, 1, 1).float(), "sample_rate": 44100},)
@classmethod
def IS_CHANGED(cls, text, voice, model, stability, similarity_boost, style, use_speaker_boost, input_text=None):
return (text, voice, model, stability, similarity_boost, style, use_speaker_boost, input_text)
# Node class mappings
NODE_CLASS_MAPPINGS = {
"ElevenLabsNode": ElevenLabsNode
}
# Node display name mappings
NODE_DISPLAY_NAME_MAPPINGS = {
"ElevenLabsNode": "ElevenLabs TTS Node"
}
+4
View File
@@ -0,0 +1,4 @@
requests
numpy
pydub
torch