File size: 2,923 Bytes
3429714
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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),
  };
}