Phase 3 of #3100 (Phase 1 — Control — shipped; see that PR). This is the only remaining phase that needs real new code, because Ingredients is multi-reference and its weight lives in a hostile repo.
The backbone from Phase 1 already handles most of it: --mode ic / run_ic_lora() in scripts/generate_ltx2.py accepts a repeatable --ic-reference and already carries IC_REFERENCE_LIMITS['ingredients'] = (2, 8); icLoraArgs in server/services/videoGen/local.js validates against the registry's min/max and emits one --ic-reference per clip; the route already accepts icReferenceVideoIds as a 1-8 array.
Work:
- Weight provisioning is the hard part. Upstream sources
ltx-2.3-22b-ic-lora-ingredients-0.9.safetensors (~1.3 GB) from the DeepBeepMeep/LTX-2 mirror, which is a ~708 GB mega-repo — a snapshot download would be catastrophic. Requirements:
- Try the official
Lightricks/LTX-2.3-22b-IC-LoRA-Ingredients first; only fall back to the mirror.
- The fallback MUST be a single-file
hf_hub_download for the exact filename, never snapshot_download. server/lib/hfDownload.js currently spawns scripts/hf_download_repo.py, which enumerates and pulls the whole repo (it has --ignore globs but no single-file mode) — so this needs either a --only <filename> flag on that helper or a dedicated single-file path. Do not wire the mirror up until the single-file path exists and is tested; a mis-fire fills the user's disk.
resolveIcLoraWeight in server/lib/icLoraWeights.js already pins the exact filename inside the snapshot and falls back to the repo id when absent. That repo-id fallback hands the bare id to ICLoraPipeline._resolve_lora_path, which calls snapshot_download — so for the mirror the fallback must be suppressed (fail with a clear "download the weight first" error instead).
- Reference kind is images, not video. Ingredients recomposes from 2-8 stills. That means
referenceKind: 'image' in the registry, and a different input surface: the route's icReference upload field is video/*-filtered and icReferenceVideoIds resolves against video history. Add an image path (gallery-file resolution mirroring keyframes, which already resolves 2-8 gallery images) rather than overloading the video fields.
- Client panel.
IcLoraPanel.jsx renders a single reference; Ingredients needs a 2-8 row list. KeyframePanel.jsx is the existing multi-row gallery-picker to model it on. Keep the single-reference path unchanged.
- LoRA stacking is the payoff — the Phase 1 split already supports it: the IC weight rides
lora_paths (fused by _fuse_loras pre-Stage-1) while user LoRAs ride _pending_loras, so an Ingredients x Character stack composes rather than replaces. Add a test that asserts both channels are present (server/services/videoGen/local.test.js has the Control version of this assertion).
- Test the 2-8 bounds at all three layers (Python
IC_REFERENCE_LIMITS, icLoraArgs, route) — the counts are a weight contract, and a wrong count yields plausible-looking garbage rather than an error.
Validate on a real render before closing, and confirm the weight is un-gated (no HF token) as the Control and Colorize weights are.
Phase 3 of #3100 (Phase 1 — Control — shipped; see that PR). This is the only remaining phase that needs real new code, because Ingredients is multi-reference and its weight lives in a hostile repo.
The backbone from Phase 1 already handles most of it:
--mode ic/run_ic_lora()inscripts/generate_ltx2.pyaccepts a repeatable--ic-referenceand already carriesIC_REFERENCE_LIMITS['ingredients'] = (2, 8);icLoraArgsinserver/services/videoGen/local.jsvalidates against the registry's min/max and emits one--ic-referenceper clip; the route already acceptsicReferenceVideoIdsas a 1-8 array.Work:
ltx-2.3-22b-ic-lora-ingredients-0.9.safetensors(~1.3 GB) from theDeepBeepMeep/LTX-2mirror, which is a ~708 GB mega-repo — a snapshot download would be catastrophic. Requirements:Lightricks/LTX-2.3-22b-IC-LoRA-Ingredientsfirst; only fall back to the mirror.hf_hub_downloadfor the exact filename, neversnapshot_download.server/lib/hfDownload.jscurrently spawnsscripts/hf_download_repo.py, which enumerates and pulls the whole repo (it has--ignoreglobs but no single-file mode) — so this needs either a--only <filename>flag on that helper or a dedicated single-file path. Do not wire the mirror up until the single-file path exists and is tested; a mis-fire fills the user's disk.resolveIcLoraWeightinserver/lib/icLoraWeights.jsalready pins the exact filename inside the snapshot and falls back to the repo id when absent. That repo-id fallback hands the bare id toICLoraPipeline._resolve_lora_path, which callssnapshot_download— so for the mirror the fallback must be suppressed (fail with a clear "download the weight first" error instead).referenceKind: 'image'in the registry, and a different input surface: the route'sicReferenceupload field isvideo/*-filtered andicReferenceVideoIdsresolves against video history. Add an image path (gallery-file resolution mirroringkeyframes, which already resolves 2-8 gallery images) rather than overloading the video fields.IcLoraPanel.jsxrenders a single reference; Ingredients needs a 2-8 row list.KeyframePanel.jsxis the existing multi-row gallery-picker to model it on. Keep the single-reference path unchanged.lora_paths(fused by_fuse_loraspre-Stage-1) while user LoRAs ride_pending_loras, so an Ingredients x Character stack composes rather than replaces. Add a test that asserts both channels are present (server/services/videoGen/local.test.jshas the Control version of this assertion).IC_REFERENCE_LIMITS,icLoraArgs, route) — the counts are a weight contract, and a wrong count yields plausible-looking garbage rather than an error.Validate on a real render before closing, and confirm the weight is un-gated (no HF token) as the Control and Colorize weights are.