Files
Meisoftcoltd-ComfyUI-Demucs…/demucs_nodes.py
T
Meisoftcoltd 4bfe6b3767 feat: Add Spanish tooltips to DemucsAudioSeparator node inputs
- Add ComfyUI "tooltip" property to all inputs in the `DemucsAudioSeparator` node
- Tooltips are provided in Spanish to explain functionality like `shifts`, `overlap`, `split`, etc.
- Did not add `bfloat16` to options as it's unsupported by cuFFT and would downgrade to `float32` anyways.
2026-03-10 16:14:50 +00:00

303 lines
13 KiB
Python

import torch
import torchaudio
import os
import json
import numpy as np
import inspect
from pathlib import Path
# Try to import ComfyUI utilities
try:
import folder_paths
except ImportError:
# Fallback for development/testing outside ComfyUI
class folder_paths:
models_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "models")
@staticmethod
def get_output_directory():
return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "output")
try:
import comfy.utils
HAS_COMFY = True
except ImportError:
HAS_COMFY = False
from demucs.pretrained import get_model
from demucs.apply import apply_model
class LoadAudioDirectory:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"directory_path": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("audio",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "load_directory"
CATEGORY = "🎵 Demucs-AudioSeparator ⚡"
def load_directory(self, directory_path):
if not os.path.isdir(directory_path):
raise ValueError(f"⚡ Directory not found: {directory_path}")
supported_exts = {".wav", ".mp3", ".flac", ".ogg", ".m4a"}
audio_files = []
for file in sorted(os.listdir(directory_path)):
ext = os.path.splitext(file)[1].lower()
if ext in supported_exts:
audio_files.append(os.path.join(directory_path, file))
if not audio_files:
raise ValueError(f"⚡ No supported audio files found in directory: {directory_path}")
print(f"⚡ Loading {len(audio_files)} audio files from {directory_path}...")
audios = []
for file_path in audio_files:
try:
try:
# Attempt 1: Force 'soundfile' backend to bypass the default torchcodec
waveform, sample_rate = torchaudio.load(file_path, backend='soundfile')
except Exception as e:
print(f"⚡ Warning: torchaudio failed (likely torchcodec issue). Falling back to librosa... Error: {e}")
import librosa
# Attempt 2: Use librosa which reads MP3s without relying on torchcodec
audio_np, sample_rate = librosa.load(file_path, sr=None, mono=False)
# Ensure shape is [channels, frames]
if audio_np.ndim == 1:
audio_np = np.expand_dims(audio_np, axis=0)
waveform = torch.from_numpy(audio_np)
# Ensure float32 precision
waveform = waveform.to(torch.float32)
# Add batch dimension to match standard audio format [batch, channels, samples]
if waveform.dim() == 2:
waveform = waveform.unsqueeze(0)
audios.append({"waveform": waveform, "sample_rate": sample_rate})
print(f"⚡ Successfully loaded {os.path.basename(file_path)}")
except Exception as e:
print(f"⚡ Error loading {file_path}: {e}")
if not audios:
raise ValueError(f"⚡ Failed to load any audio files from {directory_path}")
return (audios,)
class DemucsAudioSeparator:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio": ("AUDIO", {"tooltip": "El clip de audio a procesar."}),
"model": (["htdemucs", "htdemucs_ft", "htdemucs_6s", "hdemucs_mmi", "mdx", "mdx_extra", "mdx_q", "mdx_extra_q", "mdxc", "mdxc_fb_ft"], {"tooltip": "El modelo de Demucs a utilizar para la separación."}),
"device": (["cuda", "cpu"], {"tooltip": "El dispositivo a usar para la inferencia. 'cuda' para GPU, 'cpu' para procesador."}),
"precision": (["float32", "float16"], {"default": "float32", "tooltip": "Precisión de los cálculos. 'float32' es más seguro, 'float16' es más rápido pero menos estable."}),
"shifts": ("INT", {"default": 1, "min": 1, "max": 10, "tooltip": "Número de predicciones aleatorias (mayor valor = mayor calidad, pero más lento)."}),
"overlap": ("FLOAT", {"default": 0.25, "min": 0.1, "max": 0.9, "step": 0.05, "tooltip": "Solapamiento entre ventanas de procesamiento. Por defecto: 0.25."}),
"split": ("BOOLEAN", {"default": True, "tooltip": "Divide el audio en segmentos para ahorrar memoria VRAM. Imprescindible para audios largos."}),
"vocals": ("BOOLEAN", {"default": True, "tooltip": "Extraer la pista de voces."}),
"drums": ("BOOLEAN", {"default": True, "tooltip": "Extraer la pista de batería."}),
"bass": ("BOOLEAN", {"default": True, "tooltip": "Extraer la pista de bajo."}),
"other": ("BOOLEAN", {"default": True, "tooltip": "Extraer otros instrumentos."}),
"guitar": ("BOOLEAN", {"default": False, "tooltip": "Extraer la pista de guitarra (solo modelos de 6 pistas)."}),
"piano": ("BOOLEAN", {"default": False, "tooltip": "Extraer la pista de piano (solo modelos de 6 pistas)."}),
"instrumental": ("BOOLEAN", {"default": True, "tooltip": "Extraer la pista instrumental (audio original sin voces)."}),
}
}
RETURN_TYPES = ("AUDIO", "AUDIO", "AUDIO", "AUDIO", "AUDIO", "AUDIO", "AUDIO", "JSON")
RETURN_NAMES = ("vocals", "drums", "bass", "other", "guitar", "piano", "instrumental", "metadata")
FUNCTION = "separate"
CATEGORY = "🎵 Demucs-AudioSeparator ⚡"
def separate(self, audio, model, device, precision, shifts, overlap, split, vocals, drums, bass, other, guitar, piano, instrumental):
model_name = model
# Legacy support and safety check for bfloat16
if precision == "bfloat16":
print("⚠️ [Demucs Pro] BFloat16 not supported for FFT. Upgrading to Float32 for inference.")
precision = "float32"
if precision == "float16":
dtype = torch.float16
else:
dtype = torch.float32
# Configure model path
demucs_models_path = os.path.join(folder_paths.models_dir, "demucs")
if not os.path.exists(demucs_models_path):
os.makedirs(demucs_models_path, exist_ok=True)
torch.hub.set_dir(demucs_models_path)
# Determine device
if device == "cuda" and not torch.cuda.is_available():
print("⚡ CUDA not available, falling back to CPU")
device = "cpu"
device_obj = torch.device(device)
# Load model
print(f"⚡ Loading Demucs model: {model}...")
try:
model_inst = get_model(model)
except Exception as e:
raise RuntimeError(f"Failed to load Demucs model {model}: {str(e)}")
model_inst.to(device_obj)
model_inst.eval()
# Prepare audio
waveform = audio["waveform"]
sr = audio["sample_rate"]
# Ensure waveform is [batch, channels, samples]
if waveform.dim() == 2:
waveform = waveform.unsqueeze(0)
# --- FIX 1: CHANNEL MANAGEMENT (MONO/STEREO/SURROUND) ---
# Demucs strictly requires 2 channels (stereo)
channels = waveform.shape[1]
if channels == 1:
print("⚡ Detected mono audio, converting to stereo for Demucs...")
waveform = waveform.repeat(1, 2, 1)
elif channels > 2:
print(f"⚡ Detected {channels} channels, clipping to stereo for Demucs...")
waveform = waveform[:, :2, :]
# --- FIX 2: PRECISION SAFETY ---
# Move to device BUT keep in float32 for resampling
# (Prevents "Input type HalfTensor vs Weight type FloatTensor" error)
waveform = waveform.to(device_obj).to(torch.float32)
# Resample if necessary
if sr != model_inst.samplerate:
print(f"⚡ Resampling audio from {sr} to {model_inst.samplerate}...")
resampler = torchaudio.transforms.Resample(sr, model_inst.samplerate).to(device_obj)
waveform = resampler(waveform)
# Convert to target precision (Note: apply_model might force float32 later for stability)
waveform = waveform.to(dtype)
# Progress bar integration
pbar = None
if HAS_COMFY:
pbar = comfy.utils.ProgressBar(100)
def progress_callback(info):
if pbar:
# Support various keys from different Demucs versions
# e.g., 'progress', 'shift', 'total', 'shift_idx'
total = info.get('total') or info.get('shifts') or shifts
current = info.get('shift') or info.get('shift_idx') or info.get('progress')
if isinstance(current, float) and current <= 1.0:
pbar.update_absolute(int(current * 100))
elif total and current is not None:
# current might be 0-indexed
p_val = min(100, int((current + 1) / total * 100))
pbar.update_absolute(p_val)
# Apply model
print(f"⚡ Separating audio with {model} (shifts={shifts}, overlap={overlap}, split={split})...")
try:
with torch.no_grad():
# Check if the installed version of apply_model supports a callback
apply_kwargs = {
"shifts": shifts,
"split": split,
"overlap": overlap,
"progress": False,
"device": device_obj,
}
sig = inspect.signature(apply_model)
if 'callback' in sig.parameters:
apply_kwargs["callback"] = progress_callback
# Force float32 for inference to avoid cuFFT/BFloat16 issues
out = apply_model(model_inst, waveform.to(torch.float32), **apply_kwargs)
except Exception as e:
raise RuntimeError(f"⚡ [Demucs Pro] Error during Demucs inference: {str(e)}")
# out shape: [batch, sources, channels, samples]
sources = model_inst.sources
results = {}
for i, source_name in enumerate(sources):
# Convert back to float32 for output and move to CPU
results[source_name] = {
"waveform": out[:, i, :, :].to(torch.float32).cpu(),
"sample_rate": model_inst.samplerate
}
# Helper to get stem or zeroed audio based on user selection
def get_stem(name, enabled):
if enabled and name in results:
return results[name]
else:
# Return zeroed audio with same length and batch size if stem is disabled or unavailable
batch_size = out.shape[0]
channels = out.shape[2]
samples = out.shape[3]
return {
"waveform": torch.zeros((batch_size, channels, samples), dtype=torch.float32),
"sample_rate": model_inst.samplerate
}
# Map results to discrete outputs, respecting the boolean flags
out_vocals = get_stem("vocals", vocals)
out_drums = get_stem("drums", drums)
out_bass = get_stem("bass", bass)
out_other = get_stem("other", other)
out_guitar = get_stem("guitar", guitar)
out_piano = get_stem("piano", piano)
if instrumental:
if "vocals" in results:
# Subtract vocals from original resampled waveform
inst_waveform = waveform.cpu() - results["vocals"]["waveform"]
else:
# If the model doesn't output vocals, just return the original audio
inst_waveform = waveform.cpu()
out_instrumental = {
"waveform": inst_waveform.to(torch.float32),
"sample_rate": model_inst.samplerate
}
else:
batch_size = out.shape[0]
channels = out.shape[2]
samples = out.shape[3]
out_instrumental = {
"waveform": torch.zeros((batch_size, channels, samples), dtype=torch.float32),
"sample_rate": model_inst.samplerate
}
metadata = {
"model": model_name,
"device": str(device_obj),
"precision": precision,
"shifts": shifts,
"overlap": overlap,
"split": split,
"available_sources": sources,
"input_sample_rate": sr,
"output_sample_rate": model_inst.samplerate,
"status": "success"
}
print(f"⚡ [Demucs Pro] Separation completed successfully.")
return (out_vocals, out_drums, out_bass, out_other, out_guitar, out_piano, out_instrumental, metadata)