Merge branch 'Comfy-Org:master' into arena/01a001da-comfyui

This commit is contained in:
Sonu 2026-08-15 13:30:02 +05:30 committed by GitHub
commit 605bcb6916
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 175 additions and 28 deletions

View File

@ -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 <think>/</think> tags
# Only a close that ends a thought channel becomes </think>: generation primed with
# another channel leaves its opener in the prompt, so its close is not reasoning.
text = re.sub(r"<\|channel>thought\n(.*?)<channel\|>", r"<think>\n\1</think>", text, flags=re.DOTALL)
text = text.replace("<|channel>thought\n", "<think>\n")
text = text.replace("<channel|>", "</think>")
# Strip remaining special tokens
text = text.replace("<turn|>", "").replace("<eos>", "").strip()
text = re.sub(r"<\|channel>\w*\n?|<channel\|>|<\|turn>\w*\n?|<turn\|>", "", text)
text = text.replace("<eos>", "").strip()
return text

View File

@ -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):

View File

@ -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,

View File

@ -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"<think>.*?</think>", "", text, flags=re.DOTALL)
if "</think>" in text: # unclosed/truncated reasoning: keep what follows the last close
text = text.rsplit("</think>", 1)[-1]
text = re.sub(r"</?think>|<\|channel>\w*\n?|<channel\|>|<\|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"<think>.*?(?:</think>|$)", "", out.args[0], flags=re.DOTALL).strip()
return io.NodeOutput(text or prompt)
class TextgenExtension(ComfyExtension):

12
main.py
View File

@ -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: