Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
8.1 KiB
Python
137 lines
8.1 KiB
Python
"""LTX 2.3 pipelines, as loadable presets.
|
|
|
|
Built from ComfyUI's own LTX nodes plus FunPack's, the chain the stock templates use: the
|
|
checkpoint carries the model and the picture VAE, the audio VAE and the text encoder are loaded
|
|
from the same file, conditioning carries the frame rate, and the sampler runs the distilled
|
|
schedule written out as numbers (`project_sigmas`: the official anchors, never replaced).
|
|
|
|
The checkpoint is picked three times (model, audio VAE, text encoder): they are three nodes that
|
|
each read a file, and the first two are the same file.
|
|
|
|
Every `frame_rate` and `fps` is a role at `project.video`, so the project's FPS is the one number
|
|
the conditioning, the sound's length and the saved video agree on.
|
|
"""
|
|
|
|
#: The distilled 8-step schedule, from the official ComfyUI LTX 2.x templates.
|
|
DISTILLED_SIGMAS = "1.0, 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0"
|
|
|
|
|
|
def _base(image_to_video: bool, as_guide: bool = False):
|
|
video = {"at": "project.video", "input": "frame_rate", "label": "FPS", "drives": "fps"}
|
|
latent_source = ["latent", 0]
|
|
model_source, positive_source, negative_source = ["modifiers", 0], ["conditioning", 0], ["conditioning", 1]
|
|
slots = [
|
|
{"id": "model", "group": "Loaders", "node": "FunPackCheckpointLoader", "inputs": {"ckpt_name": ""}},
|
|
{"id": "audio_vae", "group": "Loaders", "node": "LTXVAudioVAELoader", "inputs": {"ckpt_name": ""}},
|
|
{"id": "clip", "group": "Loaders", "node": "LTXAVTextEncoderLoader",
|
|
"inputs": {"text_encoder": "", "ckpt_name": "", "device": "default"}},
|
|
|
|
{"id": "positive", "group": "Preparation", "node": "CLIPTextEncode",
|
|
"roles": [{"at": "generation.prompt", "input": "text", "label": "Prompt"}],
|
|
"inputs": {"clip": ["clip", 0], "text": ""}},
|
|
{"id": "negative", "group": "Preparation", "node": "CLIPTextEncode",
|
|
"roles": [{"at": "project.negative", "input": "text", "label": "Negative prompt"}],
|
|
"inputs": {"clip": ["clip", 0], "text": ""}},
|
|
{"id": "conditioning", "group": "Preparation", "node": "LTXVConditioning",
|
|
"roles": [video],
|
|
"inputs": {"positive": ["positive", 0], "negative": ["negative", 0], "frame_rate": 25.0}},
|
|
|
|
{"id": "latent", "group": "Preparation", "node": "FunPackEmptyLatent",
|
|
"roles": [{"at": "project.video", "input": "width", "label": "Width"},
|
|
{"at": "project.video", "input": "height", "label": "Height"},
|
|
{"at": "project.video", "input": "length", "label": "Length", "drives": "frames"},
|
|
video],
|
|
"inputs": {"model": ["model", 0], "audio_vae": ["audio_vae", 0],
|
|
"width": 768, "height": 512, "length": 121, "frame_rate": 25.0, "batch_size": 1}},
|
|
]
|
|
if image_to_video:
|
|
slots += [
|
|
{"id": "source_image", "group": "Reference media", "node": "FunPackLoadMedia",
|
|
"roles": [{"at": "assets.source_image", "input": "media_id"}],
|
|
"inputs": {"media_id": ""}},
|
|
{"id": "split", "group": "Preparation", "node": "LTXVSeparateAVLatent",
|
|
"inputs": {"av_latent": ["latent", 0]}},
|
|
]
|
|
if as_guide:
|
|
# The picture steers through guide attention at frame 0 and is NOT the clip's first frame:
|
|
# the latent stays empty, so the opening is free to move. Strength is the slot's own number.
|
|
slots += [
|
|
{"id": "anchor", "group": "Preparation", "node": "LTXVAddGuide",
|
|
"inputs": {"positive": ["conditioning", 0], "negative": ["conditioning", 1], "vae": ["model", 2],
|
|
"latent": ["split", 0], "image": ["source_image", 0], "frame_idx": 0, "strength": 0.35}},
|
|
{"id": "join", "group": "Preparation", "node": "LTXVConcatAVLatent",
|
|
"inputs": {"video_latent": ["anchor", 2], "audio_latent": ["split", 1]}},
|
|
]
|
|
guided = (["anchor", 0], ["anchor", 1])
|
|
else:
|
|
slots += [
|
|
{"id": "anchor", "group": "Preparation", "node": "LTXVImgToVideoInplace",
|
|
"inputs": {"vae": ["model", 2], "image": ["source_image", 0], "latent": ["split", 0],
|
|
"strength": 1.0, "bypass": False}},
|
|
{"id": "join", "group": "Preparation", "node": "LTXVConcatAVLatent",
|
|
"inputs": {"video_latent": ["anchor", 0], "audio_latent": ["split", 1]}},
|
|
]
|
|
guided = (["conditioning", 0], ["conditioning", 1])
|
|
latent_source = ["join", 0]
|
|
positive_source, negative_source = guided
|
|
slots += [
|
|
{"id": "settings", "group": "Preparation", "node": "FunPackModifierSettings", "inputs": {"settings": "{}"}},
|
|
{"id": "modifiers", "group": "Preparation", "node": "FunPackLoadModifiers",
|
|
"inputs": {"model": ["model", 0], "settings": ["settings", 0]}},
|
|
]
|
|
if image_to_video:
|
|
slots += [
|
|
# A reference face carried into the clip. The picture is the scene's first reference
|
|
# (assets.reference_1): with none picked this node passes everything through untouched.
|
|
{"id": "identity", "group": "Preparation", "node": "FunPackIdentityTransfer",
|
|
"inputs": {"model": ["modifiers", 0], "vae": ["model", 2], "positive": positive_source,
|
|
"negative": negative_source, "latent": latent_source, "source_id": 2.0,
|
|
"phase_scale": 1.0, "id_strength": 1.0, "arcface_mode": "auto_adjust"}},
|
|
{"id": "identity_source", "group": "Reference media", "node": "FunPackLoadMedia",
|
|
"roles": [{"at": "assets.reference_1", "input": "media_id",
|
|
"wireTo": {"slot": "identity", "input": "image"}}],
|
|
"inputs": {"media_id": ""}},
|
|
]
|
|
model_source, positive_source, negative_source = ["identity", 0], ["identity", 1], ["identity", 2]
|
|
slots += [
|
|
{"id": "sigmas", "group": "Sampling", "node": "ManualSigmas", "inputs": {"sigmas": DISTILLED_SIGMAS}},
|
|
{"id": "sampler", "group": "Sampling", "node": "FunPackSampler",
|
|
"roles": [{"at": "generation.sampling", "input": "sampler_name", "label": "Sampler"},
|
|
{"at": "generation.seed", "input": "seed", "label": "Seed"}],
|
|
"inputs": {"model": model_source, "positive": positive_source, "negative": negative_source,
|
|
"latent": latent_source, "settings": ["settings", 0], "sigmas": ["sigmas", 0],
|
|
"seed": 0, "steps": 8, "cfg": 1.0, "sampler_name": "euler", "scheduler": "normal",
|
|
"denoise": 1.0}},
|
|
|
|
*_uncrop(as_guide),
|
|
{"id": "decode", "group": "Render", "node": "FunPackDecode", "inputs": {
|
|
"samples": ["rejoin", 0] if as_guide else ["sampler", 0], "vae": ["model", 2], "model": ["model", 0], "audio_vae": ["audio_vae", 0]}},
|
|
{"id": "save", "group": "Render", "node": "FunPackSaveVideo",
|
|
"roles": [{"at": "project.video", "input": "fps", "label": "FPS", "drives": "fps"}],
|
|
"inputs": {"images": ["decode", 0], "audio": ["decode", 1], "fps": 25.0, "filename_prefix": "FunPack"}},
|
|
]
|
|
return slots
|
|
|
|
|
|
def _uncrop(as_guide):
|
|
"""A guide sits in the latent as extra frames: take them off again before the picture is decoded."""
|
|
if not as_guide:
|
|
return []
|
|
return [
|
|
{"id": "unjoin", "group": "Render", "node": "LTXVSeparateAVLatent", "inputs": {"av_latent": ["sampler", 0]}},
|
|
{"id": "crop", "group": "Render", "node": "LTXVCropGuides",
|
|
"inputs": {"positive": ["identity", 1], "negative": ["identity", 2], "latent": ["unjoin", 0]}},
|
|
{"id": "rejoin", "group": "Render", "node": "LTXVConcatAVLatent",
|
|
"inputs": {"video_latent": ["crop", 2], "audio_latent": ["unjoin", 1]}},
|
|
]
|
|
|
|
|
|
def presets():
|
|
return [{"id": "ltx23_text_to_video", "title": "LTX 2.3 · Text to Video", "slots": _base(False)},
|
|
{"id": "ltx23_image_to_video", "title": "LTX 2.3 · Image to Video", "slots": _base(True)},
|
|
{"id": "ltx23_anchor_guide", "title": "LTX 2.3 · Anchor as guide (image steers, opening stays free)",
|
|
"slots": _base(True, True)}]
|
|
|
|
|
|
PROVIDES = {"pipeline_presets": presets}
|