mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-04-20 07:22:34 +08:00
dev: PhotaLabs API nodes
Signed-off-by: bigcat88 <bigcat88@icloud.com>
This commit is contained in:
parent
55e6478526
commit
d25d14dfb6
@ -43,9 +43,55 @@ class UploadType(str, Enum):
|
|||||||
model = "file_upload"
|
model = "file_upload"
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteItemSchema:
|
||||||
|
"""Describes how to map API response objects to rich dropdown items.
|
||||||
|
|
||||||
|
All *_field parameters use dot-path notation (e.g. ``"labels.gender"``).
|
||||||
|
``label_field`` additionally supports template strings with ``{field}``
|
||||||
|
placeholders (e.g. ``"{name} ({labels.accent})"``).
|
||||||
|
"""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value_field: str,
|
||||||
|
label_field: str,
|
||||||
|
preview_url_field: str | None = None,
|
||||||
|
preview_type: Literal["image", "video", "audio"] = "image",
|
||||||
|
description_field: str | None = None,
|
||||||
|
search_fields: list[str] | None = None,
|
||||||
|
filter_field: str | None = None,
|
||||||
|
):
|
||||||
|
self.value_field = value_field
|
||||||
|
"""Dot-path to the unique identifier within each item. This value is stored in the widget and passed to execute()."""
|
||||||
|
self.label_field = label_field
|
||||||
|
"""Dot-path to the display name, or a template string with {field} placeholders."""
|
||||||
|
self.preview_url_field = preview_url_field
|
||||||
|
"""Dot-path to a preview media URL. If None, no preview is shown."""
|
||||||
|
self.preview_type = preview_type
|
||||||
|
"""How to render the preview: "image", "video", or "audio"."""
|
||||||
|
self.description_field = description_field
|
||||||
|
"""Optional dot-path or template for a subtitle line shown below the label."""
|
||||||
|
self.search_fields = search_fields
|
||||||
|
"""Dot-paths to fields included in the search index. Defaults to [label_field]."""
|
||||||
|
self.filter_field = filter_field
|
||||||
|
"""Optional dot-path to a categorical field for filter tabs."""
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return prune_dict({
|
||||||
|
"value_field": self.value_field,
|
||||||
|
"label_field": self.label_field,
|
||||||
|
"preview_url_field": self.preview_url_field,
|
||||||
|
"preview_type": self.preview_type,
|
||||||
|
"description_field": self.description_field,
|
||||||
|
"search_fields": self.search_fields,
|
||||||
|
"filter_field": self.filter_field,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class RemoteOptions:
|
class RemoteOptions:
|
||||||
def __init__(self, route: str, refresh_button: bool, control_after_refresh: Literal["first", "last"]="first",
|
def __init__(self, route: str, refresh_button: bool, control_after_refresh: Literal["first", "last"]="first",
|
||||||
timeout: int=None, max_retries: int=None, refresh: int=None):
|
timeout: int=None, max_retries: int=None, refresh: int=None,
|
||||||
|
response_key: str=None, query_params: dict[str, str]=None,
|
||||||
|
item_schema: RemoteItemSchema=None):
|
||||||
self.route = route
|
self.route = route
|
||||||
"""The route to the remote source."""
|
"""The route to the remote source."""
|
||||||
self.refresh_button = refresh_button
|
self.refresh_button = refresh_button
|
||||||
@ -58,6 +104,12 @@ class RemoteOptions:
|
|||||||
"""The maximum number of retries before aborting the request."""
|
"""The maximum number of retries before aborting the request."""
|
||||||
self.refresh = refresh
|
self.refresh = refresh
|
||||||
"""The TTL of the remote input's value in milliseconds. Specifies the interval at which the remote input's value is refreshed."""
|
"""The TTL of the remote input's value in milliseconds. Specifies the interval at which the remote input's value is refreshed."""
|
||||||
|
self.response_key = response_key
|
||||||
|
"""Dot-path to the items array in the response. If None, the entire response is used."""
|
||||||
|
self.query_params = query_params
|
||||||
|
"""Static query parameters appended to the request URL."""
|
||||||
|
self.item_schema = item_schema
|
||||||
|
"""When present, the frontend renders a rich dropdown with previews instead of a plain combo widget."""
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
return prune_dict({
|
return prune_dict({
|
||||||
@ -67,6 +119,9 @@ class RemoteOptions:
|
|||||||
"timeout": self.timeout,
|
"timeout": self.timeout,
|
||||||
"max_retries": self.max_retries,
|
"max_retries": self.max_retries,
|
||||||
"refresh": self.refresh,
|
"refresh": self.refresh,
|
||||||
|
"response_key": self.response_key,
|
||||||
|
"query_params": self.query_params,
|
||||||
|
"item_schema": self.item_schema.as_dict() if self.item_schema else None,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -2184,6 +2239,7 @@ class NodeReplace:
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"FolderType",
|
"FolderType",
|
||||||
"UploadType",
|
"UploadType",
|
||||||
|
"RemoteItemSchema",
|
||||||
"RemoteOptions",
|
"RemoteOptions",
|
||||||
"NumberDisplay",
|
"NumberDisplay",
|
||||||
"ControlAfterGenerate",
|
"ControlAfterGenerate",
|
||||||
|
|||||||
49
comfy_api_nodes/apis/phota_labs.py
Normal file
49
comfy_api_nodes/apis/phota_labs.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaGenerateRequest(BaseModel):
|
||||||
|
prompt: str = Field(...)
|
||||||
|
num_output_images: int = Field(1)
|
||||||
|
aspect_ratio: str = Field(...)
|
||||||
|
resolution: str = Field(...)
|
||||||
|
profile_ids: list[str] | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaEditRequest(BaseModel):
|
||||||
|
prompt: str = Field(...)
|
||||||
|
images: list[str] = Field(...)
|
||||||
|
num_output_images: int = Field(1)
|
||||||
|
aspect_ratio: str = Field(...)
|
||||||
|
resolution: str = Field(...)
|
||||||
|
profile_ids: list[str] | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaEnhanceRequest(BaseModel):
|
||||||
|
image: str = Field(...)
|
||||||
|
num_output_images: int = Field(1)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaKnownGeneratedSubjectCounts(BaseModel):
|
||||||
|
counts: dict[str, int] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoStudioResponse(BaseModel):
|
||||||
|
images: list[str] = Field(..., description="Base64-encoded PNG output images.")
|
||||||
|
known_subjects: PhotaKnownGeneratedSubjectCounts = Field(default_factory=PhotaKnownGeneratedSubjectCounts)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaAddProfileRequest(BaseModel):
|
||||||
|
image_urls: list[str] = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaAddProfileResponse(BaseModel):
|
||||||
|
profile_id: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaProfileStatusResponse(BaseModel):
|
||||||
|
profile_id: str = Field(...)
|
||||||
|
status: str = Field(
|
||||||
|
...,
|
||||||
|
description="Current profile status: VALIDATING, QUEUING, IN_PROGRESS, READY, ERROR, or INACTIVE.",
|
||||||
|
)
|
||||||
|
message: str | None = Field(default=None, description="Optional error or status message.")
|
||||||
@ -233,6 +233,45 @@ class ElevenLabsVoiceSelector(IO.ComfyNode):
|
|||||||
return IO.NodeOutput(voice_id)
|
return IO.NodeOutput(voice_id)
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsRichVoiceSelector(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> IO.Schema:
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ElevenLabsRichVoiceSelector",
|
||||||
|
display_name="ElevenLabs Voice Selector (Rich)",
|
||||||
|
category="api node/audio/ElevenLabs",
|
||||||
|
description="Select an ElevenLabs voice with audio preview and rich metadata.",
|
||||||
|
inputs=[
|
||||||
|
IO.Combo.Input(
|
||||||
|
"voice",
|
||||||
|
options=ELEVENLABS_VOICE_OPTIONS,
|
||||||
|
remote=IO.RemoteOptions(
|
||||||
|
route="http://localhost:9000/elevenlabs/voices",
|
||||||
|
refresh_button=True,
|
||||||
|
item_schema=IO.RemoteItemSchema(
|
||||||
|
value_field="voice_id",
|
||||||
|
label_field="name",
|
||||||
|
preview_url_field="preview_url",
|
||||||
|
preview_type="audio",
|
||||||
|
search_fields=["name", "labels.gender", "labels.accent"],
|
||||||
|
filter_field="labels.use_case",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tooltip="Choose a voice with audio preview.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Custom(ELEVENLABS_VOICE).Output(display_name="voice"),
|
||||||
|
],
|
||||||
|
is_api_node=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, voice: str) -> IO.NodeOutput:
|
||||||
|
# voice is already the voice_id from item_schema.value_field
|
||||||
|
return IO.NodeOutput(voice)
|
||||||
|
|
||||||
|
|
||||||
class ElevenLabsTextToSpeech(IO.ComfyNode):
|
class ElevenLabsTextToSpeech(IO.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def define_schema(cls) -> IO.Schema:
|
def define_schema(cls) -> IO.Schema:
|
||||||
@ -911,6 +950,7 @@ class ElevenLabsExtension(ComfyExtension):
|
|||||||
return [
|
return [
|
||||||
ElevenLabsSpeechToText,
|
ElevenLabsSpeechToText,
|
||||||
ElevenLabsVoiceSelector,
|
ElevenLabsVoiceSelector,
|
||||||
|
ElevenLabsRichVoiceSelector,
|
||||||
ElevenLabsTextToSpeech,
|
ElevenLabsTextToSpeech,
|
||||||
ElevenLabsAudioIsolation,
|
ElevenLabsAudioIsolation,
|
||||||
ElevenLabsTextToSoundEffects,
|
ElevenLabsTextToSoundEffects,
|
||||||
|
|||||||
350
comfy_api_nodes/nodes_phota_labs.py
Normal file
350
comfy_api_nodes/nodes_phota_labs.py
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from comfy_api.latest import IO, ComfyExtension, Input
|
||||||
|
from comfy_api_nodes.apis.phota_labs import (
|
||||||
|
PhotaAddProfileRequest,
|
||||||
|
PhotaAddProfileResponse,
|
||||||
|
PhotaEditRequest,
|
||||||
|
PhotaEnhanceRequest,
|
||||||
|
PhotaGenerateRequest,
|
||||||
|
PhotaProfileStatusResponse,
|
||||||
|
PhotoStudioResponse,
|
||||||
|
)
|
||||||
|
from comfy_api_nodes.util import (
|
||||||
|
ApiEndpoint,
|
||||||
|
bytesio_to_image_tensor,
|
||||||
|
poll_op,
|
||||||
|
sync_op,
|
||||||
|
upload_images_to_comfyapi,
|
||||||
|
upload_image_to_comfyapi,
|
||||||
|
validate_string,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Direct API endpoint (comment out this class to use proxy)
|
||||||
|
class ApiEndpoint(ApiEndpoint):
|
||||||
|
"""Temporary override to use direct API instead of proxy."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
path: str,
|
||||||
|
method: str = "GET",
|
||||||
|
*,
|
||||||
|
query_params: dict | None = None,
|
||||||
|
headers: dict | None = None,
|
||||||
|
):
|
||||||
|
self.path = path.replace("/proxy/phota/", "https://api.photalabs.com/")
|
||||||
|
self.method = method
|
||||||
|
self.query_params = query_params or {}
|
||||||
|
self.headers = headers or {}
|
||||||
|
if "api.photalabs.com" in self.path:
|
||||||
|
self.headers["X-API-Key"] = "YOUR_PHOTA_API_KEY"
|
||||||
|
|
||||||
|
|
||||||
|
PHOTA_LABS_PROFILE_ID = "PHOTA_LABS_PROFILE_ID"
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaLabsGenerate(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="PhotaLabsGenerate",
|
||||||
|
display_name="Phota Labs Generate",
|
||||||
|
category="api node/image/Phota Labs",
|
||||||
|
description="Generate images from a text prompt using Phota Labs.",
|
||||||
|
inputs=[
|
||||||
|
IO.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip="Text prompt describing the desired image.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"aspect_ratio",
|
||||||
|
options=["auto", "1:1", "3:4", "4:3", "9:16", "16:9"],
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"resolution",
|
||||||
|
options=["1K", "4K"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[IO.Image.Output()],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
prompt: str,
|
||||||
|
aspect_ratio: str,
|
||||||
|
resolution: str,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
validate_string(prompt, strip_whitespace=False, min_length=1)
|
||||||
|
pid_list = None # list(profile_ids.values()) if profile_ids else None
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/phota/v1/phota/generate", method="POST"),
|
||||||
|
response_model=PhotoStudioResponse,
|
||||||
|
data=PhotaGenerateRequest(
|
||||||
|
prompt=prompt,
|
||||||
|
aspect_ratio=aspect_ratio,
|
||||||
|
resolution=resolution,
|
||||||
|
profile_ids=pid_list or None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(bytesio_to_image_tensor(BytesIO(base64.b64decode(response.images[0]))))
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaLabsEdit(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="PhotaLabsEdit",
|
||||||
|
display_name="Phota Labs Edit",
|
||||||
|
category="api node/image/Phota Labs",
|
||||||
|
description="Edit images based on a text prompt using Phota Labs. "
|
||||||
|
"Provide input images and a prompt describing the desired edit.",
|
||||||
|
inputs=[
|
||||||
|
IO.Autogrow.Input(
|
||||||
|
"images",
|
||||||
|
template=IO.Autogrow.TemplatePrefix(
|
||||||
|
IO.Image.Input("image"),
|
||||||
|
prefix="image",
|
||||||
|
min=1,
|
||||||
|
max=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"aspect_ratio",
|
||||||
|
options=["auto", "1:1", "3:4", "4:3", "9:16", "16:9"],
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"resolution",
|
||||||
|
options=["1K", "4K"],
|
||||||
|
),
|
||||||
|
IO.Autogrow.Input(
|
||||||
|
"profile_ids",
|
||||||
|
template=IO.Autogrow.TemplatePrefix(
|
||||||
|
IO.Custom(PHOTA_LABS_PROFILE_ID).Input("profile_id"),
|
||||||
|
prefix="profile_id",
|
||||||
|
min=0,
|
||||||
|
max=5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[IO.Image.Output()],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
images: IO.Autogrow.Type,
|
||||||
|
prompt: str,
|
||||||
|
aspect_ratio: str,
|
||||||
|
resolution: str,
|
||||||
|
profile_ids: IO.Autogrow.Type = None,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
validate_string(prompt, strip_whitespace=False, min_length=1)
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/phota/v1/phota/edit", method="POST"),
|
||||||
|
response_model=PhotoStudioResponse,
|
||||||
|
data=PhotaEditRequest(
|
||||||
|
prompt=prompt,
|
||||||
|
images=await upload_images_to_comfyapi(cls, list(images.values()), max_images=10),
|
||||||
|
aspect_ratio=aspect_ratio,
|
||||||
|
resolution=resolution,
|
||||||
|
profile_ids=list(profile_ids.values()) if profile_ids else None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(bytesio_to_image_tensor(BytesIO(base64.b64decode(response.images[0]))))
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaLabsEnhance(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="PhotaLabsEnhance",
|
||||||
|
display_name="Phota Labs Enhance",
|
||||||
|
category="api node/image/Phota Labs",
|
||||||
|
description="Automatically enhance a photo using Phota Labs. "
|
||||||
|
"No text prompt is required — enhancement parameters are inferred automatically.",
|
||||||
|
inputs=[
|
||||||
|
IO.Image.Input(
|
||||||
|
"image",
|
||||||
|
tooltip="Input image to enhance.",
|
||||||
|
),
|
||||||
|
IO.Autogrow.Input(
|
||||||
|
"profile_ids",
|
||||||
|
template=IO.Autogrow.TemplatePrefix(
|
||||||
|
IO.Custom(PHOTA_LABS_PROFILE_ID).Input("profile_id"),
|
||||||
|
prefix="profile_id",
|
||||||
|
min=0,
|
||||||
|
max=5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[IO.Image.Output()],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
image: Input.Image,
|
||||||
|
profile_ids: IO.Autogrow.Type = None,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/phota/v1/phota/enhance", method="POST"),
|
||||||
|
response_model=PhotoStudioResponse,
|
||||||
|
data=PhotaEnhanceRequest(
|
||||||
|
image=await upload_image_to_comfyapi(cls, image),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(bytesio_to_image_tensor(BytesIO(base64.b64decode(response.images[0]))))
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaLabsSelectProfile(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="PhotaLabsSelectProfile",
|
||||||
|
display_name="Phota Labs Select Profile",
|
||||||
|
category="api node/image/Phota Labs",
|
||||||
|
description="Select a trained Phota Labs profile for use in generation.",
|
||||||
|
inputs=[
|
||||||
|
IO.Combo.Input(
|
||||||
|
"profile_id",
|
||||||
|
options=[],
|
||||||
|
remote=IO.RemoteOptions(
|
||||||
|
route="http://localhost:9000/phota/profiles",
|
||||||
|
refresh_button=True,
|
||||||
|
item_schema=IO.RemoteItemSchema(
|
||||||
|
value_field="profile_id",
|
||||||
|
label_field="profile_id",
|
||||||
|
preview_url_field="preview_url",
|
||||||
|
preview_type="image",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[IO.Custom(PHOTA_LABS_PROFILE_ID).Output(display_name="profile_id")],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(cls, profile_id: str) -> IO.NodeOutput:
|
||||||
|
return IO.NodeOutput(profile_id)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaLabsAddProfile(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="PhotaLabsAddProfile",
|
||||||
|
display_name="Phota Labs Add Profile",
|
||||||
|
category="api node/image/Phota Labs",
|
||||||
|
description="Create a training profile from 30-50 reference images using Phota Labs. "
|
||||||
|
"Uploads images and starts asynchronous training, returning the profile ID once training is queued.",
|
||||||
|
inputs=[
|
||||||
|
IO.Autogrow.Input(
|
||||||
|
"images",
|
||||||
|
template=IO.Autogrow.TemplatePrefix(
|
||||||
|
IO.Image.Input("image"),
|
||||||
|
prefix="image",
|
||||||
|
min=30,
|
||||||
|
max=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Custom(PHOTA_LABS_PROFILE_ID).Output(display_name="profile_id"),
|
||||||
|
IO.String.Output(display_name="status"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
images: IO.Autogrow.Type,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
image_urls = await upload_images_to_comfyapi(
|
||||||
|
cls,
|
||||||
|
list(images.values()),
|
||||||
|
max_images=50,
|
||||||
|
wait_label="Uploading training images",
|
||||||
|
)
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/phota/v1/phota/profiles/add", method="POST"),
|
||||||
|
response_model=PhotaAddProfileResponse,
|
||||||
|
data=PhotaAddProfileRequest(image_urls=image_urls),
|
||||||
|
)
|
||||||
|
# Poll until validation passes and training is queued/in-progress/ready
|
||||||
|
status_response = await poll_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(
|
||||||
|
path=f"/proxy/phota/v1/phota/profiles/{response.profile_id}/status"
|
||||||
|
),
|
||||||
|
response_model=PhotaProfileStatusResponse,
|
||||||
|
status_extractor=lambda r: r.status,
|
||||||
|
completed_statuses=["QUEUING", "IN_PROGRESS", "READY"],
|
||||||
|
failed_statuses=["ERROR", "INACTIVE"],
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(response.profile_id, status_response.status)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotaLabsExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
PhotaLabsGenerate,
|
||||||
|
PhotaLabsEdit,
|
||||||
|
PhotaLabsEnhance,
|
||||||
|
PhotaLabsSelectProfile,
|
||||||
|
PhotaLabsAddProfile,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> PhotaLabsExtension:
|
||||||
|
return PhotaLabsExtension()
|
||||||
@ -991,6 +991,10 @@ async def validate_inputs(prompt_id, prompt, item, validated):
|
|||||||
|
|
||||||
if isinstance(input_type, list) or input_type == io.Combo.io_type:
|
if isinstance(input_type, list) or input_type == io.Combo.io_type:
|
||||||
if input_type == io.Combo.io_type:
|
if input_type == io.Combo.io_type:
|
||||||
|
# Skip validation for combos with remote options — options
|
||||||
|
# are fetched client-side and not available on the server.
|
||||||
|
if extra_info.get("remote"):
|
||||||
|
continue
|
||||||
combo_options = extra_info.get("options", [])
|
combo_options = extra_info.get("options", [])
|
||||||
else:
|
else:
|
||||||
combo_options = input_type
|
combo_options = input_type
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user