From 30400038c27a7fc93aff7bb2b56e62187465f202 Mon Sep 17 00:00:00 2001 From: Chris Dumas Date: Wed, 2 Sep 2026 13:25:54 +0000 Subject: [PATCH] Add collapsible H3 long video sections --- js/h3_longvideos/index.js | 359 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 js/h3_longvideos/index.js diff --git a/js/h3_longvideos/index.js b/js/h3_longvideos/index.js new file mode 100644 index 0000000..2aeda16 --- /dev/null +++ b/js/h3_longvideos/index.js @@ -0,0 +1,359 @@ +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; + }; + }, +});