Updated IAMCCS-nodes to version 1.4.4
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# IAMCCS Nodes - Changelog
|
||||
|
||||
## 🆕 2026-05-04 - version 1.4.4 — Supernodes v.2 and bug fixed pplus utilities added
|
||||
|
||||
## 🆕 2026-04-24 - version 1.4.3 — Supwenodes, more LTX and WAN 2.2 nodes and utilities added
|
||||
|
||||
## 🆕 2026-04-13 - version 1.4.1 — Multigen wrappers added
|
||||
|
||||
## 🆕 2026-03-27 - version 1.4.0 — Audio extension ltx-2.3
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
### Category: ComfyUI Custom Nodes
|
||||
### Main Feature: Fix for LoRA loading in native WANAnimate workflows + general nodes 4 ComfyUI
|
||||
|
||||
Version: 1.4.3
|
||||
Version: 1.4.4 (Supernodes and wan 2.2 + LTX 2.2 utilities added)
|
||||
|
||||
## IAMCCS SuperNodes requirements
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ from .iamccs_ltx2_tools import (
|
||||
IAMCCS_LTX2_ImageBatchCropByPad,
|
||||
IAMCCS_SegmentPlanner,
|
||||
IAMCCS_SegmentPlannerSettings,
|
||||
IAMCCS_AudioSegmentAutoPlanner,
|
||||
IAMCCS_SegmentPlannerLinked,
|
||||
IAMCCS_SegmentPlanFromPlanner,
|
||||
IAMCCS_SourceRangeFromSegmentPlan,
|
||||
@@ -313,6 +314,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"IAMCCS_LTX2_ImageBatchCropByPad": IAMCCS_LTX2_ImageBatchCropByPad,
|
||||
"IAMCCS_SegmentPlanner": IAMCCS_SegmentPlanner,
|
||||
"IAMCCS_SegmentPlannerSettings": IAMCCS_SegmentPlannerSettings,
|
||||
"IAMCCS_AudioSegmentAutoPlanner": IAMCCS_AudioSegmentAutoPlanner,
|
||||
"IAMCCS_SegmentPlannerLinked": IAMCCS_SegmentPlannerLinked,
|
||||
"IAMCCS_SegmentPlanFromPlanner": IAMCCS_SegmentPlanFromPlanner,
|
||||
"IAMCCS_SourceRangeFromSegmentPlan": IAMCCS_SourceRangeFromSegmentPlan,
|
||||
@@ -501,6 +503,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"IAMCCS_LTX2_ImageBatchCropByPad": "LTX-2 Crop By Pad (IMAGE batch)",
|
||||
"IAMCCS_SegmentPlanner": "Segment Planner (song -> LTX frames)",
|
||||
"IAMCCS_SegmentPlannerSettings": "Segment Planner Settings (shared)",
|
||||
"IAMCCS_AudioSegmentAutoPlanner": "Audio Segment Auto Planner (audio -> segmenti)",
|
||||
"IAMCCS_SegmentPlannerLinked": "Segment Planner Linked (shared inputs)",
|
||||
"IAMCCS_SegmentPlanFromPlanner": "Segment Plan From Planner (per index)",
|
||||
"IAMCCS_SourceRangeFromSegmentPlan": "Source Range From Segment Plan",
|
||||
|
||||
+159
-1
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Tuple
|
||||
from typing import Any, Mapping, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
@@ -23,6 +23,22 @@ _F = torch.nn.functional
|
||||
_log = logging.getLogger("IAMCCS.LTX2.Tools")
|
||||
|
||||
|
||||
def _audio_duration_seconds(audio: Any) -> float:
|
||||
if not isinstance(audio, Mapping):
|
||||
raise ValueError("audio input is required")
|
||||
waveform = audio.get("waveform")
|
||||
sample_rate = int(audio.get("sample_rate", 0) or 0)
|
||||
if waveform is None or sample_rate <= 0:
|
||||
raise ValueError("audio input must contain waveform and sample_rate")
|
||||
try:
|
||||
total_samples = int(waveform.shape[-1])
|
||||
except Exception as exc:
|
||||
raise ValueError("audio waveform has no sample dimension") from exc
|
||||
if total_samples <= 0:
|
||||
raise ValueError("audio waveform is empty")
|
||||
return float(total_samples) / float(sample_rate)
|
||||
|
||||
|
||||
def _to_grayscale(image: torch.Tensor) -> torch.Tensor:
|
||||
# image: [B,H,W,C] float in [0,1]
|
||||
if image.shape[-1] == 1:
|
||||
@@ -893,6 +909,148 @@ class IAMCCS_SegmentPlannerSettings(IAMCCS_SegmentPlanner):
|
||||
)
|
||||
|
||||
|
||||
class IAMCCS_AudioSegmentAutoPlanner(IAMCCS_SegmentPlanner):
|
||||
SPLIT_BY_SEGMENTS = "choose segment count (audio / segments)"
|
||||
SPLIT_BY_SECONDS = "choose seconds per segment (auto count)"
|
||||
SEGMENT_COUNT_CHOICES = (1, 2, 3, 4, 5, 6, 8, 10, 12)
|
||||
SEGMENT_SECONDS_CHOICES = (5.0, 8.0, 10.0, 12.0, 15.0, 20.0, 30.0)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"audio": ("AUDIO",),
|
||||
"fps": ("FLOAT", {"default": 25.0, "min": 0.001, "max": 240.0, "step": 0.01}),
|
||||
"split_mode": ([cls.SPLIT_BY_SEGMENTS, cls.SPLIT_BY_SECONDS], {"default": cls.SPLIT_BY_SEGMENTS}),
|
||||
"segment_count": (list(cls.SEGMENT_COUNT_CHOICES), {"default": 2}),
|
||||
"target_segment_seconds": (list(cls.SEGMENT_SECONDS_CHOICES), {"default": 10.0}),
|
||||
"overlap_frames": ("INT", {"default": 9, "min": 0, "max": 4096, "step": 1}),
|
||||
"ltx_round_mode": (["up", "nearest", "down"], {"default": "up"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (
|
||||
"FLOAT", "FLOAT", "STRING", "STRING", "INT",
|
||||
"INT", "INT", "INT", "INT", "INT", "INT", "FLOAT", "STRING",
|
||||
)
|
||||
RETURN_NAMES = (
|
||||
"audio_duration_s",
|
||||
"segment_duration_s",
|
||||
"planning_mode",
|
||||
"segment_preset",
|
||||
"overlap_frames",
|
||||
"segment_count",
|
||||
"total_frames",
|
||||
"unique_segment_frames",
|
||||
"first_segment_raw_frames",
|
||||
"continuation_raw_frames",
|
||||
"last_segment_unique_frames",
|
||||
"fps_out",
|
||||
"report",
|
||||
)
|
||||
FUNCTION = "plan_audio_segments"
|
||||
CATEGORY = "IAMCCS/LTX-2"
|
||||
|
||||
def _preset_from_seconds(self, seconds: float) -> str:
|
||||
preset, _rec = self._recommend_profile_for_segment_seconds(float(seconds))
|
||||
if preset is not None:
|
||||
return str(preset)
|
||||
choices = ((5.0, "5sec"), (10.0, "10sec"), (15.0, "15sec"), (20.0, "20sec"))
|
||||
return min(choices, key=lambda item: abs(float(seconds) - item[0]))[1]
|
||||
|
||||
@staticmethod
|
||||
def _first_number(value: Any, default: float) -> float:
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
text = str(value or "").strip()
|
||||
token = ""
|
||||
seen_decimal = False
|
||||
for ch in text:
|
||||
if ch.isdigit():
|
||||
token += ch
|
||||
elif ch == "." and token and not seen_decimal:
|
||||
token += ch
|
||||
seen_decimal = True
|
||||
elif token:
|
||||
break
|
||||
if not token or token == ".":
|
||||
return float(default)
|
||||
try:
|
||||
return float(token)
|
||||
except Exception:
|
||||
return float(default)
|
||||
|
||||
def _normalize_split_mode(self, split_mode: Any) -> tuple[str, str]:
|
||||
text = str(split_mode or "").strip().lower()
|
||||
if text in ("fixed_segment_seconds", "choose_seconds_per_segment_auto_count"):
|
||||
return "fixed_segment_seconds", self.SPLIT_BY_SECONDS
|
||||
if "seconds per segment" in text or "auto count" in text:
|
||||
return "fixed_segment_seconds", self.SPLIT_BY_SECONDS
|
||||
return "fixed_segment_count", self.SPLIT_BY_SEGMENTS
|
||||
|
||||
def plan_audio_segments(
|
||||
self,
|
||||
audio,
|
||||
fps: float,
|
||||
split_mode: str,
|
||||
segment_count: int,
|
||||
target_segment_seconds: float,
|
||||
overlap_frames: int,
|
||||
ltx_round_mode: str,
|
||||
):
|
||||
audio_duration_s = max(0.01, _audio_duration_seconds(audio))
|
||||
fps = max(0.001, float(fps))
|
||||
split_mode_key, split_mode_label = self._normalize_split_mode(split_mode)
|
||||
requested_segment_count = max(1, int(round(self._first_number(segment_count, 2))))
|
||||
target_segment_seconds = max(0.01, float(self._first_number(target_segment_seconds, 10.0)))
|
||||
|
||||
if split_mode_key == "fixed_segment_seconds":
|
||||
segment_duration_s = target_segment_seconds
|
||||
else:
|
||||
segment_duration_s = max(0.01, audio_duration_s / float(requested_segment_count))
|
||||
|
||||
segment_preset = self._preset_from_seconds(segment_duration_s)
|
||||
planning_mode = "manual_segment_seconds"
|
||||
|
||||
planner_result = self.plan(
|
||||
audio_duration_s,
|
||||
fps,
|
||||
segment_duration_s,
|
||||
planning_mode,
|
||||
segment_preset,
|
||||
int(overlap_frames),
|
||||
str(ltx_round_mode),
|
||||
0,
|
||||
)
|
||||
|
||||
computed_segment_count = int(planner_result[4])
|
||||
report = (
|
||||
f"audio_duration={audio_duration_s:.3f}s @ {fps:.3f}fps | split_mode={split_mode_key} ({split_mode_label}) | "
|
||||
f"requested_segments={requested_segment_count} | computed_segments={computed_segment_count} | "
|
||||
f"segment_duration={segment_duration_s:.3f}s | total={int(planner_result[0])}f | "
|
||||
f"unique={int(planner_result[1])}f | first_raw={int(planner_result[2])}f | "
|
||||
f"continuation_raw={int(planner_result[3])}f | last_unique={int(planner_result[6])}f | "
|
||||
f"preset={segment_preset} | overlap={int(overlap_frames)}f | ltx_round={ltx_round_mode}"
|
||||
)
|
||||
_log.info("[AudioSegmentAutoPlanner] %s", report)
|
||||
|
||||
return (
|
||||
float(audio_duration_s),
|
||||
float(segment_duration_s),
|
||||
planning_mode,
|
||||
segment_preset,
|
||||
int(overlap_frames),
|
||||
computed_segment_count,
|
||||
int(planner_result[0]),
|
||||
int(planner_result[1]),
|
||||
int(planner_result[2]),
|
||||
int(planner_result[3]),
|
||||
int(planner_result[6]),
|
||||
float(planner_result[17]),
|
||||
report,
|
||||
)
|
||||
|
||||
|
||||
class IAMCCS_SegmentPlannerLinked(IAMCCS_SegmentPlanner):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@ class IAMCCS_SuperNodes_SecondStage:
|
||||
"required": {
|
||||
**merge_backend_panel_inputs({
|
||||
"second_stage_mode": (["off", "latent_upscale_refine", "latent_upscale_refine_x2_beta"],),
|
||||
"stage2_model_policy": (["replace_stage1_if_connected", "keep_stage1_model", "prefer_stage2_else_primary"],),
|
||||
"stage2_model_policy": (["replace_stage1_if_connected", "stage2_model_if_connected", "prefer_stage2_else_primary", "keep_stage1_model"],),
|
||||
"second_stage_upscale_model": (_LATENT_UPSCALE_MODEL_NAMES, {"default": _LATENT_UPSCALE_MODEL_NAMES[0]}),
|
||||
"second_stage_reinject_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"second_stage_cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0, "step": 0.1}),
|
||||
@@ -51,7 +51,7 @@ class IAMCCS_SuperNodes_SecondStage:
|
||||
},
|
||||
"optional": {
|
||||
"stage2_model": ("MODEL", {"lazy": True}),
|
||||
"linx": (SUPERNODE_LINX_TYPE,),
|
||||
"linx": (SUPERNODE_LINX_TYPE, {"lazy": True}),
|
||||
"model": ("MODEL", {"lazy": True}),
|
||||
},
|
||||
"hidden": {
|
||||
@@ -59,11 +59,13 @@ class IAMCCS_SuperNodes_SecondStage:
|
||||
},
|
||||
}
|
||||
|
||||
def check_lazy_status(self, second_stage_mode, stage2_model=None, model=None, **kwargs):
|
||||
def check_lazy_status(self, second_stage_mode, stage2_model_policy="replace_stage1_if_connected", stage2_model=None, model=None, linx=None, **kwargs):
|
||||
if str(second_stage_mode) == "off":
|
||||
return []
|
||||
if str(stage2_model_policy) == "keep_stage1_model":
|
||||
return [] if model is not None or linx is not None else ["model", "linx"]
|
||||
needed = []
|
||||
if stage2_model is None:
|
||||
if stage2_model is None and model is None:
|
||||
needed.append("stage2_model")
|
||||
return needed
|
||||
|
||||
@@ -137,7 +139,7 @@ class IAMCCS_SuperNodes_SecondStage:
|
||||
selected_model = primary_model
|
||||
policy = str(stage2_model_policy)
|
||||
if connected_stage2_model is not None:
|
||||
if policy in {"replace_stage1_if_connected", "prefer_stage2_else_primary"}:
|
||||
if policy in {"replace_stage1_if_connected", "stage2_model_if_connected", "prefer_stage2_else_primary"}:
|
||||
selected_model = connected_stage2_model
|
||||
elif selected_model is None:
|
||||
selected_model = connected_stage2_model
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "iamccs-nodes",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.4",
|
||||
"author": "Carmine Cristallo Scalzi (IAMCCS)",
|
||||
"description": "IAMCCS nodes for ComfyUI: IAMCCS echosystem for ComfyUI, nodes 4 LoRA, WAN 2.2, WAN 2.1 and LTX-2 pipelines, WANIMAGEMOTION pro added."
|
||||
}
|
||||
|
||||
+2248
-334
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user