Completes the v0.1.5 -> v1.0 roadmap and hardens the path handling around it. Features - Auto Stitch: recommends where to cut between two clips, scoring candidates on frame similarity and optical-flow motion continuity (recommend + Apply, never an automatic edit). - AI Detect: scene-change detection (ffmpeg scene score) and beat tracking (librosa), with beats used as snap targets. - Pre-render: flattens the marked range into one cached file for smooth playback; invalidated automatically by any edit. - Version Stack, Wipe Compare, Box/Shift select, per-clip Solo/Gain, MOV/WebM export, whole-timeline Send To ComfyUI. Security - Contain clip paths on export. A clip's path is read from the project file and POST /itda/api/project/<name> stores arbitrary JSON, so any local process could point a clip at any file, have ffmpeg composite it into the export, and fetch it back via /itda/api/file. - Contain the export path in ITDA Load Export. The node trusted "path" from last_export.json and handed it straight to a decoder, so a tampered manifest could emit any file's contents as node output. - Reject dot-only project names. safe_name() stripped separators but passed "." and ".." through, collapsing media/<project> to the whole ITDA root; media/delete would then accept any file under input/ITDA. - Validate drawtext colors. Unvalidated color/shadow_color were interpolated into the ffmpeg filter, so "ffffff:textfile=..." appended a real drawtext option. Also set expansion=none. Fixes - ffmpeg no longer blocks ComfyUI's event loop (an export froze the whole UI for its duration); all calls now run off-loop. - Auto Stitch no longer ranks on audio: measured against known-correct cut points it could not separate good cuts from bad, and demoted the perfect cut to last. Frame + motion scores 5/5 on ground truth. Removed itda_server.py, web/itda_editor.html/.css and an empty itda_client.js (all superseded and unreferenced), and stopped tracking __pycache__. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
145 lines
6.0 KiB
JavaScript
145 lines
6.0 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
// Adds an in-graph live preview (iframe) of the ITDA editor to the
|
|
// "ITDAOpenEditor" node, plus explicit "Open In Browser" / "Reload Preview"
|
|
// buttons. The Python node (itda_nodes.py) only ever produced a URL string
|
|
// output for workflow use - this is the actual visual node the user asked
|
|
// for: see and use the editor without leaving the ComfyUI graph, or pop it
|
|
// out to a full browser tab.
|
|
const NODE_CLASS = "ITDAOpenEditor";
|
|
|
|
function getWidget(node, name) {
|
|
return node.widgets?.find((w) => w.name === name);
|
|
}
|
|
|
|
function editorUrl(node) {
|
|
const w = getWidget(node, "project_name");
|
|
const project = encodeURIComponent(w?.value || "project");
|
|
return `${location.origin}/itda/editor?project=${project}`;
|
|
}
|
|
|
|
// Setting iframe.src to a string identical to its current src is a no-op in
|
|
// some browsers - no navigation happens at all, so "Reload Preview" and the
|
|
// project_name-change auto-refresh could silently keep showing a stale page
|
|
// (this is also just generally safer against any HTTP-cache weirdness for
|
|
// content this actively under development). A cache-busting param forces a
|
|
// genuinely new navigation/fetch every time.
|
|
function previewUrl(node) {
|
|
return `${editorUrl(node)}&_cb=${Date.now()}`;
|
|
}
|
|
|
|
function buildUI(node) {
|
|
if (node._itdaPreviewUI) return;
|
|
|
|
const wrap = document.createElement("div");
|
|
wrap.style.cssText = "display:flex;flex-direction:column;gap:4px;width:100%;height:100%;box-sizing:border-box;padding:2px;";
|
|
|
|
const row = document.createElement("div");
|
|
row.style.cssText = "display:flex;gap:4px;";
|
|
|
|
const openBtn = document.createElement("button");
|
|
openBtn.textContent = "Open In Browser";
|
|
openBtn.style.cssText = "flex:1;height:26px;border:none;border-radius:5px;background:#6a5cff;color:#fff;cursor:pointer;font-weight:700;font-size:12px;";
|
|
|
|
const reloadBtn = document.createElement("button");
|
|
reloadBtn.textContent = "Reload Preview";
|
|
reloadBtn.style.cssText = "height:26px;border:none;border-radius:5px;background:#2a2e38;color:#cfd5df;cursor:pointer;font-size:11px;padding:0 10px;";
|
|
|
|
const iframe = document.createElement("iframe");
|
|
iframe.style.cssText = "flex:1;width:100%;min-height:220px;border:0;border-radius:6px;background:#0b0c0e;";
|
|
iframe.src = previewUrl(node);
|
|
|
|
// Buttons sit inside the node body - without this, a click/drag on them
|
|
// would also be interpreted by the graph canvas as dragging the node.
|
|
[openBtn, reloadBtn, wrap].forEach((el) => {
|
|
el.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
});
|
|
|
|
// ITDA's own keyboard shortcuts (Delete, C for split, Ctrl+D, etc.) only
|
|
// fire if the iframe's own window actually has keyboard focus. LiteGraph's
|
|
// canvas has its own global shortcuts (Delete removes the *node*, Ctrl+C
|
|
// copies the *node*, ...) that otherwise win whenever focus is still on
|
|
// the graph canvas rather than inside the iframe. A capture-phase
|
|
// pointerdown listener on the wrapper sees the click before it disappears
|
|
// into the iframe's separate document, so we can force focus in
|
|
// explicitly rather than relying on default browser click-to-focus (which
|
|
// is what "editing shortcuts get eaten by ComfyUI" reports symptom-match).
|
|
const focusIframe = () => {
|
|
try {
|
|
iframe.contentWindow.focus();
|
|
} catch (e) {
|
|
/* cross-origin or not-yet-loaded - ignore */
|
|
}
|
|
};
|
|
wrap.addEventListener("pointerdown", focusIframe, true);
|
|
iframe.addEventListener("load", focusIframe);
|
|
openBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
window.open(editorUrl(node), "_blank");
|
|
});
|
|
reloadBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
// Reload must refresh whichever project is ACTUALLY open right now, not
|
|
// rebuild from the project_name widget. The ITDA editor switches
|
|
// projects via its own in-app Project menu without ever touching the
|
|
// iframe's URL (no history.pushState) - so contentWindow.location.href
|
|
// stays stuck on the URL it was first loaded with. The only place that
|
|
// reflects the true current project is the editor's own #projectName
|
|
// input inside the iframe's DOM.
|
|
let currentProject = null;
|
|
try {
|
|
currentProject = iframe.contentDocument?.getElementById("projectName")?.value || null;
|
|
} catch (err) {
|
|
currentProject = null;
|
|
}
|
|
const projectWidget = getWidget(node, "project_name");
|
|
const project = encodeURIComponent(currentProject || projectWidget?.value || "project");
|
|
if (currentProject && projectWidget) projectWidget.value = currentProject;
|
|
iframe.src = `${location.origin}/itda/editor?project=${project}&_cb=${Date.now()}`;
|
|
});
|
|
|
|
row.appendChild(openBtn);
|
|
row.appendChild(reloadBtn);
|
|
wrap.appendChild(row);
|
|
wrap.appendChild(iframe);
|
|
|
|
const domWidget = node.addDOMWidget("itda_preview", "itda_preview", wrap, {
|
|
serialize: false,
|
|
hideOnZoom: false,
|
|
});
|
|
domWidget.computeSize = function () {
|
|
const width = Math.max(320, (node.size?.[0] || 1000) - 20);
|
|
const height = Math.max(220, (node.size?.[1] || 760) - 90);
|
|
return [width, height];
|
|
};
|
|
|
|
node._itdaPreviewUI = { wrap, iframe, openBtn, reloadBtn, domWidget };
|
|
// The full editor (media bin + timeline + preview + properties, all side
|
|
// by side) needs real estate - the old 460x420 default opened cramped and
|
|
// mostly unusable until manually resized.
|
|
if (!node.size || node.size[0] < 1000 || node.size[1] < 760) node.size = [1000, 760];
|
|
|
|
// Keep the preview pointed at whichever project the widget currently names.
|
|
const projectWidget = getWidget(node, "project_name");
|
|
if (projectWidget) {
|
|
const origCallback = projectWidget.callback;
|
|
projectWidget.callback = function (...args) {
|
|
const r = origCallback ? origCallback.apply(this, args) : undefined;
|
|
iframe.src = previewUrl(node);
|
|
return r;
|
|
};
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "ITDA.PreviewNode",
|
|
nodeCreated(node) {
|
|
if (node.comfyClass !== NODE_CLASS) return;
|
|
buildUI(node);
|
|
},
|
|
loadedGraphNode(node) {
|
|
if (node.comfyClass !== NODE_CLASS) return;
|
|
buildUI(node);
|
|
},
|
|
});
|