Files
2025-12-01 18:12:01 -05:00

129 lines
4.9 KiB
Python

import numpy as np
import requests
import base64
import io
import os
import folder_paths
from PIL import Image
class ChutesWanVideoFast:
"""Generate video from image using Chutes.ai Wan 2.2 Fast API."""
# Default Chinese negative prompt from the API
DEFAULT_NEGATIVE = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"prompt": ("STRING", {"multiline": True, "default": "A cinematic video..."}),
"api_key": ("STRING", {"multiline": False, "default": ""}),
},
"optional": {
"negative_prompt": ("STRING", {
"multiline": True,
"default": ChutesWanVideoFast.DEFAULT_NEGATIVE
}),
"resolution": (["480p", "720p"], {"default": "480p"}),
"frames": ("INT", {"default": 81, "min": 21, "max": 140, "step": 1}),
"fps": ("INT", {"default": 16, "min": 16, "max": 24, "step": 1}),
"fast": ("BOOLEAN", {"default": True}),
"seed": ("INT", {"default": -1, "min": -1, "max": 2147483647}),
"guidance_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"guidance_scale_2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_path",)
FUNCTION = "generate_video"
CATEGORY = "Chutes/Wan"
def tensor_to_base64(self, image):
"""Convert ComfyUI tensor (B,H,W,C) to base64 PNG string."""
i = 255.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)[0])
buffered = io.BytesIO()
img.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode("utf-8")
def generate_video(
self,
image,
prompt,
api_key,
negative_prompt=None,
resolution="480p",
frames=81,
fps=16,
fast=True,
seed=-1,
guidance_scale=1.0,
guidance_scale_2=1.0,
):
url = "https://chutes-wan-2-2-i2v-14b-fast.chutes.ai/generate"
print("Converting image to Base64...")
image_b64 = self.tensor_to_base64(image)
payload = {
"image": image_b64,
"prompt": prompt,
"negative_prompt": negative_prompt if negative_prompt else self.DEFAULT_NEGATIVE,
"resolution": resolution,
"frames": frames,
"fps": fps,
"fast": fast,
"guidance_scale": guidance_scale,
"guidance_scale_2": guidance_scale_2,
}
# Only include seed if explicitly set (not -1)
if seed >= 0:
payload["seed"] = seed
else:
payload["seed"] = None
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
print(f"Sending request to Chutes.ai ({url})...")
print(f" resolution={resolution}, frames={frames}, fps={fps}, fast={fast}")
response = None
try:
response = requests.post(url, json=payload, headers=headers, timeout=600)
response.raise_for_status()
except requests.exceptions.RequestException as e:
error_detail = ""
if response is not None:
error_detail = f" Response: {response.text}"
print(f"API Error Response: {response.text}")
raise Exception(f"Request failed: {e}{error_detail}")
# Save video - API returns raw video/mp4 bytes
output_dir = folder_paths.get_output_directory()
filename = f"chutes_wan_{np.random.randint(100000)}.mp4"
file_path = os.path.join(output_dir, filename)
content_type = response.headers.get("Content-Type", "")
if "application/json" in content_type:
data = response.json()
if "url" in data:
print(f"Downloading video from {data['url']}...")
video_resp = requests.get(data["url"], timeout=300)
video_resp.raise_for_status()
with open(file_path, "wb") as f:
f.write(video_resp.content)
else:
raise Exception(f"Unexpected JSON response: {data}")
else:
with open(file_path, "wb") as f:
f.write(response.content)
print(f"Video saved to: {file_path}")
return (filename,)