Movie Editor: expose Studio/Sampler settings in project inspector

- Project now stores negative_prompt, sampler_inputs, studio_inputs
- Builder applies sampler_inputs/studio_inputs as widget overrides on the
  ChainSampler and Studio nodes (only when funpack slot is selected)
- Inspector renderProject shows: negative prompt textarea + full sampler knobs
  (cfg, frame_overlap, carry_i2v_guides, mid_scene_guide + strength,
  embed_guidance + strength, decode_noise_scale, decode_timestep)
- Dependent knobs only appear when their parent bool is on
- Store: setSamplerInput / setStudioInput write into sampler_inputs/studio_inputs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-06 23:46:54 +03:00
co-authored by Claude Sonnet 4.6
parent 9c9e3a02cc
commit d336f6a008
5 changed files with 93 additions and 1 deletions
+8
View File
@@ -222,6 +222,14 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No
# (the reference graph used preview-only save_output=False).
graph["vhs"]["inputs"]["save_output"] = True
# 2b. project-level widget overrides for the built-in FunPack nodes.
for k, v in (params.get("studio_inputs") or {}).items():
if k not in graph["studio"]["inputs"] or not isinstance(graph["studio"]["inputs"][k], list):
graph["studio"]["inputs"][k] = v
for k, v in (params.get("sampler_inputs") or {}).items():
if k not in graph["sampler"]["inputs"] or not isinstance(graph["sampler"]["inputs"][k], list):
graph["sampler"]["inputs"][k] = v
# 3. slot nodes.
slots = (models_config or {}).get("slots") or []
slot_node_id = {}
+8
View File
@@ -87,6 +87,7 @@ class Project:
id: str = field(default_factory=_new_id)
name: str = "Untitled"
anchor: str = "" # text prepended to every scene (character anchor)
negative_prompt: str = "" # passed to the neg primitive node in the graph
# Marker separating the anchor from the first scene (Studio needs a trigger to
# close segments[0]=anchor). Defaults resolved at assembly from the library.
intro_transition: str = ""
@@ -102,6 +103,10 @@ class Project:
# is a slot id from models.json. Stored now, full builder wiring in a future phase.
conditioning_slot: str = "funpack"
sampler_slot: str = "funpack"
# Widget-input overrides for the built-in FunPack nodes (only used when the
# corresponding slot == "funpack"). Keys match ComfyUI widget/input names exactly.
studio_inputs: dict = field(default_factory=dict)
sampler_inputs: dict = field(default_factory=dict)
created_at: float = field(default_factory=time.time)
updated_at: float = field(default_factory=time.time)
@@ -111,6 +116,7 @@ class Project:
id=d.get("id") or _new_id(),
name=str(d.get("name", "Untitled")),
anchor=str(d.get("anchor", "")),
negative_prompt=str(d.get("negative_prompt", "")),
intro_transition=str(d.get("intro_transition", "")),
scenes=[Scene.from_dict(s) for s in d.get("scenes", [])],
seed=int(d.get("seed", 1)),
@@ -121,6 +127,8 @@ class Project:
max_scenes=int(d.get("max_scenes", 8)),
conditioning_slot=str(d.get("conditioning_slot", "funpack")),
sampler_slot=str(d.get("sampler_slot", "funpack")),
studio_inputs=dict(d.get("studio_inputs") or {}),
sampler_inputs=dict(d.get("sampler_inputs") or {}),
created_at=float(d.get("created_at", time.time())),
updated_at=float(d.get("updated_at", time.time())),
)
+63
View File
@@ -132,6 +132,69 @@
row3.append(numberField("Height", p.height != null ? p.height : 512, (v) => S.patchProjectQuiet({ height: v }), "pj-h"));
body.append(row3);
body.append(el("div", "insp-hint", "Link FPS / Frames / Width / Height to node inputs in Models → Linked inputs (set Source = Project …)."));
const neg = el("textarea"); neg.rows = 2; neg.value = p.negative_prompt || ""; neg.dataset.k = "pj-neg";
neg.placeholder = "What to avoid in every scene";
neg.oninput = () => S.patchProjectQuiet({ negative_prompt: neg.value });
body.append(field("Negative prompt", neg));
if (!p.conditioning_slot || p.conditioning_slot === "funpack") {
renderStudioSettings(st);
}
if (!p.sampler_slot || p.sampler_slot === "funpack") {
renderSamplerSettings(st);
}
}
function renderStudioSettings(st) {
// Studio settings are managed through studio_settings JSON — most are best left
// at reference-workflow defaults. Currently nothing extra is exposed here beyond
// the negative_prompt above (which goes through the neg primitive node).
}
// Key ChainSampler widget inputs exposed as simple form controls.
const SAMPLER_KNOBS = [
{ name: "cfg", label: "CFG", kind: "float", default: 1.0, min: 0, max: 20, step: 0.1 },
{ name: "frame_overlap", label: "Frame overlap", kind: "int", default: 16, min: 0, max: 97, step: 1 },
{ name: "carry_i2v_guides", label: "Carry i2v guides", kind: "bool", default: false },
{ name: "mid_scene_guide", label: "Mid-scene guide", kind: "bool", default: false },
{ name: "mid_scene_guide_strength", label: "Guide strength", kind: "float", default: 0.25, min: 0, max: 1, step: 0.05, dependsOn: "mid_scene_guide" },
{ name: "embed_guidance", label: "Embed guidance", kind: "bool", default: false },
{ name: "embed_guidance_strength", label: "Embed strength", kind: "float", default: 0.02, min: 0, max: 0.5, step: 0.005, dependsOn: "embed_guidance" },
{ name: "decode_noise_scale", label: "Decode noise scale", kind: "float", default: 0.0, min: 0, max: 1, step: 0.01 },
{ name: "decode_timestep", label: "Decode timestep", kind: "float", default: 0.05, min: 0, max: 1, step: 0.01 },
];
function renderSamplerSettings(st) {
const p = st.project;
const si = p.sampler_inputs || {};
const tag = el("div", "insp-tag"); tag.textContent = "Sampler settings"; body.append(tag);
SAMPLER_KNOBS.forEach((k) => {
const dep = k.dependsOn;
if (dep) {
const depVal = si[dep] != null ? si[dep] : SAMPLER_KNOBS.find((x) => x.name === dep)?.default;
if (!depVal) return;
}
const val = si[k.name] != null ? si[k.name] : k.default;
let ctrl;
if (k.kind === "bool") {
ctrl = el("input"); ctrl.type = "checkbox"; ctrl.checked = !!val; ctrl.style.width = "auto";
ctrl.dataset.k = "si-" + k.name;
ctrl.onchange = () => S.setSamplerInput(k.name, ctrl.checked);
} else {
ctrl = el("input"); ctrl.type = "number";
if (k.step != null) ctrl.step = String(k.step);
if (k.min != null) ctrl.min = String(k.min);
if (k.max != null) ctrl.max = String(k.max);
ctrl.value = val; ctrl.dataset.k = "si-" + k.name;
ctrl.oninput = () => {
const v = k.kind === "int" ? parseInt(ctrl.value || "0", 10) : parseFloat(ctrl.value || "0");
S.setSamplerInput(k.name, v);
};
}
body.append(field(k.label, ctrl));
});
}
function renderSplit(st) {
+11 -1
View File
@@ -78,6 +78,16 @@
function setSamplerSlot(slotId) {
patchProject({ sampler_slot: slotId || "funpack" });
}
function setSamplerInput(name, value) {
if (!state.project) return;
const prev = state.project.sampler_inputs || {};
patchProjectQuiet({ sampler_inputs: { ...prev, [name]: value } });
}
function setStudioInput(name, value) {
if (!state.project) return;
const prev = state.project.studio_inputs || {};
patchProjectQuiet({ studio_inputs: { ...prev, [name]: value } });
}
// ── editing ────────────────────────────────────────────────────────────────
function scheduleSave() {
@@ -366,7 +376,7 @@
patchProject, patchProjectQuiet, patchScene, patchSceneQuiet, selectScene, addScene, removeScene, moveScene, scene,
resizeScene, splitScene, snapFrames,
refreshPreview, syncFromPreview, generate, loadModels, loadImageTargets, setModelInput, setModelLink,
setConditioningSlot, setSamplerSlot,
setConditioningSlot, setSamplerSlot, setSamplerInput, setStudioInput,
loadMedia, uploadMedia, deleteMedia, assignMediaToScene,
loadShortcuts, saveShortcut, deleteShortcut, importShortcuts, loadTransitions, saveTransition, deleteTransition, importTransitions,
applyTransitionToSelection, insertShortcutIntoSelection,
+3
View File
@@ -374,6 +374,9 @@ if web is not None and PromptServer is not None:
"num_frames_per_scene": target.num_frames_per_scene,
"frame_rate": target.frame_rate,
"width": target.width, "height": target.height,
"negative_prompt": target.negative_prompt or None,
"studio_inputs": target.studio_inputs if target.conditioning_slot == "funpack" else {},
"sampler_inputs": target.sampler_inputs if target.sampler_slot == "funpack" else {},
}, media=_prepare_media(target))
if report["blocking"]:
detail = "Generation blocked — " + "; ".join(report["blocking"])