export const VIDEO_EXTENSIONS = new Set([".mp4", ".webm", ".mov", ".mkv"]); export function alignFrameCount(durationSeconds, fps = 24) { let requested = Math.max(5, Math.round(Number(durationSeconds) * fps)); while (requested % 17 !== 5) requested += 1; return requested; } export function renderWorkflow(workflow, replacements) { const replace = (value) => { if (Array.isArray(value)) return value.map(replace); if (value && typeof value === "object") { return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replace(item)])); } if (typeof value !== "string") return value; if (Object.prototype.hasOwnProperty.call(replacements, value)) return replacements[value]; let result = value; for (const [token, replacement] of Object.entries(replacements)) { result = result.replaceAll(token, String(replacement)); } return result; }; return replace(structuredClone(workflow)); } export function findVideoDescriptor(entry) { const candidates = []; const visit = (value) => { if (Array.isArray(value)) { value.forEach(visit); return; } if (!value || typeof value !== "object") return; if (typeof value.filename === "string") candidates.push(value); Object.values(value).forEach(visit); }; visit(entry?.outputs ?? {}); const match = candidates.find((item) => { const filename = item.filename.toLowerCase(); return [...VIDEO_EXTENSIONS].some((extension) => filename.endsWith(extension)); }); if (!match) return null; return { filename: match.filename, subfolder: String(match.subfolder ?? ""), type: String(match.type ?? "output"), }; } export function parseWorkflow(raw) { if (!String(raw ?? "").trim()) throw new Error("Load or paste an API-format workflow first."); let workflow; try { workflow = JSON.parse(raw); } catch (error) { throw new Error(`Workflow JSON is invalid: ${error.message}`); } if (!workflow || Array.isArray(workflow) || typeof workflow !== "object") { throw new Error("Workflow JSON must be a non-empty object."); } if (Array.isArray(workflow.nodes)) { throw new Error( "This is a UI workflow. In ComfyUI, enable Developer mode and use Save (API format).", ); } return workflow; } export function buildTokens(values) { const [width, height] = String(values.resolution).split("x").map(Number); return { "{{PROMPT}}": String(values.prompt ?? "").trim(), "{{NEGATIVE_PROMPT}}": String(values.negativePrompt ?? "").trim(), "{{LORA_NAME}}": values.loraName, "{{LORA_STRENGTH}}": Number(values.loraStrength), "{{WIDTH}}": width, "{{HEIGHT}}": height, "{{DURATION}}": Number(values.duration), "{{LENGTH}}": alignFrameCount(values.duration), "{{FPS}}": 24, "{{STEPS}}": Number(values.steps), "{{CFG}}": Number(values.cfg), "{{SEED}}": Number(values.seed), }; }