Files
ucloud-comfyui-plugin/py/vidu_extend.py
T
jason.mei 7da714c686 refactor(vidu,wan_ai): add fallback VideoFromFile wrapper for import compatibility
- Add third-level fallback import handler for VideoFromFile class across all video modules
- Implement simple VideoFromFile wrapper class with default dimensions (1280x720) when imports fail
- Support video_io objects with getvalue() method for BytesIO compatibility
- Add clarifying comments for each import path (newer, older, and fallback)
- Apply consistent fallback pattern to vidu_extend, vidu_i2v, vidu_ref2v, vidu_startend2v, vidu_t2v, wan_ai_i2v, and wan_ai_t2v modules
- Improve robustness for different ComfyUI versions and installation configurations
2025-12-29 18:36:16 +08:00

147 lines
5.2 KiB
Python

"""
Vidu Extend - 视频延长模型
Models: viduq2-pro, viduq2-turbo
"""
import time
import requests
import io
from .modelverse_api.client import ModelverseClient
from .modelverse_api.utils import image_to_base64
from comfy.comfy_types.node_typing import IO
try:
# Try the newer import path first
from comfy_extras.nodes_video import VideoFromFile
except ImportError:
try:
# Fallback to older import path
from comfy.model_management import VideoFromFile
except ImportError:
# Final fallback - create a simple wrapper
class VideoFromFile:
def __init__(self, video_io):
self.video_io = video_io
self.video_data = video_io.getvalue() if hasattr(video_io, 'getvalue') else video_io
def get_dimensions(self):
# Return default dimensions if we can't determine them
return (1280, 720) # width, height
MODELS = ["viduq2-pro", "viduq2-turbo"]
RESOLUTIONS = ["540p", "720p", "1080p"]
class ViduExtendNode:
"""
Vidu Extend - 视频延长
Models: viduq2-pro, viduq2-turbo
输入视频时长需4秒-1分钟,延长1-7秒
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"client": ("MODELVERSE_API_CLIENT",),
"model": (MODELS, {"default": "viduq2-turbo", "tooltip": "viduq2-pro: 效果好, viduq2-turbo: 生成快"}),
"video_url": (IO.STRING, {"default": "", "tooltip": "需要延长的视频URL (4秒-1分钟)"}),
"duration": (IO.INT, {"default": 5, "min": 1, "max": 7, "step": 1, "tooltip": "延长时长(秒), 1-7"}),
"resolution": (RESOLUTIONS, {"default": "720p", "tooltip": "分辨率"}),
},
"optional": {
"last_frame_image": (IO.IMAGE, {"tooltip": "延长到尾帧的参考图像"}),
"last_frame_url": (IO.STRING, {"default": "", "tooltip": "延长到尾帧的参考图像URL"}),
"prompt": (IO.STRING, {"multiline": True, "default": "", "tooltip": "延长提示词,控制延长内容"}),
}
}
RETURN_TYPES = (IO.VIDEO,)
RETURN_NAMES = ("video",)
FUNCTION = "generate"
CATEGORY = "UCLOUD_MODELVERSE/Vidu"
def generate(self, client, model, video_url, duration, resolution,
last_frame_image=None, last_frame_url="", prompt=""):
api_key = client.get("api_key")
if not api_key:
raise ValueError("API key is not set")
if not video_url or not video_url.strip():
raise ValueError("必须提供 video_url")
mv_client = ModelverseClient(api_key)
task_input = {
"video_url": video_url.strip(),
}
# Optional last frame
has_last_url = last_frame_url and last_frame_url.strip()
has_last_image = last_frame_image is not None
if has_last_url and has_last_image:
raise ValueError("尾帧:请提供 image 或 url,不能同时提供")
if has_last_url:
task_input["last_frame_url"] = last_frame_url.strip()
elif has_last_image:
task_input["last_frame_url"] = image_to_base64(last_frame_image)
if prompt and prompt.strip():
task_input["prompt"] = prompt.strip()
parameters = {
"vidu_type": "extend",
"duration": duration,
"resolution": resolution,
}
# Submit task
submit_res = mv_client.submit_task(model, task_input, parameters)
task_id = submit_res.get("output", {}).get("task_id")
if not task_id:
raise Exception(f"Failed to submit task: {submit_res}")
print(f"Vidu Extend task submitted: {task_id}")
# Poll for result
video_url_result = self._poll_task(mv_client, task_id)
# Download and return video
return self._download_video(video_url_result)
def _poll_task(self, mv_client, task_id, max_retries=180):
for i in range(max_retries):
status_res = mv_client.get_task_status(task_id)
task_status = status_res.get("output", {}).get("task_status")
if task_status == "Success":
urls = status_res.get("output", {}).get("urls", [])
if urls:
return urls[0]
raise Exception("Task succeeded but no video URL returned")
elif task_status == "Failure":
error = status_res.get("output", {}).get("error_message", "Unknown error")
raise Exception(f"Task failed: {error}")
elif task_status in ["Pending", "Running"]:
print(f"Task {task_id}: {task_status} ({i+1}/{max_retries})")
time.sleep(5)
else:
raise Exception(f"Unknown status: {task_status}")
raise Exception("Task timed out")
def _download_video(self, video_url):
response = requests.get(video_url, timeout=120)
response.raise_for_status()
video_io = io.BytesIO(response.content)
video_io.seek(0)
return (VideoFromFile(video_io),)
NODE_CLASS_MAPPINGS = {
"Vidu_Extend": ViduExtendNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Vidu_Extend": "Vidu Extend",
}