import { api } from "/scripts/api.js"; import { app } from "/scripts/app.js"; import { hideInputsInDef, hideWidget } from "./widget_visibility.mjs"; import { mountTransformTrim } from "./transform_trim.mjs"; import { clipOutputRate, inputNumber } from "./clip_rate.mjs"; import { BRAND, chainCallback, keepDomWidgetWidthAuto, notifyAusbossChange, showToast } from "./index.mjs"; import { WIDGET_FRAME, fillNodeHeight, holdVueNodeMinWidth } from "./panel_layout.mjs"; import { mediaViewQuery } from "./media_list.mjs"; import { createMediaPicker } from "./media_picker.mjs"; import { normalizeFillColor } from "./fill_color.mjs"; import { makeScrubInput } from "./scrub_input.mjs"; import { confirmDiscard } from "./discard_prompt.mjs"; import { keepKeyInField } from "./canvas_passthrough.mjs"; import { featherGeneratedMask, overlayPlan, seeThroughMap, stitchBlendFromMask } from "./stitch_preview.mjs"; import { suppressCoreVideoPreview } from "./core_preview.mjs"; import { gearIconSvg, loadSettings, openSettingsMenu } from "./settings_menu.mjs"; import { INPUT_FOLDER_MODE, LOCAL_PATH_MODE, normalizeVideoOptions, mediaSourceState, videoSourceState, } from "./video_source_card.mjs"; import { aspectMatches, axisBands, canvasCornerCenters, canvasLocalPoint, canvasSize, centredPadding, clamp, cornerScale, cropForRotation, declaredTransformDefaults, fitSourceToAspect, cropHandleCenters, isUntouched, evenOutPadding, knobAt, knobOffset, knobStep, lockPadding, lockedPadMinimum, moveCursor, moveRoom, nearestHandle, padAround, paddingAxis, KNOB_CLEARANCE, paddingHandleCenters, paddingRingGaps, parseAspectRatio, placeKnob, ratioLabel, rememberedSource, resetTransformValues, resizeCrop, resolveCrop, resolvePadding, rotatedSize, scaleCanvasPadding, scaleToMegapixels, sizeChain, sizeChainTokens, slidePadding, sourceChanged, sourceResetValues, stageHandleLayout, stageHeightForWidth, stepWithoutStretch, tightLockPadding, turnAspect, turnedCrop, wrapDegrees, zoomAround, } from "./transform_geometry.mjs"; import { clampFrame, clipInfo, frameTime } from "./timeline_math.mjs"; import { liftSocket } from "./widget_card.mjs"; const HIDDEN_WIDGETS = [ "image", "upload", "video", "source_mode", "local_path", "rotation_degrees", "crop_aspect_ratio", "crop_x", "crop_y", "crop_width", "crop_height", "pad_left", "pad_top", "pad_right", "pad_bottom", "feather", "canvas_multiple", "fill_color", "seek_mode", "frame_index", "frame_time", "start_seconds", "end_seconds", "every_nth", "max_frames", "frame_snap", "fixed_frames", // Stitch settings (clip and image nodes), driven by the editor's Inpaint & Stitch section. "stitch_blend", "stitch_grow", // Image-node resize block; hideWidget on a missing widget is a no-op, so // the video node sharing this list is unaffected. "resize_to_megapixels", "megapixels", "resize_method", "resolution_steps", ]; const trimInputDriven = (node, name) => node.inputs?.some( (input) => (input.name === name || input.widget?.name === name) && input.link != null, ) ?? false; const RESIZE_METHODS = ["lanczos", "area", "bicubic", "bilinear", "nearest-exact"]; // The widest the control rows get on a big node; the picture takes the rest. const CONTROL_MAX_WIDTH = 460; const TRANSFORM_DEFAULTS = resetTransformValues(false); const CORE_IMAGE_PREVIEW_WIDGET = "$$canvas-image-preview"; // The gear on the image node's source card. Display choices only, kept for // every image node on this browser (settings_menu.mjs stores them). const SETTINGS_SCOPE = "image_crop_rotate_pad"; const SETTINGS_SCHEMA = [ { key: "show_mask", label: "Show the mask on the picture", type: "toggle", default: true, hint: "Teal over the parts of your picture the model paints: a mask drawn in the MaskEditor, or see-through parts of a PNG.", }, ]; let transformSettings = null; function settings() { transformSettings ??= loadSettings(SETTINGS_SCOPE, SETTINGS_SCHEMA); return transformSettings; } // Every installed node, so a changed setting redraws them all at once. const liveStates = new Set(); function installStyles() { if (document.getElementById("ausboss-transform-styles")) return; const style = document.createElement("style"); style.id = "ausboss-transform-styles"; style.textContent = ` .ausboss-transform-panel{display:flex;flex-direction:column;gap:8px;padding:8px;color:#ddd;font:12px system-ui;box-sizing:border-box;width:100%;height:100%;overflow:hidden} .ausboss-transform-panel>:not(.ausboss-transform-preview){width:100%;max-width:${CONTROL_MAX_WIDTH}px;align-self:center;box-sizing:border-box} .ausboss-transform-preview{width:100%;flex:1 1 180px;min-height:0;border:1px solid #50555b;border-radius:8px;background:#111;display:block;touch-action:none} .ausboss-transform-source{display:flex;flex-direction:column;gap:7px;flex:0 0 auto;padding:8px;border:1px solid rgba(0,184,174,.28);border-radius:8px;background:rgba(0,0,0,.24)} .ausboss-transform-source-heading{color:${BRAND};font:600 10px system-ui;letter-spacing:.08em;text-transform:uppercase} .ausboss-transform-source-head{display:flex;align-items:center;justify-content:space-between;gap:6px} .ausboss-transform-gear{flex:none;width:20px;height:20px;margin:-4px 0;display:grid;place-items:center;padding:0;border:1px solid #2a3437;border-radius:5px;background:#0f1516;color:#8ba3a1;cursor:pointer} .ausboss-transform-gear:hover{border-color:${BRAND};color:${BRAND}} .ausboss-transform-source-mode{display:grid;grid-template-columns:1fr 1fr;height:30px;padding:3px;border:1px solid #2a3437;border-radius:7px;background:#0f1516} .ausboss-transform-source-mode button{border:0;border-radius:5px;background:transparent;color:#8ba3a1;font:600 12px system-ui;cursor:pointer} .ausboss-transform-source-mode button:hover{color:#fff}.ausboss-transform-source-mode button.on{background:${BRAND};color:#04201d} .ausboss-transform-source-field{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center} .ausboss-transform-source-field select,.ausboss-transform-source-field input{box-sizing:border-box;width:100%;height:34px;min-width:0;padding:0 10px;border:1px solid #2a3437;border-radius:7px;outline:0;background:#0b0f10;color:#dce9e8;font:12px ui-monospace,"SF Mono",Menlo,Consolas,monospace} .ausboss-transform-source-field select:focus,.ausboss-transform-source-field input:focus{border-color:${BRAND}} .ausboss-transform-source-field .ausboss-media-pick{box-sizing:border-box;width:100%;height:34px;min-width:0;padding:0 28px 0 10px;border:1px solid #2a3437;border-radius:7px;background:#0b0f10 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238ba3a1' stroke-width='1.5'/%3E%3C/svg%3E") no-repeat right 10px center;color:#dce9e8;font:12px ui-monospace,"SF Mono",Menlo,Consolas,monospace} .ausboss-transform-source-field .ausboss-media-pick:hover,.ausboss-transform-source-field .ausboss-media-pick:focus-visible{border-color:${BRAND};outline:0} .ausboss-transform-source-action{height:34px;box-sizing:border-box;white-space:nowrap} .ausboss-transform-source-field:has(input[type=text]){grid-template-columns:minmax(0,1fr)} .ausboss-transform-source-hint{overflow:hidden;color:#6f8886;font-size:10.5px;line-height:1.25;white-space:nowrap;text-overflow:ellipsis} .lg-node:has(.ausboss-transform-panel) .image-preview{display:none!important} .ausboss-transform-row{display:flex;gap:7px;align-items:center;flex:0 0 auto}.ausboss-transform-row>*{min-width:0;flex:1} .ausboss-transform-canvas-row{justify-content:space-between;flex-wrap:wrap;row-gap:6px} .ausboss-transform-canvas-row>label{flex:0 0 auto;display:flex;align-items:center;gap:6px;color:#aeb4ba;font-size:11px;white-space:nowrap;cursor:default;user-select:none} .ausboss-transform-canvas-row>label>span{color:#8ca8a5;font-size:11px} .ausboss-transform-canvas-row>.ausboss-transform-centre-label{flex:0 0 auto;display:flex;align-items:center;gap:6px;color:#8ca8a5;font-size:11px;white-space:nowrap;cursor:default;user-select:none} .ausboss-transform-swatch{width:30px;height:22px;padding:1px;border:1px solid #555b63;border-radius:5px;background:#23272c;cursor:pointer} .ausboss-transform-swatch::-webkit-color-swatch-wrapper{padding:1px}.ausboss-transform-swatch::-webkit-color-swatch{border:0;border-radius:3px} .ausboss-transform-canvas-row input[type=checkbox]{accent-color:${BRAND};margin:0;cursor:pointer} .ausboss-transform-resize-row{justify-content:flex-start;gap:14px} .ausboss-transform-readout{flex:0 0 auto;margin-top:-3px;color:#c9d0d6;font:11px/14px system-ui;font-variant-numeric:tabular-nums;text-align:center;overflow:hidden;max-height:56px;cursor:default;user-select:none} .ausboss-transform-readout:empty{display:none} .ausboss-transform-readout span{color:#8ca8a5}.ausboss-transform-readout b{color:#fff;font-weight:600} .ausboss-transform-readout i{display:block;color:#ffc46b;font-style:normal} .ausboss-transform-readout em{color:#8ca8a5;font-style:normal} .ausboss-transform-readout em.note{color:#ffc46b} .ausboss-transform-readout button{margin:0 0 0 6px;height:14px;padding:0 6px;border:1px solid #6b5a33;border-radius:3px;background:#2a2417;color:#ffd79a;font:600 9.5px/12px system-ui;cursor:pointer;vertical-align:0} .ausboss-transform-readout button:hover{border-color:#ffc46b;color:#fff} .ausboss-transform-aspects{display:flex;gap:5px;align-items:center;flex:0 0 auto} .ausboss-transform-aspects>span{flex:0 0 auto;color:#8ca8a5;font-size:10px;padding:0 3px;user-select:none} .ausboss-transform-aspect{flex:1 1 0;min-width:0;background:#262a30;color:#cfd6dc;border:1px solid #4a5058;border-radius:4px;height:28px;padding:3px 2px;font:600 10px system-ui;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer;text-align:center} .ausboss-transform-aspect-flip{flex:0 0 30px;display:flex;align-items:center;justify-content:center;margin-right:3px} .ausboss-transform-aspect-glyph{display:block;border:1px solid currentColor;border-radius:1px;box-sizing:border-box} .ausboss-transform-aspect:hover{border-color:#9aa5ad;color:#fff} .ausboss-transform-aspect.active,.ausboss-transform-aspect-hold.on{background:${BRAND};border-color:${BRAND};color:#04201d} .ausboss-transform-aspect.active:hover,.ausboss-transform-aspect-hold.on:hover{border-color:#e5fffc;color:#04201d} .ausboss-transform-aspects>.ausboss-transform-aspect-caption{min-width:34px;text-align:center} .ausboss-transform-aspects>.ausboss-transform-aspect-caption.custom{color:#e3e8ec} .ausboss-transform-aspects>.ausboss-transform-aspect-caption.held{color:${BRAND};font-weight:600} .ausboss-transform-aspect-hold{flex:0 0 28px;display:flex;align-items:center;justify-content:center;color:#8d9aa2} .ausboss-transform-aspect-hold.idle{opacity:.4;cursor:default} .ausboss-transform-aspect-hold.idle:hover{border-color:#4a5058;color:#8d9aa2} .ausboss-transform-aspect-lock{display:inline-block;line-height:0} .ausboss-transform-centre{display:inline-flex;gap:4px;flex:0 0 auto;align-items:center} .ausboss-transform-centre>.ausboss-transform-centre-button{flex:0 0 28px;width:28px;height:24px;padding:0;display:flex;align-items:center;justify-content:center;color:#cfd6dc} .ausboss-transform-centre>.ausboss-transform-centre-button.idle{opacity:.4;cursor:default} .ausboss-transform-centre>.ausboss-transform-centre-button.idle:hover{border-color:#4a5058;color:#cfd6dc} .ausboss-transform-centre-glyph{display:inline-block;line-height:0} .ausboss-transform-section .ausboss-transform-centre-row{display:grid;grid-template-columns:108px minmax(0,1fr);gap:7px;align-items:center;margin:7px 0} .ausboss-transform-fit-label{display:flex;align-items:center;gap:6px;color:#aeb4ba;font-size:11px;cursor:default;user-select:none} .ausboss-transform-fit{display:grid;grid-template-columns:1fr 1fr;flex:1 1 auto;height:26px;padding:2px;border:1px solid #2a3437;border-radius:6px;background:#0f1516;box-sizing:border-box} .ausboss-transform-fit button{border:0;border-radius:4px;background:transparent;color:#8ba3a1;font:600 11px system-ui;cursor:pointer} .ausboss-transform-fit button:hover{color:#fff}.ausboss-transform-fit button.on{background:${BRAND};color:#04201d} .ausboss-transform-fit.idle{opacity:.45} .ausboss-transform-fit-hint{flex:0 1 auto;min-width:0;color:#6f8886;font-size:10.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .ausboss-transform-aspect-modes{flex-wrap:wrap;row-gap:6px} .ausboss-transform-aspect-modes>.ausboss-transform-fit-label{flex:1 0 auto} .ausboss-transform-aspect-modes .ausboss-transform-fit{min-width:96px} .ausboss-transform-aspect-modes>.ausboss-transform-alignment{margin-left:auto} .ausboss-transform-switch{flex:0 0 76px;height:24px} .ausboss-transform-canvas-row .ausboss-transform-switch{width:76px} .ausboss-transform-button,.ausboss-transform-modal button:not(.ausboss-scrub-step>button):not(.ausboss-transform-aspect):not(.ausboss-transform-fit>button):not(.ausboss-transform-trim-reset):not(.ausboss-transform-trim-pill>button):not(.ausboss-transform-trim-handle){background:#30343a;color:#eee;border:1px solid #555b63;border-radius:5px;padding:7px 10px;cursor:pointer} .ausboss-transform-button:hover,.ausboss-transform-modal button:not(.ausboss-scrub-step>button):not(.ausboss-transform-aspect):not(.ausboss-transform-fit>button):not(.ausboss-transform-trim-reset):not(.ausboss-transform-trim-pill>button):not(.ausboss-transform-trim-handle):hover{border-color:${BRAND};background:#383e44} .ausboss-transform-file{position:relative;text-align:center;overflow:hidden}.ausboss-transform-file input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer} .ausboss-transform-modal{position:fixed;inset:0;z-index:100000;background:#101214;color:#e6e8ea;font:13px system-ui;display:grid;grid-template-rows:42px minmax(0,1fr) auto} .ausboss-transform-header{display:flex;align-items:center;gap:12px;padding:0 12px;border-bottom:1px solid #30343a;background:#17191c} .ausboss-transform-header strong{color:#fff}.ausboss-transform-header .spacer{flex:1} .ausboss-transform-close{background:${BRAND}!important;border-color:${BRAND}!important;color:#06231f!important;font-weight:600} .ausboss-transform-close:hover{filter:brightness(1.15)} .ausboss-transform-danger{background:#4a1717!important;border-color:#a13a3a!important;color:#ffd9d9!important} .ausboss-transform-danger:hover{background:#6b1f1f!important;border-color:#c74e4e!important} .ausboss-transform-body{display:grid;grid-template-columns:304px minmax(320px,1fr) 250px;min-height:0} .ausboss-transform-sidebar{padding:12px;border-right:1px solid #30343a;overflow:auto;background:#181b1e} .ausboss-transform-sidebar.right{border-right:0;border-left:1px solid #30343a} .ausboss-transform-section{border-bottom:1px solid #34383d;padding:0 0 13px;margin:0 0 13px} .ausboss-transform-section h3{font-size:11px;color:${BRAND};text-transform:uppercase;margin:0 0 8px;display:flex;align-items:center;gap:7px} .ausboss-legend{display:inline-block;flex:0 0 auto;width:10px;height:10px} canvas.ausboss-legend{width:16px;height:16px} .ausboss-legend-crop{background:#4bd8ef;border:1px solid #08272d} .ausboss-legend-pad{background:#ff9d42;border:1px solid #3b2108;width:9px;height:9px;transform:rotate(45deg)} .ausboss-final-preview{display:block;max-width:100%;margin:2px auto 0;border:1px solid #34383d;border-radius:6px;background:#0c0e10} .ausboss-transform-section label{display:grid;grid-template-columns:88px 1fr 58px;gap:7px;align-items:center;margin:7px 0} .ausboss-transform-section label>.ausboss-scrub:last-child{width:58px} .ausboss-transform-section label.ausboss-transform-field{grid-template-columns:108px minmax(0,1fr)} .ausboss-transform-section label.ausboss-transform-field>.ausboss-scrub{width:100%} .ausboss-transform-section .ausboss-transform-aspects{margin:4px 0 8px} .ausboss-transform-section .ausboss-transform-aspect-modes{margin:0 0 8px} .ausboss-transform-section .ausboss-transform-aspects{gap:4px} .ausboss-transform-section .ausboss-transform-aspect{padding:3px 0} .ausboss-transform-section label>*{min-width:0} .ausboss-transform-section input:not(.ausboss-scrub-input):not(.ausboss-transform-swatch),.ausboss-transform-section select{box-sizing:border-box;width:100%;background:#0e1012;color:#eee;border:1px solid #454b52;border-radius:4px;padding:5px} .ausboss-transform-stage{position:relative;min-width:0;min-height:0;background-color:#0c0e10;background-image:radial-gradient(#292d31 1px,transparent 1px);background-size:18px 18px;overflow:hidden} .ausboss-transform-canvas{width:100%;height:100%;display:block;touch-action:none} .ausboss-transform-status{line-height:1.55;color:#b8bec5;white-space:pre-wrap}.ausboss-transform-help{line-height:1.55;color:#aeb4ba} .ausboss-transform-sidebar.right .ausboss-transform-section{margin-top:13px} .ausboss-transform-section details{margin:8px 0 0}.ausboss-transform-section summary{cursor:pointer;color:#8de0da;font-size:11px;text-transform:uppercase;letter-spacing:.06em;list-style:none;user-select:none} .ausboss-transform-section summary::before{content:"▸";display:inline-block;width:12px;transition:transform .12s}.ausboss-transform-section details[open] summary::before{transform:rotate(90deg)} .ausboss-transform-section input[type=checkbox]{width:auto;justify-self:start;accent-color:${BRAND}} .ausboss-transform-timeline{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:8px 12px;border-top:1px solid #30343a;background:#17191c} .ausboss-transform-timeline>.ausboss-transform-trim{flex-basis:100%} .ausboss-transform-transport{display:flex;flex-wrap:wrap;align-items:center;gap:8px;flex-basis:100%} .ausboss-transform-steps{display:flex;gap:4px;flex-wrap:wrap}.ausboss-transform-steps button{padding:5px 7px} .ausboss-transform-badge{padding:3px 7px;border-radius:99px;background:#263034;color:#8de0da;font-size:11px} @media(max-width:900px){.ausboss-transform-body{grid-template-columns:290px minmax(260px,1fr)}.ausboss-transform-sidebar.right{display:none}} `; document.head.appendChild(style); } function widget(node, name) { return node.widgets?.find((item) => item.name === name); } function value(node, name, fallback = 0) { return widget(node, name)?.value ?? fallback; } function setValue(node, name, next) { const target = widget(node, name); if (!target) return; target.value = next; target.callback?.(next); } function values(node) { return Object.fromEntries(Object.keys(TRANSFORM_DEFAULTS).map((name) => [name, value(node, name, TRANSFORM_DEFAULTS[name])])); } function suppressCoreImagePreview(node) { const previewIndex = node.widgets?.findIndex((item) => item.name === CORE_IMAGE_PREVIEW_WIDGET) ?? -1; if (previewIndex >= 0) { node.widgets[previewIndex].onRemove?.(); node.widgets.splice(previewIndex, 1); } if (node.__ausbossImgsSuppressed) return; node.__ausbossImgsSuppressed = true; node.__ausbossAddCustomWidget = node.addCustomWidget; if (typeof node.addCustomWidget === "function") { node.addCustomWidget = function (customWidget) { if (customWidget?.name === CORE_IMAGE_PREVIEW_WIDGET) hideWidget(customWidget); return node.__ausbossAddCustomWidget.call(this, customWidget); }; } node.__ausbossImgsDescriptor = Object.getOwnPropertyDescriptor(node, "imgs"); Object.defineProperty(node, "imgs", { configurable: true, enumerable: true, get() { return undefined; }, // The MaskEditor sets this right after it writes the picture it saved. set() { const state = node.__ausbossTransformState; if (state) queueMicrotask(() => followSource(state)); }, }); } // The MaskEditor saves its picture (the mask is the picture's alpha) into // the image widget's stored value without calling the widget back, so the // stage never heard of it and kept the old picture. A source changed that // way is followed here, keeping the framing, as a workflow load does: the // mask was drawn on the same picture. function followSource(state) { if (state.disposed || !state.ready || state.kind !== "image") return; const key = sourceKey(state.node, "image"); if (!key || key === state.source) return; state.syncSourceCard?.(); void onSourceChanged(state, false); } function sourceKey(node, kind) { if (kind === "image") return String(value(node, "image", "")); return videoSourceState( value(node, "source_mode", INPUT_FOLDER_MODE), value(node, "video", ""), value(node, "local_path", ""), ).key; } function parseInputReference(selection) { const normalized = String(selection || "").replaceAll("\\", "/"); const parts = normalized.split("/"); const filename = parts.pop() || ""; return { filename, subfolder: parts.join("/"), type: "input" }; } function imageSourceUrl(selection) { const reference = parseInputReference(selection); return api.apiURL(`/view?${new URLSearchParams(reference)}`); } function videoParams(node, maxSize = 1600) { return new URLSearchParams({ source_mode: String(value(node, "source_mode", "input folder")), video: String(value(node, "video", "")), local_path: String(value(node, "local_path", "")), seek_mode: String(value(node, "seek_mode", "frame index")), frame_index: String(Math.max(0, Math.round(Number(value(node, "frame_index", 0)) || 0))), frame_time: String(Math.max(0, Number(value(node, "frame_time", 0)) || 0)), max_width: String(maxSize), max_height: String(maxSize), }); } async function uploadMedia(node, kind, file) { const body = new FormData(); body.append("image", file, file.name); body.append("type", "input"); const route = kind === "video" ? "/ausboss/transform/video/upload" : "/upload/image"; const response = await api.fetchApi(route, { method: "POST", body }); if (!response.ok) { const text = await response.text(); let message = text; try { message = JSON.parse(text).error || text; } catch {} throw new Error(message || "Upload failed."); } const result = await response.json(); const selection = result.subfolder ? `${result.subfolder}/${result.name}` : result.name; const target = widget(node, kind); if (Array.isArray(target?.options?.values) && !target.options.values.includes(selection)) target.options.values.push(selection); if (kind === "video") setValue(node, "source_mode", "input folder"); setValue(node, kind, selection); return selection; } function resetTransform(node, includeTimeline = false) { if (node.properties) { delete node.properties.ausboss_fit_aspect; node.properties.ausboss_aspect_lock = false; } for (const [name, next] of Object.entries(declaredTransformDefaults(node.constructor?.nodeData, includeTimeline))) setValue(node, name, next); node.setDirtyCanvas?.(true, true); } // The node face's Reset: only the shape - rotation, crop, padding. Fill, // feather and Align stay, as they do when the source changes. function resetGeometry(node) { if (node.properties) { delete node.properties.ausboss_fit_aspect; node.properties.ausboss_aspect_lock = false; } for (const [name, next] of Object.entries(sourceResetValues(false))) setValue(node, name, next); node.setDirtyCanvas?.(true, true); } function createElement(tag, className, text) { const element = document.createElement(tag); if (className) element.className = className; if (text !== undefined) element.textContent = text; return element; } function addLabeledControl(section, title, control, suffix = "") { const label = createElement("label"); label.append(createElement("span", "", title), control, createElement("span", "", suffix)); section.append(label); return label; } function buildMediaSourceCard(state) { const { node, kind } = state; const root = createElement("div", "ausboss-transform-source"); const modes = createElement("div", "ausboss-transform-source-mode"); const uploadsMode = createElement("button", "", "Uploads"); const localMode = createElement("button", "", "Server file"); uploadsMode.type = localMode.type = "button"; uploadsMode.title = "Choose a video already in ComfyUI's input folder or upload another."; localMode.title = "A video already on the ComfyUI server, inside its input, output or temp folder, read in place without copying it."; modes.append(uploadsMode, localMode); const field = createElement("div", "ausboss-transform-source-field"); // The media picker previews the hovered file, which a native