Spaces:
Running
Running
Download app.js from SupraLabs/Supra-IMG-Studio: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/spaces/SupraLabs/Supra-IMG-Studio/resolve/main/app.js
- Command line
-
hf download hf://spaces/SupraLabs/Supra-IMG-Studio/app.js
-
curl -L -o app.js https://huggingface.co/spaces/SupraLabs/Supra-IMG-Studio/resolve/main/app.js
10.8 kB
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@2.7.1/dist/index.min.js"; | |
| const BACKEND = "SupraLabs/supra2-img-demo"; | |
| const BACKEND_URL = "https://supralabs-supra2-img-demo.hf.space"; | |
| const STEPS = { Fast: 3, Quality: 8, "Ultra Design": 16 }; | |
| const form = document.querySelector("#generator-form"); | |
| const prompt = document.querySelector("#prompt"); | |
| const generateButton = document.querySelector("#generate"); | |
| const buttonLabel = document.querySelector("#button-label"); | |
| const guidance = document.querySelector("#guidance"); | |
| const guidanceValue = document.querySelector("#guidance-value"); | |
| const seed = document.querySelector("#seed"); | |
| const randomize = document.querySelector("#randomize"); | |
| const image = document.querySelector("#result-image"); | |
| const placeholder = document.querySelector("#placeholder"); | |
| const download = document.querySelector("#download"); | |
| const seedUsed = document.querySelector("#seed-used"); | |
| const resultState = document.querySelector("#result-state"); | |
| const status = document.querySelector("#status"); | |
| const modeLabel = document.querySelector("#selected-mode"); | |
| const modeAvailability = document.querySelector("#mode-availability"); | |
| const level = document.querySelector("#level"); | |
| const levelTrack = document.querySelector("#level-track"); | |
| const levelNodes = [...document.querySelectorAll(".level-node")]; | |
| const levelLabels = [...document.querySelectorAll(".level-labels button")]; | |
| const progressPanel = document.querySelector("#render-progress"); | |
| const progressTrack = document.querySelector("#progress-track"); | |
| const progressFill = document.querySelector("#progress-fill"); | |
| const progressPercent = document.querySelector("#progress-percent"); | |
| const progressStage = document.querySelector("#progress-stage"); | |
| const LEVELS = ["Fast", "Quality", "Ultra Design"]; | |
| let mode = "Quality"; | |
| let clientPromise; | |
| let dragging = false; | |
| let levelTouched = false; | |
| let shownProgress = 0; | |
| let displayedProgress = 0; | |
| let progressFrame = 0; | |
| let backendMinimumSteps = 10; | |
| function setStatus(message, kind = "") { | |
| status.textContent = message; | |
| status.className = `status ${kind}`.trim(); | |
| } | |
| function setMode(nextMode) { | |
| if (!(nextMode in STEPS)) return; | |
| const previous = mode; | |
| mode = nextMode; | |
| level.dataset.level = mode; | |
| modeLabel.textContent = `${mode} 路 ${STEPS[mode]} steps`; | |
| levelTrack.setAttribute("aria-valuenow", String(LEVELS.indexOf(mode))); | |
| levelTrack.setAttribute("aria-valuetext", `${mode}, ${STEPS[mode]} steps`); | |
| modeAvailability.textContent = STEPS[mode] < backendMinimumSteps | |
| ? `${mode} can be selected, but generation at ${STEPS[mode]} steps is awaiting the CPU service update.` | |
| : ""; | |
| if (mode === "Ultra Design" && previous !== mode) requestAnimationFrame(playUltra); | |
| if (mode !== "Ultra Design" && previous === "Ultra Design") cancelUltra(); | |
| } | |
| function cancelUltra() { | |
| level.querySelectorAll(".level-rainbow, .level-node").forEach((item) => | |
| item.getAnimations().forEach((animation) => { if (animation.id === "rainbow-grow") animation.cancel(); })); | |
| } | |
| function playUltra() { | |
| cancelUltra(); | |
| if (matchMedia("(prefers-reduced-motion: reduce)").matches) return; | |
| const rainbow = level.querySelector(".level-rainbow"); | |
| const knob = level.querySelector('.level-node[data-mode="Ultra Design"]'); | |
| const grow = rainbow.animate( | |
| [{ width: "0%", opacity: 1 }, { width: "100%", opacity: 1 }], | |
| { duration: 1500, delay: 520, easing: "cubic-bezier(.45,.05,.25,1)", fill: "backwards" } | |
| ); | |
| grow.id = "rainbow-grow"; | |
| const pulse = knob.animate( | |
| [{ transform: "scale(1)" }, { transform: "scale(1.9)", offset: .55 }, { transform: "scale(1.65)" }], | |
| { duration: 520, delay: 1900, easing: "ease-out", fill: "backwards" } | |
| ); | |
| pulse.id = "rainbow-grow"; | |
| } | |
| function setProgress(percent, stage) { | |
| shownProgress = Math.max(shownProgress, Math.min(100, Math.max(0, Math.round(percent)))); | |
| progressPanel.hidden = false; | |
| progressFill.style.width = `${shownProgress}%`; | |
| progressStage.textContent = stage; | |
| progressTrack.setAttribute("aria-valuenow", String(shownProgress)); | |
| cancelAnimationFrame(progressFrame); | |
| if (matchMedia("(prefers-reduced-motion: reduce)").matches) { | |
| displayedProgress = shownProgress; | |
| progressPercent.textContent = `${shownProgress}%`; | |
| return; | |
| } | |
| const start = displayedProgress; | |
| const target = shownProgress; | |
| const started = performance.now(); | |
| const tick = (now) => { | |
| const t = Math.min(1, (now - started) / 700); | |
| displayedProgress = start + (target - start) * (1 - (1 - t) ** 3); | |
| progressPercent.textContent = `${Math.round(displayedProgress)}%`; | |
| if (t < 1) progressFrame = requestAnimationFrame(tick); | |
| }; | |
| progressFrame = requestAnimationFrame(tick); | |
| } | |
| function pickLevel(clientX) { | |
| const bounds = levelTrack.getBoundingClientRect(); | |
| const fraction = Math.min(1, Math.max(0, (clientX - bounds.left - 18) / (bounds.width - 36))); | |
| setMode(LEVELS[Math.round(fraction * 2)]); | |
| } | |
| async function refreshAvailableModes() { | |
| try { | |
| const response = await fetch(`${BACKEND_URL}/config`, { cache: "no-store" }); | |
| if (!response.ok) throw new Error("CPU backend unavailable"); | |
| const config = await response.json(); | |
| const stepControl = config.components?.find((part) => part.type === "slider" && part.props?.label === "Sampling steps"); | |
| backendMinimumSteps = Number(stepControl?.props?.minimum ?? 10); | |
| if (!levelTouched && STEPS[mode] < backendMinimumSteps) setMode("Ultra Design"); | |
| else setMode(mode); | |
| } catch { | |
| modeAvailability.textContent = "The CPU service is waking up. You can try generating an image shortly."; | |
| } | |
| } | |
| function setBusy(busy) { | |
| generateButton.disabled = busy; | |
| buttonLabel.textContent = busy ? "Generating..." : "Generate image"; | |
| resultState.textContent = busy ? "Working on CPU" : "Ready to create"; | |
| } | |
| function imageUrl(value) { | |
| if (typeof value === "string") return value; | |
| if (value && typeof value.url === "string") return value.url; | |
| if (value && typeof value.path === "string") { | |
| return new URL(`/gradio_api/file=${value.path}`, "https://supralabs-supra2-img-demo.hf.space").href; | |
| } | |
| throw new Error("The image service returned no image URL."); | |
| } | |
| for (const option of levelLabels) option.addEventListener("click", () => { levelTouched = true; setMode(option.dataset.mode); }); | |
| levelTrack.addEventListener("pointerdown", (event) => { | |
| dragging = true; | |
| levelTouched = true; | |
| levelTrack.focus({ preventScroll: true }); | |
| try { levelTrack.setPointerCapture(event.pointerId); } catch { /* Pointer capture is optional. */ } | |
| pickLevel(event.clientX); | |
| event.preventDefault(); | |
| }); | |
| levelTrack.addEventListener("pointermove", (event) => { if (dragging) pickLevel(event.clientX); }); | |
| levelTrack.addEventListener("pointerup", () => { dragging = false; }); | |
| levelTrack.addEventListener("pointercancel", () => { dragging = false; }); | |
| levelTrack.addEventListener("keydown", (event) => { | |
| levelTouched = true; | |
| const index = LEVELS.indexOf(mode); | |
| const next = event.key === "ArrowRight" || event.key === "ArrowUp" ? index + 1 | |
| : event.key === "ArrowLeft" || event.key === "ArrowDown" ? index - 1 | |
| : event.key === "Home" ? 0 : event.key === "End" ? 2 : null; | |
| if (next !== null) { event.preventDefault(); setMode(LEVELS[Math.max(0, Math.min(2, next))]); } | |
| }); | |
| refreshAvailableModes(); | |
| guidance.addEventListener("input", () => { guidanceValue.value = Number(guidance.value).toFixed(1); }); | |
| document.querySelectorAll("#examples button").forEach((example) => { | |
| example.addEventListener("click", () => { prompt.value = example.textContent.trim(); prompt.focus(); }); | |
| }); | |
| form.addEventListener("submit", async (event) => { | |
| event.preventDefault(); | |
| const description = prompt.value.trim(); | |
| if (!description) { prompt.focus(); return; } | |
| if (STEPS[mode] < backendMinimumSteps) { | |
| setStatus(`${mode} needs the CPU service update before it can generate at ${STEPS[mode]} steps.`, "error"); | |
| return; | |
| } | |
| const seedNumber = Number(seed.value); | |
| if (!randomize.checked && (!Number.isInteger(seedNumber) || seedNumber < 0 || seedNumber > 2147483647)) { | |
| setStatus("Choose a seed between 0 and 2147483647.", "error"); | |
| seed.focus(); | |
| return; | |
| } | |
| setBusy(true); | |
| setStatus("Connecting to the CPU image service..."); | |
| shownProgress = 0; | |
| displayedProgress = 0; | |
| cancelAnimationFrame(progressFrame); | |
| progressPercent.textContent = "0%"; | |
| setProgress(0, "Connecting to CPU"); | |
| try { | |
| clientPromise ??= Client.connect(BACKEND, { events: ["status", "data"] }); | |
| const client = await clientPromise; | |
| const job = client.submit("/generate", [description, STEPS[mode], Number(guidance.value), seedNumber, randomize.checked]); | |
| let gotImage = false; | |
| for await (const message of job) { | |
| if (message.type === "status") { | |
| if (message.stage === "pending") { | |
| const position = Number.isInteger(message.position) ? ` 路 position ${message.position + 1}` : ""; | |
| setStatus(`Queued for CPU generation${position}`); | |
| setProgress(0, "Waiting in queue"); | |
| } else if (message.stage === "generating") { | |
| const progress = message.progress_data?.find((part) => part?.desc); | |
| setStatus(progress?.desc || "Generating your image on CPU..."); | |
| let fraction = Number(progress?.progress); | |
| if (!Number.isFinite(fraction) && Number(progress?.length) > 0) fraction = Number(progress.index) / Number(progress.length); | |
| if (!Number.isFinite(fraction) && progress?.desc) { | |
| const steps = progress.desc.match(/(\d+)\s*\/\s*(\d+)/); | |
| if (steps) fraction = Number(steps[1]) / Number(steps[2]); | |
| } | |
| if (Number.isFinite(fraction)) setProgress(4 + 90 * Math.min(1, Math.max(0, fraction)), progress?.desc || "Rendering image"); | |
| else setProgress(4, "Preparing image"); | |
| } else if (message.stage === "error") { | |
| throw new Error(message.message || "Image generation failed."); | |
| } | |
| } | |
| if (message.type === "data" && Array.isArray(message.data)) { | |
| const url = imageUrl(message.data[0]); | |
| image.src = url; | |
| image.hidden = false; | |
| placeholder.hidden = true; | |
| download.href = url; | |
| download.hidden = false; | |
| seedUsed.textContent = `Seed ${message.data[1]}`; | |
| setProgress(100, "Image ready"); | |
| gotImage = true; | |
| } | |
| } | |
| if (!gotImage) throw new Error("The image service did not return an image."); | |
| setStatus(`${mode} complete 路 ${STEPS[mode]} steps`, "success"); | |
| } catch (error) { | |
| console.error(error); | |
| clientPromise = undefined; | |
| setStatus(error instanceof Error ? error.message : "Image generation failed. Please try again.", "error"); | |
| } finally { | |
| setBusy(false); | |
| } | |
| }); | |