fix(onboarding): setup panes open over the wizard instead of replacing it

The wizard sat at z-index 800, above .modal-overlay (500) and below .fw (4000),
so "Open Models…" opened behind it and did nothing visible, and the extras step
worked around that by closing the wizard first — which dropped you into a
half-finished editor. It now sits at 400: Models, Settings and the Composer layer
over it the way they layer over the editor, and closing one returns to the step.

Prompt splits called Composer.open(), which does not exist; the export is toggle().

Also: MiniMax H3 no longer pulls in ComfyUI-LTXVideo. LTXFloatToInt only converted
the project's frame rate for user slots, and H3's rate is fixed at 24 by the model,
so an LTX pack was a hard requirement of a pipeline with no use for it. Core
producers are now filtered by what the family's graph actually built.

The Cutting Room brand links to the hub, as Easy Gen's already did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-08-12 14:41:02 +03:00
co-authored by Claude Opus 5
parent f28e57fbe3
commit 03c0b2dcd9
5 changed files with 40 additions and 5 deletions
+8 -1
View File
@@ -116,6 +116,9 @@ CORE_PRODUCERS: list[tuple[str, int, str]] = [ # (core_id, output_index, type)
# concat and the latent slot feeds the sampler directly.
# * LTXVAudioVAEDecode reads `audio_vae.first_stage_model.output_sample_rate`, which
# H3's audio VAE does not define — it would raise. Core's VAEDecodeAudio is generic.
# * LTXFloatToInt only exists to offer an INT copy of the project's frame rate to user
# slots. H3's frame rate is fixed at 24 by the model, so there is nothing to convert —
# and keeping it made an LTX node pack a hard requirement of a non-LTX pipeline.
#
# Anything not listed here is shared, so a fix to the core graph reaches both families.
DEFAULT_FAMILY = "ltxav"
@@ -124,7 +127,7 @@ FAMILIES: dict[str, dict] = {
"ltxav": {"label": "LTX-2 / LTXAV"},
"minimax_h3": {
"label": "MiniMax H3 (Hailuo)",
"drop": ("cond", "concat"),
"drop": ("cond", "concat", "f2i"),
"core": {"audiodec": "VAEDecodeAudio"},
"links": {
# positive/negative come straight from Studio, with no LTXVConditioning between
@@ -976,7 +979,11 @@ def _resolve_target(target: str, port_to_core, slot_node_id) -> Optional[tuple[s
def _producers(graph, slots, slot_node_id, slot_def, object_info):
"""type -> list of (node_id, output_index). Slots + core producers."""
out: dict[str, list] = {}
# Only what this family's core actually built — offering a producer the graph dropped
# would auto-wire a slot to a node that is not there.
for cid, oidx, t in CORE_PRODUCERS:
if cid not in graph:
continue
out.setdefault(t, []).append((cid, oidx))
for s in slots:
nd = slot_def[s["id"]]
+1 -1
View File
@@ -48,7 +48,7 @@
<body>
<!-- ───────────────────────── MENU BAR ───────────────────────── -->
<nav class="menubar" id="menubar">
<div class="brand"><span class="brand-mark">◉</span> FunPack <span class="brand-sub">Cutting Room</span></div>
<a class="brand" href="/funpack/" title="FunPack Hub"><span class="brand-mark">◉</span> FunPack <span class="brand-sub">Cutting Room</span></a>
<div class="menus" id="menus"><!-- populated by menubar.js --></div>
<div class="menubar-right">
<span class="save-chip" id="save-chip"></span>
+5 -2
View File
@@ -345,7 +345,7 @@
splits: {
title: "Prompt splits",
sub: "Markers that cut one written prompt into separate scenes.",
open: () => window.Composer?.open?.(),
open: () => { if (!window.Composer?.isOpen?.()) window.Composer?.toggle?.(); },
},
};
const list = el("div", "oo-rows");
@@ -359,7 +359,10 @@
row.append(main);
const b = el("button", "oo-btn oo-btn-small", "Set up");
b.type = "button";
b.onclick = () => { save(); close(); r.open(); };
// The pane opens OVER the wizard; closing it puts you back on this step with
// Skip/Continue still there. Closing the wizard here dropped you into the
// editor half-set-up.
b.onclick = () => { save(); r.open(); };
row.append(b);
list.append(row);
});
+8 -1
View File
@@ -293,6 +293,9 @@ button, input, select, textarea { font-family: inherit; color: var(--text); }
.brand { font-family: var(--font-display); font-weight: 800; letter-spacing: .2px; font-size: 16px; display: flex; align-items: center; gap: 8px; }
.brand-mark { color: var(--accent); font-size: 12px; transform: translateY(-1px); filter: drop-shadow(0 0 6px var(--accent-soft)); }
.brand-sub { color: var(--faint); font-weight: 500; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; }
/* The brand is a link back to the hub in both frontends. */
a.brand { text-decoration: none; color: inherit; }
a.brand:hover .brand-sub { color: var(--accent-hi); }
.menus { display: flex; gap: 2px; }
.menu { position: relative; }
@@ -2153,7 +2156,11 @@ body.tour-mode { overflow: hidden; }
}
.oo-root {
position: fixed; inset: 0; z-index: 800;
/* Below .modal-overlay (500) and .fw (4000) on purpose: Models, Settings and the
Composer open OVER the wizard the same way they open over the editor, and closing
one puts you back on the step you left. Above the menubar (200/300) so the editor
chrome behind stays covered. */
position: fixed; inset: 0; z-index: 400;
display: flex; align-items: center; justify-content: center;
padding: 72px 32px;
background:
+18
View File
@@ -493,11 +493,29 @@ def test_h3_family_drops_the_ltx_only_core_nodes():
# LTXVAudioVAEDecode reads an output_sample_rate H3's audio VAE does not define
assert "LTXVAudioVAEDecode" not in classes
assert "VAEDecodeAudio" in classes
# LTXFloatToInt only converted the project's frame rate for user slots. H3's rate is
# fixed at 24 by the model, so keeping it made an LTX pack a hard requirement of a
# pipeline that has no use for it.
assert "LTXFloatToInt" not in classes
# the shared half is untouched
assert {"FunPackStudio", "FunPackLTXAVSceneChainSampler", "LTXVSeparateAVLatent"} <= classes
assert not report["blocking"], report["blocking"]
def test_h3_does_not_offer_the_dropped_frame_rate_converter_as_a_producer():
"""A dropped core node must not be auto-wired into a slot that wants an INT."""
models = dict(H3_MODELS)
models["slots"] = H3_MODELS["slots"] + [
{"id": "ip", "role": "image_processing", "node_class": "IntEater", "label": "eats an int"},
]
oi = dict(H3_OI)
oi["IntEater"] = {"input": {"required": {"n": ["INT", {"default": 0}]}}, "output": ["IMAGE"]}
graph, _report = builder.build(oi, models, {"prompt": "a shot"})
for node in graph.values():
for value in node["inputs"].values():
assert not (isinstance(value, list) and value and value[0] == "f2i"), value
def test_h3_wires_studio_conditioning_straight_to_the_sampler():
graph, _ = builder.build(H3_OI, H3_MODELS, {"prompt": "a shot"})
sampler = graph["sampler"]["inputs"]