Files
alisson-anjos-ComfyUI-BFSNodes/web/js/shotloop/view_video.js
T

95 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 🎞 Video tab: the source video, how it is split into shots, and the generation size.
import { h, GRIDS, fld, select, section, pill, hint, fmtT } from "./common.js";
export function videoTab(c) {
const { plan, an, files, size, busy, detectorUsed, maxLen } = c;
const fps = plan.fps;
const num = (k, step = 0.1, min = 0) => h("input", { type: "number", step, min, value: plan[k],
onChange: e => c.setPlan(k, parseFloat(e.target.value) || 0,
["max_s", "min_s"].includes(k) ? () => c.autoSplit(true) : undefined) });
const sel_ = (k, opts, after) => select(plan[k], opts, v =>
c.setPlan(k, v, after || (k === "mode" ? () => c.autoSplit(true) : undefined)));
const au = an.value?.audio;
const source = section("Source video", [
h("div", { class: "row" }, [
h("div", { style: "flex:1;min-width:180px" }, [h("select", { value: plan.video, onChange: e => c.setVideo(e.target.value) },
[h("option", { value: "" }, "— choose a video from the input folder —"), ...files.videos.map(v => h("option", { value: v }, v))])]),
h("button", { class: plan.video ? "" : "pri", onClick: () => c.pickFile("video/*", c.setVideo) }, "⬆ Upload"),
h("button", { onClick: c.refreshFiles, title: "refresh the input folder" }, "↻"),
]),
an.value ? h("div", { class: "row", style: "margin-top:6px" }, [
pill(`${an.value.width}×${an.value.height}`), pill(`${an.value.fps_src.toFixed(2)} fps`), pill(`${an.value.duration.toFixed(2)} s`),
pill(`timeline ${fps} fps · ${an.value.n} frames`), pill(`generate at ${size.w}×${size.h}`, "acc"),
au?.has_audio
? pill(`🔊 audio · ${au.sample_rate ? (au.sample_rate / 1000).toFixed(1) + " kHz" : "?"} · ${au.channels || "?"} ch`, "ok", au.codec || "")
: pill(`🔇 ${au?.reason || "no audio"} → silent track`, "warn", "The outputs carry a silent track of the right length, so Create Video works"),
h("select", { value: plan.audio_mode, style: "width:auto", disabled: !au?.has_audio, title: "Audio of the planner's and the join's outputs",
onChange: e => c.setPlan("audio_mode", e.target.value) },
[h("option", { value: "auto" }, "use the video's audio"), h("option", { value: "silent" }, "silent track")]),
]) : plan.video ? null : h("div", { class: "empty" }, [h("b", "Pick or upload a video to start"),
"It is split into shots the model can generate; every shot then gets its own reference, prompt and mask."]),
]);
const R = c.range.value, N = c.n.value;
const playF = Math.min(N - 1, Math.max(0, c.play.frame || 0));
// a player right here, so the range is picked by watching (scrub it, or click the timeline above)
const rng = section("Range", [
h("div", { class: "player" }, [
h("video", { ref: c.vid, src: c.viewUrl(plan.video), preload: "metadata", controls: true, playsinline: true,
style: "max-height:240px" }),
h("div", { class: "pinfo" }, [
h("div", { class: "tc" }, [
h("div", ["now ", h("b", fmtT(playF, fps)), ` (frame ${playF})`]),
h("div", ["start ", h("b", fmtT(R.lo, fps)), ` (frame ${R.lo})`]),
h("div", ["end ", h("b", fmtT(R.hi, fps)), ` (frame ${R.hi})`]),
]),
h("div", { class: "rgrid" }, [
h("button", { class: "pri", title: "the range starts at the player's frame", onClick: () => c.setRange("start_f", playF) }, "⇤ start here"),
fld("Start frame", h("input", { type: "number", step: 1, min: 0, max: N - 1, value: R.lo,
onChange: e => c.setRange("start_f", e.target.value) }), fmtT(R.lo, fps)),
h("button", { class: "pri", title: "the range ends after the player's frame", onClick: () => c.setRange("end_f", playF + 1) }, "⇥ end here"),
fld("End frame (0 = end)", h("input", { type: "number", step: 1, min: 0, max: N, value: R.hi >= N ? 0 : R.hi,
onChange: e => c.setRange("end_f", e.target.value) }), fmtT(R.hi, fps)),
(R.lo > 0 || R.hi < N) ? h("button", { class: "ghost", onClick: () => { plan.start_f = 0; c.setRange("end_f", 0); } }, "✕ whole video") : h("span"),
]),
hint(`Play or scrub the video (or click the timeline), then ⇤ / ⇥. ${R.hi - R.lo} frames (${((R.hi - R.lo) / fps).toFixed(2)} s) of ${N}: only this part is split, analysed, generated and joined.`),
]),
]),
], { sub: "work on a part of the video" });
const split = section("Split into shots", [
h("div", { class: "grid" }, [
fld("Mode", sel_("mode", [["shots", "Camera cuts"], ["fixed", "Fixed length"], ["single", "One shot (whole range)"]]),
plan.mode === "single" ? "ignores Max seconds / shot" : ""),
// the range in seconds, here next to the mode: "15 s in one shot" is set without leaving this tab
fld("Range length, seconds (0 = to the end)", h("input", { type: "number", step: 0.5, min: 0,
value: c.range.value.hi < c.n.value ? +((c.range.value.hi - c.range.value.lo) / plan.fps).toFixed(2) : 0,
onChange: e => { const v = parseFloat(e.target.value) || 0;
c.setRange("end_f", v > 0 ? c.range.value.lo + Math.round(v * plan.fps) : 0); } }),
`from the start frame · ${c.range.value.hi - c.range.value.lo} frames`),
fld("Detector", sel_("detector", [["adaptive", "PySceneDetect adaptive"], ["content", "PySceneDetect content"], ["builtin", "Built-in"]], () => c.autoSplit(true))),
fld(`Sensitivity ${Number(plan.sensitivity).toFixed(2)}`, h("input", { type: "range", min: 0, max: 1, step: 0.05, value: plan.sensitivity,
onInput: e => { plan.sensitivity = parseFloat(e.target.value); }, onChange: () => { c.save(); c.autoSplit(true); } })),
fld("Frame grid", sel_("grid", Object.keys(GRIDS))),
fld("Max seconds / shot", num("max_s", 0.1, 0.2), `= ${maxLen.value} frames (${(maxLen.value / fps).toFixed(2)} s)`),
fld("Min seconds / shot", num("min_s", 0.1, 0)),
fld("Max shots (0 = all)", num("max_parts", 1, 0), "limit how many detected shots are used"),
fld("Timeline fps", num("fps", 1, 1), "re-analyses"),
]),
h("div", { class: "row", style: "margin-top:8px" }, [
h("button", { class: "pri", disabled: !plan.video || !!busy.value, onClick: () => c.autoSplit(true) }, "✂ Auto split"),
h("button", { disabled: !plan.video, onClick: c.analyze }, "↻ Re-analyse"),
detectorUsed.value ? hint(`detector: ${detectorUsed.value}`) : null,
hint("Auto split replaces the boundaries; per-shot settings stay with the shot at the same position."),
]),
], { sub: "camera cuts or fixed length, on the model's frame grid" });
const gen = section("Generation size", h("div", { class: "grid" }, [
fld("Megapixels", num("megapixels", 0.01, 0.02), size.w ? `${size.w}×${size.h}` : ""),
fld("Size multiple", num("multiple", 8, 8)),
]), { sub: "cropped shots use it for the crop" });
return h("div", { class: "card" }, [source, an.value ? rng : null, an.value ? split : null, an.value ? gen : null]);
}