converted pixverse, rodin nodes

This commit is contained in:
bigcat88 2025-08-01 16:57:37 +03:00
parent ed354f08fd
commit c8f75cb322
No known key found for this signature in database
GPG Key ID: 1F0BF0EC3CF22721
2 changed files with 104 additions and 89 deletions

View File

@ -30,7 +30,7 @@ from comfy.comfy_types.node_typing import IO, ComfyNodeABC
from comfy_api.input_impl import VideoFromFile from comfy_api.input_impl import VideoFromFile
import torch import torch
import requests import aiohttp
from io import BytesIO from io import BytesIO
@ -47,7 +47,7 @@ def get_video_url_from_response(
return str(response.Resp.url) return str(response.Resp.url)
def upload_image_to_pixverse(image: torch.Tensor, auth_kwargs=None): async def upload_image_to_pixverse(image: torch.Tensor, auth_kwargs=None):
# first, upload image to Pixverse and get image id to use in actual generation call # first, upload image to Pixverse and get image id to use in actual generation call
files = {"image": tensor_to_bytesio(image)} files = {"image": tensor_to_bytesio(image)}
operation = SynchronousOperation( operation = SynchronousOperation(
@ -62,7 +62,7 @@ def upload_image_to_pixverse(image: torch.Tensor, auth_kwargs=None):
content_type="multipart/form-data", content_type="multipart/form-data",
auth_kwargs=auth_kwargs, auth_kwargs=auth_kwargs,
) )
response_upload: PixverseImageUploadResponse = operation.execute() response_upload: PixverseImageUploadResponse = await operation.execute()
if response_upload.Resp is None: if response_upload.Resp is None:
raise Exception( raise Exception(
@ -164,7 +164,7 @@ class PixverseTextToVideoNode(ComfyNodeABC):
}, },
} }
def api_call( async def api_call(
self, self,
prompt: str, prompt: str,
aspect_ratio: str, aspect_ratio: str,
@ -205,7 +205,7 @@ class PixverseTextToVideoNode(ComfyNodeABC):
), ),
auth_kwargs=kwargs, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = await operation.execute()
if response_api.Resp is None: if response_api.Resp is None:
raise Exception(f"PixVerse request failed: '{response_api.ErrMsg}'") raise Exception(f"PixVerse request failed: '{response_api.ErrMsg}'")
@ -229,11 +229,11 @@ class PixverseTextToVideoNode(ComfyNodeABC):
result_url_extractor=get_video_url_from_response, result_url_extractor=get_video_url_from_response,
estimated_duration=AVERAGE_DURATION_T2V, estimated_duration=AVERAGE_DURATION_T2V,
) )
response_poll = operation.execute() response_poll = await operation.execute()
vid_response = requests.get(response_poll.Resp.url) async with aiohttp.ClientSession() as session:
async with session.get(response_poll.Resp.url) as vid_response:
return (VideoFromFile(BytesIO(vid_response.content)),) return (VideoFromFile(BytesIO(await vid_response.content.read())),)
class PixverseImageToVideoNode(ComfyNodeABC): class PixverseImageToVideoNode(ComfyNodeABC):
@ -302,7 +302,7 @@ class PixverseImageToVideoNode(ComfyNodeABC):
}, },
} }
def api_call( async def api_call(
self, self,
image: torch.Tensor, image: torch.Tensor,
prompt: str, prompt: str,
@ -316,7 +316,7 @@ class PixverseImageToVideoNode(ComfyNodeABC):
**kwargs, **kwargs,
): ):
validate_string(prompt, strip_whitespace=False) validate_string(prompt, strip_whitespace=False)
img_id = upload_image_to_pixverse(image, auth_kwargs=kwargs) img_id = await upload_image_to_pixverse(image, auth_kwargs=kwargs)
# 1080p is limited to 5 seconds duration # 1080p is limited to 5 seconds duration
# only normal motion_mode supported for 1080p or for non-5 second duration # only normal motion_mode supported for 1080p or for non-5 second duration
@ -345,7 +345,7 @@ class PixverseImageToVideoNode(ComfyNodeABC):
), ),
auth_kwargs=kwargs, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = await operation.execute()
if response_api.Resp is None: if response_api.Resp is None:
raise Exception(f"PixVerse request failed: '{response_api.ErrMsg}'") raise Exception(f"PixVerse request failed: '{response_api.ErrMsg}'")
@ -369,10 +369,11 @@ class PixverseImageToVideoNode(ComfyNodeABC):
result_url_extractor=get_video_url_from_response, result_url_extractor=get_video_url_from_response,
estimated_duration=AVERAGE_DURATION_I2V, estimated_duration=AVERAGE_DURATION_I2V,
) )
response_poll = operation.execute() response_poll = await operation.execute()
vid_response = requests.get(response_poll.Resp.url) async with aiohttp.ClientSession() as session:
return (VideoFromFile(BytesIO(vid_response.content)),) async with session.get(response_poll.Resp.url) as vid_response:
return (VideoFromFile(BytesIO(await vid_response.content.read())),)
class PixverseTransitionVideoNode(ComfyNodeABC): class PixverseTransitionVideoNode(ComfyNodeABC):
@ -436,7 +437,7 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
}, },
} }
def api_call( async def api_call(
self, self,
first_frame: torch.Tensor, first_frame: torch.Tensor,
last_frame: torch.Tensor, last_frame: torch.Tensor,
@ -450,8 +451,8 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
**kwargs, **kwargs,
): ):
validate_string(prompt, strip_whitespace=False) validate_string(prompt, strip_whitespace=False)
first_frame_id = upload_image_to_pixverse(first_frame, auth_kwargs=kwargs) first_frame_id = await upload_image_to_pixverse(first_frame, auth_kwargs=kwargs)
last_frame_id = upload_image_to_pixverse(last_frame, auth_kwargs=kwargs) last_frame_id = await upload_image_to_pixverse(last_frame, auth_kwargs=kwargs)
# 1080p is limited to 5 seconds duration # 1080p is limited to 5 seconds duration
# only normal motion_mode supported for 1080p or for non-5 second duration # only normal motion_mode supported for 1080p or for non-5 second duration
@ -480,7 +481,7 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
), ),
auth_kwargs=kwargs, auth_kwargs=kwargs,
) )
response_api = operation.execute() response_api = await operation.execute()
if response_api.Resp is None: if response_api.Resp is None:
raise Exception(f"PixVerse request failed: '{response_api.ErrMsg}'") raise Exception(f"PixVerse request failed: '{response_api.ErrMsg}'")
@ -504,10 +505,11 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
result_url_extractor=get_video_url_from_response, result_url_extractor=get_video_url_from_response,
estimated_duration=AVERAGE_DURATION_T2V, estimated_duration=AVERAGE_DURATION_T2V,
) )
response_poll = operation.execute() response_poll = await operation.execute()
vid_response = requests.get(response_poll.Resp.url) async with aiohttp.ClientSession() as session:
return (VideoFromFile(BytesIO(vid_response.content)),) async with session.get(response_poll.Resp.url) as vid_response:
return (VideoFromFile(BytesIO(await vid_response.content.read())),)
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {

View File

@ -9,11 +9,10 @@ from __future__ import annotations
from inspect import cleandoc from inspect import cleandoc
from comfy.comfy_types.node_typing import IO from comfy.comfy_types.node_typing import IO
import folder_paths as comfy_paths import folder_paths as comfy_paths
import requests import aiohttp
import os import os
import datetime import datetime
import shutil import asyncio
import time
import io import io
import logging import logging
import math import math
@ -66,7 +65,6 @@ def create_task_error(response: Rodin3DGenerateResponse):
return hasattr(response, "error") return hasattr(response, "error")
class Rodin3DAPI: class Rodin3DAPI:
""" """
Generate 3D Assets using Rodin API Generate 3D Assets using Rodin API
@ -123,8 +121,8 @@ class Rodin3DAPI:
else: else:
return "Generating" return "Generating"
def CreateGenerateTask(self, images=None, seed=1, material="PBR", quality="medium", tier="Regular", mesh_mode="Quad", **kwargs): async def create_generate_task(self, images=None, seed=1, material="PBR", quality="medium", tier="Regular", mesh_mode="Quad", **kwargs):
if images == None: if images is None:
raise Exception("Rodin 3D generate requires at least 1 image.") raise Exception("Rodin 3D generate requires at least 1 image.")
if len(images) >= 5: if len(images) >= 5:
raise Exception("Rodin 3D generate requires up to 5 image.") raise Exception("Rodin 3D generate requires up to 5 image.")
@ -155,7 +153,7 @@ class Rodin3DAPI:
auth_kwargs=kwargs, auth_kwargs=kwargs,
) )
response = operation.execute() response = await operation.execute()
if create_task_error(response): if create_task_error(response):
error_message = f"Rodin3D Create 3D generate Task Failed. Message: {response.message}, error: {response.error}" error_message = f"Rodin3D Create 3D generate Task Failed. Message: {response.message}, error: {response.error}"
@ -168,7 +166,7 @@ class Rodin3DAPI:
logging.info(f"[ Rodin3D API - Submit Jobs ] UUID: {task_uuid}") logging.info(f"[ Rodin3D API - Submit Jobs ] UUID: {task_uuid}")
return task_uuid, subscription_key return task_uuid, subscription_key
def poll_for_task_status(self, subscription_key, **kwargs) -> Rodin3DCheckStatusResponse: async def poll_for_task_status(self, subscription_key, **kwargs) -> Rodin3DCheckStatusResponse:
path = "/proxy/rodin/api/v2/status" path = "/proxy/rodin/api/v2/status"
@ -191,11 +189,9 @@ class Rodin3DAPI:
logging.info("[ Rodin3D API - CheckStatus ] Generate Start!") logging.info("[ Rodin3D API - CheckStatus ] Generate Start!")
return poll_operation.execute() return await poll_operation.execute()
async def get_rodin_download_list(self, uuid, **kwargs) -> Rodin3DDownloadResponse:
def GetRodinDownloadList(self, uuid, **kwargs) -> Rodin3DDownloadResponse:
logging.info("[ Rodin3D API - Downloading ] Generate Successfully!") logging.info("[ Rodin3D API - Downloading ] Generate Successfully!")
path = "/proxy/rodin/api/v2/download" path = "/proxy/rodin/api/v2/download"
@ -212,53 +208,59 @@ class Rodin3DAPI:
auth_kwargs=kwargs auth_kwargs=kwargs
) )
return operation.execute() return await operation.execute()
def GetQualityAndMode(self, PolyCount): def get_quality_mode(self, poly_count):
if PolyCount == "200K-Triangle": if poly_count == "200K-Triangle":
mesh_mode = "Raw" mesh_mode = "Raw"
quality = "medium" quality = "medium"
else: else:
mesh_mode = "Quad" mesh_mode = "Quad"
if PolyCount == "4K-Quad": if poly_count == "4K-Quad":
quality = "extra-low" quality = "extra-low"
elif PolyCount == "8K-Quad": elif poly_count == "8K-Quad":
quality = "low" quality = "low"
elif PolyCount == "18K-Quad": elif poly_count == "18K-Quad":
quality = "medium" quality = "medium"
elif PolyCount == "50K-Quad": elif poly_count == "50K-Quad":
quality = "high" quality = "high"
else: else:
quality = "medium" quality = "medium"
return mesh_mode, quality return mesh_mode, quality
def DownLoadFiles(self, Url_List): async def download_files(self, url_list):
Save_path = os.path.join(comfy_paths.get_output_directory(), "Rodin3D", datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) save_path = os.path.join(comfy_paths.get_output_directory(), "Rodin3D", datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
os.makedirs(Save_path, exist_ok=True) os.makedirs(save_path, exist_ok=True)
model_file_path = None model_file_path = None
for Item in Url_List.list: async with aiohttp.ClientSession() as session:
url = Item.url for i in url_list.list:
file_name = Item.name url = i.url
file_path = os.path.join(Save_path, file_name) file_name = i.name
if file_path.endswith(".glb"): file_path = os.path.join(save_path, file_name)
model_file_path = file_path if file_path.endswith(".glb"):
logging.info(f"[ Rodin3D API - download_files ] Downloading file: {file_path}") model_file_path = file_path
max_retries = 5 logging.info(f"[ Rodin3D API - download_files ] Downloading file: {file_path}")
for attempt in range(max_retries): max_retries = 5
try: for attempt in range(max_retries):
with requests.get(url, stream=True) as r: try:
r.raise_for_status() async with session.get(url) as resp:
with open(file_path, "wb") as f: resp.raise_for_status()
shutil.copyfileobj(r.raw, f) with open(file_path, "wb") as f:
break async for chunk in resp.content.iter_chunked(32 * 1024):
except Exception as e: f.write(chunk)
logging.info(f"[ Rodin3D API - download_files ] Error downloading {file_path}:{e}") break
if attempt < max_retries - 1: except Exception as e:
logging.info("Retrying...") logging.info(f"[ Rodin3D API - download_files ] Error downloading {file_path}:{e}")
time.sleep(2) if attempt < max_retries - 1:
else: logging.info("Retrying...")
logging.info(f"[ Rodin3D API - download_files ] Failed to download {file_path} after {max_retries} attempts.") await asyncio.sleep(2)
else:
logging.info(
"[ Rodin3D API - download_files ] Failed to download %s after %s attempts.",
file_path,
max_retries,
)
return model_file_path return model_file_path
@ -285,7 +287,7 @@ class Rodin3D_Regular(Rodin3DAPI):
}, },
} }
def api_call( async def api_call(
self, self,
Images, Images,
Seed, Seed,
@ -298,14 +300,17 @@ class Rodin3D_Regular(Rodin3DAPI):
m_images = [] m_images = []
for i in range(num_images): for i in range(num_images):
m_images.append(Images[i]) m_images.append(Images[i])
mesh_mode, quality = self.GetQualityAndMode(Polygon_count) mesh_mode, quality = self.get_quality_mode(Polygon_count)
task_uuid, subscription_key = self.CreateGenerateTask(images=m_images, seed=Seed, material=Material_Type, quality=quality, tier=tier, mesh_mode=mesh_mode, **kwargs) task_uuid, subscription_key = await self.create_generate_task(images=m_images, seed=Seed, material=Material_Type,
self.poll_for_task_status(subscription_key, **kwargs) quality=quality, tier=tier, mesh_mode=mesh_mode,
Download_List = self.GetRodinDownloadList(task_uuid, **kwargs) **kwargs)
model = self.DownLoadFiles(Download_List) await self.poll_for_task_status(subscription_key, **kwargs)
download_list = await self.get_rodin_download_list(task_uuid, **kwargs)
model = await self.download_files(download_list)
return (model,) return (model,)
class Rodin3D_Detail(Rodin3DAPI): class Rodin3D_Detail(Rodin3DAPI):
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
@ -328,7 +333,7 @@ class Rodin3D_Detail(Rodin3DAPI):
}, },
} }
def api_call( async def api_call(
self, self,
Images, Images,
Seed, Seed,
@ -341,14 +346,17 @@ class Rodin3D_Detail(Rodin3DAPI):
m_images = [] m_images = []
for i in range(num_images): for i in range(num_images):
m_images.append(Images[i]) m_images.append(Images[i])
mesh_mode, quality = self.GetQualityAndMode(Polygon_count) mesh_mode, quality = self.get_quality_mode(Polygon_count)
task_uuid, subscription_key = self.CreateGenerateTask(images=m_images, seed=Seed, material=Material_Type, quality=quality, tier=tier, mesh_mode=mesh_mode, **kwargs) task_uuid, subscription_key = await self.create_generate_task(images=m_images, seed=Seed, material=Material_Type,
self.poll_for_task_status(subscription_key, **kwargs) quality=quality, tier=tier, mesh_mode=mesh_mode,
Download_List = self.GetRodinDownloadList(task_uuid, **kwargs) **kwargs)
model = self.DownLoadFiles(Download_List) await self.poll_for_task_status(subscription_key, **kwargs)
download_list = await self.get_rodin_download_list(task_uuid, **kwargs)
model = await self.download_files(download_list)
return (model,) return (model,)
class Rodin3D_Smooth(Rodin3DAPI): class Rodin3D_Smooth(Rodin3DAPI):
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
@ -371,7 +379,7 @@ class Rodin3D_Smooth(Rodin3DAPI):
}, },
} }
def api_call( async def api_call(
self, self,
Images, Images,
Seed, Seed,
@ -384,14 +392,17 @@ class Rodin3D_Smooth(Rodin3DAPI):
m_images = [] m_images = []
for i in range(num_images): for i in range(num_images):
m_images.append(Images[i]) m_images.append(Images[i])
mesh_mode, quality = self.GetQualityAndMode(Polygon_count) mesh_mode, quality = self.get_quality_mode(Polygon_count)
task_uuid, subscription_key = self.CreateGenerateTask(images=m_images, seed=Seed, material=Material_Type, quality=quality, tier=tier, mesh_mode=mesh_mode, **kwargs) task_uuid, subscription_key = await self.create_generate_task(images=m_images, seed=Seed, material=Material_Type,
self.poll_for_task_status(subscription_key, **kwargs) quality=quality, tier=tier, mesh_mode=mesh_mode,
Download_List = self.GetRodinDownloadList(task_uuid, **kwargs) **kwargs)
model = self.DownLoadFiles(Download_List) await self.poll_for_task_status(subscription_key, **kwargs)
download_list = await self.get_rodin_download_list(task_uuid, **kwargs)
model = await self.download_files(download_list)
return (model,) return (model,)
class Rodin3D_Sketch(Rodin3DAPI): class Rodin3D_Sketch(Rodin3DAPI):
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
@ -423,7 +434,7 @@ class Rodin3D_Sketch(Rodin3DAPI):
}, },
} }
def api_call( async def api_call(
self, self,
Images, Images,
Seed, Seed,
@ -437,10 +448,12 @@ class Rodin3D_Sketch(Rodin3DAPI):
material_type = "PBR" material_type = "PBR"
quality = "medium" quality = "medium"
mesh_mode = "Quad" mesh_mode = "Quad"
task_uuid, subscription_key = self.CreateGenerateTask(images=m_images, seed=Seed, material=material_type, quality=quality, tier=tier, mesh_mode=mesh_mode, **kwargs) task_uuid, subscription_key = await self.create_generate_task(
self.poll_for_task_status(subscription_key, **kwargs) images=m_images, seed=Seed, material=material_type, quality=quality, tier=tier, mesh_mode=mesh_mode, **kwargs
Download_List = self.GetRodinDownloadList(task_uuid, **kwargs) )
model = self.DownLoadFiles(Download_List) await self.poll_for_task_status(subscription_key, **kwargs)
download_list = await self.get_rodin_download_list(task_uuid, **kwargs)
model = await self.download_files(download_list)
return (model,) return (model,)