Compare commits

...

12 Commits

Author SHA1 Message Date
grtninja
e7faa60968
Merge d6e94b7dfc into 97f58baaaf 2026-04-30 21:56:43 -04:00
Jedrzej Kosinski
97f58baaaf
Add alexisrolland and rattus128 as code owners (#13648)
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Build package / Build Test (3.10) (push) Waiting to run
Build package / Build Test (3.11) (push) Waiting to run
Build package / Build Test (3.12) (push) Waiting to run
Build package / Build Test (3.13) (push) Waiting to run
Build package / Build Test (3.14) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-04-30 21:49:31 -04:00
Daxiong (Lin)
e8e8fee224
chore: update workflow templates to v0.9.65 (#13644) 2026-04-30 18:14:28 -07:00
Rainer
e9c311b245
OneTainer ERNIE LoRA support (#13640) 2026-04-30 19:33:41 -04:00
comfyanonymous
e6e0936128
Load other jpeg formats without taking so much memory. (#13642) 2026-04-30 19:33:09 -04:00
Alexander Piskun
b633244635
[Partner Nodes] ByteDance: virtual portrait library for regular images (#13638)
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
Build package / Build Test (3.10) (push) Waiting to run
Build package / Build Test (3.11) (push) Waiting to run
Build package / Build Test (3.12) (push) Waiting to run
Build package / Build Test (3.13) (push) Waiting to run
Build package / Build Test (3.14) (push) Waiting to run
* feat(api-nodes-bytedance): use the virtual portrait library for regular images

Signed-off-by: bigcat88 <bigcat88@icloud.com>

* fix: include shape in image dedup hash

Signed-off-by: bigcat88 <bigcat88@icloud.com>

---------

Signed-off-by: bigcat88 <bigcat88@icloud.com>
2026-04-30 11:49:08 -07:00
grtninja
d6e94b7dfc
Merge branch 'master' into codex/history-query-validation 2026-04-27 11:18:38 -04:00
grtninja
65c5dba4e3
Merge branch 'master' into codex/history-query-validation 2026-04-15 11:02:24 -04:00
grtninja
2274a5d3d3
Merge branch 'master' into codex/history-query-validation 2026-04-13 10:18:02 -04:00
grtninja
74547bf49b
Merge branch 'master' into codex/history-query-validation 2026-04-12 19:20:33 -04:00
grtninja
39e5f74129
Merge branch 'master' into codex/history-query-validation 2026-04-11 22:05:06 -04:00
grtninja
187e9f03a9 fix: validate history query parameters 2026-04-11 07:31:29 -04:00
10 changed files with 122 additions and 14 deletions

View File

@ -1,2 +1,2 @@
# Admins
* @comfyanonymous @kosinkadink @guill
* @comfyanonymous @kosinkadink @guill @alexisrolland @rattus128

View File

@ -0,0 +1,12 @@
from collections.abc import Mapping
def parse_optional_int_query_param(query: Mapping[str, str], name: str) -> int | None:
value = query.get(name)
if value is None:
return None
try:
return int(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"{name} must be an integer") from exc

View File

@ -342,6 +342,12 @@ def model_lora_keys_unet(model, key_map={}):
key_map["base_model.model.{}".format(key_lora)] = k # Official base model loras
key_map["lycoris_{}".format(key_lora.replace(".", "_"))] = k # LyCORIS/LoKR format
if isinstance(model, comfy.model_base.ErnieImage):
for k in sdk:
if k.startswith("diffusion_model.") and k.endswith(".weight"):
key_lora = k[len("diffusion_model."):-len(".weight")]
key_map["transformer.{}".format(key_lora)] = k
return key_map

View File

@ -290,7 +290,7 @@ class VideoFromFile(VideoInput):
alphas = []
alpha_channel = True
break
if frame.format.name in ("yuvj420p", "rgb24", "rgba", "pal8"):
if frame.format.name in ("yuvj420p", "yuvj422p", "yuvj444p", "rgb24", "rgba", "pal8"):
process_image_format = lambda a: a.float() / 255.0
if alpha_channel:
image_format = 'rgba'

View File

@ -157,6 +157,11 @@ class SeedanceCreateAssetResponse(BaseModel):
asset_id: str = Field(...)
class SeedanceVirtualLibraryCreateAssetRequest(BaseModel):
url: str = Field(..., description="Publicly accessible URL of the image asset to upload.")
hash: str = Field(..., description="Dedup key. Re-submitting the same hash returns the existing asset id.")
# Dollars per 1K tokens, keyed by (model_id, has_video_input).
SEEDANCE2_PRICE_PER_1K_TOKENS = {
("dreamina-seedance-2-0-260128", False): 0.007,

View File

@ -1,3 +1,4 @@
import hashlib
import logging
import math
import re
@ -20,6 +21,7 @@ from comfy_api_nodes.apis.bytedance import (
SeedanceCreateAssetResponse,
SeedanceCreateVisualValidateSessionResponse,
SeedanceGetVisualValidateSessionResponse,
SeedanceVirtualLibraryCreateAssetRequest,
Seedream4Options,
Seedream4TaskCreationRequest,
TaskAudioContent,
@ -271,6 +273,30 @@ async def _wait_for_asset_active(cls: type[IO.ComfyNode], asset_id: str, group_i
)
async def _seedance_virtual_library_upload_image_asset(
cls: type[IO.ComfyNode],
image: torch.Tensor,
*,
wait_label: str = "Uploading image",
) -> str:
"""Upload an image into the caller's per-customer Seedance virtual library."""
public_url = await upload_image_to_comfyapi(cls, image, wait_label=wait_label)
normalized = image.detach().cpu().contiguous().to(torch.float32)
digest = hashlib.sha256()
digest.update(str(tuple(normalized.shape)).encode("utf-8"))
digest.update(b"\0")
digest.update(normalized.numpy().tobytes())
image_hash = digest.hexdigest()
create_resp = await sync_op(
cls,
ApiEndpoint(path="/proxy/seedance/virtual-library/assets", method="POST"),
response_model=SeedanceCreateAssetResponse,
data=SeedanceVirtualLibraryCreateAssetRequest(url=public_url, hash=image_hash),
)
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))
@ -1507,7 +1533,9 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
if first_frame_asset_id:
first_frame_url = image_assets[first_frame_asset_id]
else:
first_frame_url = await upload_image_to_comfyapi(cls, first_frame, wait_label="Uploading first frame.")
first_frame_url = await _seedance_virtual_library_upload_image_asset(
cls, first_frame, wait_label="Uploading first frame."
)
content: list[TaskTextContent | TaskImageContent] = [
TaskTextContent(text=model["prompt"]),
@ -1527,7 +1555,9 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
content.append(
TaskImageContent(
image_url=TaskImageContentUrl(
url=await upload_image_to_comfyapi(cls, last_frame, wait_label="Uploading last frame.")
url=await _seedance_virtual_library_upload_image_asset(
cls, last_frame, wait_label="Uploading last frame."
)
),
role="last_frame",
),
@ -1805,9 +1835,9 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
content.append(
TaskImageContent(
image_url=TaskImageContentUrl(
url=await upload_image_to_comfyapi(
url=await _seedance_virtual_library_upload_image_asset(
cls,
image=reference_images[key],
reference_images[key],
wait_label=f"Uploading image {i}",
),
),

View File

@ -1,5 +1,5 @@
comfyui-frontend-package==1.42.15
comfyui-workflow-templates==0.9.63
comfyui-workflow-templates==0.9.65
comfyui-embedded-docs==0.4.4
torch
torchsde

View File

@ -45,6 +45,7 @@ from app.subgraph_manager import SubgraphManager
from app.node_replace_manager import NodeReplaceManager
from typing import Optional, Union
from api_server.routes.internal.internal_routes import InternalRoutes
from api_server.utils.query_params import parse_optional_int_query_param
from protocol import BinaryEventTypes
# Import cache control middleware
@ -887,14 +888,15 @@ class PromptServer():
@routes.get("/history")
async def get_history(request):
max_items = request.rel_url.query.get("max_items", None)
if max_items is not None:
max_items = int(max_items)
query = request.rel_url.query
offset = request.rel_url.query.get("offset", None)
if offset is not None:
offset = int(offset)
else:
try:
max_items = parse_optional_int_query_param(query, "max_items")
offset = parse_optional_int_query_param(query, "offset")
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
if offset is None:
offset = -1
return web.json_response(self.prompt_queue.get_history(max_items=max_items, offset=offset))

View File

@ -0,0 +1,39 @@
import pytest
from api_server.utils.query_params import parse_optional_int_query_param
def test_parse_optional_int_query_param_returns_none_when_missing():
assert parse_optional_int_query_param({}, "offset") is None
@pytest.mark.parametrize(
("raw_value", "expected"),
[
("0", 0),
("5", 5),
("-1", -1),
],
)
def test_parse_optional_int_query_param_parses_integers(raw_value, expected):
query = {"offset": raw_value}
assert parse_optional_int_query_param(query, "offset") == expected
@pytest.mark.parametrize(
("name", "raw_value"),
[
("offset", "not-an-integer"),
("offset", "1.5"),
("offset", ""),
("max_items", "not-an-integer"),
],
)
def test_parse_optional_int_query_param_rejects_invalid_integers(name, raw_value):
query = {name: raw_value}
with pytest.raises(ValueError) as exc_info:
parse_optional_int_query_param(query, name)
assert str(exc_info.value) == f"{name} must be an integer"

View File

@ -909,6 +909,20 @@ class TestExecution:
assert len(result) <= 1, "Should return at most 1 item when offset is near end"
def test_history_api_rejects_non_integer_max_items(self, client: ComfyClient):
with pytest.raises(urllib.error.HTTPError) as exc_info:
client.get_all_history(max_items="not-an-integer")
assert exc_info.value.code == 400
assert json.loads(exc_info.value.read()) == {"error": "max_items must be an integer"}
def test_history_api_rejects_non_integer_offset(self, client: ComfyClient):
with pytest.raises(urllib.error.HTTPError) as exc_info:
client.get_all_history(offset="not-an-integer")
assert exc_info.value.code == 400
assert json.loads(exc_info.value.read()) == {"error": "offset must be an integer"}
# Jobs API tests
def test_jobs_api_job_structure(
self, client: ComfyClient, builder: GraphBuilder