From 9f04f1de0123b24709fe749ea2e7afca1084f5f0 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:56:25 +0300 Subject: [PATCH] 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 --- movie_editor/frontend/engine_settings.js | 24 ++++--- movie_editor/frontend/pipeline_caps.js | 57 ++++++++++------ movie_editor/frontend/pipeline_caps.test.js | 72 +++++++++++++++++++++ 3 files changed, 125 insertions(+), 28 deletions(-) create mode 100644 movie_editor/frontend/pipeline_caps.test.js diff --git a/movie_editor/frontend/engine_settings.js b/movie_editor/frontend/engine_settings.js index 4597d171..b5f82124 100644 --- a/movie_editor/frontend/engine_settings.js +++ b/movie_editor/frontend/engine_settings.js @@ -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; diff --git a/movie_editor/frontend/pipeline_caps.js b/movie_editor/frontend/pipeline_caps.js index 3ce6d68f..86b670fa 100644 --- a/movie_editor/frontend/pipeline_caps.js +++ b/movie_editor/frontend/pipeline_caps.js @@ -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; })(); diff --git a/movie_editor/frontend/pipeline_caps.test.js b/movie_editor/frontend/pipeline_caps.test.js new file mode 100644 index 00000000..3fc04eb0 --- /dev/null +++ b/movie_editor/frontend/pipeline_caps.test.js @@ -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"); +});