From 55b6a9b11dffecdd65a3ccd5eb6a1b3a178c96dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jukka=20Sepp=C3=A4nen?=
<40791699+kijai@users.noreply.github.com>
Date: Fri, 14 Aug 2026 22:17:18 +0300
Subject: [PATCH 1/3] Fix thinking handling (#15611)
---
comfy/text_encoders/gemma4.py | 9 ++++++---
comfy_extras/nodes_textgen.py | 10 ++++------
2 files changed, 10 insertions(+), 9 deletions(-)
diff --git a/comfy/text_encoders/gemma4.py b/comfy/text_encoders/gemma4.py
index 61bc3a3f0..da116ab27 100644
--- a/comfy/text_encoders/gemma4.py
+++ b/comfy/text_encoders/gemma4.py
@@ -6,6 +6,7 @@ import numpy as np
from tokenizers import Tokenizer
from dataclasses import dataclass
import math
+import re
from comfy import sd1_clip
import comfy.model_management
@@ -1624,11 +1625,13 @@ class Gemma4SDTokenizer(Gemma4_Tokenizer, sd1_clip.SDTokenizer):
def decode(self, token_ids, **kwargs):
text = super().decode(token_ids, skip_special_tokens=False)
- # Translate thinking channel markers to standard / tags
+ # Only a close that ends a thought channel becomes : generation primed with
+ # another channel leaves its opener in the prompt, so its close is not reasoning.
+ text = re.sub(r"<\|channel>thought\n(.*?)", r"\n\1", text, flags=re.DOTALL)
text = text.replace("<|channel>thought\n", "\n")
- text = text.replace("", "")
# Strip remaining special tokens
- text = text.replace("", "").replace("", "").strip()
+ text = re.sub(r"<\|channel>\w*\n?||<\|turn>\w*\n?|", "", text)
+ text = text.replace("", "").strip()
return text
diff --git a/comfy_extras/nodes_textgen.py b/comfy_extras/nodes_textgen.py
index 40004652c..ef5858a7d 100644
--- a/comfy_extras/nodes_textgen.py
+++ b/comfy_extras/nodes_textgen.py
@@ -256,12 +256,10 @@ class TextGenerateLTX2Prompt(TextGenerate):
out = super().execute(clip, formatted_prompt, max_length, sampling_mode, image=image, thinking=thinking, use_default_template=use_default_template, video=video, audio=audio)
- text = out.args[0]
- text = re.sub(r".*?", "", text, flags=re.DOTALL)
- if "" in text: # unclosed/truncated reasoning: keep what follows the last close
- text = text.rsplit("", 1)[-1]
- text = re.sub(r"?think>|<\|channel>\w*\n?||<\|turn>\w*\n?", "", text).strip()
- return io.NodeOutput(text)
+ # Drop reasoning, including a block left unclosed by max_length. Both system prompts ask
+ # for the original prompt back when there is nothing to give; empty conditions on nothing.
+ text = re.sub(r".*?(?:|$)", "", out.args[0], flags=re.DOTALL).strip()
+ return io.NodeOutput(text or prompt)
class TextgenExtension(ComfyExtension):
From a7365071e47175fb06572d0a56d1bf4116c2f581 Mon Sep 17 00:00:00 2001
From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com>
Date: Fri, 14 Aug 2026 17:34:40 -0700
Subject: [PATCH 2/3] Enable dynamic vram by default on ROCm 7.14 and higher.
(#15633)
---
main.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/main.py b/main.py
index eb64726ea..b6f7d6cef 100644
--- a/main.py
+++ b/main.py
@@ -248,7 +248,17 @@ import hook_breaker_ac10a0
import comfy.memory_management
import comfy.model_patcher
-if args.enable_dynamic_vram or (enables_dynamic_vram() and comfy.model_management.is_nvidia()):
+
+def dynamic_vram_supported():
+ if comfy.model_management.is_nvidia():
+ return True
+ if comfy.model_management.is_amd():
+ if comfy.model_management.rocm_version >= (7, 14):
+ return True
+ return False
+
+
+if args.enable_dynamic_vram or (enables_dynamic_vram() and dynamic_vram_supported()):
if (not args.enable_dynamic_vram) and (comfy.model_management.torch_version_numeric < (2, 8)):
logging.warning("Unsupported Pytorch detected. DynamicVRAM support requires Pytorch version 2.8 or later. Falling back to legacy ModelPatcher. VRAM estimates may be unreliable especially on Windows")
else:
From 0f1fa67ad8a68b62c65ebc97a7bf485df2459c3a Mon Sep 17 00:00:00 2001
From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
Date: Sat, 15 Aug 2026 08:37:17 +0400
Subject: [PATCH 3/3] [Partner Nodes] feat(ByteDance): add Seedance 2.5
task_type for video extension (#15579)
Signed-off-by: Alexander Piskun
---
comfy_api_nodes/apis/bytedance.py | 1 +
comfy_api_nodes/nodes_bytedance.py | 171 ++++++++++++++++++++++++++---
2 files changed, 154 insertions(+), 18 deletions(-)
diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py
index 7ee83e5f3..64f8cde37 100644
--- a/comfy_api_nodes/apis/bytedance.py
+++ b/comfy_api_nodes/apis/bytedance.py
@@ -116,6 +116,7 @@ class Seedance2TaskCreationRequest(BaseModel):
seed: int | None = Field(None, ge=0, le=2147483647)
watermark: bool | None = Field(None)
output_format: str | None = Field(None)
+ omni_reference_task_type: str | None = Field(None, description="One of: auto, reference, edit, extend.")
class TaskCreationResponse(BaseModel):
diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py
index 09fc445d8..265f94d13 100644
--- a/comfy_api_nodes/nodes_bytedance.py
+++ b/comfy_api_nodes/nodes_bytedance.py
@@ -2069,7 +2069,7 @@ def _seedance2_text_inputs(resolutions: list[str], default_ratio: str = "16:9"):
]
-def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool = False):
+def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool = False, with_task_type: bool = False):
return [
IO.String.Input(
"prompt",
@@ -2124,6 +2124,29 @@ def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool =
if with_video_editing
else []
),
+ *(
+ [
+ IO.Combo.Input(
+ "task_type",
+ options=["auto", "reference", "edit", "extend"],
+ default="auto",
+ tooltip="What to do with the reference media. Every value except auto is "
+ "validated when the task is submitted, so mismatched settings fail before "
+ "generation starts. auto: the model infers the task from the prompt and "
+ "inputs, and settings that conflict with its reading fail only after "
+ "generation has started. reference: generate a new video guided by the "
+ "reference images, videos, and audio. edit: change a connected reference "
+ "video (add, remove, replace); the output keeps the source clip's own length "
+ "and aspect ratio, and the duration and ratio widgets are ignored. extend: "
+ "continue a connected reference video forward or backward; the prompt should "
+ "say 'extend forward', 'extend backward', or 'continue', the aspect ratio "
+ "follows the source clip, and the output contains only the newly generated "
+ "segment of the duration you set, not the source clip.",
+ )
+ ]
+ if with_task_type
+ else []
+ ),
IO.Combo.Input(
"output_format",
options=["mp4"],
@@ -2133,9 +2156,9 @@ def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool =
]
-def _seedance25_reference_inputs():
+def _seedance25_reference_inputs(with_video_editing: bool = False, with_task_type: bool = False):
return [
- *_seedance25_text_inputs(with_video_editing=True),
+ *_seedance25_text_inputs(with_video_editing=with_video_editing, with_task_type=with_task_type),
IO.Autogrow.Input(
"reference_images",
template=IO.Autogrow.TemplateNames(
@@ -2196,17 +2219,23 @@ def _seedance2_build_request(
watermark: bool,
ratio: str,
) -> Seedance2TaskCreationRequest:
- video_editing = bool(model.get("video_editing"))
+ task_type = model.get("task_type", "auto")
+ duration = model["duration"]
+ if model.get("video_editing") or task_type == "edit":
+ ratio, duration = "adaptive", -1
+ elif task_type == "extend":
+ ratio = "adaptive"
return Seedance2TaskCreationRequest(
model=model_id,
content=content,
generate_audio=model["generate_audio"],
resolution=model["resolution"],
- ratio="adaptive" if video_editing else ratio,
- duration=-1 if video_editing else model["duration"],
+ ratio=ratio,
+ duration=duration,
seed=seed,
watermark=watermark,
output_format=model.get("output_format"),
+ omni_reference_task_type=None if task_type == "auto" else task_type,
)
@@ -2216,7 +2245,7 @@ _SEEDANCE2_PRICE_EXPR_TEMPLATE = """
$res := $lookup(widgets, "model.resolution");
$ratio := $lookup(widgets, "model.ratio");
$dur := $lookup(widgets, "model.duration");
- $auto := $lookup(widgets, "model.video_editing") = true;
+ $auto := __IS_EDIT__;
$hasVideo := __HAS_VIDEO__;
$ready := $type($m) = "string" and $type($res) = "string" and ($auto or $type($dur) = "number");
$ready ? (
@@ -2261,6 +2290,7 @@ _SEEDANCE2_PRICE_EXPR_TEMPLATE = """
_SEEDANCE_AUDIO_POLICY_CODE = "OutputAudioSensitiveContentDetected.PolicyViolation"
_SEEDANCE_TASK_TYPE_CONSTRAINT_CODE = "InvalidParameter.TaskTypeConstraint"
+_SEEDANCE_TASK_TYPE_MISMATCH_CODE = "InvalidParameter.TaskTypeMismatch"
async def _seedance2_poll_video_task(
@@ -2269,6 +2299,7 @@ async def _seedance2_poll_video_task(
model_id: str,
resolution: str,
has_video_input: bool,
+ task_type: str | None = None,
) -> TaskStatusResponse:
try:
return await poll_op(
@@ -2289,19 +2320,48 @@ async def _seedance2_poll_video_task(
"to get a silent video, or adjust the prompt and try again."
) from exc
if _SEEDANCE_TASK_TYPE_CONSTRAINT_CODE in str(exc):
+ if task_type is None:
+ raise ValueError(
+ "Seedance read this prompt as editing the reference video, and an edit always "
+ "takes its duration and aspect ratio from that video. Enable video_editing on "
+ "this node and run again, or reword the prompt so it describes a new video "
+ "rather than a change to the reference one."
+ ) from exc
+ if task_type == "edit":
+ raise ValueError(
+ "The request does not satisfy the 'edit' constraints: the clip being edited "
+ "must be 4 to 30 seconds long."
+ ) from exc
+ if task_type == "extend":
+ raise ValueError(
+ "The request does not satisfy the 'extend' constraints: the clip being "
+ "extended must be 1.9 to 30 seconds long."
+ ) from exc
raise ValueError(
- "Seedance read this prompt as editing the reference video, and an edit always "
- "takes its duration and aspect ratio from that video. Enable video_editing on "
- "this node and run again, or reword the prompt so it describes a new video "
- "rather than a change to the reference one."
+ "Seedance decided from the prompt that this task's duration or aspect ratio "
+ "must come from the reference video, and the current settings conflict with "
+ "that. Set task_type to the task you mean ('edit' or 'extend') and run again, "
+ "or reword the prompt so it describes a new video rather than a change to the "
+ "reference one."
+ ) from exc
+ if _SEEDANCE_TASK_TYPE_MISMATCH_CODE in str(exc):
+ raise ValueError(
+ f"Seedance read this prompt as a different task than the selected task_type "
+ f"'{task_type}'. Reword the prompt so it matches: an extend prompt should say "
+ "'extend forward', 'extend backward', or 'continue'; an edit prompt should use "
+ "words like add, remove, replace, or change. Or set task_type to auto."
) from exc
raise
-def _seedance2_price_badge(with_reference_videos: bool) -> IO.PriceBadge:
+def _seedance2_price_badge(with_reference_videos: bool, legacy_video_editing: bool = False) -> IO.PriceBadge:
widgets = ["model", "model.resolution", "model.ratio", "model.duration"]
+ if legacy_video_editing:
+ is_edit = '$lookup(widgets, "model.video_editing") = true'
+ else:
+ is_edit = '$lookup(widgets, "model.task_type") = "edit"'
if with_reference_videos:
- widgets.append("model.video_editing")
+ widgets.append("model.video_editing" if legacy_video_editing else "model.task_type")
has_video = (
'$exists(inputGroups) and $lookup(inputGroups, "model.reference_videos") > 0'
if with_reference_videos
@@ -2312,7 +2372,7 @@ def _seedance2_price_badge(with_reference_videos: bool) -> IO.PriceBadge:
widgets=widgets,
input_groups=["model.reference_videos"] if with_reference_videos else [],
),
- expr=_SEEDANCE2_PRICE_EXPR_TEMPLATE.replace("__HAS_VIDEO__", has_video),
+ expr=_SEEDANCE2_PRICE_EXPR_TEMPLATE.replace("__HAS_VIDEO__", has_video).replace("__IS_EDIT__", is_edit),
)
@@ -2662,12 +2722,12 @@ def _seedance2_reference_inputs(resolutions: list[str], default_ratio: str = "16
]
-class ByteDance2ReferenceNode(IO.ComfyNode):
+class ByteDance2ReferenceNodeV2(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
- node_id="ByteDance2ReferenceNode",
+ node_id="ByteDance2ReferenceNodeV2",
display_name="ByteDance Seedance 2.5 Reference to Video",
category="partner/video/ByteDance",
description="Generate, edit, or extend video using Seedance 2.5 or 2.0 with reference "
@@ -2676,7 +2736,7 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
IO.DynamicCombo.Input(
"model",
options=[
- IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs()),
+ IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs(with_task_type=True)),
IO.DynamicCombo.Option(
"Seedance 2.0",
_seedance2_reference_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"),
@@ -2761,6 +2821,13 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
f"(videos={len(reference_videos)}, video assets={len(reference_video_assets)}). "
f"Maximum is {limits['max_videos']}."
)
+ task_type = model.get("task_type")
+ if task_type in ("edit", "extend") and total_videos == 0:
+ raise ValueError(
+ f"A '{task_type}' task needs at least one reference video. Connect the video "
+ f"you want to {'change' if task_type == 'edit' else 'continue'}, or set "
+ "task_type to 'reference' to generate a new video from the references you have."
+ )
total_audios = len(reference_audios) + len(reference_audio_assets)
if total_audios > limits["max_audios"]:
raise ValueError(
@@ -2893,11 +2960,78 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
response_model=TaskCreationResponse,
)
response = await _seedance2_poll_video_task(
- cls, initial_response.id, model_id, model["resolution"], has_video_input=has_video_input
+ cls,
+ initial_response.id,
+ model_id,
+ model["resolution"],
+ has_video_input=has_video_input,
+ task_type=task_type,
)
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
+class ByteDance2ReferenceNode(ByteDance2ReferenceNodeV2):
+
+ @classmethod
+ def define_schema(cls):
+ return IO.Schema(
+ node_id="ByteDance2ReferenceNode",
+ display_name="ByteDance Seedance 2.5 Reference to Video (Legacy)",
+ category="partner/video/ByteDance",
+ description="Generate, edit, or extend video using Seedance 2.5 or 2.0 with reference "
+ "images, videos, and audio. Supports multimodal reference, video editing, and video extension.",
+ inputs=[
+ IO.DynamicCombo.Input(
+ "model",
+ options=[
+ IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs(with_video_editing=True)),
+ IO.DynamicCombo.Option(
+ "Seedance 2.0",
+ _seedance2_reference_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"),
+ ),
+ IO.DynamicCombo.Option(
+ "Seedance 2.0 Fast",
+ _seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"),
+ ),
+ IO.DynamicCombo.Option(
+ "Seedance 2.0 Mini",
+ _seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"),
+ ),
+ ],
+ tooltip=SEEDANCE_MODEL_TOOLTIP,
+ ),
+ IO.Int.Input(
+ "seed",
+ default=0,
+ min=0,
+ max=2147483647,
+ step=1,
+ 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.",
+ ),
+ IO.Boolean.Input(
+ "watermark",
+ default=False,
+ tooltip="Whether to add a watermark to the video.",
+ advanced=True,
+ ),
+ ],
+ outputs=[
+ IO.Video.Output(),
+ ],
+ hidden=[
+ IO.Hidden.auth_token_comfy_org,
+ IO.Hidden.api_key_comfy_org,
+ IO.Hidden.unique_id,
+ ],
+ is_api_node=True,
+ is_deprecated=True,
+ price_badge=_seedance2_price_badge(with_reference_videos=True, legacy_video_editing=True),
+ )
+
+
async def process_video_task(
cls: type[IO.ComfyNode],
payload: Text2VideoTaskCreationRequest | Image2VideoTaskCreationRequest,
@@ -3413,6 +3547,7 @@ class ByteDanceExtension(ComfyExtension):
ByteDance2TextToVideoNode,
ByteDance2FirstLastFrameNode,
ByteDance2ReferenceNode,
+ ByteDance2ReferenceNodeV2,
ByteDanceCreateImageAsset,
ByteDanceCreateVideoAsset,
ByteDanceSeedAudioNode,