mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-09-02 05:47:06 +08:00
add basic meshy node
This commit is contained in:
parent
0aa1c58b04
commit
f58db0e325
87
comfy_api_nodes/apis/meshy_api.py
Normal file
87
comfy_api_nodes/apis/meshy_api.py
Normal file
@ -0,0 +1,87 @@
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class MeshyAIModel(str, Enum):
|
||||
meshy_4 = "meshy-4"
|
||||
meshy_5 = "meshy-5"
|
||||
|
||||
|
||||
class MeshyTopology(str, Enum):
|
||||
quad = "quad"
|
||||
triangle = "triangle"
|
||||
|
||||
|
||||
class MeshySymmetryMode(str, Enum):
|
||||
off = "off"
|
||||
auto = "auto"
|
||||
on = "on"
|
||||
|
||||
|
||||
class MeshyArtStyle(str, Enum):
|
||||
realistic = "realistic"
|
||||
sculpture = "sculpture"
|
||||
|
||||
|
||||
class MeshyTaskStatus(str, Enum):
|
||||
pending = "PENDING"
|
||||
in_progress = "IN_PROGRESS"
|
||||
completed = "SUCCEEDED"
|
||||
failed = "FAILED"
|
||||
canceled = "CANCELED"
|
||||
|
||||
|
||||
class MeshyTask(BaseModel):
|
||||
id: str = Field(..., description="The task ID")
|
||||
status: MeshyTaskStatus = Field(..., description="The status of the task")
|
||||
progress: int = Field(..., description="The progress of the task")
|
||||
message: str = Field(..., description="The message of the task")
|
||||
|
||||
|
||||
class MeshyTaskResponse(BaseModel):
|
||||
code: int = Field(..., description="The response code")
|
||||
data: MeshyTask = Field(..., description="The task data")
|
||||
|
||||
|
||||
class MeshyTextToModelPreviewRequest(BaseModel):
|
||||
prompt: str = Field(
|
||||
...,
|
||||
description="The text prompt describing the model to generate",
|
||||
max_length=1024,
|
||||
)
|
||||
mode: str = "preview"
|
||||
art_style: Optional[MeshyArtStyle] = None
|
||||
seed: Optional[int] = None
|
||||
ai_model: Optional[MeshyAIModel] = None
|
||||
topology: Optional[MeshyTopology] = None
|
||||
target_polycount: Optional[int] = None
|
||||
should_remesh: Optional[bool] = None
|
||||
symmetry_mode: Optional[MeshySymmetryMode] = None
|
||||
should_simplify: Optional[bool] = None
|
||||
|
||||
|
||||
class MeshyTaskResponse(BaseModel):
|
||||
model_file: str = Field(..., description="The model file")
|
||||
model_task_id: str = Field(..., description="The model task ID")
|
||||
|
||||
|
||||
class MeshyTextToModelRefineRequest(BaseModel):
|
||||
mode: str = "refine"
|
||||
preview_task_id: str = Field(..., description="The preview task ID")
|
||||
enable_pbr: bool = Field(..., description="Whether to enable PBR")
|
||||
texture_prompt: str = Field(..., description="The texture prompt")
|
||||
ai_model: Optional[MeshyAIModel] = None
|
||||
|
||||
|
||||
class MeshyImageToModelRequest(BaseModel):
|
||||
image_url: str = Field(..., description="The image URL")
|
||||
ai_model: Optional[MeshyAIModel] = None
|
||||
topology: Optional[MeshyTopology] = None
|
||||
target_polycount: Optional[int] = None
|
||||
symmetry_mode: Optional[MeshySymmetryMode] = None
|
||||
should_remesh: Optional[bool] = None
|
||||
should_texture: Optional[bool] = None
|
||||
enable_pbr: Optional[bool] = None
|
||||
texture_prompt: Optional[str] = None
|
||||
moderation: Optional[bool] = None
|
||||
363
comfy_api_nodes/nodes_meshy.py
Normal file
363
comfy_api_nodes/nodes_meshy.py
Normal file
@ -0,0 +1,363 @@
|
||||
import os
|
||||
from folder_paths import get_output_directory
|
||||
from comfy_api_nodes.mapper_utils import model_field_to_node_input
|
||||
from comfy.comfy_types.node_typing import IO
|
||||
from comfy_api_nodes.apis.meshy_api import (
|
||||
MeshyArtStyle,
|
||||
MeshyAIModel,
|
||||
MeshySymmetryMode,
|
||||
MeshyTopology,
|
||||
MeshyTextToModelPreviewRequest,
|
||||
MeshyTaskResponse,
|
||||
MeshyTaskResponse,
|
||||
MeshyTaskStatus,
|
||||
MeshyTextToModelRefineRequest,
|
||||
MeshyImageToModelRequest,
|
||||
)
|
||||
|
||||
from comfy_api_nodes.apis.client import (
|
||||
ApiEndpoint,
|
||||
HttpMethod,
|
||||
SynchronousOperation,
|
||||
PollingOperation,
|
||||
EmptyRequest,
|
||||
)
|
||||
from comfy_api_nodes.apinode_utils import (
|
||||
upload_images_to_comfyapi,
|
||||
download_url_to_bytesio,
|
||||
)
|
||||
|
||||
|
||||
def get_model_url_from_response(response: MeshyTaskResponse) -> str:
|
||||
if response.data is not None:
|
||||
return response.data.model_urls.glb
|
||||
raise RuntimeError(f"Failed to get model url from response: {response}")
|
||||
|
||||
|
||||
def poll_until_finished(
|
||||
kwargs: dict[str, str],
|
||||
response: MeshyTaskResponse,
|
||||
) -> tuple[str, str]:
|
||||
"""Polls the Meshy API endpoint until the task reaches a terminal state, then returns the response."""
|
||||
if response.code != 0:
|
||||
raise RuntimeError(f"Failed to generate mesh: {response.error}")
|
||||
task_id = response.data.task_id
|
||||
response_poll = PollingOperation(
|
||||
poll_endpoint=ApiEndpoint(
|
||||
path=f"/proxy/meshy/openapi/v2/task/{task_id}",
|
||||
method=HttpMethod.GET,
|
||||
request_model=EmptyRequest,
|
||||
response_model=MeshyTaskResponse,
|
||||
),
|
||||
completed_statuses=[MeshyTaskStatus.completed],
|
||||
failed_statuses=[
|
||||
MeshyTaskStatus.failed,
|
||||
MeshyTaskStatus.canceled,
|
||||
],
|
||||
status_extractor=lambda x: x.data.status,
|
||||
auth_kwargs=kwargs,
|
||||
node_id=kwargs["unique_id"],
|
||||
result_url_extractor=get_model_url_from_response,
|
||||
progress_extractor=lambda x: x.data.progress,
|
||||
).execute()
|
||||
if response_poll.data.status == MeshyTaskStatus.completed:
|
||||
url = get_model_url_from_response(response_poll)
|
||||
bytesio = download_url_to_bytesio(url)
|
||||
# Save the downloaded model file
|
||||
model_file = f"meshy_model_{task_id}.glb"
|
||||
with open(os.path.join(get_output_directory(), model_file), "wb") as f:
|
||||
f.write(bytesio.getvalue())
|
||||
return model_file, task_id
|
||||
raise RuntimeError(f"Failed to generate mesh: {response_poll}")
|
||||
|
||||
|
||||
class MeshyTextToModelPreviewNode:
|
||||
"""
|
||||
Generates 3D models preview based on a text prompt using Meshy's API.
|
||||
"""
|
||||
|
||||
AVERAGE_DURATION = 80
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"multiline": True}),
|
||||
},
|
||||
"optional": {
|
||||
"art_style": model_field_to_node_input(
|
||||
IO.COMBO,
|
||||
MeshyTextToModelPreviewRequest,
|
||||
"art_style",
|
||||
enum_type=MeshyArtStyle,
|
||||
default="realistic",
|
||||
),
|
||||
"seed": ("INT", {"default": 42}),
|
||||
"ai_model": model_field_to_node_input(
|
||||
IO.COMBO,
|
||||
MeshyTextToModelPreviewRequest,
|
||||
"ai_model",
|
||||
enum_type=MeshyAIModel,
|
||||
),
|
||||
"topology": model_field_to_node_input(
|
||||
IO.COMBO,
|
||||
MeshyTextToModelPreviewRequest,
|
||||
"topology",
|
||||
enum_type=MeshyTopology,
|
||||
),
|
||||
"target_polycount": (
|
||||
"INT",
|
||||
{"min": 100, "max": 300000, "default": 30000},
|
||||
),
|
||||
"should_remesh": ("BOOLEAN", {"default": True}),
|
||||
"symmetry_mode": model_field_to_node_input(
|
||||
IO.COMBO,
|
||||
MeshyTextToModelPreviewRequest,
|
||||
"symmetry_mode",
|
||||
enum_type=MeshySymmetryMode,
|
||||
),
|
||||
"should_simplify": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"hidden": {
|
||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||
"unique_id": "UNIQUE_ID",
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (
|
||||
"STRING",
|
||||
"MODEL_TASK_ID",
|
||||
)
|
||||
RETURN_NAMES = ("model_file", "model task_id")
|
||||
FUNCTION = "generate_mesh"
|
||||
CATEGORY = "api node/3d/Meshy"
|
||||
API_NODE = True
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def generate_mesh(
|
||||
self,
|
||||
prompt,
|
||||
art_style=None,
|
||||
seed=None,
|
||||
ai_model=None,
|
||||
topology=None,
|
||||
target_polycount=None,
|
||||
should_remesh=None,
|
||||
symmetry_mode=None,
|
||||
should_simplify=None,
|
||||
**kwargs,
|
||||
):
|
||||
if not prompt:
|
||||
raise RuntimeError("Prompt is required")
|
||||
response = SynchronousOperation(
|
||||
endpoint=ApiEndpoint(
|
||||
path="/proxy/meshy/openapi/v2/text-to-3d",
|
||||
method=HttpMethod.POST,
|
||||
request_model=MeshyTextToModelPreviewRequest,
|
||||
response_model=MeshyTaskResponse,
|
||||
),
|
||||
request=MeshyTextToModelPreviewRequest(
|
||||
mode="preview",
|
||||
prompt=prompt,
|
||||
art_style=art_style,
|
||||
seed=seed,
|
||||
ai_model=ai_model,
|
||||
topology=topology,
|
||||
target_polycount=target_polycount,
|
||||
should_remesh=should_remesh,
|
||||
symmetry_mode=symmetry_mode,
|
||||
should_simplify=should_simplify,
|
||||
),
|
||||
auth_kwargs=kwargs.get("auth_kwargs", {}),
|
||||
).execute()
|
||||
return poll_until_finished(kwargs, response)
|
||||
|
||||
|
||||
class MeshyTextToModelRefineNode:
|
||||
"""
|
||||
Refines a 3D model based on a text prompt using Meshy's API.
|
||||
"""
|
||||
|
||||
AVERAGE_DURATION = 80
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"preview_task_id": ("MODEL_TASK_ID",),
|
||||
},
|
||||
"optional": {
|
||||
"enable_pbr": ("BOOLEAN", {"default": True}),
|
||||
"texture_prompt": ("STRING", {"multiline": True}),
|
||||
"ai_model": model_field_to_node_input(
|
||||
IO.COMBO,
|
||||
MeshyTextToModelRefineRequest,
|
||||
"ai_model",
|
||||
enum_type=MeshyAIModel,
|
||||
),
|
||||
"moderation": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"hidden": {
|
||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||
"unique_id": "UNIQUE_ID",
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (
|
||||
"STRING",
|
||||
"STRING",
|
||||
)
|
||||
RETURN_NAMES = ("model_file", "preview task_id")
|
||||
FUNCTION = "generate_mesh"
|
||||
CATEGORY = "api node/3d/Meshy"
|
||||
API_NODE = True
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def generate_mesh(
|
||||
self,
|
||||
preview_task_id,
|
||||
enable_pbr=None,
|
||||
texture_prompt=None,
|
||||
ai_model=None,
|
||||
moderation=None,
|
||||
**kwargs,
|
||||
):
|
||||
if not preview_task_id:
|
||||
raise RuntimeError("Preview task ID is required")
|
||||
|
||||
response = SynchronousOperation(
|
||||
endpoint=ApiEndpoint(
|
||||
path="/proxy/meshy/openapi/v2/text-to-3d",
|
||||
method=HttpMethod.POST,
|
||||
request_model=MeshyTextToModelRefineRequest,
|
||||
response_model=MeshyTaskResponse,
|
||||
),
|
||||
request=MeshyTextToModelRefineRequest(
|
||||
mode="refine",
|
||||
preview_task_id=preview_task_id,
|
||||
enable_pbr=enable_pbr,
|
||||
texture_prompt=texture_prompt,
|
||||
ai_model=ai_model,
|
||||
moderation=moderation,
|
||||
),
|
||||
auth_kwargs=kwargs,
|
||||
).execute()
|
||||
return poll_until_finished(kwargs, response)
|
||||
|
||||
|
||||
class MeshyImageToModelNode:
|
||||
"""
|
||||
Generates 3D models synchronously based on a single image using Meshy's API.
|
||||
"""
|
||||
|
||||
AVERAGE_DURATION = 80
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
},
|
||||
"optional": {
|
||||
"ai_model": model_field_to_node_input(
|
||||
IO.COMBO,
|
||||
MeshyImageToModelRequest,
|
||||
"ai_model",
|
||||
enum_type=MeshyAIModel,
|
||||
),
|
||||
"topology": model_field_to_node_input(
|
||||
IO.COMBO,
|
||||
MeshyImageToModelRequest,
|
||||
"topology",
|
||||
enum_type=MeshyTopology,
|
||||
),
|
||||
"target_polycount": (
|
||||
"INT",
|
||||
{"min": 100, "max": 300000, "default": 30000},
|
||||
),
|
||||
"should_remesh": ("BOOLEAN", {"default": True}),
|
||||
"symmetry_mode": model_field_to_node_input(
|
||||
IO.COMBO,
|
||||
MeshyImageToModelRequest,
|
||||
"symmetry_mode",
|
||||
enum_type=MeshySymmetryMode,
|
||||
),
|
||||
"should_remesh": ("BOOLEAN", {"default": True}),
|
||||
"should_texture": ("BOOLEAN", {"default": True}),
|
||||
"enable_pbr": ("BOOLEAN", {"default": True}),
|
||||
"texture_prompt": ("STRING", {"multiline": True}),
|
||||
"moderation": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"hidden": {
|
||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||
"unique_id": "UNIQUE_ID",
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (
|
||||
"STRING",
|
||||
"STRING",
|
||||
)
|
||||
RETURN_NAMES = ("model_file", "model task_id")
|
||||
FUNCTION = "generate_mesh"
|
||||
CATEGORY = "api node/3d/Meshy"
|
||||
API_NODE = True
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def generate_mesh(
|
||||
self,
|
||||
image,
|
||||
ai_model=None,
|
||||
topology=None,
|
||||
target_polycount=None,
|
||||
should_remesh=None,
|
||||
symmetry_mode=None,
|
||||
should_texture=None,
|
||||
enable_pbr=None,
|
||||
texture_prompt=None,
|
||||
moderation=None,
|
||||
**kwargs,
|
||||
):
|
||||
if not image:
|
||||
raise RuntimeError("Image is required")
|
||||
response = SynchronousOperation(
|
||||
endpoint=ApiEndpoint(
|
||||
path="/proxy/meshy/openapi/v2/image-to-3d",
|
||||
method=HttpMethod.POST,
|
||||
request_model=MeshyImageToModelRequest,
|
||||
response_model=MeshyTaskResponse,
|
||||
),
|
||||
request=MeshyImageToModelRequest(
|
||||
mode="image",
|
||||
image_url=image,
|
||||
ai_model=ai_model,
|
||||
topology=topology,
|
||||
target_polycount=target_polycount,
|
||||
should_remesh=should_remesh,
|
||||
symmetry_mode=symmetry_mode,
|
||||
should_texture=should_texture,
|
||||
enable_pbr=enable_pbr,
|
||||
texture_prompt=texture_prompt,
|
||||
moderation=moderation,
|
||||
),
|
||||
auth_kwargs=kwargs,
|
||||
).execute()
|
||||
return poll_until_finished(kwargs, response)
|
||||
|
||||
|
||||
# A dictionary that contains all nodes you want to export with their names
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"MeshyTextToModelPreviewNode": MeshyTextToModelPreviewNode,
|
||||
"MeshyTextToModelRefineNode": MeshyTextToModelRefineNode,
|
||||
"MeshyImageToModelNode": MeshyImageToModelNode,
|
||||
}
|
||||
|
||||
# A dictionary that contains the friendly/humanly readable titles for the nodes
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"MeshyTextToModelPreviewNode": "Meshy: Text to Model (Preview)",
|
||||
"MeshyTextToModelRefineNode": "Meshy: Text to Model (Refine)",
|
||||
"MeshyImageToModelNode": "Meshy: Image to Model",
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user