feat: add external timeline nodes
This commit is contained in:
@@ -33,6 +33,7 @@ from .prompt_relay import (
|
||||
|
||||
from .patches import detect_model_type, apply_patches
|
||||
from .msr_character import MSRCharacterSetData
|
||||
from .timeline_nodes import TimelineData
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -166,6 +167,47 @@ def _load_image_reference(b64_or_url: str = "", filename: str = None, cache: dic
|
||||
return _empty_image_tensor()
|
||||
|
||||
|
||||
def _build_runtime_timeline_payload(external_timeline, frame_rate: float) -> tuple[dict, str, str, float, int]:
|
||||
tdata = {
|
||||
"segments": [],
|
||||
"motionSegments": [],
|
||||
"audioSegments": [],
|
||||
}
|
||||
if not isinstance(external_timeline, dict):
|
||||
return tdata, "", "", 0.0, 0
|
||||
|
||||
sections = external_timeline.get("sections") or []
|
||||
duration_seconds = max(0.1, float(external_timeline.get("duration") or 0.0))
|
||||
duration_frames = max(1, int(round(duration_seconds * frame_rate)))
|
||||
current_start = 0
|
||||
local_prompts = []
|
||||
segment_lengths = []
|
||||
|
||||
for index, section in enumerate(sections):
|
||||
if not isinstance(section, dict):
|
||||
continue
|
||||
prompt = (section.get("text") or "").strip()
|
||||
if not prompt:
|
||||
continue
|
||||
section_seconds = max(0.1, float(section.get("duration") or 0.0))
|
||||
section_frames = max(1, int(round(section_seconds * frame_rate)))
|
||||
seg = {
|
||||
"id": f"external_{index}",
|
||||
"start": current_start,
|
||||
"length": section_frames,
|
||||
"prompt": prompt,
|
||||
"type": "image" if section.get("image") is not None else "text",
|
||||
}
|
||||
if section.get("image") is not None:
|
||||
seg["runtime_image"] = section.get("image")
|
||||
tdata["segments"].append(seg)
|
||||
local_prompts.append(prompt)
|
||||
segment_lengths.append(str(section_frames))
|
||||
current_start += section_frames
|
||||
|
||||
return tdata, " | ".join(local_prompts), ",".join(segment_lengths), duration_seconds, duration_frames
|
||||
|
||||
|
||||
def _compute_signal_peaks(samples: np.ndarray, num_peaks: int = PEAK_BUCKETS) -> list[float]:
|
||||
if samples.size == 0:
|
||||
return [0.0] * num_peaks
|
||||
@@ -1048,6 +1090,8 @@ async def ltx_director_upload_chunk(request):
|
||||
def _load_image_tensor(seg: dict, cache: dict | None = None, input_dir: str = None) -> torch.Tensor:
|
||||
"""Decode an image from the ComfyUI input folder (if imageFile provided) or fallback to base64
|
||||
to a ComfyUI-style image tensor of shape [1, H, W, 3], float32 in [0, 1]."""
|
||||
if seg.get("runtime_image") is not None:
|
||||
return seg.get("runtime_image")
|
||||
return _load_image_reference(
|
||||
b64_or_url=seg.get("imageB64", ""),
|
||||
filename=seg.get("imageFile"),
|
||||
@@ -2023,6 +2067,11 @@ class LTXDirector(io.ComfyNode):
|
||||
optional=True,
|
||||
tooltip="Optional external MSR Character Set. Replaces the legacy inline character panel in the director UI.",
|
||||
),
|
||||
TimelineData.Input(
|
||||
"timeline",
|
||||
optional=True,
|
||||
tooltip="Optional external Timeline DUMAS payload. When connected, it populates the main timeline track and duration.",
|
||||
),
|
||||
io.Float.Input(
|
||||
"start", force_input=True, optional=True, default=0.0,
|
||||
tooltip="Automation (connection-only). Start time in SECONDS. Overrides the panel Start when connected.",
|
||||
@@ -2058,7 +2107,7 @@ class LTXDirector(io.ComfyNode):
|
||||
custom_width=768, custom_height=512, resize_method="maintain aspect ratio",
|
||||
divisible_by=32, img_compression=0, audio_vae=None, optional_latent=None,
|
||||
use_custom_audio=False, inpaint_audio=True, use_custom_motion=True, override_audio=False,
|
||||
vae=None, reference_strength=1.0, ref_images=None, character_set=None,
|
||||
vae=None, reference_strength=1.0, ref_images=None, character_set=None, timeline=None,
|
||||
start=None, end=None, duration=None) -> io.NodeOutput:
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
image_cache = {}
|
||||
@@ -2075,6 +2124,26 @@ class LTXDirector(io.ComfyNode):
|
||||
processed_image_cache[cache_key] = processed
|
||||
return processed
|
||||
|
||||
if timeline is not None:
|
||||
external_tdata, external_local_prompts, external_segment_lengths, external_duration_seconds, external_duration_frames = _build_runtime_timeline_payload(
|
||||
timeline,
|
||||
float(frame_rate) if frame_rate else 24.0,
|
||||
)
|
||||
try:
|
||||
existing_tdata = _safe_json_loads(timeline_data)
|
||||
except Exception:
|
||||
existing_tdata = {}
|
||||
merged_tdata = dict(existing_tdata or {})
|
||||
merged_tdata["segments"] = external_tdata["segments"]
|
||||
merged_tdata.setdefault("motionSegments", existing_tdata.get("motionSegments", []))
|
||||
merged_tdata.setdefault("audioSegments", existing_tdata.get("audioSegments", []))
|
||||
timeline_data = json.dumps(merged_tdata)
|
||||
local_prompts = external_local_prompts
|
||||
segment_lengths = external_segment_lengths
|
||||
if duration is None:
|
||||
duration_seconds = external_duration_seconds
|
||||
duration_frames = external_duration_frames
|
||||
|
||||
# Parse timeline data
|
||||
try:
|
||||
tdata = _safe_json_loads(timeline_data)
|
||||
|
||||
Reference in New Issue
Block a user