| 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), |
| }; |
| } |
|
|