feat(editor): hide settings the loaded model cannot use
Family is known at loader-save, so Engine Settings drops H3-dead knobs (and LTX-dead h3_audio_clock) instead of explaining them per run. Values survive a family switch; the now-redundant main-window chips are removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d77b3e2eed
commit
9f04f1de01
@@ -385,8 +385,12 @@
|
||||
return value !== undefined ? depVal === value : !!depVal;
|
||||
}
|
||||
|
||||
function knobVisible(k, si) {
|
||||
function knobVisible(k, si, st) {
|
||||
if (EASY() && RATING_GATED_KNOBS.has(k.name)) return false;
|
||||
// The loaded model cannot use this one. Hide it rather than offer a control and then
|
||||
// explain per run that it does nothing — what is not wired does not appear. The stored
|
||||
// value survives, so switching family back brings the setting back with it.
|
||||
if (st && window.PipelineCaps?.familyInertInputs(st).has(k.name)) return false;
|
||||
// dependsOn/dependsValue: single condition (dependsValue absent = plain truthy
|
||||
// gate, as every existing boolean dependsOn already relies on).
|
||||
if (k.dependsOn && !_depSatisfied(k.dependsOn, k.dependsValue, si)) return false;
|
||||
@@ -402,7 +406,7 @@
|
||||
}
|
||||
|
||||
function renderSamplerKnob(parentGroup, st, k, si, multiScene) {
|
||||
if (!knobVisible(k, si)) return;
|
||||
if (!knobVisible(k, si, st)) return;
|
||||
const val = si[k.name] != null ? si[k.name] : k.default;
|
||||
const gs = normGuideSettings(st.project);
|
||||
const cs = normContinuitySettings(st.project);
|
||||
@@ -467,11 +471,15 @@
|
||||
chain_experimental: ["context_windows", "context_window_length", "context_window_overlap", "context_window_schedule", "context_window_fuse", "context_window_freenoise", "context_window_retain_first", "plateau_cache", "plateau_cache_threshold", "h3_audio_clock", "segmented_detailing", "detail_targets", "detail_strength", "detail_threshold", "detail_max_area", "detail_mode", "detail_denoise", "mid_scene_guide", "mid_scene_guide_strength", "joyai_memory", "joyai_memory_size", "joyai_fix_frames", "joyai_frame_select", "joyai_memory_strength", "joyai_audio_memory", "v2a_grad_scale", "alg_blur_guides", "alg_guide_blur_strength", "alg_guide_blur_sigma_threshold", "bounded_attention_enabled", "identity_transfer_enabled", "source_id", "phase_scale", "id_strength", "arcface_mode", "debug_log"],
|
||||
};
|
||||
|
||||
function countChainView(p, id) {
|
||||
function countChainView(p, id, st) {
|
||||
const si = p.sampler_inputs || {};
|
||||
// A knob the model cannot use is hidden, so a badge counting it would advertise a
|
||||
// change the user cannot see or undo from this pane.
|
||||
const inert = st ? (window.PipelineCaps?.familyInertInputs(st) || new Set()) : new Set();
|
||||
let n = 0;
|
||||
(CHAIN_VIEW_KNOBS[id] || []).forEach((name) => {
|
||||
if (EASY() && RATING_GATED_KNOBS.has(name)) return;
|
||||
if (inert.has(name)) return;
|
||||
const k = SAMPLER_KNOB_MAP[name];
|
||||
if (k && si[name] != null && si[name] !== k.default) n++;
|
||||
});
|
||||
@@ -494,11 +502,11 @@
|
||||
}
|
||||
if (chainOn) {
|
||||
out.push(
|
||||
{ id: "chain_continuity", group: "Chain Sampler", title: "Continuity", icon: "∞", badge: countChainView(p, "chain_continuity") || null },
|
||||
{ id: "chain_timing", group: "Chain Sampler", title: "Timing & Seed", icon: "⏱", badge: countChainView(p, "chain_timing") || null },
|
||||
{ id: "chain_guidance", group: "Chain Sampler", title: "Guidance", icon: "◇", badge: countChainView(p, "chain_guidance") || null },
|
||||
{ id: "chain_decode", group: "Chain Sampler", title: "Decode", icon: "▣", badge: countChainView(p, "chain_decode") || null },
|
||||
{ id: "chain_experimental", group: "Chain Sampler", title: "Experimental", icon: "⚗", badge: countChainView(p, "chain_experimental") || null },
|
||||
{ id: "chain_continuity", group: "Chain Sampler", title: "Continuity", icon: "∞", badge: countChainView(p, "chain_continuity", st) || null },
|
||||
{ id: "chain_timing", group: "Chain Sampler", title: "Timing & Seed", icon: "⏱", badge: countChainView(p, "chain_timing", st) || null },
|
||||
{ id: "chain_guidance", group: "Chain Sampler", title: "Guidance", icon: "◇", badge: countChainView(p, "chain_guidance", st) || null },
|
||||
{ id: "chain_decode", group: "Chain Sampler", title: "Decode", icon: "▣", badge: countChainView(p, "chain_decode", st) || null },
|
||||
{ id: "chain_experimental", group: "Chain Sampler", title: "Experimental", icon: "⚗", badge: countChainView(p, "chain_experimental", st) || null },
|
||||
);
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -149,29 +149,40 @@
|
||||
+ "schedules. LTX puts both streams on one schedule, so there is nothing to correct.",
|
||||
};
|
||||
|
||||
// Sub-settings of identity transfer: meaningless wherever the feature itself cannot run,
|
||||
// so they travel with it rather than each needing its own entry above.
|
||||
const IDENTITY_SUB_INPUTS = ["source_id", "phase_scale", "id_strength", "arcface_mode"];
|
||||
|
||||
// Non-boolean knobs H3 cannot use. v2a_grad_scale hooks LTXAV's video_to_audio_attn
|
||||
// submodule; H3 has no separate video->audio cross-attention to scale, and the sampler
|
||||
// forces it back to 1.0. Its parent feature (joyai_audio_memory) DOES work on H3, so only
|
||||
// the coupling knob goes.
|
||||
const H3_DEAD_VALUE_INPUTS = ["v2a_grad_scale"];
|
||||
|
||||
// The sampler inputs the LOADED model cannot use. Engine Settings hides these outright
|
||||
// rather than offering a control and explaining once per run that it does nothing — a
|
||||
// toggle that is not offered needs no explanation. The stored value is left untouched, so
|
||||
// switching family back restores the user's setting instead of silently resetting it.
|
||||
//
|
||||
// Safe because the sampler already forces every one of these off for the family in
|
||||
// question, so hiding removes a control that was inert either way — it never makes a
|
||||
// working knob unreachable.
|
||||
function familyInertInputs(st) {
|
||||
if (!usesChainSampler(st)) return new Set();
|
||||
if (!isH3(st)) return new Set(Object.keys(H3_ONLY_SAMPLER_INPUTS));
|
||||
return new Set([...Object.keys(H3_DEAD_SAMPLER_INPUTS), ...IDENTITY_SUB_INPUTS,
|
||||
...H3_DEAD_VALUE_INPUTS]);
|
||||
}
|
||||
|
||||
// Returns [{short, detail}] for settings that are ON but cannot do anything on H3.
|
||||
function h3InertSettings(st) {
|
||||
const p = st && st.project;
|
||||
if (!p || !usesChainSampler(st)) return [];
|
||||
const si = p.sampler_inputs || {};
|
||||
if (!isH3(st)) {
|
||||
return Object.keys(H3_ONLY_SAMPLER_INPUTS)
|
||||
.filter((key) => si[key])
|
||||
.map((key) => ({
|
||||
short: key.replace(/_/g, " ") + " needs MiniMax H3",
|
||||
detail: H3_ONLY_SAMPLER_INPUTS[key]
|
||||
+ " Turn it off in Settings → Engine to stop it showing here.",
|
||||
}));
|
||||
}
|
||||
const issues = [];
|
||||
Object.keys(H3_DEAD_SAMPLER_INPUTS).forEach((key) => {
|
||||
if (si[key]) issues.push([key, H3_DEAD_SAMPLER_INPUTS[key]]);
|
||||
});
|
||||
issues.push(...h3DeadValueIssues(si));
|
||||
const out = issues.map(([key, detail]) => ({
|
||||
short: "H3: " + key.replace(/_enabled$/, "").replace(/_/g, " ") + " can't run",
|
||||
detail: detail + " Turn it off in Settings → Engine to stop it showing here.",
|
||||
}));
|
||||
// Everything family-inert is now HIDDEN from Engine Settings, so a chip here would
|
||||
// point at a control the user cannot find ("turn it off in Settings → Engine" names a
|
||||
// row that is not rendered). What is left is the one issue hiding cannot express: a
|
||||
// frame rate the model will not honour, which is a value the user must change.
|
||||
const out = [];
|
||||
const fps = frameRateIssue(st);
|
||||
if (fps) out.push(fps);
|
||||
return out;
|
||||
@@ -297,7 +308,7 @@
|
||||
return map[type] || type;
|
||||
}
|
||||
|
||||
window.PipelineCaps = {
|
||||
const api = {
|
||||
usesFunpackStudio,
|
||||
usesChainSampler,
|
||||
caps,
|
||||
@@ -309,6 +320,7 @@
|
||||
snapFramesIfRequired,
|
||||
frameRateIssue,
|
||||
h3InertSettings,
|
||||
familyInertInputs,
|
||||
effectiveSourceType,
|
||||
isChainOnlySource,
|
||||
isT2V,
|
||||
@@ -319,4 +331,9 @@
|
||||
promptSourceIssue,
|
||||
sourceLabel,
|
||||
};
|
||||
// Node can require this for the pure predicates (familyInertInputs, snapFramesTo, …);
|
||||
// the browser keeps the window global. `window` does not exist under Node, so neither
|
||||
// assignment may assume the other's environment.
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||
if (typeof window !== "undefined") window.PipelineCaps = api;
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Which sampler settings the loaded model cannot use (pipeline_caps.js).
|
||||
// Run: node --test movie_editor/frontend/pipeline_caps.test.js
|
||||
//
|
||||
// The rule these guard: a feature the model cannot run DISAPPEARS from Engine Settings
|
||||
// rather than being offered and then explained away once per generation. So the predicate
|
||||
// has to be exactly right in both directions — hiding a knob that works is worse than
|
||||
// showing one that doesn't.
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
|
||||
const PC = require("./pipeline_caps.js");
|
||||
|
||||
const h3 = { models: { model_family: "minimax_h3" }, project: {} };
|
||||
const ltx = { models: { model_family: "ltxav" }, project: {} };
|
||||
|
||||
test("H3 hides the features that need LTX cross-attention", () => {
|
||||
const inert = PC.familyInertInputs(h3);
|
||||
assert.ok(inert.has("bounded_attention_enabled"));
|
||||
assert.ok(inert.has("identity_transfer_enabled"));
|
||||
});
|
||||
|
||||
test("H3 hides identity transfer's sub-settings with it, not just the toggle", () => {
|
||||
const inert = PC.familyInertInputs(h3);
|
||||
for (const k of ["source_id", "phase_scale", "id_strength", "arcface_mode"]) {
|
||||
assert.ok(inert.has(k), `${k} should travel with its parent feature`);
|
||||
}
|
||||
});
|
||||
|
||||
test("H3 hides v2a_grad_scale but keeps the feature it couples to", () => {
|
||||
const inert = PC.familyInertInputs(h3);
|
||||
assert.ok(inert.has("v2a_grad_scale"));
|
||||
// JoyAI audio memory itself works on H3 — only the LTXAV coupling knob is dead.
|
||||
assert.ok(!inert.has("joyai_audio_memory"));
|
||||
});
|
||||
|
||||
test("LTX hides the H3-only audio clock", () => {
|
||||
const inert = PC.familyInertInputs(ltx);
|
||||
assert.ok(inert.has("h3_audio_clock"));
|
||||
});
|
||||
|
||||
test("neither family hides the other's live settings", () => {
|
||||
const onH3 = PC.familyInertInputs(h3);
|
||||
const onLtx = PC.familyInertInputs(ltx);
|
||||
assert.ok(!onH3.has("h3_audio_clock"), "the audio clock is the point of H3");
|
||||
assert.ok(!onLtx.has("bounded_attention_enabled"), "bounded attention is an LTX feature");
|
||||
assert.ok(!onLtx.has("identity_transfer_enabled"), "Best-FaceID is an LTX feature");
|
||||
});
|
||||
|
||||
test("nothing is hidden when the chain sampler is not in the pipeline", () => {
|
||||
// Without the Chain Sampler these knobs are inert for a different reason, and the
|
||||
// pipeline warning already covers it — hiding here would attribute it to the model.
|
||||
const off = { models: { model_family: "minimax_h3", disable_core: true }, project: {} };
|
||||
assert.strictEqual(PC.familyInertInputs(off).size, 0);
|
||||
});
|
||||
|
||||
test("an unknown family is treated as LTX, never as 'hide everything'", () => {
|
||||
const inert = PC.familyInertInputs({ models: { model_family: "wan2.2" }, project: {} });
|
||||
assert.ok(!inert.has("bounded_attention_enabled"));
|
||||
assert.ok(inert.has("h3_audio_clock"));
|
||||
});
|
||||
|
||||
test("the main-window chips no longer name a control that is now hidden", () => {
|
||||
// Every family-inert setting is hidden, so "turn it off in Settings → Engine" would
|
||||
// point at a row that is not rendered. Only value-shaped issues survive there.
|
||||
const st = {
|
||||
models: { model_family: "minimax_h3" },
|
||||
project: { sampler_inputs: { bounded_attention_enabled: true, identity_transfer_enabled: true } },
|
||||
};
|
||||
const chips = PC.h3InertSettings(st);
|
||||
assert.ok(!chips.some((c) => /can't run/.test(c.short)),
|
||||
"a hidden setting should not also be chipped");
|
||||
});
|
||||
Reference in New Issue
Block a user