mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-23 21:00:16 +08:00
feat(api-nodes): add Meshy 3D nodes (#11843)
* feat(api-nodes): add Meshy 3D nodes * rebased, added JSONata price badges
This commit is contained in:
parent
d150440466
commit
07f2462eae
160
comfy_api_nodes/apis/meshy.py
Normal file
160
comfy_api_nodes/apis/meshy.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from comfy_api.latest import Input
|
||||||
|
|
||||||
|
|
||||||
|
class InputShouldRemesh(TypedDict):
|
||||||
|
should_remesh: str
|
||||||
|
topology: str
|
||||||
|
target_polycount: int
|
||||||
|
|
||||||
|
|
||||||
|
class InputShouldTexture(TypedDict):
|
||||||
|
should_texture: str
|
||||||
|
enable_pbr: bool
|
||||||
|
texture_prompt: str
|
||||||
|
texture_image: Input.Image | None
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyTaskResponse(BaseModel):
|
||||||
|
result: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyTextToModelRequest(BaseModel):
|
||||||
|
mode: str = Field("preview")
|
||||||
|
prompt: str = Field(..., max_length=600)
|
||||||
|
art_style: str = Field(..., description="'realistic' or 'sculpture'")
|
||||||
|
ai_model: str = Field(...)
|
||||||
|
topology: str | None = Field(..., description="'quad' or 'triangle'")
|
||||||
|
target_polycount: int | None = Field(..., ge=100, le=300000)
|
||||||
|
should_remesh: bool = Field(
|
||||||
|
True,
|
||||||
|
description="False returns the original mesh, ignoring topology and polycount.",
|
||||||
|
)
|
||||||
|
symmetry_mode: str = Field(..., description="'auto', 'off' or 'on'")
|
||||||
|
pose_mode: str = Field(...)
|
||||||
|
seed: int = Field(...)
|
||||||
|
moderation: bool = Field(False)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyRefineTask(BaseModel):
|
||||||
|
mode: str = Field("refine")
|
||||||
|
preview_task_id: str = Field(...)
|
||||||
|
enable_pbr: bool | None = Field(...)
|
||||||
|
texture_prompt: str | None = Field(...)
|
||||||
|
texture_image_url: str | None = Field(...)
|
||||||
|
ai_model: str = Field(...)
|
||||||
|
moderation: bool = Field(False)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyImageToModelRequest(BaseModel):
|
||||||
|
image_url: str = Field(...)
|
||||||
|
ai_model: str = Field(...)
|
||||||
|
topology: str | None = Field(..., description="'quad' or 'triangle'")
|
||||||
|
target_polycount: int | None = Field(..., ge=100, le=300000)
|
||||||
|
symmetry_mode: str = Field(..., description="'auto', 'off' or 'on'")
|
||||||
|
should_remesh: bool = Field(
|
||||||
|
True,
|
||||||
|
description="False returns the original mesh, ignoring topology and polycount.",
|
||||||
|
)
|
||||||
|
should_texture: bool = Field(...)
|
||||||
|
enable_pbr: bool | None = Field(...)
|
||||||
|
pose_mode: str = Field(...)
|
||||||
|
texture_prompt: str | None = Field(None, max_length=600)
|
||||||
|
texture_image_url: str | None = Field(None)
|
||||||
|
seed: int = Field(...)
|
||||||
|
moderation: bool = Field(False)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyMultiImageToModelRequest(BaseModel):
|
||||||
|
image_urls: list[str] = Field(...)
|
||||||
|
ai_model: str = Field(...)
|
||||||
|
topology: str | None = Field(..., description="'quad' or 'triangle'")
|
||||||
|
target_polycount: int | None = Field(..., ge=100, le=300000)
|
||||||
|
symmetry_mode: str = Field(..., description="'auto', 'off' or 'on'")
|
||||||
|
should_remesh: bool = Field(
|
||||||
|
True,
|
||||||
|
description="False returns the original mesh, ignoring topology and polycount.",
|
||||||
|
)
|
||||||
|
should_texture: bool = Field(...)
|
||||||
|
enable_pbr: bool | None = Field(...)
|
||||||
|
pose_mode: str = Field(...)
|
||||||
|
texture_prompt: str | None = Field(None, max_length=600)
|
||||||
|
texture_image_url: str | None = Field(None)
|
||||||
|
seed: int = Field(...)
|
||||||
|
moderation: bool = Field(False)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyRiggingRequest(BaseModel):
|
||||||
|
input_task_id: str = Field(...)
|
||||||
|
height_meters: float = Field(...)
|
||||||
|
texture_image_url: str | None = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyAnimationRequest(BaseModel):
|
||||||
|
rig_task_id: str = Field(...)
|
||||||
|
action_id: int = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyTextureRequest(BaseModel):
|
||||||
|
input_task_id: str = Field(...)
|
||||||
|
ai_model: str = Field(...)
|
||||||
|
enable_original_uv: bool = Field(...)
|
||||||
|
enable_pbr: bool = Field(...)
|
||||||
|
text_style_prompt: str | None = Field(...)
|
||||||
|
image_style_url: str | None = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyModelsUrls(BaseModel):
|
||||||
|
glb: str = Field("")
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyRiggedModelsUrls(BaseModel):
|
||||||
|
rigged_character_glb_url: str = Field("")
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyAnimatedModelsUrls(BaseModel):
|
||||||
|
animation_glb_url: str = Field("")
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyResultTextureUrls(BaseModel):
|
||||||
|
base_color: str = Field(...)
|
||||||
|
metallic: str | None = Field(None)
|
||||||
|
normal: str | None = Field(None)
|
||||||
|
roughness: str | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyTaskError(BaseModel):
|
||||||
|
message: str | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyModelResult(BaseModel):
|
||||||
|
id: str = Field(...)
|
||||||
|
type: str = Field(...)
|
||||||
|
model_urls: MeshyModelsUrls = Field(MeshyModelsUrls())
|
||||||
|
thumbnail_url: str = Field(...)
|
||||||
|
video_url: str | None = Field(None)
|
||||||
|
status: str = Field(...)
|
||||||
|
progress: int = Field(0)
|
||||||
|
texture_urls: list[MeshyResultTextureUrls] | None = Field([])
|
||||||
|
task_error: MeshyTaskError | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyRiggedResult(BaseModel):
|
||||||
|
id: str = Field(...)
|
||||||
|
type: str = Field(...)
|
||||||
|
status: str = Field(...)
|
||||||
|
progress: int = Field(0)
|
||||||
|
result: MeshyRiggedModelsUrls = Field(MeshyRiggedModelsUrls())
|
||||||
|
task_error: MeshyTaskError | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyAnimationResult(BaseModel):
|
||||||
|
id: str = Field(...)
|
||||||
|
type: str = Field(...)
|
||||||
|
status: str = Field(...)
|
||||||
|
progress: int = Field(0)
|
||||||
|
result: MeshyAnimatedModelsUrls = Field(MeshyAnimatedModelsUrls())
|
||||||
|
task_error: MeshyTaskError | None = Field(None)
|
||||||
790
comfy_api_nodes/nodes_meshy.py
Normal file
790
comfy_api_nodes/nodes_meshy.py
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from comfy_api.latest import IO, ComfyExtension, Input
|
||||||
|
from comfy_api_nodes.apis.meshy import (
|
||||||
|
InputShouldRemesh,
|
||||||
|
InputShouldTexture,
|
||||||
|
MeshyAnimationRequest,
|
||||||
|
MeshyAnimationResult,
|
||||||
|
MeshyImageToModelRequest,
|
||||||
|
MeshyModelResult,
|
||||||
|
MeshyMultiImageToModelRequest,
|
||||||
|
MeshyRefineTask,
|
||||||
|
MeshyRiggedResult,
|
||||||
|
MeshyRiggingRequest,
|
||||||
|
MeshyTaskResponse,
|
||||||
|
MeshyTextToModelRequest,
|
||||||
|
MeshyTextureRequest,
|
||||||
|
)
|
||||||
|
from comfy_api_nodes.util import (
|
||||||
|
ApiEndpoint,
|
||||||
|
download_url_to_bytesio,
|
||||||
|
poll_op,
|
||||||
|
sync_op,
|
||||||
|
upload_images_to_comfyapi,
|
||||||
|
validate_string,
|
||||||
|
)
|
||||||
|
from folder_paths import get_output_directory
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyTextToModelNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="MeshyTextToModelNode",
|
||||||
|
display_name="Meshy: Text to Model",
|
||||||
|
category="api node/3d/Meshy",
|
||||||
|
inputs=[
|
||||||
|
IO.Combo.Input("model", options=["latest"]),
|
||||||
|
IO.String.Input("prompt", multiline=True, default=""),
|
||||||
|
IO.Combo.Input("style", options=["realistic", "sculpture"]),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"should_remesh",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"true",
|
||||||
|
[
|
||||||
|
IO.Combo.Input("topology", options=["triangle", "quad"]),
|
||||||
|
IO.Int.Input(
|
||||||
|
"target_polycount",
|
||||||
|
default=300000,
|
||||||
|
min=100,
|
||||||
|
max=300000,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Option("false", []),
|
||||||
|
],
|
||||||
|
tooltip="When set to false, returns an unprocessed triangular mesh.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input("symmetry_mode", options=["auto", "on", "off"]),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"pose_mode",
|
||||||
|
options=["", "A-pose", "T-pose"],
|
||||||
|
tooltip="Specify the pose mode for the generated model.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=2147483647,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed controls whether the node should re-run; "
|
||||||
|
"results are non-deterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.String.Output(display_name="model_file"),
|
||||||
|
IO.Custom("MESHY_TASK_ID").Output(display_name="meshy_task_id"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
is_output_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.8}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
model: str,
|
||||||
|
prompt: str,
|
||||||
|
style: str,
|
||||||
|
should_remesh: InputShouldRemesh,
|
||||||
|
symmetry_mode: str,
|
||||||
|
pose_mode: str,
|
||||||
|
seed: int,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
validate_string(prompt, field_name="prompt", min_length=1, max_length=600)
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/meshy/openapi/v2/text-to-3d", method="POST"),
|
||||||
|
response_model=MeshyTaskResponse,
|
||||||
|
data=MeshyTextToModelRequest(
|
||||||
|
prompt=prompt,
|
||||||
|
art_style=style,
|
||||||
|
ai_model=model,
|
||||||
|
topology=should_remesh.get("topology", None),
|
||||||
|
target_polycount=should_remesh.get("target_polycount", None),
|
||||||
|
should_remesh=should_remesh["should_remesh"] == "true",
|
||||||
|
symmetry_mode=symmetry_mode,
|
||||||
|
pose_mode=pose_mode.lower(),
|
||||||
|
seed=seed,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/meshy/openapi/v2/text-to-3d/{response.result}"),
|
||||||
|
response_model=MeshyModelResult,
|
||||||
|
status_extractor=lambda r: r.status,
|
||||||
|
progress_extractor=lambda r: r.progress,
|
||||||
|
)
|
||||||
|
model_file = f"meshy_model_{response.result}.glb"
|
||||||
|
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||||
|
return IO.NodeOutput(model_file, response.result)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyRefineNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="MeshyRefineNode",
|
||||||
|
display_name="Meshy: Refine Draft Model",
|
||||||
|
category="api node/3d/Meshy",
|
||||||
|
description="Refine a previously created draft model.",
|
||||||
|
inputs=[
|
||||||
|
IO.Combo.Input("model", options=["latest"]),
|
||||||
|
IO.Custom("MESHY_TASK_ID").Input("meshy_task_id"),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"enable_pbr",
|
||||||
|
default=False,
|
||||||
|
tooltip="Generate PBR Maps (metallic, roughness, normal) in addition to the base color. "
|
||||||
|
"Note: this should be set to false when using Sculpture style, "
|
||||||
|
"as Sculpture style generates its own set of PBR maps.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"texture_prompt",
|
||||||
|
default="",
|
||||||
|
multiline=True,
|
||||||
|
tooltip="Provide a text prompt to guide the texturing process. "
|
||||||
|
"Maximum 600 characters. Cannot be used at the same time as 'texture_image'.",
|
||||||
|
),
|
||||||
|
IO.Image.Input(
|
||||||
|
"texture_image",
|
||||||
|
tooltip="Only one of 'texture_image' or 'texture_prompt' may be used at the same time.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.String.Output(display_name="model_file"),
|
||||||
|
IO.Custom("MESHY_TASK_ID").Output(display_name="meshy_task_id"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
is_output_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.4}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
model: str,
|
||||||
|
meshy_task_id: str,
|
||||||
|
enable_pbr: bool,
|
||||||
|
texture_prompt: str,
|
||||||
|
texture_image: Input.Image | None = None,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
if texture_prompt and texture_image is not None:
|
||||||
|
raise ValueError("texture_prompt and texture_image cannot be used at the same time")
|
||||||
|
texture_image_url = None
|
||||||
|
if texture_prompt:
|
||||||
|
validate_string(texture_prompt, field_name="texture_prompt", max_length=600)
|
||||||
|
if texture_image is not None:
|
||||||
|
texture_image_url = (await upload_images_to_comfyapi(cls, texture_image, wait_label="Uploading texture"))[0]
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
endpoint=ApiEndpoint(path="/proxy/meshy/openapi/v2/text-to-3d", method="POST"),
|
||||||
|
response_model=MeshyTaskResponse,
|
||||||
|
data=MeshyRefineTask(
|
||||||
|
preview_task_id=meshy_task_id,
|
||||||
|
enable_pbr=enable_pbr,
|
||||||
|
texture_prompt=texture_prompt if texture_prompt else None,
|
||||||
|
texture_image_url=texture_image_url,
|
||||||
|
ai_model=model,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/meshy/openapi/v2/text-to-3d/{response.result}"),
|
||||||
|
response_model=MeshyModelResult,
|
||||||
|
status_extractor=lambda r: r.status,
|
||||||
|
progress_extractor=lambda r: r.progress,
|
||||||
|
)
|
||||||
|
model_file = f"meshy_model_{response.result}.glb"
|
||||||
|
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||||
|
return IO.NodeOutput(model_file, response.result)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyImageToModelNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="MeshyImageToModelNode",
|
||||||
|
display_name="Meshy: Image to Model",
|
||||||
|
category="api node/3d/Meshy",
|
||||||
|
inputs=[
|
||||||
|
IO.Combo.Input("model", options=["latest"]),
|
||||||
|
IO.Image.Input("image"),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"should_remesh",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"true",
|
||||||
|
[
|
||||||
|
IO.Combo.Input("topology", options=["triangle", "quad"]),
|
||||||
|
IO.Int.Input(
|
||||||
|
"target_polycount",
|
||||||
|
default=300000,
|
||||||
|
min=100,
|
||||||
|
max=300000,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Option("false", []),
|
||||||
|
],
|
||||||
|
tooltip="When set to false, returns an unprocessed triangular mesh.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input("symmetry_mode", options=["auto", "on", "off"]),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"should_texture",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"true",
|
||||||
|
[
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"enable_pbr",
|
||||||
|
default=False,
|
||||||
|
tooltip="Generate PBR Maps (metallic, roughness, normal) "
|
||||||
|
"in addition to the base color.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"texture_prompt",
|
||||||
|
default="",
|
||||||
|
multiline=True,
|
||||||
|
tooltip="Provide a text prompt to guide the texturing process. "
|
||||||
|
"Maximum 600 characters. Cannot be used at the same time as 'texture_image'.",
|
||||||
|
),
|
||||||
|
IO.Image.Input(
|
||||||
|
"texture_image",
|
||||||
|
tooltip="Only one of 'texture_image' or 'texture_prompt' "
|
||||||
|
"may be used at the same time.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Option("false", []),
|
||||||
|
],
|
||||||
|
tooltip="Determines whether textures are generated. "
|
||||||
|
"Setting it to false skips the texture phase and returns a mesh without textures.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"pose_mode",
|
||||||
|
options=["", "A-pose", "T-pose"],
|
||||||
|
tooltip="Specify the pose mode for the generated model.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=2147483647,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed controls whether the node should re-run; "
|
||||||
|
"results are non-deterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.String.Output(display_name="model_file"),
|
||||||
|
IO.Custom("MESHY_TASK_ID").Output(display_name="meshy_task_id"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
is_output_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
depends_on=IO.PriceBadgeDepends(widgets=["should_texture"]),
|
||||||
|
expr="""
|
||||||
|
(
|
||||||
|
$prices := {"true": 1.2, "false": 0.8};
|
||||||
|
{"type":"usd","usd": $lookup($prices, widgets.should_texture)}
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
model: str,
|
||||||
|
image: Input.Image,
|
||||||
|
should_remesh: InputShouldRemesh,
|
||||||
|
symmetry_mode: str,
|
||||||
|
should_texture: InputShouldTexture,
|
||||||
|
pose_mode: str,
|
||||||
|
seed: int,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
texture = should_texture["should_texture"] == "true"
|
||||||
|
texture_image_url = texture_prompt = None
|
||||||
|
if texture:
|
||||||
|
if should_texture["texture_prompt"] and should_texture["texture_image"] is not None:
|
||||||
|
raise ValueError("texture_prompt and texture_image cannot be used at the same time")
|
||||||
|
if should_texture["texture_prompt"]:
|
||||||
|
validate_string(should_texture["texture_prompt"], field_name="texture_prompt", max_length=600)
|
||||||
|
texture_prompt = should_texture["texture_prompt"]
|
||||||
|
if should_texture["texture_image"] is not None:
|
||||||
|
texture_image_url = (
|
||||||
|
await upload_images_to_comfyapi(
|
||||||
|
cls, should_texture["texture_image"], wait_label="Uploading texture"
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/meshy/openapi/v1/image-to-3d", method="POST"),
|
||||||
|
response_model=MeshyTaskResponse,
|
||||||
|
data=MeshyImageToModelRequest(
|
||||||
|
image_url=(await upload_images_to_comfyapi(cls, image, wait_label="Uploading base image"))[0],
|
||||||
|
ai_model=model,
|
||||||
|
topology=should_remesh.get("topology", None),
|
||||||
|
target_polycount=should_remesh.get("target_polycount", None),
|
||||||
|
symmetry_mode=symmetry_mode,
|
||||||
|
should_remesh=should_remesh["should_remesh"] == "true",
|
||||||
|
should_texture=texture,
|
||||||
|
enable_pbr=should_texture.get("enable_pbr", None),
|
||||||
|
pose_mode=pose_mode.lower(),
|
||||||
|
texture_prompt=texture_prompt,
|
||||||
|
texture_image_url=texture_image_url,
|
||||||
|
seed=seed,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/image-to-3d/{response.result}"),
|
||||||
|
response_model=MeshyModelResult,
|
||||||
|
status_extractor=lambda r: r.status,
|
||||||
|
progress_extractor=lambda r: r.progress,
|
||||||
|
)
|
||||||
|
model_file = f"meshy_model_{response.result}.glb"
|
||||||
|
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||||
|
return IO.NodeOutput(model_file, response.result)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyMultiImageToModelNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="MeshyMultiImageToModelNode",
|
||||||
|
display_name="Meshy: Multi-Image to Model",
|
||||||
|
category="api node/3d/Meshy",
|
||||||
|
inputs=[
|
||||||
|
IO.Combo.Input("model", options=["latest"]),
|
||||||
|
IO.Autogrow.Input(
|
||||||
|
"images",
|
||||||
|
template=IO.Autogrow.TemplatePrefix(IO.Image.Input("image"), prefix="image", min=2, max=4),
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"should_remesh",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"true",
|
||||||
|
[
|
||||||
|
IO.Combo.Input("topology", options=["triangle", "quad"]),
|
||||||
|
IO.Int.Input(
|
||||||
|
"target_polycount",
|
||||||
|
default=300000,
|
||||||
|
min=100,
|
||||||
|
max=300000,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Option("false", []),
|
||||||
|
],
|
||||||
|
tooltip="When set to false, returns an unprocessed triangular mesh.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input("symmetry_mode", options=["auto", "on", "off"]),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"should_texture",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"true",
|
||||||
|
[
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"enable_pbr",
|
||||||
|
default=False,
|
||||||
|
tooltip="Generate PBR Maps (metallic, roughness, normal) "
|
||||||
|
"in addition to the base color.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"texture_prompt",
|
||||||
|
default="",
|
||||||
|
multiline=True,
|
||||||
|
tooltip="Provide a text prompt to guide the texturing process. "
|
||||||
|
"Maximum 600 characters. Cannot be used at the same time as 'texture_image'.",
|
||||||
|
),
|
||||||
|
IO.Image.Input(
|
||||||
|
"texture_image",
|
||||||
|
tooltip="Only one of 'texture_image' or 'texture_prompt' "
|
||||||
|
"may be used at the same time.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Option("false", []),
|
||||||
|
],
|
||||||
|
tooltip="Determines whether textures are generated. "
|
||||||
|
"Setting it to false skips the texture phase and returns a mesh without textures.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"pose_mode",
|
||||||
|
options=["", "A-pose", "T-pose"],
|
||||||
|
tooltip="Specify the pose mode for the generated model.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=2147483647,
|
||||||
|
display_mode=IO.NumberDisplay.number,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed controls whether the node should re-run; "
|
||||||
|
"results are non-deterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.String.Output(display_name="model_file"),
|
||||||
|
IO.Custom("MESHY_TASK_ID").Output(display_name="meshy_task_id"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
is_output_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
depends_on=IO.PriceBadgeDepends(widgets=["should_texture"]),
|
||||||
|
expr="""
|
||||||
|
(
|
||||||
|
$prices := {"true": 0.6, "false": 0.2};
|
||||||
|
{"type":"usd","usd": $lookup($prices, widgets.should_texture)}
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
model: str,
|
||||||
|
images: IO.Autogrow.Type,
|
||||||
|
should_remesh: InputShouldRemesh,
|
||||||
|
symmetry_mode: str,
|
||||||
|
should_texture: InputShouldTexture,
|
||||||
|
pose_mode: str,
|
||||||
|
seed: int,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
texture = should_texture["should_texture"] == "true"
|
||||||
|
texture_image_url = texture_prompt = None
|
||||||
|
if texture:
|
||||||
|
if should_texture["texture_prompt"] and should_texture["texture_image"] is not None:
|
||||||
|
raise ValueError("texture_prompt and texture_image cannot be used at the same time")
|
||||||
|
if should_texture["texture_prompt"]:
|
||||||
|
validate_string(should_texture["texture_prompt"], field_name="texture_prompt", max_length=600)
|
||||||
|
texture_prompt = should_texture["texture_prompt"]
|
||||||
|
if should_texture["texture_image"] is not None:
|
||||||
|
texture_image_url = (
|
||||||
|
await upload_images_to_comfyapi(
|
||||||
|
cls, should_texture["texture_image"], wait_label="Uploading texture"
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/meshy/openapi/v1/multi-image-to-3d", method="POST"),
|
||||||
|
response_model=MeshyTaskResponse,
|
||||||
|
data=MeshyMultiImageToModelRequest(
|
||||||
|
image_urls=await upload_images_to_comfyapi(
|
||||||
|
cls, list(images.values()), wait_label="Uploading base images"
|
||||||
|
),
|
||||||
|
ai_model=model,
|
||||||
|
topology=should_remesh.get("topology", None),
|
||||||
|
target_polycount=should_remesh.get("target_polycount", None),
|
||||||
|
symmetry_mode=symmetry_mode,
|
||||||
|
should_remesh=should_remesh["should_remesh"] == "true",
|
||||||
|
should_texture=texture,
|
||||||
|
enable_pbr=should_texture.get("enable_pbr", None),
|
||||||
|
pose_mode=pose_mode.lower(),
|
||||||
|
texture_prompt=texture_prompt,
|
||||||
|
texture_image_url=texture_image_url,
|
||||||
|
seed=seed,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/multi-image-to-3d/{response.result}"),
|
||||||
|
response_model=MeshyModelResult,
|
||||||
|
status_extractor=lambda r: r.status,
|
||||||
|
progress_extractor=lambda r: r.progress,
|
||||||
|
)
|
||||||
|
model_file = f"meshy_model_{response.result}.glb"
|
||||||
|
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||||
|
return IO.NodeOutput(model_file, response.result)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyRigModelNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="MeshyRigModelNode",
|
||||||
|
display_name="Meshy: Rig Model",
|
||||||
|
category="api node/3d/Meshy",
|
||||||
|
description="Provides a rigged character in standard formats. "
|
||||||
|
"Auto-rigging is currently not suitable for untextured meshes, non-humanoid assets, "
|
||||||
|
"or humanoid assets with unclear limb and body structure.",
|
||||||
|
inputs=[
|
||||||
|
IO.Custom("MESHY_TASK_ID").Input("meshy_task_id"),
|
||||||
|
IO.Float.Input(
|
||||||
|
"height_meters",
|
||||||
|
min=0.1,
|
||||||
|
max=15.0,
|
||||||
|
default=1.7,
|
||||||
|
tooltip="The approximate height of the character model in meters. "
|
||||||
|
"This aids in scaling and rigging accuracy.",
|
||||||
|
),
|
||||||
|
IO.Image.Input(
|
||||||
|
"texture_image",
|
||||||
|
tooltip="The model's UV-unwrapped base color texture image.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.String.Output(display_name="model_file"),
|
||||||
|
IO.Custom("MESHY_RIGGED_TASK_ID").Output(display_name="rig_task_id"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
is_output_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.2}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
meshy_task_id: str,
|
||||||
|
height_meters: float,
|
||||||
|
texture_image: Input.Image | None = None,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
texture_image_url = None
|
||||||
|
if texture_image is not None:
|
||||||
|
texture_image_url = (await upload_images_to_comfyapi(cls, texture_image, wait_label="Uploading texture"))[0]
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
endpoint=ApiEndpoint(path="/proxy/meshy/openapi/v1/rigging", method="POST"),
|
||||||
|
response_model=MeshyTaskResponse,
|
||||||
|
data=MeshyRiggingRequest(
|
||||||
|
input_task_id=meshy_task_id,
|
||||||
|
height_meters=height_meters,
|
||||||
|
texture_image_url=texture_image_url,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/rigging/{response.result}"),
|
||||||
|
response_model=MeshyRiggedResult,
|
||||||
|
status_extractor=lambda r: r.status,
|
||||||
|
progress_extractor=lambda r: r.progress,
|
||||||
|
)
|
||||||
|
model_file = f"meshy_model_{response.result}.glb"
|
||||||
|
await download_url_to_bytesio(
|
||||||
|
result.result.rigged_character_glb_url, os.path.join(get_output_directory(), model_file)
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(model_file, response.result)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyAnimateModelNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="MeshyAnimateModelNode",
|
||||||
|
display_name="Meshy: Animate Model",
|
||||||
|
category="api node/3d/Meshy",
|
||||||
|
description="Apply a specific animation action to a previously rigged character.",
|
||||||
|
inputs=[
|
||||||
|
IO.Custom("MESHY_RIGGED_TASK_ID").Input("rig_task_id"),
|
||||||
|
IO.Int.Input(
|
||||||
|
"action_id",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=696,
|
||||||
|
tooltip="Visit https://docs.meshy.ai/en/api/animation-library for a list of available values.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.String.Output(display_name="model_file"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
is_output_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.12}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
rig_task_id: str,
|
||||||
|
action_id: int,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
endpoint=ApiEndpoint(path="/proxy/meshy/openapi/v1/animations", method="POST"),
|
||||||
|
response_model=MeshyTaskResponse,
|
||||||
|
data=MeshyAnimationRequest(
|
||||||
|
rig_task_id=rig_task_id,
|
||||||
|
action_id=action_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/animations/{response.result}"),
|
||||||
|
response_model=MeshyAnimationResult,
|
||||||
|
status_extractor=lambda r: r.status,
|
||||||
|
progress_extractor=lambda r: r.progress,
|
||||||
|
)
|
||||||
|
model_file = f"meshy_model_{response.result}.glb"
|
||||||
|
await download_url_to_bytesio(result.result.animation_glb_url, os.path.join(get_output_directory(), model_file))
|
||||||
|
return IO.NodeOutput(model_file, response.result)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyTextureNode(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="MeshyTextureNode",
|
||||||
|
display_name="Meshy: Texture Model",
|
||||||
|
category="api node/3d/Meshy",
|
||||||
|
inputs=[
|
||||||
|
IO.Combo.Input("model", options=["latest"]),
|
||||||
|
IO.Custom("MESHY_TASK_ID").Input("meshy_task_id"),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"enable_original_uv",
|
||||||
|
default=True,
|
||||||
|
tooltip="Use the original UV of the model instead of generating new UVs. "
|
||||||
|
"When enabled, Meshy preserves existing textures from the uploaded model. "
|
||||||
|
"If the model has no original UV, the quality of the output might not be as good.",
|
||||||
|
),
|
||||||
|
IO.Boolean.Input("pbr", default=False),
|
||||||
|
IO.String.Input(
|
||||||
|
"text_style_prompt",
|
||||||
|
default="",
|
||||||
|
multiline=True,
|
||||||
|
tooltip="Describe your desired texture style of the object using text. Maximum 600 characters."
|
||||||
|
"Maximum 600 characters. Cannot be used at the same time as 'image_style'.",
|
||||||
|
),
|
||||||
|
IO.Image.Input(
|
||||||
|
"image_style",
|
||||||
|
optional=True,
|
||||||
|
tooltip="A 2d image to guide the texturing process. "
|
||||||
|
"Can not be used at the same time with 'text_style_prompt'.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.String.Output(display_name="model_file"),
|
||||||
|
IO.Custom("MODEL_TASK_ID").Output(display_name="meshy_task_id"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
is_output_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.4}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
model: str,
|
||||||
|
meshy_task_id: str,
|
||||||
|
enable_original_uv: bool,
|
||||||
|
pbr: bool,
|
||||||
|
text_style_prompt: str,
|
||||||
|
image_style: Input.Image | None = None,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
if text_style_prompt and image_style is not None:
|
||||||
|
raise ValueError("text_style_prompt and image_style cannot be used at the same time")
|
||||||
|
if not text_style_prompt and image_style is None:
|
||||||
|
raise ValueError("Either text_style_prompt or image_style is required")
|
||||||
|
image_style_url = None
|
||||||
|
if image_style is not None:
|
||||||
|
image_style_url = (await upload_images_to_comfyapi(cls, image_style, wait_label="Uploading style"))[0]
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
endpoint=ApiEndpoint(path="/proxy/meshy/openapi/v1/retexture", method="POST"),
|
||||||
|
response_model=MeshyTaskResponse,
|
||||||
|
data=MeshyTextureRequest(
|
||||||
|
input_task_id=meshy_task_id,
|
||||||
|
ai_model=model,
|
||||||
|
enable_original_uv=enable_original_uv,
|
||||||
|
enable_pbr=pbr,
|
||||||
|
text_style_prompt=text_style_prompt if text_style_prompt else None,
|
||||||
|
image_style_url=image_style_url,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
result = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/meshy/openapi/v1/retexture/{response.result}"),
|
||||||
|
response_model=MeshyModelResult,
|
||||||
|
status_extractor=lambda r: r.status,
|
||||||
|
progress_extractor=lambda r: r.progress,
|
||||||
|
)
|
||||||
|
model_file = f"meshy_model_{response.result}.glb"
|
||||||
|
await download_url_to_bytesio(result.model_urls.glb, os.path.join(get_output_directory(), model_file))
|
||||||
|
return IO.NodeOutput(model_file, response.result)
|
||||||
|
|
||||||
|
|
||||||
|
class MeshyExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
MeshyTextToModelNode,
|
||||||
|
MeshyRefineNode,
|
||||||
|
MeshyImageToModelNode,
|
||||||
|
MeshyMultiImageToModelNode,
|
||||||
|
MeshyRigModelNode,
|
||||||
|
MeshyAnimateModelNode,
|
||||||
|
MeshyTextureNode,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> MeshyExtension:
|
||||||
|
return MeshyExtension()
|
||||||
@ -43,7 +43,7 @@ class UploadResponse(BaseModel):
|
|||||||
|
|
||||||
async def upload_images_to_comfyapi(
|
async def upload_images_to_comfyapi(
|
||||||
cls: type[IO.ComfyNode],
|
cls: type[IO.ComfyNode],
|
||||||
image: torch.Tensor,
|
image: torch.Tensor | list[torch.Tensor],
|
||||||
*,
|
*,
|
||||||
max_images: int = 8,
|
max_images: int = 8,
|
||||||
mime_type: str | None = None,
|
mime_type: str | None = None,
|
||||||
@ -55,15 +55,28 @@ async def upload_images_to_comfyapi(
|
|||||||
Uploads images to ComfyUI API and returns download URLs.
|
Uploads images to ComfyUI API and returns download URLs.
|
||||||
To upload multiple images, stack them in the batch dimension first.
|
To upload multiple images, stack them in the batch dimension first.
|
||||||
"""
|
"""
|
||||||
|
tensors: list[torch.Tensor] = []
|
||||||
|
if isinstance(image, list):
|
||||||
|
for img in image:
|
||||||
|
is_batch = len(img.shape) > 3
|
||||||
|
if is_batch:
|
||||||
|
tensors.extend(img[i] for i in range(img.shape[0]))
|
||||||
|
else:
|
||||||
|
tensors.append(img)
|
||||||
|
else:
|
||||||
|
is_batch = len(image.shape) > 3
|
||||||
|
if is_batch:
|
||||||
|
tensors.extend(image[i] for i in range(image.shape[0]))
|
||||||
|
else:
|
||||||
|
tensors.append(image)
|
||||||
|
|
||||||
# if batched, try to upload each file if max_images is greater than 0
|
# if batched, try to upload each file if max_images is greater than 0
|
||||||
download_urls: list[str] = []
|
download_urls: list[str] = []
|
||||||
is_batch = len(image.shape) > 3
|
num_to_upload = min(len(tensors), max_images)
|
||||||
batch_len = image.shape[0] if is_batch else 1
|
|
||||||
num_to_upload = min(batch_len, max_images)
|
|
||||||
batch_start_ts = time.monotonic()
|
batch_start_ts = time.monotonic()
|
||||||
|
|
||||||
for idx in range(num_to_upload):
|
for idx in range(num_to_upload):
|
||||||
tensor = image[idx] if is_batch else image
|
tensor = tensors[idx]
|
||||||
img_io = tensor_to_bytesio(tensor, total_pixels=total_pixels, mime_type=mime_type)
|
img_io = tensor_to_bytesio(tensor, total_pixels=total_pixels, mime_type=mime_type)
|
||||||
|
|
||||||
effective_label = wait_label
|
effective_label = wait_label
|
||||||
|
|||||||
1
nodes.py
1
nodes.py
@ -2401,6 +2401,7 @@ async def init_builtin_api_nodes():
|
|||||||
"nodes_sora.py",
|
"nodes_sora.py",
|
||||||
"nodes_topaz.py",
|
"nodes_topaz.py",
|
||||||
"nodes_tripo.py",
|
"nodes_tripo.py",
|
||||||
|
"nodes_meshy.py",
|
||||||
"nodes_moonvalley.py",
|
"nodes_moonvalley.py",
|
||||||
"nodes_rodin.py",
|
"nodes_rodin.py",
|
||||||
"nodes_gemini.py",
|
"nodes_gemini.py",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user