add video async api
* add gitignore * add video api * fix: export to video * fix * fix * fix * fix * fix * add i2v * fix * fix --------- Co-authored-by: jason.mei <jason.mei@ucloud.cn>
This commit is contained in:
@@ -3,3 +3,5 @@ __pycache__
|
||||
py/__pycache__
|
||||
py/modelverse_api/__pycache__
|
||||
py/modelverse_api/requests/__pycache__
|
||||
/venv
|
||||
.idea/
|
||||
+2
-1
@@ -39,7 +39,8 @@ for file in files:
|
||||
serialized_CLASS_MAPPINGS = {k: serialize(v) for k, v in imported_module.NODE_CLASS_MAPPINGS.items()}
|
||||
serialized_DISPLAY_NAME_MAPPINGS = {k: serialize(v) for k, v in imported_module.NODE_DISPLAY_NAME_MAPPINGS.items()}
|
||||
all_nodes[file]={"NODE_CLASS_MAPPINGS": serialized_CLASS_MAPPINGS, "NODE_DISPLAY_NAME_MAPPINGS": serialized_DISPLAY_NAME_MAPPINGS}
|
||||
except:
|
||||
except Exception as e:
|
||||
print(f"Failed to import {file}: {e}")
|
||||
pass
|
||||
|
||||
|
||||
|
||||
+38
-43
@@ -4,47 +4,35 @@ import asyncio
|
||||
from .utils import BaseRequest
|
||||
|
||||
|
||||
import asyncio
|
||||
from .utils import BaseRequest
|
||||
|
||||
class ModelverseClient:
|
||||
"""
|
||||
UCloud Modelverse API Client
|
||||
|
||||
This class handles the core communication with the Modelverse API.
|
||||
"""
|
||||
|
||||
BASE_URL = "https://api.modelverse.cn"
|
||||
API_PATH = "/v1/images/generations"
|
||||
|
||||
def __init__(self, api_key):
|
||||
"""
|
||||
Initialize Modelverse API client
|
||||
|
||||
Args:
|
||||
api_key (str): Modelverse API key
|
||||
"""
|
||||
self.api_key = api_key
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
self.headers = {"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"}
|
||||
def post(self, endpoint, payload, timeout=180):
|
||||
url = f"{self.BASE_URL}{endpoint}"
|
||||
response = requests.post(url, headers=self.headers, json=payload, timeout=timeout)
|
||||
return self._handle_response(response)
|
||||
|
||||
def post(self, payload, timeout=180):
|
||||
"""
|
||||
Send POST request to Modelverse API
|
||||
|
||||
Args:
|
||||
endpoint (str): API endpoint
|
||||
payload (dict): Request payload
|
||||
timeout (float, optional): Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
dict: API response
|
||||
"""
|
||||
url = f"{self.BASE_URL}{self.API_PATH}"
|
||||
response = requests.post(
|
||||
url, headers=self.headers, json=payload, timeout=timeout)
|
||||
def get(self, endpoint, params=None, timeout=180):
|
||||
url = f"{self.BASE_URL}{endpoint}"
|
||||
headers = {"Authorization": f"Bearer {self.api_key}"}
|
||||
response = requests.get(url, headers=headers, params=params, timeout=timeout)
|
||||
return self._handle_response(response)
|
||||
|
||||
def _handle_response(self, response):
|
||||
if response.status_code == 401:
|
||||
raise Exception("Unauthorized: Invalid API key")
|
||||
|
||||
|
||||
# For backward compatibility with older error formats
|
||||
if response.status_code != 200:
|
||||
error_message = f"Error: {response.status_code}"
|
||||
try:
|
||||
@@ -60,26 +48,33 @@ class ModelverseClient:
|
||||
if response_data['code'] == 401:
|
||||
raise Exception("Unauthorized: Invalid API key")
|
||||
if response_data['code'] != 200:
|
||||
raise Exception(
|
||||
f"API Error: {response_data.get('message', 'Unknown error')}")
|
||||
raise Exception(f"API Error: {response_data.get('message', 'Unknown error')}")
|
||||
return response_data.get('data', {})
|
||||
return response_data
|
||||
|
||||
# --- New Methods for T2V ---
|
||||
def submit_task(self, model, task_input, parameters):
|
||||
endpoint = "/v1/tasks/submit"
|
||||
payload = {
|
||||
"model": model,
|
||||
"input": task_input,
|
||||
"parameters": parameters
|
||||
}
|
||||
return self.post(endpoint, payload)
|
||||
|
||||
def get_task_status(self, task_id):
|
||||
endpoint = f"/v1/tasks/status"
|
||||
params = {"task_id": task_id}
|
||||
return self.get(endpoint, params=params)
|
||||
|
||||
# --- Restored Async Methods for existing nodes ---
|
||||
async def async_send_request(self, request: BaseRequest):
|
||||
"""
|
||||
Sends an API request using a request object.
|
||||
|
||||
Args:
|
||||
request (BaseRequest): The request object containing payload and endpoint logic.
|
||||
|
||||
Returns:
|
||||
dict: API response or task result.
|
||||
"""
|
||||
payload = request.build_payload()
|
||||
endpoint = request.API_PATH
|
||||
if "seed" in payload:
|
||||
payload["seed"] = payload["seed"] % 2147483647 if payload["seed"] != -1 else -1
|
||||
|
||||
response = self.post(payload)
|
||||
response = self.post(endpoint, payload)
|
||||
return response.get("data", [])
|
||||
|
||||
async def run_tasks(self, tasks):
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import time
|
||||
import requests
|
||||
import os
|
||||
import tempfile
|
||||
import io
|
||||
from .modelverse_api.client import ModelverseClient
|
||||
from .modelverse_api.utils import image_to_base64
|
||||
|
||||
# Import VideoFromFile class for ComfyUI video handling
|
||||
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
|
||||
|
||||
class Modelverse_WanAII2V:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"client": ("MODELVERSE_API_CLIENT",),
|
||||
"prompt": ("STRING", {"multiline": True, "default": "Convert to video","tooltip": "Text prompt to guide video generation"}),
|
||||
},
|
||||
"optional": {
|
||||
"first_frame_image": ("IMAGE",),
|
||||
"first_frame_url": ("STRING", {"default": "", "tooltip": "First frame image URL (use either this OR first_frame_image, not both)"}),
|
||||
"last_frame_image": ("IMAGE",),
|
||||
"last_frame_url": ("STRING", {"default": "", "tooltip": "Optional: URL for the last frame of the video"}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": "low quality, blurry","tooltip": "Negative prompt to avoid unwanted content"}),
|
||||
"resolution": (["720P", "480P"], {"default": "720P", "tooltip": "Output video resolution"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 2147483647, "tooltip": "Random seed for reproducible results"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("VIDEO",)
|
||||
RETURN_NAMES = ("video",)
|
||||
FUNCTION = "generate_video"
|
||||
CATEGORY = "UCLOUD_MODELVERSE"
|
||||
|
||||
def generate_video(self, client, prompt, first_frame_image=None, first_frame_url="", last_frame_image=None, last_frame_url="", negative_prompt="", resolution="720P", seed=0):
|
||||
api_key = client.get("api_key")
|
||||
if not api_key:
|
||||
raise ValueError("API key is not set in the client")
|
||||
|
||||
mv_client = ModelverseClient(api_key)
|
||||
|
||||
# Prepare the input data
|
||||
task_input = {"prompt": prompt}
|
||||
|
||||
# Validate first frame input - must provide either image or URL, but not both
|
||||
has_url = first_frame_url and first_frame_url.strip()
|
||||
has_image = first_frame_image is not None
|
||||
|
||||
if has_url and has_image:
|
||||
raise ValueError("Please provide either first_frame_image OR first_frame_url, not both")
|
||||
elif not has_url and not has_image:
|
||||
raise ValueError("Must provide either first_frame_image or first_frame_url")
|
||||
|
||||
# Handle first frame
|
||||
if has_url:
|
||||
task_input["first_frame_url"] = first_frame_url.strip()
|
||||
print(f"Using first frame URL: {first_frame_url}")
|
||||
else:
|
||||
# Convert IMAGE tensor to base64
|
||||
first_frame_base64 = image_to_base64(first_frame_image)
|
||||
if not first_frame_base64:
|
||||
raise ValueError("Failed to convert first frame image to base64")
|
||||
task_input["first_frame_url"] = first_frame_base64
|
||||
print("Using first frame from IMAGE input (converted to base64)")
|
||||
|
||||
# Handle last frame (optional)
|
||||
if last_frame_url and last_frame_url.strip():
|
||||
task_input["last_frame_url"] = last_frame_url.strip()
|
||||
print(f"Using last frame URL: {last_frame_url}")
|
||||
elif last_frame_image is not None:
|
||||
# Convert IMAGE tensor to base64
|
||||
last_frame_base64 = image_to_base64(last_frame_image)
|
||||
if last_frame_base64:
|
||||
task_input["last_frame_url"] = last_frame_base64
|
||||
print("Using last frame from IMAGE input (converted to base64)")
|
||||
|
||||
# Add negative prompt if provided
|
||||
if negative_prompt and negative_prompt.strip():
|
||||
task_input["negative_prompt"] = negative_prompt.strip()
|
||||
|
||||
# Set parameters
|
||||
parameters = {
|
||||
"resolution": resolution,
|
||||
"duration": 5, # Fixed as per documentation
|
||||
"seed": seed,
|
||||
}
|
||||
|
||||
print(f"Submitting I2V task with model: Wan-AI/Wan2.2-I2V")
|
||||
print(f"Parameters: resolution={resolution}, seed={seed}")
|
||||
|
||||
# 1. Submit the task
|
||||
submit_res = mv_client.submit_task("Wan-AI/Wan2.2-I2V", 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.get('request_id')}")
|
||||
|
||||
print(f"Task submitted successfully with ID: {task_id}")
|
||||
|
||||
# 2. Poll for the result
|
||||
video_url = ""
|
||||
max_retries = 120 # Maximum 10 minutes (120 * 5 seconds)
|
||||
retry_count = 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
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 and len(urls) > 0:
|
||||
video_url = urls[0]
|
||||
print(f"Task completed successfully! Video URL: {video_url}")
|
||||
break
|
||||
else:
|
||||
raise Exception("Task succeeded but no video URL was returned.")
|
||||
|
||||
elif task_status == "Failure":
|
||||
error_message = status_res.get("output", {}).get("error_message", "Unknown error")
|
||||
raise Exception(f"Task failed: {error_message}")
|
||||
|
||||
elif task_status in ["Pending", "Running"]:
|
||||
print(f"Task {task_id} is {task_status}, waiting... ({retry_count + 1}/{max_retries})")
|
||||
time.sleep(5) # Wait for 5 seconds before polling again
|
||||
retry_count += 1
|
||||
else:
|
||||
raise Exception(f"Unknown task status: {task_status}")
|
||||
|
||||
except Exception as e:
|
||||
if "Task failed" in str(e) or "Unknown task status" in str(e):
|
||||
raise e
|
||||
print(f"Error checking task status: {e}, retrying...")
|
||||
retry_count += 1
|
||||
time.sleep(5)
|
||||
|
||||
if not video_url:
|
||||
raise Exception(f"Task timed out after {max_retries * 5} seconds")
|
||||
|
||||
# 3. Download video
|
||||
print(f"Downloading video from: {video_url}")
|
||||
try:
|
||||
response = requests.get(video_url, timeout=60)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise Exception(f"Failed to download video: {e}")
|
||||
|
||||
# Extract video data
|
||||
video_data = response.content
|
||||
|
||||
if not video_data:
|
||||
raise Exception("No video data was returned")
|
||||
|
||||
print("Video generation completed successfully")
|
||||
print(f"Downloaded video data size: {len(video_data)} bytes")
|
||||
|
||||
# Convert video data to BytesIO object
|
||||
video_io = io.BytesIO(video_data)
|
||||
|
||||
# Ensure the BytesIO object is at the beginning
|
||||
video_io.seek(0)
|
||||
|
||||
# Return VideoFromFile object
|
||||
try:
|
||||
video_obj = VideoFromFile(video_io)
|
||||
print("VideoFromFile object created successfully")
|
||||
return (video_obj,)
|
||||
except Exception as e:
|
||||
print(f"Error creating VideoFromFile object: {e}")
|
||||
# If VideoFromFile fails, try returning the raw video data
|
||||
# This is a fallback that might work with some ComfyUI versions
|
||||
return (video_data,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Modelverse_WanAII2V": Modelverse_WanAII2V
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Modelverse_WanAII2V": "Modelverse Wan-AI I2V"
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import time
|
||||
import requests
|
||||
import os
|
||||
import tempfile
|
||||
import io
|
||||
from .modelverse_api.client import ModelverseClient
|
||||
|
||||
# Import VideoFromFile class for ComfyUI video handling
|
||||
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:
|
||||
raise ImportError("VideoFromFile not found")
|
||||
|
||||
class Modelverse_WanAIT2V:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"client": ("MODELVERSE_API_CLIENT",),
|
||||
"prompt": ("STRING", {"multiline": True, "default": "a beautiful flower","tooltip": "Text prompt of the image to generate"}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": "low quality","tooltip": "Negative prompt of the image to generate"}),
|
||||
"resolution": (["720P", "480P"], {"default": "720P"}),
|
||||
"size": (["1280x720", "720x1280", "832x480", "480x832"], {"default": "1280x720"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("VIDEO",)
|
||||
RETURN_NAMES = ("video",)
|
||||
FUNCTION = "generate_video"
|
||||
CATEGORY = "UCLOUD_MODELVERSE"
|
||||
|
||||
def generate_video(self, client, prompt, negative_prompt, resolution, size, seed):
|
||||
api_key = client.get("api_key")
|
||||
if not api_key:
|
||||
raise ValueError("API key is not set in the client")
|
||||
|
||||
mv_client = ModelverseClient(api_key)
|
||||
|
||||
task_input = {"prompt": prompt}
|
||||
if negative_prompt:
|
||||
task_input["negative_prompt"] = negative_prompt
|
||||
|
||||
parameters = {
|
||||
"resolution": resolution,
|
||||
"size": size,
|
||||
"seed": seed,
|
||||
"duration": 5 # Fixed as per documentation
|
||||
}
|
||||
|
||||
# 1. Submit the task
|
||||
submit_res = mv_client.submit_task("Wan-AI/Wan2.2-T2V", 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.get('request_id')}")
|
||||
|
||||
# 2. Poll for the result
|
||||
video_url = ""
|
||||
while True:
|
||||
status_res = mv_client.get_task_status(task_id)
|
||||
task_status = status_res.get("output", {}).get("task_status")
|
||||
|
||||
if task_status == "Success":
|
||||
video_url = status_res.get("output", {}).get("urls", [None])[0]
|
||||
if not video_url:
|
||||
raise Exception("Task succeeded but no video URL was returned.")
|
||||
break
|
||||
elif task_status == "Failure":
|
||||
error_message = status_res.get("output", {}).get("error_message", "Unknown error")
|
||||
raise Exception(f"Task failed: {error_message}")
|
||||
elif task_status in ["Pending", "Running"]:
|
||||
print(f"Task {task_id} is {task_status}, waiting...")
|
||||
time.sleep(5) # Wait for 5 seconds before polling again
|
||||
else:
|
||||
raise Exception(f"Unknown task status: {task_status}")
|
||||
|
||||
# 3. Download video
|
||||
response = requests.get(video_url)
|
||||
response.raise_for_status()
|
||||
|
||||
# Extract video data
|
||||
video_data = response.content
|
||||
|
||||
if not video_data:
|
||||
raise Exception("No video data was returned")
|
||||
|
||||
print("Video generation completed successfully")
|
||||
print(f"Downloaded video data size: {len(video_data)} bytes")
|
||||
|
||||
# Convert video data to BytesIO object
|
||||
video_io = io.BytesIO(video_data)
|
||||
|
||||
# Ensure the BytesIO object is at the beginning
|
||||
video_io.seek(0)
|
||||
|
||||
# Return VideoFromFile object
|
||||
try:
|
||||
video_obj = VideoFromFile(video_io)
|
||||
print("VideoFromFile object created successfully")
|
||||
return (video_obj,)
|
||||
except Exception as e:
|
||||
print(f"Error creating VideoFromFile object: {e}")
|
||||
# If VideoFromFile fails, try returning the raw video data
|
||||
# This is a fallback that might work with some ComfyUI versions
|
||||
return (video_data,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Modelverse_WanAIT2V": Modelverse_WanAIT2V
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Modelverse_WanAIT2V": "Modelverse Wan-AI T2V"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user