Files
alisson-anjosandClaude Opus 5.5 d94e1c2b19 Shot Planner: Colored Mark per picture, refs outputs, object preset, fixed points window (1.113.0)
- Colored Mark: every picture has a mark color (12-color palette, editable under each
  global picture); a shot's selection and each extra person ("+ Mark another person",
  text or points) pick the picture they become ("Picture 3 · red", with its
  thumbnail) and are drawn in its color. The prompt gets a model-agnostic mapping
  ("the person marked in red becomes the character from <Picture 3>"). The
  conditioning node checks each marked picture is passed and colors are not shared.
  Plans with the old second region open with it as <Picture 2>.
- Points window: fixed size; the frame fits a fixed stage (points are stored relative to
  the frame); one point set per marked person.
- BFS Shot Planner outputs refs (every picture the shots use, as a list) and ref_count
  where ref_image / ref_image_2 were.
- Saved workflows are upgraded on load: the planner's ref_image / ref_image_2 inputs are
  removed, old ref outputs become refs / ref_count, and what fed Repack's ref_image
  feeds refs.
- New describe preset "object" (a noun phrase: what it is, shape, materials, colors,
  text), written for the prompt sentence like "short".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:27:19 +00:00

120 lines
8.9 KiB
JavaScript

// The busy bar and the points window (🎯 Points…: click on a frame to pick the subject for SAM 3).
import { Teleport } from "../vendor/vue.esm-browser.prod.mjs";
import { h, hint, t, pictureColor, markName } from "./common.js";
export function busyBar(c, msg) {
const { busySince, now, upPct, status } = c;
const secs = busySince.value ? Math.max(0, Math.round((now.value - busySince.value) / 1000)) : 0;
const pct = upPct.value >= 0 ? upPct.value : (status.total > 0 ? Math.min(1, status.done / status.total) : -1);
const stage = status.stage && !msg.startsWith(status.stage) ? status.stage : "";
return h("div", { class: "busybar" }, [
h("div", { class: "brow" }, [h("span", { class: "spin" }), h("b", msg), stage ? hint(`· ${stage}`) : null,
h("span", { class: "grow" }), pct >= 0 ? h("span", `${Math.round(pct * 100)}%`) : null, hint(`${secs}s`)]),
h("div", { class: ["bprog", pct < 0 && "ind"] }, [h("div", { style: pct >= 0 ? `width:${(pct * 100).toFixed(1)}%` : "" })]),
]);
}
export function pointsModal(c) {
const { modal, segs, plan, api, setMeta } = c;
const ms = segs.value[modal.idx];
if (!modal.open || !ms) return null;
// Colored Mark: region 1 = the shot's own selection, 2.. = its extra regions; each one is a picture, in its color
const regionPicture = r => (r === 1 ? ms.color_guide_picture : ms.regions[r - 2]?.picture) || 1;
const regionColor = r => pictureColor(plan, regionPicture(r));
const regionsN = ms.color_guide ? 1 + ms.regions.length : 1;
const pointOverlays = ms.color_guide
? Array.from({ length: regionsN }, (_, q) => q + 1).flatMap(region => (region === modal.region ? modal.points : modal.groups[region]?.points || []).map((p, k) => h("div", {
class: ["pt", "cg", region !== modal.region && "other", p.label ? "pos" : "neg"],
style: `left:${p.x * 100}%;top:${p.y * 100}%;--point-color:${regionColor(region)}`,
title: `Region ${region} · Picture ${regionPicture(region)}${region === modal.region ? " · click to remove" : ""}`,
onPointerdown: e => { if (region !== modal.region) return; e.stopPropagation(); modal.points = modal.points.filter((_, j) => j !== k); modal.prev = null; },
})))
: modal.points.map((p, k) => h("div", { class: ["pt", p.label ? "pos" : "neg"], style: `left:${p.x * 100}%;top:${p.y * 100}%`,
title: "click to remove", onPointerdown: e => { e.stopPropagation(); modal.points = modal.points.filter((_, j) => j !== k); modal.prev = null; } }));
const regionButton = r => {
const k = regionPicture(r), name = ms.names[k - 1];
return h("button", { class: modal.region === r ? "pri" : "", onClick: () => c.switchPointRegion(r),
title: `Region ${r}: becomes <Picture ${k}> (${markName(regionColor(r))})` }, [
name ? h("img", { class: "mthumb", src: c.viewUrl(name), style: `--mark:${regionColor(r)}` }) : null,
` Picture ${k} · ${markName(regionColor(r))} (${(r === modal.region ? modal.points : modal.groups[r]?.points || []).length})`]);
};
return h(Teleport, { to: "body" }, h("div", { class: "bsl", style: "background:none;padding:0" }, h("div", { class: "mmodal",
onKeydown: e => e.stopPropagation(), onPointerdown: e => { if (e.target === e.currentTarget) modal.open = false; } }, [
h("div", { class: "mbox" }, [
h("div", { class: "row", style: "margin-bottom:6px" }, [h("span", { class: "sect" }, `Shot #${modal.idx + 1} · points`),
ms.color_guide ? h("div", { class: "row" }, [hint("One point set per marked person; switch to edit each. All use the frame selected below."),
...Array.from({ length: regionsN }, (_, q) => regionButton(q + 1))]) : null,
hint("click = keep · right-click or shift+click = exclude · click a point to remove it")]),
h("div", { class: "mstage" }, [
h("div", { class: "mimg", onContextmenu: e => e.preventDefault(), onPointerdown: e => {
const box = e.currentTarget.getBoundingClientRect();
const x = (e.clientX - box.left) / box.width, y = (e.clientY - box.top) / box.height;
modal.points = [...modal.points, { x: +x.toFixed(4), y: +y.toFixed(4), label: (e.button === 2 || e.shiftKey) ? 0 : 1 }];
modal.prev = null;
} }, [
h("img", { draggable: false, src: api.apiURL(`/bfs/shotloop/frame?video=${encodeURIComponent(plan.video)}&fps=${plan.fps}&f=${ms.start + modal.key}&w=900`) }),
...pointOverlays,
])]),
h("div", { class: "row", style: "margin-top:6px" }, [
hint(`frame ${ms.start + modal.key} (${modal.key + 1}/${ms.len})`),
h("input", { type: "range", min: 0, max: ms.len - 1, step: 1, value: modal.key, style: "flex:1",
onInput: e => { modal.key = parseInt(e.target.value); modal.prev = null; } }),
]),
h("div", { class: "row", style: "margin-top:6px" }, [
h("input", { type: "text", value: modal.text, style: "flex:1", placeholder: "text prompt (used when there are no points)",
onChange: e => { modal.text = e.target.value; } }),
h("button", { onClick: () => { modal.points = []; modal.prev = null; } }, "Clear points"),
h("button", { class: "pri", disabled: !!modal.busy || !(modal.points.length || modal.text),
onClick: () => c.previewMask(modal.idx, { points: modal.points, key: modal.key, text: modal.text,
preview_color: ms.color_guide ? regionColor(modal.region) : undefined }, true) }, "👁 Segment"),
]),
modal.busy ? busyBar(c, modal.busy) : null,
modal.prev ? h("div", { class: "mstrip" }, [...modal.prev.frames.map(f => h("img", { src: f.src, title: `frame ${f.f}` })),
hint(modal.prev.empty ? "nothing found" : `covers ${(modal.prev.coverage * 100).toFixed(1)}%`)]) : null,
h("div", { class: "row", style: "margin-top:8px;justify-content:flex-end" }, [
h("button", { onClick: () => { modal.open = false; } }, "Cancel"),
!ms.color_guide ? h("button", { title: "Apply these points to EVERY shot, each on its frame at the same relative position (a subject that stays in place)",
onClick: c.savePointsAll }, "Save → all shots") : null,
h("button", { disabled: !modal.points.length || ms.color_guide,
title: "Track this selection with SAM 3 through the whole video (or the range), forward and backward from this frame; every shot without its own selection takes its part of the track. Also type a short text (e.g. 'woman in pink top') so the subject is found again after camera cuts.",
onClick: c.saveTrack }, "🎯 Track → whole video"),
h("button", { class: "pri", onClick: c.savePoints }, "Save (this shot)"),
]),
]),
])));
}
const TOUR_STEPS = [
["video", "Video", "Choose a video, set the frame range, split at camera cuts, then set the generation size."],
["shots", "Shots", "Review each shot, assign its references and prompt, and choose how the result is applied."],
["people", "People", "Track detected face identities to filter or split shots. Set masks here or per shot."],
["prompts", "Prompts", "Set shared references, prompts and optional VLM assistance. Written prompts are kept exactly as authored."],
["shots", "Colored Mark", "Colored Mark adds a translucent color overlay to a selected region in the source video. It does not enable inpainting. Choose the color before marking the region with points or a description."],
["run", "Run", "Check warnings, choose auto or queue loop, and set filters or quick-run limits."],
];
export function tourModal(c) {
const { tour, tab } = c;
const step = tour.value - 1;
if (step < 0 || step >= TOUR_STEPS.length) return null;
const [targetTab, title, body] = TOUR_STEPS[step];
return h(Teleport, { to: "body" }, h("div", { class: "bsl", style: "background:none;padding:0" },
h("div", { class: "tourback", role: "dialog", "aria-modal": "true", "aria-label": t("Shot Planner tour"),
onKeydown: e => { if (e.key === "Escape") tour.value = 0; }, onPointerdown: e => { if (e.target === e.currentTarget) tour.value = 0; } }, [
h("div", { class: "tourbox" }, [
h("div", { class: "row" }, [h("b", t("Shot Planner tour")), h("span", { class: "grow" }), h("span", { class: "hint" }, `${step + 1} / ${TOUR_STEPS.length}`),
h("button", { class: "ghost", title: t("Close tour"), "aria-label": t("Close tour"), onClick: () => { tour.value = 0; } }, "✕")]),
h("h3", t(title)),
h("p", t(body)),
h("p", { class: "hint" }, t("This tour is optional. Use Next to visit each main tab; close it whenever you like.")),
h("div", { class: "row", style: "justify-content:flex-end;margin-top:12px" }, [
step > 0 ? h("button", { onClick: () => { tour.value--; tab.value = TOUR_STEPS[step - 1][0]; } }, t("Back")) : null,
h("button", { class: "pri", onClick: () => {
if (step === TOUR_STEPS.length - 1) tour.value = 0;
else { tour.value++; tab.value = TOUR_STEPS[step + 1][0]; }
} }, t(step === TOUR_STEPS.length - 1 ? "Close tour" : "Next")),
]),
]),
])));
}