Fix Color Guide reference history and add global colors
This commit is contained in:
+4
-1
@@ -429,7 +429,8 @@ def plan_segments(n: int, cuts: list[int], mode: str, max_len: int, min_len: int
|
||||
DEFAULT_PLAN = {
|
||||
"video": "", "fps": 24.0, "grid": DEFAULT_GRID, "mode": "shots", "max_s": 4.5, "min_s": 1.0,
|
||||
"sensitivity": 0.5, "max_parts": 0, "max_total_s": 0.0, "start_s": 0.0, "end_s": 0.0, "bounds": [], "segs": [],
|
||||
"global_ref": "", "global_ref2": "", "global_prompt": "", "megapixels": 0.5, "multiple": 32,
|
||||
"global_ref": "", "global_ref2": "", "global_prompt": "", "global_color_guide_ref1": "#00ff00",
|
||||
"global_color_guide_ref2": "#9b30ff", "megapixels": 0.5, "multiple": 32,
|
||||
"detector": "adaptive", "run": "auto", "filters": {}, "skip_fill": "original",
|
||||
"cast": {}, "cast_assign": True, "cast_split": False, "cast_only": False, "mask_cfg": {}, "vlm_cfg": {}, "audio_mode": "auto", "ref_details": {},
|
||||
}
|
||||
@@ -502,6 +503,8 @@ def resolve_plan(plan: dict, analysis: dict, path: str | None = None) -> list[di
|
||||
if guide_pid2 is not None:
|
||||
s["color_guide_color2"] = str(m.get("color_guide_color2") or
|
||||
(plan.get("cast") or {}).get(str(guide_pid2), {}).get("guide_color") or "")
|
||||
s["color_guide_color"] = s["color_guide_color"] or str(plan.get("global_color_guide_ref1") or "#00ff00")
|
||||
s["color_guide_color2"] = s["color_guide_color2"] or str(plan.get("global_color_guide_ref2") or "#9b30ff")
|
||||
if s.get("color_guide"):
|
||||
cast_entries = plan.get("cast") or {}
|
||||
first_person = cast_entries.get(str(s.get("color_guide_person"))) or {}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "bfsnodes"
|
||||
description = "Custom ComfyUI nodes for reference-guided video: BFS Sampler (guide-driven IC-LoRAs, auto crop, mask inpainting, temporal chunking), LTX Multiple Controls / Identity Transfer / Edit Anything, Head-Swap and Anime2Real conditioning, MiniMax-H3 latent guides, RoPE layouts and loaders, LoRA Surgery, and Shot Planner / Shot Loop (tabbed panel): split long videos into shots at camera cuts, face-tracked cast, SAM 3 mask per shot with full frame, paste, mask, crop and crop + mask generation modes, optional translucent Colored Mark overlay for manually selected regions, optional Qwen3-VL to suggest settings, describe references ({details}) and the person to replace ({target}), continuity between shots, H3 conditioning with an optional duet and prompt writer, join with the original timing, audio and a side-by-side comparison. H3 Duet: pin a source clip beside the video, training-free."
|
||||
version = "1.96.0"
|
||||
version = "1.97.0"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = [
|
||||
"torch",
|
||||
|
||||
@@ -248,7 +248,7 @@ const meta = i => { while (plan.segs.length <= i) plan.segs.push({}); return pla
|
||||
}
|
||||
const setCast = (id, k, v) => {
|
||||
plan.cast = { ...plan.cast, [String(id)]: { ...castOf(id), [k]: v } };
|
||||
if (k !== "ignore") remember(v);
|
||||
if (k === "ref" || k === "ref2") remember(v);
|
||||
save();
|
||||
};
|
||||
async function describeTarget(i) {
|
||||
@@ -583,14 +583,18 @@ const meta = i => { while (plan.segs.length <= i) plan.segs.push({}); return pla
|
||||
// ---- references
|
||||
// the last 10 references picked, remembered across workflows (per browser)
|
||||
const RECENT_KEY = "bfs.shotloop.recentRefs";
|
||||
const recent = ref(store.get(RECENT_KEY, []));
|
||||
const isColorValue = name => typeof name === "string" && /^#[0-9a-f]{6}$/i.test(name);
|
||||
const savedRecent = store.get(RECENT_KEY, []);
|
||||
const recent = ref((Array.isArray(savedRecent) ? savedRecent : []).filter(name => typeof name === "string" && !isColorValue(name)));
|
||||
if (recent.value.length !== (Array.isArray(savedRecent) ? savedRecent.length : 0)) store.set(RECENT_KEY, recent.value);
|
||||
const remember = name => {
|
||||
if (!name) return;
|
||||
if (!name || typeof name !== "string" || isColorValue(name)) return;
|
||||
recent.value = [name, ...recent.value.filter(x => x !== name)].slice(0, 10);
|
||||
store.set(RECENT_KEY, recent.value);
|
||||
};
|
||||
// target: shot index or "global"; field: ref | ref2 (global_ref | global_ref2 for "global")
|
||||
const setRef = (target, field, name) => {
|
||||
if (isColorValue(name)) return; // colors belong to Color Guide settings, never to image references
|
||||
if (target === "global") plan[field] = name; else meta(target)[field] = name;
|
||||
remember(name); save();
|
||||
};
|
||||
|
||||
@@ -197,7 +197,8 @@ person. With `guide_mode = aligned guide` the model also sees the whole original
|
||||
the motion either way. (Not applied with the duet panel.)
|
||||
|
||||
**🎨 Color Guide** is a separate optional visual cue. Turn it on for a shot with a mask and choose the marker color
|
||||
for ref 1. The selected region is tinted in the source guide, while the image detail,
|
||||
for ref 1. The default colors for the global references are set below each image in **Prompts & refs**; a shot may
|
||||
override them. The selected region is tinted in the source guide, while the image detail,
|
||||
movement and expression remain visible underneath. This does not turn on `Mask only` or create an H3 generation
|
||||
mask. The prompt must explain that the tint is temporary and that the matching reference replaces the tinted person.
|
||||
|
||||
|
||||
@@ -129,6 +129,8 @@ const TEXT = {
|
||||
"off": ["desligado", "desactivado", "关闭"], "on": ["ligado", "activado", "开启"], "run": ["executar", "ejecutar", "运行"], "skip": ["ignorar", "omitir", "跳过"], "filters decide": ["filtros decidem", "deciden los filtros", "由筛选条件决定"], "always run": ["sempre executar", "ejecutar siempre", "始终运行"], "always skip": ["sempre ignorar", "omitir siempre", "始终跳过"], "⏸ Disable": ["⏸ Desativar", "⏸ Desactivar", "⏸ 禁用"], "▶ Enable": ["▶ Ativar", "▶ Activar", "▶ 启用"], "✂ Split": ["✂ Dividir", "✂ Dividir", "✂ 拆分"], "⇥ Merge": ["⇥ Unir", "⇥ Unir", "⇥ 合并"],
|
||||
"Advanced": ["Avançado", "Avanzado", "高级"], "English": ["Inglês", "Inglés", "英语"], "Português": ["Português", "Portugués", "葡萄牙语"], "Español": ["Espanhol", "Español", "西班牙语"], "中文": ["Chinês", "Chino", "中文"],
|
||||
"Color Guide": ["Guia de cores", "Guía de color", "颜色引导"], "Video": ["Vídeo", "Vídeo", "视频"], "Shots": ["Planos", "Planos", "镜头"], "Prompts": ["Prompts", "Prompts", "提示词"],
|
||||
"Guide color · Ref 1": ["Cor guia · Ref. 1", "Color guía · Ref. 1", "引导颜色 · 参考图 1"], "Guide color · Ref 2": ["Cor guia · Ref. 2", "Color guía · Ref. 2", "引导颜色 · 参考图 2"],
|
||||
"These colors are defaults for the global references. Turn on Color Guide on a shot to apply them; shot-level colors can override them.": ["Estas cores são os padrões das referências globais. Ative o Guia de cores no plano para aplicá-las; cada plano pode substituí-las.", "Estos colores son los valores predeterminados de las referencias globales. Activa la Guía de color en un plano para aplicarlos; cada plano puede cambiarlos.", "这些颜色是全局参考图的默认值。启用镜头的颜色引导即可应用;镜头也可以单独覆盖。"],
|
||||
"Audio": ["Áudio", "Audio", "音频"], "use the video's audio": ["usar o áudio do vídeo", "usar el audio del vídeo", "使用视频音频"], "silent track": ["faixa silenciosa", "pista silenciosa", "静音音轨"],
|
||||
"auto": ["automático", "automático", "自动"], "fixed": ["fixo", "fijo", "固定"], "single": ["único", "único", "单个"], "adaptive": ["adaptativo", "adaptativo", "自适应"], "content": ["conteúdo", "contenido", "内容"], "builtin": ["integrado", "integrado", "内置"],
|
||||
"PySceneDetect adaptive": ["PySceneDetect adaptativo", "PySceneDetect adaptativo", "PySceneDetect 自适应"], "PySceneDetect content": ["PySceneDetect por conteúdo", "PySceneDetect por contenido", "PySceneDetect 内容检测"], "Built-in": ["Integrado", "Integrado", "内置"],
|
||||
@@ -217,7 +219,7 @@ export function h(type, props, children) {
|
||||
export const GRIDS = { "H3 (17n+5)": [17, 5], "LTX / Wan (8n+1)": [8, 1], "Wan (4n+1)": [4, 1], "any": [1, 0] };
|
||||
export const DEFAULTS = {
|
||||
video: "", fps: 24, grid: "H3 (17n+5)", mode: "shots", max_s: 4.5, min_s: 1.0, sensitivity: 0.5,
|
||||
max_parts: 0, max_total_s: 0, start_s: 0, end_s: 0, start_f: null, end_f: null, track: null, bounds: [], segs: [], global_ref: "", global_ref2: "", global_prompt: "", mask_video: "",
|
||||
max_parts: 0, max_total_s: 0, start_s: 0, end_s: 0, start_f: null, end_f: null, track: null, bounds: [], segs: [], global_ref: "", global_ref2: "", global_prompt: "", global_color_guide_ref1: "#00ff00", global_color_guide_ref2: "#9b30ff", mask_video: "",
|
||||
megapixels: 0.5, multiple: 32, detector: "adaptive", run: "auto", auto_continue: true,
|
||||
skip_fill: "original", audio_mode: "auto", ref_details: {}, mask_cfg: {}, vlm_cfg: {}, cast: {}, cast_assign: true, cast_split: false, cast_only: false,
|
||||
filters: { person: false, min_person_area: 0, max_persons: 0, face: false, skip_dark: false, dark_level: 0.06,
|
||||
|
||||
@@ -141,6 +141,8 @@ const CSS = `
|
||||
.bsl .refslot.sm{width:56px;height:56px;font-size:10px}
|
||||
.bsl .refslot.via{border-style:solid;border-color:#3b5b3b}
|
||||
.bsl .rslot{display:flex;flex-direction:column;gap:3px;align-items:center}
|
||||
.bsl .guidecolor{display:flex;flex-direction:column;align-items:center;gap:2px;max-width:112px}
|
||||
.bsl .guidecolor select{width:100%;font-size:10px;padding:2px 4px}
|
||||
.bsl .rbtns{display:flex;gap:3px;min-height:20px}
|
||||
.bsl .rbtns button{padding:1px 6px;font-size:10px}
|
||||
.bsl .recent{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px;align-items:center}
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
// 📝 Prompts & refs tab: the global reference / prompt, the VLM (shot suggestions) and the reference descriptions ({details}).
|
||||
import { h, fld, check, section, pill, hint, select } from "./common.js";
|
||||
|
||||
const GUIDE_COLORS = [["#00ff00", "Green"], ["#9b30ff", "Purple"], ["#ff2020", "Red"], ["#168cff", "Blue"],
|
||||
["#ffe600", "Yellow"], ["#ff00d4", "Magenta"], ["#00e5c0", "Cyan"]];
|
||||
|
||||
function globalSection(c) {
|
||||
const { plan } = c;
|
||||
const slot = (name, label, field) => h("div", { class: "rslot" }, [
|
||||
h("div", { class: "refslot", title: name ? name + " (click to replace)" : "click to upload", onClick: () => c.uploadRef("global", field) },
|
||||
name ? [h("img", { src: c.viewUrl(name) })] : [label]),
|
||||
h("div", { class: "rbtns" }, [name ? h("button", { title: "remove", onClick: () => c.setRef("global", field, "") }, "✕") : null]),
|
||||
h("label", { class: "guidecolor" }, [
|
||||
h("span", { class: "hint" }, field === "global_ref" ? "Guide color · Ref 1" : "Guide color · Ref 2"),
|
||||
h("select", { value: field === "global_ref" ? (plan.global_color_guide_ref1 || "#00ff00") : (plan.global_color_guide_ref2 || "#9b30ff"),
|
||||
disabled: !name,
|
||||
title: "Default Color Guide marker when this global reference is used",
|
||||
onChange: e => c.setPlan(field === "global_ref" ? "global_color_guide_ref1" : "global_color_guide_ref2", e.target.value) },
|
||||
GUIDE_COLORS.map(([v, text]) => h("option", { value: v }, text))),
|
||||
]),
|
||||
]);
|
||||
return section("Global reference & prompt", [
|
||||
h("div", { class: "row", style: "align-items:flex-start;gap:10px" }, [
|
||||
@@ -20,6 +31,7 @@ function globalSection(c) {
|
||||
"Used by shots without their own. Connected ref_image / ref_image_2 / prompt inputs on the node override these. Placeholders: ",
|
||||
h("b", "{target}"), " who is replaced (per shot) · ", h("b", "{details}"), " description of the shot's references · ",
|
||||
h("b", "{shot}"), " the VLM's description of the shot · ", h("b", "{setting}"), " the setting picture (H3 Conditioning)."]),
|
||||
h("div", { class: "hint", style: "margin-top:4px" }, "These colors are defaults for the global references. Turn on Color Guide on a shot to apply them; shot-level colors can override them."),
|
||||
], { sub: "defaults for every shot" });
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@ function editor(c, cur) {
|
||||
const i = sel.value, S = segs.value, k = segKey(cur), mp = maskPrev.value[k], g = vlmSug.value[k];
|
||||
const issues = c.checksFor(i);
|
||||
const guideColor1 = cur.color_guide_color || (cur.color_guide_person != null
|
||||
? (c.castOf(cur.color_guide_person).guide_color || "#00ff00") : "#00ff00");
|
||||
? (c.castOf(cur.color_guide_person).guide_color || plan.global_color_guide_ref1 || "#00ff00")
|
||||
: (plan.global_color_guide_ref1 || "#00ff00"));
|
||||
const viaRef = f => cur[f] ? "" : (c.castRef(cur, f) || plan["global_" + f] || "");
|
||||
|
||||
const head = h("div", { class: "edhd" }, [
|
||||
@@ -177,7 +178,8 @@ function editor(c, cur) {
|
||||
cur.color_guide ? h("label", { class: "row", style: "gap:6px;align-items:center" }, [
|
||||
h("span", { class: "hint" }, "Ref 1 color"),
|
||||
h("select", { value: cur.color_guide_color ||
|
||||
(cur.color_guide_person != null ? (c.castOf(cur.color_guide_person).guide_color || "#00ff00") : "#00ff00"),
|
||||
(cur.color_guide_person != null ? (c.castOf(cur.color_guide_person).guide_color || plan.global_color_guide_ref1 || "#00ff00")
|
||||
: (plan.global_color_guide_ref1 || "#00ff00")),
|
||||
onChange: e => c.setMeta(i, "color_guide_color", e.target.value) },
|
||||
GUIDE_COLORS.map(([v, label]) => h("option", { value: v }, label))),
|
||||
]) : null,
|
||||
@@ -192,7 +194,8 @@ function editor(c, cur) {
|
||||
h("button", { disabled: !!busy.value || !cur.color_guide_text2,
|
||||
onClick: () => c.previewMask(i, { text: cur.color_guide_text2, max_objects: 1 }) }, "👁 Preview"),
|
||||
h("select", { value: cur.color_guide_color2 || (cur.color_guide_person2 != null
|
||||
? (c.castOf(cur.color_guide_person2).guide_color || "#9b30ff") : "#9b30ff"),
|
||||
? (c.castOf(cur.color_guide_person2).guide_color || plan.global_color_guide_ref2 || "#9b30ff")
|
||||
: (plan.global_color_guide_ref2 || "#9b30ff")),
|
||||
title: "Choose the marker color for the person matched to ref 2",
|
||||
onChange: e => c.setMeta(i, "color_guide_color2", e.target.value) },
|
||||
GUIDE_COLORS.map(([v, label]) => h("option", { value: v, disabled: v === guideColor1 }, label))),
|
||||
|
||||
Reference in New Issue
Block a user