Files
razv23andClaude 728d83611f Fix directory structure: move files from subfolder to root
Moved all files from comfyui-razv-wavespeed-custom/ subfolder directly to razv-wavespeed/ root directory to fix ComfyUI custom node loading. ComfyUI expects node files to be in the main directory, not in a subfolder.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 16:07:23 +02:00

217 lines
7.2 KiB
Python

import requests
import base64
import io
import os
import numpy
import PIL
import requests
import torch
from collections.abc import Iterable
from typing import List
from pydantic import BaseModel, Field
from comfy_api.input import ImageInput, AudioInput, VideoInput
import torchaudio
import av
def imageurl2tensor(image_urls: List[str]):
images = []
if not image_urls:
return torch.zeros((1, 3, 1, 1))
for url in image_urls:
image_data = fetch_image(url)
image = decode_image(image_data)
images.append(image)
return images2tensor(images)
def fetch_image(url, stream=True):
return requests.get(url, stream=stream).content
def save_video(video: VideoInput, save_path: str):
video.save_to(
save_path,
format="mp4",
codec="h264",
)
def save_audio(audio: AudioInput, save_path: str):
quality = "128k"
format = "mp3"
# Use original sample rate initially
sample_rate = audio["sample_rate"]
# Opus supported sample rates
OPUS_RATES = [8000, 12000, 16000, 24000, 48000]
for (batch_number, waveform) in enumerate(audio["waveform"].cpu()):
# Use original sample rate initially
sample_rate = audio["sample_rate"]
# Handle Opus sample rate requirements
if format == "opus":
if sample_rate > 48000:
sample_rate = 48000
elif sample_rate not in OPUS_RATES:
# Find the next highest supported rate
for rate in sorted(OPUS_RATES):
if rate > sample_rate:
sample_rate = rate
break
if sample_rate not in OPUS_RATES: # Fallback if still not supported
sample_rate = 48000
# Resample if necessary
if sample_rate != audio["sample_rate"]:
waveform = torchaudio.functional.resample(waveform, audio["sample_rate"], sample_rate)
# Create output with specified format
output_buffer = io.BytesIO()
output_container = av.open(output_buffer, mode='w', format=format)
# Set up the output stream with appropriate properties
if format == "opus":
out_stream = output_container.add_stream("libopus", rate=sample_rate)
if quality == "64k":
out_stream.bit_rate = 64000
elif quality == "96k":
out_stream.bit_rate = 96000
elif quality == "128k":
out_stream.bit_rate = 128000
elif quality == "192k":
out_stream.bit_rate = 192000
elif quality == "320k":
out_stream.bit_rate = 320000
elif format == "mp3":
out_stream = output_container.add_stream("libmp3lame", rate=sample_rate)
if quality == "V0":
#TODO i would really love to support V3 and V5 but there doesn't seem to be a way to set the qscale level, the property below is a bool
out_stream.codec_context.qscale = 1
elif quality == "128k":
out_stream.bit_rate = 128000
elif quality == "320k":
out_stream.bit_rate = 320000
else: #format == "flac":
out_stream = output_container.add_stream("flac", rate=sample_rate)
frame = av.AudioFrame.from_ndarray(waveform.movedim(0, 1).reshape(1, -1).float().numpy(), format='flt', layout='mono' if waveform.shape[0] == 1 else 'stereo')
frame.sample_rate = sample_rate
frame.pts = 0
output_container.mux(out_stream.encode(frame))
# Flush encoder
output_container.mux(out_stream.encode(None))
# Close containers
output_container.close()
# Write the output to file
output_buffer.seek(0)
with open(save_path, 'wb') as f:
f.write(output_buffer.getbuffer())
break
def tensor2images(tensor):
np_imgs = numpy.clip(tensor.cpu().numpy() * 255.0,
0.0, 255.0).astype(numpy.uint8)
return [PIL.Image.fromarray(np_img) for np_img in np_imgs]
def images2tensor(images):
if isinstance(images, Iterable):
return torch.stack([torch.from_numpy(numpy.array(image)).float() / 255.0 for image in images])
return torch.from_numpy(numpy.array(images)).unsqueeze(0).float() / 255.0
def decode_image(data_bytes, rtn_mask=False):
with io.BytesIO(data_bytes) as bytes_io:
img = PIL.Image.open(bytes_io)
if not rtn_mask:
img = img.convert('RGB')
elif 'A' in img.getbands():
img = img.getchannel('A')
else:
img = None
return img
def encode_image(img, mask=None):
if mask is not None:
img = img.copy()
img.putalpha(mask)
with io.BytesIO() as bytes_io:
if mask is not None:
img.save(bytes_io, format='PNG')
else:
img.save(bytes_io, format='JPEG')
data_bytes = bytes_io.getvalue()
return data_bytes
def image_to_base64(image):
if image is None:
return None
return base64.b64encode(encode_image(tensor2images(image)[0])).decode("utf-8")
def image_to_base64s(tensor):
if tensor is None:
return None
images = tensor2images(tensor)
return [base64.b64encode(encode_image(image)).decode("utf-8") for image in images]
def check_lora_path(path):
"""Checks if the LoRA path is valid."""
if path.startswith('http://') or path.startswith('https://'):
return path
elif '/' in path and not path.startswith('/'):
# Ensure format is 'username/model-name'
parts = path.split('/')
if len(parts) == 2 and all(part.strip() for part in parts):
return path
raise ValueError(
"Invalid LoRA path format. It should be either a full URL or in the format 'username/model-name'.")
def normalization_loras(loras, scale_max, scale_default):
"""Normalizes and validates a list of LoRA dictionaries."""
_loras = []
if not loras:
return _loras
for lora in loras:
if "path" in lora:
lora_path = lora["path"]
if lora_path:
lora_path = lora_path.strip()
if lora_path:
lora_scale = lora.get("scale", scale_default)
if lora_scale < 0 or lora_scale > scale_max:
raise ValueError(
f"Invalid {lora_path} LoRA scale. It should be between 0 and {scale_max}.")
_loras.append({"path": check_lora_path(
lora_path), "scale": lora_scale})
return _loras
class BaseRequest(BaseModel):
"""Base class for all API request objects."""
def build_payload(self):
"""Builds the request payload dictionary."""
raise NotImplementedError("Subclasses must implement build_payload")
def get_api_path(self):
"""Gets the API path for the request."""
raise NotImplementedError("Subclasses must implement path")
def _remove_empty_fields(self, payload):
"""Removes None, empty string, and empty dict values from payload."""
return {k: v for k, v in payload.items() if v is not None and (v != "" and v != {})}
def field_order(self):
"""Get field order"""
raise NotImplementedError("Subclasses must implement field order")