Add audio inference functionality with ElevenLabs support

- Add RunwareAudioInference node for text-to-audio generation
- Add RunwareAudioModelSearch node for audio model selection
- Add RunwareAudioSections node for music composition structure
- Add RunwareElevenLabsProviderSettings node for advanced music generation
- Add conditional parameter logic for positivePrompt and duration
- Add debug logging for API error responses
This commit is contained in:
Sirshendu Ganguly
2025-09-21 14:04:59 -04:00
parent 94f5158268
commit 9f742b899b
9 changed files with 779 additions and 333 deletions
+232
View File
@@ -0,0 +1,232 @@
import json
import uuid
import requests
import torch
import torchaudio
import numpy as np
from io import BytesIO
import tempfile
import os
from .utils import runwareUtils as rwUtils
class RunwareAudioInference:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"positivePrompt": ("STRING", {
"multiline": True,
"default": "classical piano piece, gentle and melodic",
"tooltip": "Text description that guides the audio generation process"
}),
"usePositivePrompt": ("BOOLEAN", {
"default": True,
"tooltip": "Enable/disable positivePrompt parameter in API request"
}),
"negativePrompt": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Text description of what you don't want in the audio"
}),
"model": ("RUNWAREAUDIOMODEL", {
"tooltip": "AI model to use for audio generation"
}),
"duration": ("INT", {
"default": 10,
"min": 10,
"max": 300,
"step": 1,
"tooltip": "Length of generated audio in seconds (10-300)"
}),
"useDuration": ("BOOLEAN", {
"default": True,
"tooltip": "Enable/disable duration parameter in API request"
}),
"sampleRate": ("INT", {
"default": 22050,
"min": 8000,
"max": 48000,
"step": 1,
"tooltip": "Sample rate of the generated audio in Hz (8000-48000)"
}),
"bitrate": ("INT", {
"default": 32,
"min": 32,
"max": 320,
"step": 32,
"tooltip": "Bitrate of the generated audio in kbps (32-320)"
}),
"outputType": (["URL", "dataURI", "base64Data"], {
"default": "URL",
"tooltip": "Output type for the generated audio"
}),
"outputFormat": (["MP3"], {
"default": "MP3",
"tooltip": "Format of the output audio"
}),
"numberResults": ("INT", {
"default": 1,
"min": 1,
"max": 3,
"tooltip": "Number of audio files to generate"
}),
},
"optional": {
"providerSettings": ("RUNWAREPROVIDERSETTINGS", {
"tooltip": "Provider-specific configuration settings"
}),
}
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("audio",)
FUNCTION = "generateAudio"
CATEGORY = "Runware/Audio"
def generateAudio(self, **kwargs):
# Get required parameters
positivePrompt = kwargs.get("positivePrompt", "")
usePositivePrompt = kwargs.get("usePositivePrompt", True)
model = kwargs.get("model", "")
duration = kwargs.get("duration", 30)
useDuration = kwargs.get("useDuration", True)
sampleRate = kwargs.get("sampleRate", 44100)
bitrate = kwargs.get("bitrate", 128)
outputType = kwargs.get("outputType", "URL")
outputFormat = kwargs.get("outputFormat", "MP3")
negativePrompt = kwargs.get("negativePrompt", "")
numberResults = kwargs.get("numberResults", 1)
providerSettings = kwargs.get("providerSettings", None)
# Create task UUID
taskUUID = str(uuid.uuid4())
# Build the generation config
genConfig = [{
"taskType": "audioInference",
"taskUUID": taskUUID,
"model": model,
"outputType": outputType,
"outputFormat": outputFormat,
"deliveryMethod": "sync",
"includeCost": True,
"numberResults": numberResults,
"audioSettings": {
"sampleRate": sampleRate,
"bitrate": bitrate
}
}]
# Add positivePrompt only if usePositivePrompt is True AND no sections are provided
has_sections = False
if providerSettings is not None:
# Check if there are sections in the provider settings
if isinstance(providerSettings, dict):
elevenlabs_settings = providerSettings.get("elevenlabs", {})
music_settings = elevenlabs_settings.get("music", {})
composition_plan = music_settings.get("compositionPlan", {})
sections = composition_plan.get("sections", [])
has_sections = len(sections) > 0
# When sections are provided, disable duration but add minimal positivePrompt (ElevenLabs requires it)
if has_sections:
useDuration = False
# ElevenLabs requires positivePrompt even with composition plans
#genConfig[0]["positivePrompt"] = "Music composition"
print(f"[DEBUG] Disabled duration and added minimal positivePrompt because sections are provided")
if usePositivePrompt:
genConfig[0]["positivePrompt"] = positivePrompt
print(f"[DEBUG] Added positivePrompt: '{positivePrompt}' (usePositivePrompt={usePositivePrompt})")
else:
print(f"[DEBUG] Skipped positivePrompt (usePositivePrompt={usePositivePrompt})")
# Add duration only if useDuration is True
if useDuration:
genConfig[0]["duration"] = duration
print(f"[DEBUG] Added duration: {duration} (useDuration={useDuration})")
else:
print(f"[DEBUG] Skipped duration (useDuration={useDuration})")
# Add optional parameters if provided
if negativePrompt:
genConfig[0]["negativePrompt"] = negativePrompt
if providerSettings is not None:
genConfig[0]["providerSettings"] = providerSettings
# Debug: Print the request being sent
print(f"[DEBUG] Sending Audio Inference Request:")
print(f"[DEBUG] Request Payload: {json.dumps(genConfig, indent=2)}")
# Make the API request
genResult = rwUtils.inferenecRequest(genConfig)
# Debug: Print the response received
print(f"[DEBUG] Received Audio Inference Response:")
print(f"[DEBUG] Response: {json.dumps(genResult, indent=2)}")
# Check for errors
if "errors" in genResult:
error_message = genResult["errors"][0]["message"]
raise Exception(f"Audio generation failed: {error_message}")
# Extract audio data from response
if "data" in genResult and len(genResult["data"]) > 0:
audioData = genResult["data"][0]
# Get the audio URL
audioURL = audioData.get("audioURL", "")
if not audioURL:
# Fallback to other audio data formats
audioURL = audioData.get("audioDataURI", "")
if not audioURL:
audioURL = audioData.get("audioBase64Data", "")
if audioURL:
# Download the audio and create proper audio data for ComfyUI
try:
# Download the audio file
response = requests.get(audioURL, timeout=30)
response.raise_for_status()
# Save to temporary file and load with torchaudio
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file:
temp_file.write(response.content)
temp_file_path = temp_file.name
try:
# Load audio with torchaudio
waveform, original_sample_rate = torchaudio.load(temp_file_path)
# Resample if necessary
if original_sample_rate != sampleRate:
resampler = torchaudio.transforms.Resample(original_sample_rate, sampleRate)
waveform = resampler(waveform)
# Ensure correct format: [batch, channels, samples]
if waveform.dim() == 2:
waveform = waveform.unsqueeze(0) # Add batch dimension
# Create the audio object
audio_obj = {
"waveform": waveform,
"sample_rate": sampleRate
}
finally:
# Clean up temporary file
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
return (audio_obj,)
except Exception as e:
raise Exception(f"Failed to download audio: {e}")
else:
raise Exception("No audio URL received from API")
else:
raise Exception("No audio data received from API")