fix: montage pieces do not feed Shortcut Ideas habits; tiny clips are not montage material; truthful Update, blend, key and erase hints
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
6a03ebe045
commit
fd3224d9a4
@@ -10,6 +10,7 @@ const clone = (x) => JSON.parse(JSON.stringify(x));
|
||||
/** What a clip can play from: {fps, from (seconds into its file), total (seconds), render?} or null when it has nothing yet. */
|
||||
export function playable(sc, p) {
|
||||
const fps = effFps(sc, p);
|
||||
if (seconds(sc, p) * fps < 9) return null; // shorter than the shortest piece the model can make
|
||||
if (isVideoClip(sc)) return sc.source && sc.source.media_ref ? { fps, from: sc.source_in || 0, total: seconds(sc, p) } : null;
|
||||
const r = (p.scene_renders || {})[sc.id];
|
||||
return r && r.media ? { fps, from: (r.inSec || 0) + (sc.source_in || 0), total: seconds(sc, p), render: r } : null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { build, chop } from "./build.js";
|
||||
import { build, chop, playable } from "./build.js";
|
||||
import { isGenerative, genUnitId } from "../../shell/scenes.js";
|
||||
import { removeScene } from "../../shell/edits.js";
|
||||
|
||||
@@ -58,3 +58,9 @@ test("build: deleting a piece leaves no ghost, and an off-grid clip is never rea
|
||||
removeScene(p, piece.id);
|
||||
assert.equal((p.scene_ghosts || []).some((g) => g.id === piece.id), false);
|
||||
});
|
||||
|
||||
test("playable: a clip shorter than the shortest piece offers nothing to cut", () => {
|
||||
const p = project();
|
||||
p.scenes[1].frames = 5; p.scenes[1].frames_mode = "timeline";
|
||||
assert.equal(playable(p.scenes[1], p), null);
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
c.field.default({ label: "Acceleration", hint: "1.0 = equal segments; lower = the lead clip's segments get shorter toward the end", control: c.slider.readout({ label: "Acceleration", value: decay, min: 0.5, max: 1, step: 0.05, onCommit: (v) => { decay = v; } }) }),
|
||||
]);
|
||||
draw();
|
||||
const win = c.modal.generic({ title: "Auto Montage", subtitle: "Adds a new cut at the end of the timeline: the lead clip in shrinking pieces with pieces of the cutaway clips between them. Your original clips stay as they are.", size: "md", body });
|
||||
const win = c.modal.generic({ title: "Auto Montage", subtitle: "Adds a new cut at the end of the timeline: the lead clip in pieces (shorter and shorter, set by Acceleration) with pieces of the cutaway clips between them. Your original clips stay as they are.", size: "md", body });
|
||||
win.setFooter({ actions: [c.button.sm({ label: "Build montage", tone: "primary", onClick: () => {
|
||||
if (!pool.size) return c.toast.warn({ text: "Pick at least one cutaway clip." });
|
||||
const added = p.edit((pr) => build(pr, { leadId: lead, poolIds: [...pool], segmentFrames: seg, decay }) || false);
|
||||
|
||||
@@ -97,7 +97,7 @@ const zoom = {
|
||||
needs: ["say"],
|
||||
setup({ host, app }) {
|
||||
const b = (label, what, title) => c.button.sm({ label, tone: "ghost", title, onClick: () => app.say(what) }).node;
|
||||
const keys = c.hint.default({ text: "J back 1 s · K pause · L play · S split · I / O clip start / end · + − zoom" }).node;
|
||||
const keys = c.hint.default({ text: "J back 1 s · K pause · L play · S split · I / O jump to clip start / end · + − zoom" }).node;
|
||||
keys.classList.add("fp-keyhint"); // the first thing to go when the row runs out of room
|
||||
host.append(keys,
|
||||
b("−", "zoom.out", "Zoom out"), b("+", "zoom.in", "Zoom in"), b("fit", "zoom.fit", "Fit the cut to the window"));
|
||||
|
||||
@@ -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; ComfyUI restarts if it changed and nothing is generating", () => app.maintenance.update()), card("Switch branch", "Pick another branch", () => app.maintenance.switchBranch()),
|
||||
card("Update", "Pull the newest version from origin, install missing requirements and restart ComfyUI (after a running generation finishes)", () => 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({
|
||||
|
||||
@@ -72,7 +72,7 @@ export function createMaintenance({ api, flush }) {
|
||||
if (!(await ask("Check requirements", "FunPack has local changes, so the code cannot be updated. Check this install's requirements and install any that are missing?"))) return;
|
||||
return run("Checking requirements…", () => api.requirements(), (r) => deps({ requirements: r }));
|
||||
}
|
||||
if (!(await ask("Update", `Pull the latest "${branch}" from origin, check requirements and install any that are missing, then restart ComfyUI? Any running generation will be lost.${gs.behind > 0 ? ` ${gs.behind} commit(s) available.` : ""}`))) return;
|
||||
if (!(await ask("Update", `Pull the latest "${branch}" from origin, check requirements and install any that are missing, then restart ComfyUI. If a generation is running, the restart waits until you press Restart after it finishes.${gs.behind > 0 ? ` ${gs.behind} commit(s) available.` : ""}`))) return;
|
||||
// Requirements first and on their own, so a failed pull (no network, a refused merge) does not skip them.
|
||||
const action = async () => {
|
||||
let requirements; try { requirements = await api.requirements(); } catch (err) { requirements = { ran: false, ok: false, detail: err.message }; }
|
||||
@@ -85,7 +85,7 @@ export function createMaintenance({ api, flush }) {
|
||||
if (!(gs.branches || []).length) return tell("No git branches found.");
|
||||
const branch = await c.modal.choice({ title: "Switch FunPack branch", items: gs.branches.map((b) => ({ id: b, label: b, hint: b === gs.branch ? "current" : "" })) }).result;
|
||||
if (!branch || branch === gs.branch) return;
|
||||
if (!(await ask("Switch branch", `Switch to "${branch}", pull from origin, and restart ComfyUI? Any running generation will be lost.`))) return;
|
||||
if (!(await ask("Switch branch", `Switch to "${branch}", pull from origin, and restart ComfyUI. If a generation is running, the restart waits until you press Restart after it finishes.`))) return;
|
||||
return run(`Switching to ${branch}…`, () => api.git("checkout", { branch }), (r) => `On ${branch}${r.updated ? ` (${r.before} → ${r.after})` : ", already up to date"}.${deps(r)}`);
|
||||
},
|
||||
async rollback() {
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ def stats() -> dict:
|
||||
if k < b:
|
||||
votes.setdefault((k, b), []).append((when, v))
|
||||
# Habits: what the cut uses now.
|
||||
if sc.excluded or not (sc.text or "").strip():
|
||||
if sc.excluded or sc.reused or not (sc.text or "").strip(): # a montage piece replays a shot, it is not a new one
|
||||
continue
|
||||
present = fired(sc.text)
|
||||
scanned += 1
|
||||
|
||||
@@ -44,6 +44,17 @@ def test_excluded_scenes_and_sub_clips_are_skipped_and_follows_use_timeline_orde
|
||||
assert s["follows"] == [["neon", "rain", 1]]
|
||||
|
||||
|
||||
def test_auto_montage_pieces_are_not_shots_the_user_wrote():
|
||||
for n in ("rain", "neon"):
|
||||
shortcuts.save({"name": n, "triggers": [n], "replacements": ["r"]})
|
||||
scenes = [projects.Scene(id="a", text="rain"), projects.Scene(id="b", text="neon"),
|
||||
projects.Scene(id="p1", text="rain", gen_unit_id="p1", reused=True),
|
||||
projects.Scene(id="p2", text="neon", gen_unit_id="p2", reused=True)]
|
||||
projects.save(projects.Project(name="x", scenes=scenes, timeline_order=["a", "b", "p1", "p2"]))
|
||||
s = suggestions.stats()
|
||||
assert s["scenes"] == 2 and s["counts"] == {"rain": 1, "neon": 1} and s["follows"] == [["rain", "neon", 1]]
|
||||
|
||||
|
||||
def test_ratings_score_shortcuts_and_pairs_and_a_bad_image_blames_nothing():
|
||||
for n in ("rain", "neon", "fox"):
|
||||
shortcuts.save({"name": n, "triggers": [n], "replacements": ["r"]})
|
||||
|
||||
@@ -17,7 +17,7 @@ class FunPackNegativeErase(io.ComfyNode):
|
||||
io.Float.Input("strength", default=0.0, min=0.0, max=2.0, step=0.05,
|
||||
tooltip="0 = off. How much of the negative prompt is taken out of the positive prompt's words (1 = all of it)."),
|
||||
io.Combo.Input("mode", options=list(erase.MODES), default="project",
|
||||
tooltip="project: removes only the part of each word that points toward the negative. subtract: pulls every word away from the negative by a share of its own size."),
|
||||
tooltip="project: removes the part of each word that lies along the negative's direction. subtract: pulls every word away from the negative by a share of its own size."),
|
||||
io.Boolean.Input("keep_size", default=True,
|
||||
tooltip="Put each word back on its original size, changing its direction only.")],
|
||||
outputs=[io.Conditioning.Output(display_name="positive"), io.String.Output(display_name="status")],
|
||||
|
||||
@@ -46,7 +46,7 @@ SETTINGS = {
|
||||
"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}},
|
||||
"fuse": {"type": "enum", "default": "pyramid", "label": "Window blend",
|
||||
"hint": "How overlapping windows are mixed: pyramid, relative, overlap-linear fade between neighbours; flat averages them equally.",
|
||||
"hint": "How windows are mixed: pyramid (relative is the same in this ComfyUI) trusts the middle of each window most; overlap-linear fades across the overlap only; flat averages equally.",
|
||||
"options": [{"value": v, "label": v} for v in FUSES], "when": {"enabled": True}},
|
||||
"freenoise": {"type": "bool", "default": True, "label": "FreeNoise blending", "when": {"enabled": True},
|
||||
"hint": "Shuffle the starting noise so neighbouring windows share it."},
|
||||
|
||||
Reference in New Issue
Block a user