Files
DumasNodes/js/h3_longvideos/index.js
T

360 lines
10 KiB
JavaScript

import { app } from "/scripts/app.js";
import { applyAdaptiveCanvasOnly } from "../shared/nodes2.mjs";
const COMFY_CLASS = "DumasH3LongVideos";
const STATE_PROPERTY = "dumas_h3_longvideos_section_state";
const DOM_WIDGET_NAME = "dumas_h3_longvideos_sections";
const MIN_WIDTH = 520;
const MIN_HEIGHT = 280;
const GROUPS = [
{
id: "prompt",
label: "Prompt",
defaultCollapsed: false,
widgets: ["prompt", "resolution", "megapixels", "beat_split", "anchor_override", "per_beat_length", "shot_seconds", "plan_only", "fps"],
},
{
id: "refs",
label: "Refs",
defaultCollapsed: false,
widgets: ["ref_mode", "ref_image_size", "ref_noise_aug", "character_memory", "trim_seam", "vary_seed_per_shot", "handoff_offset"],
},
{
id: "sampling",
label: "Sampling",
defaultCollapsed: true,
widgets: [
"steps", "cfg", "sampler_name", "scheduler", "seed",
"apply_model_sampling", "shift_video", "shift_audio",
"allow_oversize_shots", "vram_headroom_gb", "allow_res_backoff",
"decode_tile_frames", "decode_tile_size", "cleanup_between_shots",
],
},
{
id: "audio",
label: "Audio",
defaultCollapsed: true,
widgets: [
"global_soundscape", "non_diegetic_music", "auto_soundscape",
"auto_silence_nonspeech", "allow_nonspeech_vocals",
"mute_nonspeech_audio", "mute_fade_ms",
],
},
{
id: "scene",
label: "Scene Logic",
defaultCollapsed: true,
widgets: [
"auto_wardrobe", "auto_props", "prevent_nudity", "exposed_terms",
"anatomy_guard", "subject_count_guard", "lock_restraints",
"contact_guard", "motion_guard", "solidity_guard",
],
},
{
id: "finish",
label: "Upscale/Detail",
defaultCollapsed: true,
widgets: [
"upscale", "upscale_model", "upscale_target_short_edge", "upscale_batch",
"detail_pass", "detail_sampler_name", "detail_scheduler", "detail_steps", "detail_denoise",
],
},
{
id: "overlay",
label: "Overlays",
defaultCollapsed: true,
widgets: [
"watermark_text", "watermark_position", "watermark_size", "watermark_opacity", "watermark_margin",
"intro_text", "intro_position", "intro_seconds", "intro_fade", "intro_size",
"overlay_font", "overlay_stroke",
],
},
];
function injectCSS() {
if (document.getElementById("dumas-h3lv-sections-css")) return;
const style = document.createElement("style");
style.id = "dumas-h3lv-sections-css";
style.textContent = `
.dh3lv-sections {
box-sizing: border-box;
width: 100%;
padding: 8px 10px 6px;
color: #e6e7eb;
font: 12px/1.35 "Segoe UI", sans-serif;
pointer-events: auto;
background: linear-gradient(180deg, rgba(33, 36, 42, 0.96), rgba(22, 24, 29, 0.96));
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.dh3lv-sections-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.dh3lv-sections-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #9da5b1;
}
.dh3lv-sections-actions {
display: flex;
gap: 6px;
}
.dh3lv-sections-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.dh3lv-chip,
.dh3lv-action {
appearance: none;
border: 1px solid #464d59;
background: #262c35;
color: #d7dce3;
border-radius: 999px;
padding: 5px 9px;
cursor: pointer;
font: inherit;
line-height: 1.1;
}
.dh3lv-chip[data-open="true"] {
background: #d96f2b;
border-color: #f09358;
color: #fff7f0;
}
.dh3lv-chip:hover,
.dh3lv-action:hover {
filter: brightness(1.06);
}
.dh3lv-count {
opacity: 0.78;
margin-left: 4px;
font-size: 11px;
}
`;
document.head.appendChild(style);
}
function defaultState() {
const state = {};
for (const group of GROUPS) state[group.id] = !group.defaultCollapsed;
return state;
}
function parseState(value) {
let parsed = value;
if (typeof parsed === "string") {
try {
parsed = JSON.parse(parsed);
} catch (_error) {
parsed = null;
}
}
const base = defaultState();
if (!parsed || typeof parsed !== "object") return base;
for (const group of GROUPS) {
if (typeof parsed[group.id] === "boolean") base[group.id] = parsed[group.id];
}
return base;
}
function readState(node) {
return parseState(node.properties?.[STATE_PROPERTY] || node._dh3lvSectionState || "");
}
function writeState(node, state) {
const normalized = parseState(state);
const serialized = JSON.stringify(normalized);
node._dh3lvSectionState = serialized;
node.properties = node.properties || {};
node.properties[STATE_PROPERTY] = serialized;
}
function findWidget(node, name) {
return (node.widgets || []).find((widget) => widget?.name === name) || null;
}
function setWidgetHidden(widget, hidden) {
if (!widget) return;
if (!widget._dh3lvOriginal) {
widget._dh3lvOriginal = {
type: widget.type,
computeSize: widget.computeSize,
hidden: widget.hidden,
};
}
if (hidden) {
widget.type = "hidden";
widget.hidden = true;
widget.computeSize = () => [0, -4];
return;
}
widget.type = widget._dh3lvOriginal.type;
widget.hidden = !!widget._dh3lvOriginal.hidden;
widget.computeSize = widget._dh3lvOriginal.computeSize;
}
function applyVisibility(node) {
const state = readState(node);
for (const group of GROUPS) {
for (const name of group.widgets) {
const widget = findWidget(node, name);
if (!widget || widget.name === DOM_WIDGET_NAME) continue;
setWidgetHidden(widget, !state[group.id]);
}
}
}
function resizeNode(node) {
requestAnimationFrame(() => {
const size = node.computeSize?.();
if (Array.isArray(size)) {
node.size[0] = Math.max(MIN_WIDTH, size[0] || 0, node.size?.[0] || 0);
node.size[1] = Math.max(MIN_HEIGHT, size[1] || 0);
}
node.setDirtyCanvas?.(true, true);
});
}
function renderToolbar(node) {
const ui = node._dh3lvUI;
if (!ui) return;
const state = readState(node);
ui.list.innerHTML = "";
for (const group of GROUPS) {
const button = document.createElement("button");
button.type = "button";
button.className = "dh3lv-chip";
button.dataset.open = state[group.id] ? "true" : "false";
button.textContent = state[group.id] ? `Hide ${group.label}` : `Show ${group.label}`;
const count = document.createElement("span");
count.className = "dh3lv-count";
count.textContent = String(group.widgets.filter((name) => findWidget(node, name)).length);
button.appendChild(count);
button.addEventListener("click", () => {
const next = readState(node);
next[group.id] = !next[group.id];
writeState(node, next);
applyVisibility(node);
renderToolbar(node);
resizeNode(node);
});
ui.list.appendChild(button);
}
}
function setAll(node, open) {
const next = {};
for (const group of GROUPS) next[group.id] = !!open;
writeState(node, next);
applyVisibility(node);
renderToolbar(node);
resizeNode(node);
}
function setupNode(node) {
if (node._dh3lvUI) return;
injectCSS();
writeState(node, readState(node));
const root = document.createElement("div");
root.className = "dh3lv-sections";
const head = document.createElement("div");
head.className = "dh3lv-sections-head";
const title = document.createElement("div");
title.className = "dh3lv-sections-title";
title.textContent = "Sections";
const actions = document.createElement("div");
actions.className = "dh3lv-sections-actions";
const expandAll = document.createElement("button");
expandAll.type = "button";
expandAll.className = "dh3lv-action";
expandAll.textContent = "Expand All";
expandAll.addEventListener("click", () => setAll(node, true));
const collapseAll = document.createElement("button");
collapseAll.type = "button";
collapseAll.className = "dh3lv-action";
collapseAll.textContent = "Collapse Extras";
collapseAll.addEventListener("click", () => {
const next = defaultState();
writeState(node, next);
applyVisibility(node);
renderToolbar(node);
resizeNode(node);
});
actions.append(expandAll, collapseAll);
head.append(title, actions);
const list = document.createElement("div");
list.className = "dh3lv-sections-list";
root.append(head, list);
node._dh3lvUI = { root, list };
const widget = node.addDOMWidget(DOM_WIDGET_NAME, "custom", root, {
getValue: () => null,
setValue: () => {},
serialize: false,
getMinHeight: () => 52,
hideOnZoom: false,
});
applyAdaptiveCanvasOnly(widget);
const widgets = node.widgets || [];
const index = widgets.indexOf(widget);
if (index > 0) {
widgets.splice(index, 1);
widgets.unshift(widget);
}
applyVisibility(node);
renderToolbar(node);
resizeNode(node);
}
app.registerExtension({
name: "Dumas.H3LongVideosSections",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== COMFY_CLASS) return;
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function onNodeCreated() {
const result = originalOnNodeCreated?.apply(this, arguments);
setupNode(this);
return result;
};
const originalConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function onConfigure() {
const result = originalConfigure?.apply(this, arguments);
setupNode(this);
writeState(this, readState(this));
applyVisibility(this);
renderToolbar(this);
resizeNode(this);
return result;
};
const originalSerialize = nodeType.prototype.onSerialize;
nodeType.prototype.onSerialize = function onSerialize(o) {
writeState(this, readState(this));
const result = originalSerialize?.apply(this, arguments);
if (o && this.properties?.[STATE_PROPERTY]) {
o.properties = o.properties || {};
o.properties[STATE_PROPERTY] = this.properties[STATE_PROPERTY];
}
return result;
};
},
});