mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-28 11:53:04 +08:00
Compare commits
4 Commits
v0.22.3-rc
...
v0.22.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c47f4db5b | |||
| 0cc5934b5b | |||
| 5f11972e5b | |||
| 718e03617c |
@ -158,8 +158,9 @@ class SeedanceCreateAssetResponse(BaseModel):
|
||||
|
||||
|
||||
class SeedanceVirtualLibraryCreateAssetRequest(BaseModel):
|
||||
url: str = Field(..., description="Publicly accessible URL of the image asset to upload.")
|
||||
url: str = Field(..., description="Publicly accessible URL of the asset to upload.")
|
||||
hash: str = Field(..., description="Dedup key. Re-submitting the same hash returns the existing asset id.")
|
||||
asset_type: str | None = Field(None, description="BytePlus asset type. Defaults to Image server-side when omitted.")
|
||||
|
||||
|
||||
# Dollars per 1K tokens, keyed by (model_id, has_video_input).
|
||||
|
||||
46
comfy_api_nodes/apis/krea.py
Normal file
46
comfy_api_nodes/apis/krea.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Pydantic models for the Krea image-generation API."""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class KreaMoodboard(BaseModel):
|
||||
id: str = Field(...)
|
||||
strength: float = Field(default=0.35, ge=-0.5, le=1.5)
|
||||
|
||||
|
||||
class KreaImageStyleReference(BaseModel):
|
||||
strength: float = Field(..., ge=-2.0, le=2.0)
|
||||
url: str | None = Field(default=None)
|
||||
|
||||
|
||||
class KreaGenerateImageRequest(BaseModel):
|
||||
prompt: str = Field(...)
|
||||
aspect_ratio: str = Field(...)
|
||||
resolution: str = Field(...)
|
||||
seed: int | None = Field(default=None)
|
||||
creativity: str = Field(default="medium")
|
||||
moodboards: list[KreaMoodboard] | None = Field(default=None)
|
||||
image_style_references: list[KreaImageStyleReference] | None = Field(default=None)
|
||||
|
||||
|
||||
class KreaJobResult(BaseModel):
|
||||
urls: list[str] | None = Field(default=None)
|
||||
style_id: str | None = Field(default=None)
|
||||
|
||||
|
||||
class KreaJob(BaseModel):
|
||||
job_id: str = Field(...)
|
||||
status: str = Field(...)
|
||||
created_at: str = Field(...)
|
||||
completed_at: str | None = Field(default=None)
|
||||
result: KreaJobResult | None = Field(default=None)
|
||||
|
||||
|
||||
class KreaAssetResponse(BaseModel):
|
||||
id: str = Field(...)
|
||||
image_url: str = Field(...)
|
||||
uploaded_at: str = Field(...)
|
||||
width: float | None = Field(default=None)
|
||||
height: float | None = Field(default=None)
|
||||
size_bytes: float | None = Field(default=None)
|
||||
mime_type: str | None = Field(default=None)
|
||||
@ -2,11 +2,12 @@ import hashlib
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
import torch
|
||||
from typing_extensions import override
|
||||
|
||||
from comfy_api.latest import IO, ComfyExtension, Input
|
||||
from comfy_api.latest import IO, ComfyExtension, Input, Types
|
||||
from comfy_api_nodes.apis.bytedance import (
|
||||
RECOMMENDED_PRESETS,
|
||||
RECOMMENDED_PRESETS_SEEDREAM_4,
|
||||
@ -308,6 +309,26 @@ async def _seedance_virtual_library_upload_image_asset(
|
||||
return f"asset://{create_resp.asset_id}"
|
||||
|
||||
|
||||
async def _seedance_virtual_library_upload_video_asset(
|
||||
cls: type[IO.ComfyNode],
|
||||
video: Input.Video,
|
||||
*,
|
||||
wait_label: str = "Uploading video",
|
||||
) -> str:
|
||||
buf = BytesIO()
|
||||
video.save_to(buf, format=Types.VideoContainer.MP4, codec=Types.VideoCodec.H264)
|
||||
video_hash = hashlib.sha256(buf.getbuffer()).hexdigest()
|
||||
public_url = await upload_video_to_comfyapi(cls, video, wait_label=wait_label)
|
||||
create_resp = await sync_op(
|
||||
cls,
|
||||
ApiEndpoint(path="/proxy/seedance/virtual-library/assets", method="POST"),
|
||||
response_model=SeedanceCreateAssetResponse,
|
||||
data=SeedanceVirtualLibraryCreateAssetRequest(url=public_url, hash=video_hash, asset_type="Video"),
|
||||
)
|
||||
await _wait_for_asset_active(cls, create_resp.asset_id, group_id="virtual-library")
|
||||
return f"asset://{create_resp.asset_id}"
|
||||
|
||||
|
||||
def _seedance2_price_extractor(model_id: str, has_video_input: bool):
|
||||
"""Returns a price_extractor closure for Seedance 2.0 poll_op."""
|
||||
rate = SEEDANCE2_PRICE_PER_1K_TOKENS.get((model_id, has_video_input))
|
||||
@ -2106,7 +2127,7 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
|
||||
content.append(
|
||||
TaskVideoContent(
|
||||
video_url=TaskVideoContentUrl(
|
||||
url=await upload_video_to_comfyapi(
|
||||
url=await _seedance_virtual_library_upload_video_asset(
|
||||
cls,
|
||||
reference_videos[key],
|
||||
wait_label=f"Uploading video {i}",
|
||||
|
||||
290
comfy_api_nodes/nodes_krea.py
Normal file
290
comfy_api_nodes/nodes_krea.py
Normal file
@ -0,0 +1,290 @@
|
||||
"""Krea image-generation nodes."""
|
||||
|
||||
import re
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from comfy_api.latest import IO, ComfyExtension, Input
|
||||
from comfy_api_nodes.apis.krea import (
|
||||
KreaAssetResponse,
|
||||
KreaGenerateImageRequest,
|
||||
KreaImageStyleReference,
|
||||
KreaJob,
|
||||
KreaMoodboard,
|
||||
)
|
||||
from comfy_api_nodes.util import (
|
||||
ApiEndpoint,
|
||||
download_url_to_image_tensor,
|
||||
poll_op,
|
||||
sync_op,
|
||||
tensor_to_bytesio,
|
||||
validate_string,
|
||||
)
|
||||
|
||||
|
||||
class KreaIO:
|
||||
STYLE_REF = "KREA_STYLE_REF"
|
||||
|
||||
|
||||
async def _upload_image_to_krea_assets(cls: type[IO.ComfyNode], image: Input.Image) -> str:
|
||||
"""Upload an image to Krea's /assets endpoint and return the Krea-hosted image URL."""
|
||||
img_io = tensor_to_bytesio(image, total_pixels=2048 * 2048, mime_type="image/png")
|
||||
response = await sync_op(
|
||||
cls,
|
||||
endpoint=ApiEndpoint(path="/proxy/krea/assets", method="POST"),
|
||||
response_model=KreaAssetResponse,
|
||||
files=[("file", (img_io.name, img_io, "image/png"))],
|
||||
content_type="multipart/form-data",
|
||||
max_retries=1,
|
||||
wait_label="Uploading reference",
|
||||
)
|
||||
return response.image_url
|
||||
|
||||
|
||||
_MODEL_MEDIUM = "Krea 2 Medium"
|
||||
_MODEL_LARGE = "Krea 2 Large"
|
||||
_MODEL_ENDPOINTS: dict[str, str] = {
|
||||
_MODEL_MEDIUM: "/proxy/krea/generate/image/krea/krea-2/medium",
|
||||
_MODEL_LARGE: "/proxy/krea/generate/image/krea/krea-2/large",
|
||||
}
|
||||
|
||||
_ASPECT_RATIOS = ["1:1", "4:3", "3:2", "16:9", "2.35:1", "4:5", "2:3", "9:16"]
|
||||
_RESOLUTIONS = ["1K"]
|
||||
_CREATIVITY_LEVELS = ["raw", "low", "medium", "high"]
|
||||
_KREA_QUEUED_STATUSES = ["backlogged", "queued", "scheduled"]
|
||||
|
||||
_UUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||
|
||||
|
||||
def _krea_model_inputs() -> list:
|
||||
"""Nested inputs shared by both Krea 2 Medium and Large under the DynamicCombo."""
|
||||
return [
|
||||
IO.Combo.Input(
|
||||
"aspect_ratio",
|
||||
options=_ASPECT_RATIOS,
|
||||
tooltip="Output aspect ratio.",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"resolution",
|
||||
options=_RESOLUTIONS,
|
||||
tooltip="Resolution scale.",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"creativity",
|
||||
options=_CREATIVITY_LEVELS,
|
||||
default="medium",
|
||||
tooltip="Prompt interpretation strength: raw stays closest to the prompt; high is most creative.",
|
||||
),
|
||||
IO.String.Input(
|
||||
"moodboard_id",
|
||||
default="",
|
||||
tooltip="Optional Krea moodboard UUID (e.g. from the Krea website). "
|
||||
"Leave empty to disable. Only one moodboard is supported per request.",
|
||||
optional=True,
|
||||
),
|
||||
IO.Float.Input(
|
||||
"moodboard_strength",
|
||||
default=0.35,
|
||||
min=-0.5,
|
||||
max=1.5,
|
||||
step=0.05,
|
||||
tooltip="Moodboard influence; ignored when moodboard_id is empty.",
|
||||
optional=True,
|
||||
),
|
||||
IO.Custom(KreaIO.STYLE_REF).Input(
|
||||
"style_reference",
|
||||
optional=True,
|
||||
tooltip="Optional chain of style references (max 10) from Krea 2 Style Reference nodes.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Krea2ImageNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="Krea2ImageNode",
|
||||
display_name="Krea 2 Image",
|
||||
category="api node/image/Krea",
|
||||
description=(
|
||||
"Generate images via Krea 2 — pick Medium (expressive illustrations) or "
|
||||
"Large (expressive photorealism). Supports an optional moodboard and up "
|
||||
"to 10 chained image style references."
|
||||
),
|
||||
inputs=[
|
||||
IO.String.Input(
|
||||
"prompt",
|
||||
multiline=True,
|
||||
default="",
|
||||
tooltip="Text prompt for the image.",
|
||||
),
|
||||
IO.DynamicCombo.Input(
|
||||
"model",
|
||||
options=[
|
||||
IO.DynamicCombo.Option(_MODEL_MEDIUM, _krea_model_inputs()),
|
||||
IO.DynamicCombo.Option(_MODEL_LARGE, _krea_model_inputs()),
|
||||
],
|
||||
tooltip="Krea 2 Medium is best for expressive illustrations; "
|
||||
"Krea 2 Large is best for expressive photorealism.",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"seed",
|
||||
default=0,
|
||||
min=0,
|
||||
max=2147483647,
|
||||
control_after_generate=True,
|
||||
tooltip="Random seed for reproducibility.",
|
||||
),
|
||||
],
|
||||
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,
|
||||
price_badge=IO.PriceBadge(
|
||||
depends_on=IO.PriceBadgeDepends(
|
||||
widgets=["model", "model.moodboard_id"],
|
||||
inputs=["model.style_reference"],
|
||||
),
|
||||
expr="""
|
||||
(
|
||||
$isLarge := widgets.model = "krea 2 large";
|
||||
$hasMoodboard := $length($lookup(widgets, "model.moodboard_id")) > 0;
|
||||
$hasStyle := $lookup(inputs, "model.style_reference").connected;
|
||||
$usd := $hasMoodboard
|
||||
? ($isLarge ? 0.07 : 0.04)
|
||||
: ($hasStyle
|
||||
? ($isLarge ? 0.065 : 0.035)
|
||||
: ($isLarge ? 0.06 : 0.03));
|
||||
{"type":"usd","usd": $usd}
|
||||
)
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
prompt: str,
|
||||
model: dict,
|
||||
seed: int,
|
||||
) -> IO.NodeOutput:
|
||||
validate_string(prompt, strip_whitespace=False, min_length=1)
|
||||
|
||||
model_choice = model["model"]
|
||||
endpoint_path = _MODEL_ENDPOINTS.get(model_choice)
|
||||
if endpoint_path is None:
|
||||
raise ValueError(f"Unknown Krea 2 model: {model_choice!r}")
|
||||
|
||||
moodboards: list[KreaMoodboard] | None = None
|
||||
mb_id = (model.get("moodboard_id") or "").strip()
|
||||
if mb_id:
|
||||
if not _UUID_RE.match(mb_id):
|
||||
raise ValueError(f"moodboard_id must be a UUID (received {mb_id!r}); copy it from the Krea website.")
|
||||
mb_strength = model.get("moodboard_strength")
|
||||
moodboards = [KreaMoodboard(id=mb_id, strength=0.35 if mb_strength is None else float(mb_strength))]
|
||||
|
||||
style_reference = model.get("style_reference")
|
||||
image_style_references: list[KreaImageStyleReference] | None = None
|
||||
if style_reference:
|
||||
if len(style_reference) > 10:
|
||||
raise ValueError(f"Krea 2 accepts at most 10 image_style_references; received {len(style_reference)}.")
|
||||
image_style_references = [
|
||||
KreaImageStyleReference(url=ref["url"], strength=float(ref["strength"])) for ref in style_reference
|
||||
]
|
||||
initial = await sync_op(
|
||||
cls,
|
||||
ApiEndpoint(path=endpoint_path, method="POST"),
|
||||
response_model=KreaJob,
|
||||
data=KreaGenerateImageRequest(
|
||||
prompt=prompt,
|
||||
aspect_ratio=model["aspect_ratio"],
|
||||
resolution=model["resolution"],
|
||||
seed=seed,
|
||||
creativity=model["creativity"],
|
||||
moodboards=moodboards,
|
||||
image_style_references=image_style_references,
|
||||
),
|
||||
)
|
||||
job = await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/krea/jobs/{initial.job_id}", method="GET"),
|
||||
response_model=KreaJob,
|
||||
status_extractor=lambda r: r.status,
|
||||
queued_statuses=_KREA_QUEUED_STATUSES,
|
||||
)
|
||||
if not job.result or not job.result.urls:
|
||||
raise RuntimeError(f"Krea 2 job {job.job_id} completed without any image URLs.")
|
||||
image = await download_url_to_image_tensor(job.result.urls[0])
|
||||
return IO.NodeOutput(image)
|
||||
|
||||
|
||||
class Krea2StyleReferenceNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="Krea2StyleReferenceNode",
|
||||
display_name="Krea 2 Style Reference",
|
||||
category="api node/image/Krea",
|
||||
description=(
|
||||
"Add an image style reference to a Krea 2 generation. Chain multiple Krea 2 "
|
||||
"Style Reference nodes (max 10) and feed the final `style_reference` output "
|
||||
"into Krea 2 Image. Each image is uploaded to ComfyAPI storage and passed as URL."
|
||||
),
|
||||
inputs=[
|
||||
IO.Image.Input(
|
||||
"image",
|
||||
tooltip="Reference image whose style influences the generation.",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"strength",
|
||||
default=1.0,
|
||||
min=-2.0,
|
||||
max=2.0,
|
||||
step=0.05,
|
||||
tooltip="Reference strength; negative values invert the style influence.",
|
||||
),
|
||||
IO.Custom(KreaIO.STYLE_REF).Input(
|
||||
"style_reference",
|
||||
optional=True,
|
||||
tooltip="Optional incoming chain of style references; this node appends one more.",
|
||||
),
|
||||
],
|
||||
outputs=[IO.Custom(KreaIO.STYLE_REF).Output(display_name="style_reference")],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
image: Input.Image,
|
||||
strength: float,
|
||||
style_reference: list[dict] | None = None,
|
||||
) -> IO.NodeOutput:
|
||||
chain: list[dict] = list(style_reference) if style_reference else []
|
||||
if len(chain) >= 10:
|
||||
raise ValueError("Krea 2 accepts at most 10 image_style_references in one generation.")
|
||||
url = await _upload_image_to_krea_assets(cls, image)
|
||||
chain.append({"url": url, "strength": float(strength)})
|
||||
return IO.NodeOutput(chain)
|
||||
|
||||
|
||||
class KreaExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||
return [
|
||||
Krea2ImageNode,
|
||||
Krea2StyleReferenceNode,
|
||||
]
|
||||
|
||||
|
||||
async def comfy_entrypoint() -> KreaExtension:
|
||||
return KreaExtension()
|
||||
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by the build process when version is
|
||||
# updated in pyproject.toml.
|
||||
__version__ = "0.22.2"
|
||||
__version__ = "0.22.3"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "ComfyUI"
|
||||
version = "0.22.2"
|
||||
version = "0.22.3"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
comfyui-frontend-package==1.43.18
|
||||
comfyui-workflow-templates==0.9.82
|
||||
comfyui-workflow-templates==0.9.85
|
||||
comfyui-embedded-docs==0.5.0
|
||||
torch
|
||||
torchsde
|
||||
|
||||
Reference in New Issue
Block a user