fix: deleting a montage piece leaves no ghost; pieces never read past an off-grid clip; calmer montage default; context-window overlap hidden when batched; Engine pane honours list conditions; Generate and Update wording

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-15 14:07:29 +03:00
co-authored by Claude Sonnet 5.5
parent 1e26876bdb
commit 6a03ebe045
9 changed files with 22 additions and 9 deletions
+2 -2
View File
@@ -135,7 +135,7 @@ export default {
if (kept === false) tell(`Scene ${snap.scenes.findIndex((s) => s.id === root.id) + 1} was removed while it was being made, so its result is not on the timeline. It is in ComfyUI's output folder as ${images[images.length - 1].filename}.`);
snap.scene_renders = structuredClone(p.project.scene_renders || {}); // ...and in what the next shot reads
}
if (!made && !stopped && !said) tell("Nothing to generate: every scene is left out or is a video clip.");
if (!made && !stopped && !said) tell("Nothing to generate: every scene is left out, a video clip or an Auto Montage piece.");
} finally { busy = false; draw(); }
}
app.runner.units = (units, times) => runUnits(units, times);
@@ -150,7 +150,7 @@ export default {
if (key === drawn) return; // progress ticks must not rebuild the buttons
drawn = key;
const working = busy || held || g.state().phase === QUEUED || g.state().phase === RUNNING; // also a run this page did not start
const go = c.button.sm({ label: working ? "Generating…" : "▶ Generate", tone: "primary", disabled: working || !open, title: "Generate every scene on the timeline, except left-out scenes and video clips. An earlier render of a scene stays as a take.",
const go = c.button.sm({ label: working ? "Generating…" : "▶ Generate", tone: "primary", disabled: working || !open, title: "Generate every scene on the timeline, except left-out scenes, video clips and Auto Montage pieces. An earlier render of a scene stays as a take.",
onClick: () => runUnits(unitsOf(p.scenes)) });
const one = c.button.sm({ label: picked > 1 ? `Selected (${picked})` : "Selected", title: "Generate only the picked scenes", disabled: working || !picked, onClick: () => runUnits(unitsOf(pickedScenes())) });
const stop = c.button.sm({ label: "■ Stop", tone: "danger", title: "Stop the run that is going", onClick: () => { stopped = true; g.cancel(); } });
+2 -2
View File
@@ -33,9 +33,9 @@ export function build(p, { leadId, poolIds, segmentFrames = 100, decay = 1, rand
const pool = (poolIds || []).map(find).filter((s) => s && playable(s, p));
if (!leadSrc || !pool.length) return 0;
const base = Math.max(9, Math.round(segmentFrames)), dk = Math.min(1, Math.max(0.3, decay));
const leadSegs = chop(snapFrames(leadSrc.total * leadSrc.fps), base, dk);
const leadSegs = chop(snapFrames(leadSrc.total * leadSrc.fps, "floor"), base, dk);
// Each pool clip is its own queue read front to back: chance only picks WHICH clip to draw from next.
const queues = pool.map((sc) => ({ sc, src: playable(sc, p), cursor: 0, chunks: chop(snapFrames(playable(sc, p).total * playable(sc, p).fps), base, 1) })).filter((q) => q.chunks.length);
const queues = pool.map((sc) => ({ sc, src: playable(sc, p), cursor: 0, chunks: chop(snapFrames(playable(sc, p).total * playable(sc, p).fps, "floor"), base, 1) })).filter((q) => q.chunks.length);
if (!leadSegs.length || !queues.length) return 0;
const next = () => {
let open = queues.filter((q) => q.cursor < q.chunks.length);
+12
View File
@@ -46,3 +46,15 @@ test("build: nothing is built without a rendered lead and a rendered pool; the p
assert.equal(build(p, { leadId: "L", poolIds: ["N"] }), 0);
assert.equal(JSON.stringify(p), before);
});
test("build: deleting a piece leaves no ghost, and an off-grid clip is never read past its end", () => {
const p = project();
p.scene_renders.L = { media: media("L") };
p.scenes[0].frames = 103; p.scenes[0].frames_mode = "timeline"; // 103 is off the 8k+1 grid; its nearest grid length, 105, is past the end
build(p, { leadId: "L", poolIds: ["A"], segmentFrames: 33, decay: 1, random: () => 0 });
const lastLead = p.scenes.filter((s) => s.reused).filter((_, i) => i % 2 === 0).pop();
assert.ok(p.scene_renders[lastLead.id].inSec * 25 + lastLead.frames <= 103, "no piece plays past the clip");
const piece = p.scenes.find((s) => s.reused);
removeScene(p, piece.id);
assert.equal((p.scene_ghosts || []).some((g) => g.id === piece.id), false);
});
+1 -1
View File
@@ -23,7 +23,7 @@ export default {
let lead = (picked[0] || list[0]).id;
const pool = new Set(picked.slice(1).map((s) => s.id));
const leadFrames = Math.round(playable(picked[0] || list[0], p.project).total * playable(picked[0] || list[0], p.project).fps);
let seg = Math.max(9, snapFrames(leadFrames / 4, "floor")), decay = 0.85; // about four lead pieces, not one lump
let seg = Math.max(9, snapFrames(leadFrames / 2.5, "floor")), decay = 0.85; // a handful of lead pieces, not one lump and not a flicker
const label = (s) => `${snippet(s)} — ${Math.round(playable(s, p.project).total * playable(s, p.project).fps)}f`;
const body = c.region.stack({ gap: "md" });
const draw = () => body.set([
+1 -1
View File
@@ -3,7 +3,7 @@
import { composer as c } from "../../composer/composer.js";
/** A `when` clause names sibling settings on the same module that must hold a value for the row to show. */
export const whenSatisfied = (values, id, when) => !when || Object.entries(when).every(([k, v]) => (values[id] || {})[k] === v);
export const whenSatisfied = (values, id, when) => !when || Object.entries(when).every(([k, v]) => (Array.isArray(v) ? v : [v]).includes((values[id] || {})[k]));
export function settingRow(id, name, spec, values, onChange) {
const label = spec.label || name;
+1 -1
View File
@@ -11,7 +11,7 @@ const STEPS = [
{ at: zone(2), side: "left", title: "Properties", text: "Project settings (size, length, postfix) and the picked scene's prompt, source, references and length." },
{ at: zone(3), side: "top", title: "Timeline", text: "One clip per scene. Drag the edges to trim, press S to split, Space to play. Lanes below carry separated sound." },
{ at: button("◆ Composer"), side: "top", title: "Composer", text: "Write the whole story in one box, cut at the cut word; keep templates, $variables and shortcuts; chat with the prompt enhancer." },
{ at: button("▶ Generate"), side: "top", title: "Generate", text: "Makes every scene on the timeline, except left-out scenes and video clips; an earlier render of a scene stays as a take. “Generate this scene” in Properties makes just the picked one; rate a result so FunPack learns your taste." },
{ at: button("▶ Generate"), side: "top", title: "Generate", text: "Makes every scene on the timeline, except left-out scenes, video clips and Auto Montage pieces; an earlier render of a scene stays as a take. “Generate this scene” in Properties makes just the picked one; rate a result so FunPack learns your taste." },
{ at: () => document.querySelector(".cx-frame-main"), side: "bottom", title: "FAQ", text: "Nothing happens? Check the ComfyUI chip at the top right and the Log window. Wrong look? Settings ▸ Appearance. Lost a layout? View ▸ Reset Layout. Lost a project? File ▸ Open recent. That is the tour: Help ▸ Start tour runs it again." },
];
+1 -1
View File
@@ -38,7 +38,7 @@ export default {
const recent = app.project.project;
const done = () => { markSeen(); if (win) win.close("done"); };
const tiles = c.region.stack({ gap: "sm", children: [
card("Update", "Pull the newest version from origin and restart ComfyUI", () => app.maintenance.update()), card("Switch branch", "Pick another branch", () => app.maintenance.switchBranch()),
card("Update", "Pull the newest version from origin; ComfyUI restarts if it changed and nothing is generating", () => app.maintenance.update()), card("Switch branch", "Pick another branch", () => app.maintenance.switchBranch()),
card("Restart ComfyUI", "Reload the server without updating", () => app.maintenance.restart()), card("Rollback update", "Undo the last update", () => app.maintenance.rollback())] });
tiles.node.classList.add("fp-tiles");
win = c.modal.generic({
+1
View File
@@ -22,6 +22,7 @@ function ghostOrDrop(p, sc, afterId, pending) {
const renders = p.scene_renders || (p.scene_renders = {});
const r = renders[sc.id];
delete renders[sc.id];
if (sc.reused) return; // a montage piece is a copy: nothing to regenerate, so nothing to leave behind
p.scene_ghosts = (p.scene_ghosts || []).filter((g) => g.id !== sc.id);
if (!(r && r.media) && !pending) return;
p.scene_ghosts.push({
+1 -1
View File
@@ -41,7 +41,7 @@ SETTINGS = {
"when": {"enabled": True}},
"overlap": {"type": "int", "default": 40, "min": 0, "max": 512, "step": 8, "label": "Window overlap (frames)",
"hint": "Frames neighbouring windows share. More overlap joins smoother but runs slower; it must be shorter than the window.",
"when": {"enabled": True}},
"when": {"enabled": True, "schedule": [v for v in SCHEDULES if v != "batched"]}},
"schedule": {"type": "enum", "default": "standard_uniform", "label": "Window schedule",
"hint": "standard_uniform: windows shift a little each step. standard_static: the same windows every step. looped_uniform: the end wraps round to the start. batched: back-to-back windows with no overlap.",
"options": [{"value": v, "label": v} for v in SCHEDULES], "when": {"enabled": True}},