129 lines
4.9 KiB
Python
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,)
|