Fix Color Guide reference history and add global colors

This commit is contained in:
alisson-anjos
2026-10-13 16:28:18 +00:00
parent 27090c979a
commit 2b0caac30a
8 changed files with 37 additions and 10 deletions
+4 -1
View File
@@ -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
View File
@@ -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",
+7 -3
View File
@@ -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();
};
+2 -1
View File
@@ -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.
+3 -1
View File
@@ -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,
+2
View File
@@ -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}
+12
View File
@@ -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" });
}
+6 -3
View File
@@ -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))),