import os import shutil import subprocess from pathlib import Path import numpy as np import torch import folder_paths from scipy.io.wavfile import write class LikeSpiderUINode: """ Base class for declarative UI-driven ComfyUI nodes. Extend this class and define `UI_CONFIG` and `run(...)`. """ @classmethod def INPUT_TYPES(cls): config = cls.UI_CONFIG required = {} for field in config["inputs"]: name = field["name"] type_ = field["type"] default = field.get("default") if type_ == "string": required[name] = ("STRING", {"default": default or ""}) elif type_ == "select": required[name] = (field["options"], {"default": default}) elif type_ == "audio": required[name] = ("AUDIO",) else: raise ValueError(f"Unsupported field type: {type_}") return {"required": required} RETURN_TYPES = ("AUDIO",) RETURN_NAMES = ("audio",) FUNCTION = "interface_entry" OUTPUT_NODE = True CATEGORY = "LikeSpiderAI" def interface_entry(self, **kwargs): return self.run(**kwargs) def save_audio_with_ffmpeg(self, audio_data, filename_base, format="mp3", bitrate=192): samples = audio_data["waveform"] if isinstance(samples, torch.Tensor): samples = samples.detach().cpu().numpy() if samples.ndim == 3 and samples.shape[0] == 1: samples = samples[0] max_val = np.max(np.abs(samples)) if max_val > 1.0: samples = samples / max_val samples = np.clip(samples, -1.0, 1.0) audio_int16 = (samples * 32767).astype(np.int16) if audio_int16.ndim == 2 and audio_int16.shape[0] in [1, 2]: wav_data = audio_int16.T elif audio_int16.ndim == 1: wav_data = audio_int16 else: raise Exception(f"❌ Unsupported audio shape: {audio_int16.shape}") output_dir = Path(folder_paths.get_output_directory()) / "audio" output_dir.mkdir(parents=True, exist_ok=True) existing = sorted(output_dir.glob(f"{filename_base}_*.{format}")) index = len(existing) file_path = output_dir / f"{filename_base}_{index:05d}.{format}" temp_wav = output_dir / "temp_input.wav" write(temp_wav, 44100, wav_data) if format == "mp3": subprocess.run([ "ffmpeg", "-y", "-i", str(temp_wav), "-codec:a", "libmp3lame", "-b:a", f"{bitrate}k", str(file_path) ], check=True) elif format == "wav": shutil.copy(temp_wav, file_path) elif format == "flac": subprocess.run([ "ffmpeg", "-y", "-i", str(temp_wav), "-codec:a", "flac", str(file_path) ], check=True) else: raise Exception(f"Unsupported format: {format}") os.remove(temp_wav) print(f"[LikeSpiderUINode] ✅ Saved: {file_path}") return file_path