Files
Designloves2andClaude Opus 4.8 8796713022 v1.0: Auto Stitch + AI Detect + Pre-render, and fix four security issues
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>
2026-07-17 15:00:21 +09:00

71 lines
2.7 KiB
JavaScript

import { app } from "../../scripts/app.js";
// Consumes ?itda_send=<subfolder/file>&itda_kind=<image|video|audio> (single
// clip/range) or ?itda_send_batch=<JSON array of {relative,kind}> (whole
// timeline) set by the ITDA editor's "Send To ComfyUI" button, and drops
// matching loader node(s) pointed at the exported file(s) into the graph.
//
// ComfyUI's own frontend strips/normalizes the URL (history.replaceState)
// during its early startup, before any extension's setup() hook runs. So the
// query string must be captured here at module top-level (which executes
// synchronously the moment this script is imported), not inside setup().
const _params = new URLSearchParams(window.location.search);
const _pendingSend = _params.get("itda_send");
const _pendingKind = _params.get("itda_kind");
let _pendingBatch = null;
try {
const raw = _params.get("itda_send_batch");
if (raw) _pendingBatch = JSON.parse(raw);
} catch (e) {
console.warn("[ITDA] Could not parse itda_send_batch:", e);
}
if (_pendingSend || _pendingBatch) {
_params.delete("itda_send");
_params.delete("itda_kind");
_params.delete("itda_send_batch");
const clean = _params.toString();
window.history.replaceState({}, "", clean ? `${window.location.pathname}?${clean}` : window.location.pathname);
}
let _consumed = false;
function nodeTypeFor(kind) {
return kind === "image" ? "LoadImage" : kind === "audio" ? "LoadAudio" : "LoadVideo";
}
function widgetNameFor(kind) {
return kind === "image" ? "image" : kind === "audio" ? "audio" : "file";
}
function dropLoaderNode(relative, kind, pos) {
const node = LiteGraph.createNode(nodeTypeFor(kind));
if (!node) {
console.warn(`[ITDA] Node type "${nodeTypeFor(kind)}" not available in this ComfyUI install.`);
return null;
}
node.pos = pos;
app.graph.add(node);
const widget = node.widgets?.find((w) => w.name === widgetNameFor(kind));
if (widget) widget.value = relative;
return node;
}
app.registerExtension({
name: "ITDA.Bridge",
// setup() fires before app.graph exists ("ComfyApp graph accessed before
// initialization"); afterConfigureGraph fires once the graph is loaded and
// ready, which is what every other node-inserting extension here uses.
afterConfigureGraph() {
if (_consumed || (!_pendingSend && !_pendingBatch)) return;
_consumed = true;
if (_pendingBatch && Array.isArray(_pendingBatch)) {
_pendingBatch.forEach((item, i) => {
if (!item?.relative || !item?.kind) return;
dropLoaderNode(item.relative, item.kind, [200, 120 + i * 220]);
});
} else if (_pendingSend) {
dropLoaderNode(_pendingSend, _pendingKind, [200, 200]);
}
app.graph.setDirtyCanvas(true, true);
},
});