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:
co-authored by
Claude Sonnet 4.6
parent
9c9e3a02cc
commit
d336f6a008
@@ -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 = {}
|
||||
|
||||
@@ -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())),
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user