Temporal style auto picks a style per shot from its words; registry node name back to ComfyUI-FunPack (display name stays FunUI)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tw9fjYZhHqk196LykrWy3T
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import hub from "./index.js";
|
||||
import { styleFor } from "./intent.js";
|
||||
|
||||
test("words choose the style", () => {
|
||||
assert.equal(styleFor(""), "natural");
|
||||
assert.equal(styleFor("a woman walks"), "natural");
|
||||
assert.equal(styleFor("a fast chase, running"), "decelerate");
|
||||
assert.equal(styleFor("a calm, still portrait"), "freeze");
|
||||
assert.equal(styleFor("a slow pan"), "accelerate");
|
||||
assert.equal(styleFor("a seamless loop of waves"), "loop");
|
||||
});
|
||||
|
||||
const make = (values, off = false) => { const app = { pipeline: { currentValues: () => values, allOff: () => off }, inputHooks: [] }; hub.setup({ app }); return app.inputHooks[0]; };
|
||||
const slots = [{ id: "5", node: "FunUIModifierSettings" }];
|
||||
|
||||
test("auto becomes the shot's own style in that run's settings, other values kept", async () => {
|
||||
const hook = make({ temporal_style: { style: "auto" }, other: { a: 1 } });
|
||||
const out = await hook({ scene: { text: "calm still portrait" }, slots });
|
||||
assert.deepEqual(JSON.parse(out.inputs[5].settings), { temporal_style: { style: "freeze" }, other: { a: 1 } });
|
||||
});
|
||||
|
||||
test("nothing happens for a fixed style, with everything off, or without a settings node", async () => {
|
||||
assert.deepEqual(await make({ temporal_style: { style: "pulse" } })({ scene: { text: "x" }, slots }), {});
|
||||
assert.deepEqual(await make({ temporal_style: { style: "auto" } }, true)({ scene: { text: "x" }, slots }), {});
|
||||
assert.deepEqual(await make({ temporal_style: { style: "auto" } })({ scene: { text: "x" }, slots: [] }), {});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
// Temporal style "auto": each shot gets the style its own words ask for. The choice is made here, per shot, and sent as the concrete
|
||||
// style in that run's modifier settings; the module itself only knows concrete styles.
|
||||
import { styleFor } from "./intent.js";
|
||||
|
||||
export default {
|
||||
id: "temporal",
|
||||
mount: "menubar.menus",
|
||||
needs: ["pipeline", "inputHooks"],
|
||||
modules: ["temporal_style"],
|
||||
setup({ app }) {
|
||||
const hook = async ({ scene, slots }) => {
|
||||
if (app.pipeline.allOff?.(slots)) return {};
|
||||
const values = app.pipeline.currentValues(slots) || {};
|
||||
if ((values.temporal_style || {}).style !== "auto") return {};
|
||||
const slot = (slots || []).find((s) => s.node === "FunUIModifierSettings");
|
||||
if (!slot) return {};
|
||||
const style = styleFor(scene.text);
|
||||
const settings = JSON.stringify({ ...values, temporal_style: { ...values.temporal_style, style } });
|
||||
return { inputs: { [slot.id]: { settings } }, notes: [`Temporal style auto: ${style} for this shot.`] };
|
||||
};
|
||||
app.inputHooks.push(hook);
|
||||
return () => { const i = app.inputHooks.indexOf(hook); if (i >= 0) app.inputHooks.splice(i, 1); };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// Temporal style "auto": reads a shot's words and picks how its motion should feel (ported from v4's director).
|
||||
// Energy words want punchier steps, stillness words want a hold, loop words want the end to join the start.
|
||||
const ENERGY = ["run", "running", "sprint", "chase", "fight", "punch", "kick", "explosion", "explode", "burst", "jump", "leap", "dash", "race",
|
||||
"fast", "rapid", "quick", "frantic", "spin", "whirl", "fall", "crash", "swing", "throw", "action", "dance", "dancing", "gallop", "charge",
|
||||
"shatter", "blast", "speeding"];
|
||||
const STILL = ["still", "stillness", "motionless", "static", "frozen", "freeze", "portrait", "calm", "serene", "quiet", "slow", "slowly", "gentle",
|
||||
"gradual", "drift", "drifting", "float", "floating", "hover", "stare", "gaze", "meditat", "sleep", "resting", "rest", "standing", "posed", "pose",
|
||||
"close-up", "closeup", "macro", "landscape", "ambient", "tranquil", "lingering"];
|
||||
const LOOP = ["loop", "looping", "seamless", "cycle", "cyclic", "repeat", "repeating", "endless", "perpetual", "continuous loop"];
|
||||
|
||||
const count = (text, words) => words.filter((w) => text.includes(w)).length;
|
||||
|
||||
/** -> one of the concrete Temporal styles for these words. */
|
||||
export function styleFor(words) {
|
||||
const t = String(words || "").toLowerCase();
|
||||
if (!t.trim()) return "natural";
|
||||
if (count(t, LOOP)) return "loop";
|
||||
const net = count(t, STILL) - count(t, ENERGY);
|
||||
if (net === 0) return "natural";
|
||||
if (net < 0) return "decelerate";
|
||||
return Math.min(2.0, 1.0 + 0.35 * net) >= 1.7 ? "freeze" : "accelerate";
|
||||
}
|
||||
@@ -39,6 +39,7 @@ export default [
|
||||
"./features/generate/warn.js",
|
||||
"./features/generate/progress.js",
|
||||
"./features/continuity/index.js",
|
||||
"./features/temporal/index.js",
|
||||
"./features/composer/index.js",
|
||||
"./features/settings/index.js",
|
||||
"./features/welcome/index.js",
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
Everything but `loop` tells the model a different frame rate than the clip's (see style.py);
|
||||
`loop` rolls the latent in time so the clip's end joins its start (loop.py).
|
||||
|
||||
Not here: v4's `auto`, which read each scene's prompt and chose per scene. That choice is a
|
||||
conditioning-side decision (the prompt lives there); it is still to be ported with the scene-chain
|
||||
work, and a style set here applies to every scene of the run alike.
|
||||
`auto` reads each shot's words and picks one of these styles per shot. The module cannot see the words,
|
||||
so the app makes that choice when it builds each run (app/ui/features/temporal) and sends the concrete
|
||||
style; an `auto` that reaches here unresolved does nothing.
|
||||
"""
|
||||
|
||||
from ..._core import patching
|
||||
@@ -20,14 +20,14 @@ CATEGORY = "sampling"
|
||||
STATUS = "experimental"
|
||||
REQUIRES = ["ltx_av"]
|
||||
|
||||
STYLES = ["natural", "accelerate", "decelerate", "freeze", "pulse",
|
||||
STYLES = ["natural", "auto", "accelerate", "decelerate", "freeze", "pulse",
|
||||
"rapid_start", "rapid_end", "rapid_start_end", "loop"]
|
||||
|
||||
SETTINGS = {
|
||||
"style": {
|
||||
"type": "enum", "default": "natural", "label": "Temporal style",
|
||||
"options": [{"value": v, "label": v.replace("_", " ")} for v in STYLES],
|
||||
"hint": "How motion feels. natural: unchanged. accelerate, freeze: smoother, held motion (freeze most). decelerate: punchier, bigger steps. "
|
||||
"hint": "How motion feels. natural: unchanged. auto: each shot gets the style its own words ask for (motion words punch, calm words hold). accelerate, freeze: smoother, held motion (freeze most). decelerate: punchier, bigger steps. "
|
||||
"pulse: repeated punch-then-ease. rapid start/end: punchy at the start and/or end. loop: the end joins the start.",
|
||||
},
|
||||
}
|
||||
@@ -53,7 +53,7 @@ def install(patcher, values, key):
|
||||
if "context_handler" in patcher.model_options:
|
||||
note += " (context windows are on: each WINDOW is looped, not the clip -- turn them off for a seamless clip)"
|
||||
else:
|
||||
return None
|
||||
return None # natural, or an auto the app did not resolve
|
||||
patcher.set_model_unet_function_wrapper(patching.tag(wrapper, key))
|
||||
installed = patcher.model_options["model_function_wrapper"]
|
||||
installed._funui_prev = old # so stripping ours puts the earlier one back
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[project]
|
||||
name = "FunUI" # Unique identifier for your node. Immutable after creation.
|
||||
name = "ComfyUI-FunPack" # Unique identifier for your node. Immutable after creation.
|
||||
description = "An AI movie studio inside ComfyUI: a browser-based studio with timeline, per-scene prompts and one-click montage generation, plus a self-improving engine that learns from your ratings. Every feature is a module, so a broken one is simply absent."
|
||||
version = "5.0.3" # Custom Node version. Must be semantically versioned.
|
||||
# Release codename ("Classy Chopper" for 5.x) lives in core/update.py:CODENAMES, keyed by major, shown in Settings > About.
|
||||
|
||||
Reference in New Issue
Block a user