yingzhac's picture
Create static PinkFluffyBunny MiniMax H3 ComfyUI client
3429714 verified
Raw
History Blame Contribute Delete
8.38 kB
import { buildTokens, findVideoDescriptor, parseWorkflow, renderWorkflow } from "./core.js";
const $ = (id) => document.getElementById(id);
const state = {
stopRequested: false,
objectUrl: null,
};
function cleanBaseUrl() {
const raw = $("backend-url").value.trim().replace(/\/+$/, "");
let url;
try {
url = new URL(raw);
} catch {
throw new Error("Enter a valid ComfyUI API URL.");
}
if (!["http:", "https:"].includes(url.protocol)) {
throw new Error("The ComfyUI URL must use HTTP or HTTPS.");
}
if (window.location.protocol === "https:" && url.protocol !== "https:") {
throw new Error("This HTTPS Space cannot call an HTTP backend because browsers block mixed content.");
}
return raw;
}
function requestHeaders(includeJson = false) {
const headers = {};
const token = $("backend-token").value.trim();
if (token) headers.Authorization = `Bearer ${token}`;
if (includeJson) headers["Content-Type"] = "application/json";
return headers;
}
async function apiFetch(path, options = {}) {
const base = cleanBaseUrl();
try {
return await fetch(`${base}${path}`, options);
} catch (error) {
throw new Error(
`Browser could not reach the backend. Check HTTPS, DNS and CORS for ${window.location.origin}. ${error.message}`,
);
}
}
function setBadge(element, text, kind) {
element.textContent = text;
element.className = `badge ${kind}`;
}
function setStatus(message, progress = null, kind = "working") {
$("status").textContent = message;
setBadge($("output-state"), kind === "error" ? "Error" : kind === "success" ? "Ready" : "Working", kind);
if (progress !== null) $("progress-bar").style.width = `${Math.max(0, Math.min(100, progress))}%`;
}
function validateResponse(response, label) {
if (!response.ok) throw new Error(`${label} failed: HTTP ${response.status} ${response.statusText}`);
return response;
}
async function testBackend() {
const button = $("test-backend");
button.disabled = true;
setBadge($("connection-badge"), "Testing", "working");
try {
parseWorkflow($("workflow-json").value);
const response = await apiFetch("/system_stats", {
headers: requestHeaders(),
mode: "cors",
});
validateResponse(response, "Backend test");
await response.json();
setBadge($("connection-badge"), "Connected", "success");
setStatus("Backend and API-format workflow are ready.", 0, "success");
} catch (error) {
setBadge($("connection-badge"), "Failed", "error");
setStatus(error.message, 0, "error");
} finally {
button.disabled = false;
}
}
function valuesFromForm() {
const prompt = $("prompt").value.trim();
if (!prompt) throw new Error("Enter a prompt.");
const duration = Number($("duration").value);
const loraStrength = Number($("lora-strength").value);
const steps = Number($("steps").value);
const cfg = Number($("cfg").value);
if (duration < 4 || duration > 15) throw new Error("Duration must be between 4 and 15 seconds.");
if (loraStrength < 0 || loraStrength > 1.5) throw new Error("LoRA strength must be between 0 and 1.5.");
if (steps < 4 || steps > 30) throw new Error("Steps must be between 4 and 30.");
if (cfg < 1 || cfg > 8) throw new Error("CFG must be between 1 and 8.");
let seed = Number($("seed").value);
if ($("randomize-seed").checked) {
const random = new Uint32Array(2);
crypto.getRandomValues(random);
seed = random[0] * 2 ** 21 + (random[1] & (2 ** 21 - 1));
$("seed").value = String(seed);
}
return {
prompt,
negativePrompt: $("negative-prompt").value,
loraName: $("lora-file").value,
loraStrength,
resolution: $("resolution").value,
duration,
steps,
cfg,
seed,
};
}
async function queuePrompt(workflow) {
const response = await apiFetch("/prompt", {
method: "POST",
headers: requestHeaders(true),
mode: "cors",
body: JSON.stringify({ prompt: workflow, client_id: crypto.randomUUID() }),
});
validateResponse(response, "Workflow submission");
const payload = await response.json();
if (!payload.prompt_id) {
throw new Error(`ComfyUI returned no prompt_id: ${JSON.stringify(payload.node_errors ?? payload.error ?? payload)}`);
}
return String(payload.prompt_id);
}
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
async function waitForHistory(promptId, timeoutMilliseconds = 30 * 60 * 1000) {
const started = Date.now();
while (Date.now() - started < timeoutMilliseconds) {
if (state.stopRequested) throw new Error("Polling stopped. The ComfyUI task may still be running.");
const response = await apiFetch(`/history/${encodeURIComponent(promptId)}`, {
headers: requestHeaders(),
mode: "cors",
});
validateResponse(response, "History request");
const payload = await response.json();
const entry = payload[promptId];
const elapsed = Date.now() - started;
setStatus(`ComfyUI task ${promptId.slice(0, 8)} is running…`, 8 + (elapsed / timeoutMilliseconds) * 82);
if (entry) {
const status = entry.status ?? {};
if (status.status_str === "error" || status.completed === false) {
throw new Error(`ComfyUI execution failed: ${JSON.stringify(status.messages ?? status)}`);
}
if (entry.outputs && Object.keys(entry.outputs).length) return entry;
}
await sleep(3000);
}
throw new Error("ComfyUI task timed out after 30 minutes.");
}
async function downloadVideo(descriptor) {
const params = new URLSearchParams(descriptor);
const response = await apiFetch(`/view?${params}`, {
headers: requestHeaders(),
mode: "cors",
});
validateResponse(response, "Video download");
return response.blob();
}
function showVideo(blob, descriptor) {
if (state.objectUrl) URL.revokeObjectURL(state.objectUrl);
state.objectUrl = URL.createObjectURL(blob);
const video = $("output-video");
video.src = state.objectUrl;
video.hidden = false;
$("empty-state").hidden = true;
const download = $("download");
download.href = state.objectUrl;
download.download = descriptor.filename.split("/").pop() || "pinkfluffybunny-h3.mp4";
download.hidden = false;
}
async function generate() {
const generateButton = $("generate");
try {
if (!$("authorization-confirmed").checked) {
throw new Error("Confirm the backend's MiniMax H3 license authorization before submitting.");
}
cleanBaseUrl();
const workflow = parseWorkflow($("workflow-json").value);
const values = valuesFromForm();
const tokens = buildTokens(values);
const rendered = renderWorkflow(workflow, tokens);
state.stopRequested = false;
generateButton.disabled = true;
$("stop").disabled = false;
setStatus("Submitting the parameterized workflow…", 3);
const promptId = await queuePrompt(rendered);
setStatus(`Queued as ${promptId}. Waiting for ComfyUI…`, 7);
const entry = await waitForHistory(promptId);
$("history-output").textContent = JSON.stringify(entry, null, 2);
const descriptor = findVideoDescriptor(entry);
if (!descriptor) {
throw new Error("ComfyUI finished but no MP4/WebM/MOV/MKV output was found. End the workflow with SaveVideo.");
}
setStatus("Downloading the generated video…", 95);
const blob = await downloadVideo(descriptor);
showVideo(blob, descriptor);
setStatus(
`Video ready · seed ${tokens["{{SEED}}"]} · ${tokens["{{LENGTH}}"]} frames · LoRA ${tokens["{{LORA_STRENGTH}}"]}`,
100,
"success",
);
} catch (error) {
setStatus(error.message, 0, "error");
} finally {
generateButton.disabled = false;
$("stop").disabled = true;
}
}
$("workflow-file").addEventListener("change", async (event) => {
const [file] = event.target.files;
if (!file) return;
try {
$("workflow-json").value = await file.text();
parseWorkflow($("workflow-json").value);
setBadge($("connection-badge"), "Workflow loaded", "working");
setStatus(`Loaded ${file.name}. Test the backend next.`, 0, "working");
} catch (error) {
setStatus(error.message, 0, "error");
}
});
$("test-backend").addEventListener("click", testBackend);
$("generate").addEventListener("click", generate);
$("stop").addEventListener("click", () => {
state.stopRequested = true;
$("stop").disabled = true;
});