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