Spaces:
Running
Running
| const $ = (id) => document.getElementById(id); | |
| const statusEl = $("status"); | |
| const imageEl = $("image"); | |
| const canvasEl = $("image-canvas"); | |
| const placeholderEl = $("placeholder"); | |
| const downloadGateEl = $("download-gate"); | |
| const downloadGateTitleEl = $("download-gate-title"); | |
| const startDownloadButton = $("start-download"); | |
| const actionsEl = $("actions"); | |
| const runButton = $("run"); | |
| const warmButton = $("warm"); | |
| const abortButton = $("abort"); | |
| const elapsedEl = $("elapsed"); | |
| const initImageInput = $("init-image"); | |
| const initImageLabelEl = $("init-image-label"); | |
| const clearInitImageButton = $("clear-init-image"); | |
| const sourcePreviewWrap = $("source-preview-wrap"); | |
| const sourcePreviewEl = $("source-preview"); | |
| const sourceMetaEl = $("source-meta"); | |
| const img2imgControlsEl = $("img2img-controls"); | |
| const strengthEl = $("strength"); | |
| const strengthCurveEl = $("strength-curve"); | |
| const strengthValueEl = $("strength-value"); | |
| const resolutionPresetEl = $("resolution-preset"); | |
| const resolutionAdvancedEl = $("resolution-advanced"); | |
| const aspectLockButton = $("aspect-lock"); | |
| const aspectLockIconEl = $("aspect-lock-icon"); | |
| const aspectRatioEl = $("aspect-ratio"); | |
| const settingsToggleButton = $("settings-toggle"); | |
| const settingsPopoverEl = $("settings-popover"); | |
| const themeToggleButton = $("theme-toggle"); | |
| const themeIconEl = $("theme-icon"); | |
| const galleryPanelEl = $("gallery-panel"); | |
| const galleryGridEl = $("gallery-grid"); | |
| const clearGalleryButton = $("clear-gallery"); | |
| const loadProgressEl = $("load-progress"); | |
| const loadProgressTitleEl = $("load-progress-title"); | |
| const loadProgressSummaryEl = $("load-progress-summary"); | |
| const loadProgressTrackEl = $("load-progress-track"); | |
| const loadProgressFillEl = $("load-progress-fill"); | |
| const loadProgressDetailEl = $("load-progress-detail"); | |
| const loadProgressRateEl = $("load-progress-rate"); | |
| const urlDefaults = new URLSearchParams(window.location.search); | |
| const DEFAULT_PROMPT = "A small ceramic robot on a workbench, product photo lighting"; | |
| const COMMON_RESOLUTIONS = [256, 512, 768, 1024]; | |
| const ASPECT_RATIOS = { | |
| "1:1": 1, | |
| "4:3": 4 / 3, | |
| "3:4": 3 / 4, | |
| "16:9": 16 / 9, | |
| "9:16": 9 / 16, | |
| }; | |
| const THEME_STORAGE_KEY = "flux2-static-theme"; | |
| const GALLERY_LIMIT = 24; | |
| const DEFAULT_LOCAL_MODEL_BASE_URL = "/models"; | |
| const DEFAULT_LOCAL_RUNTIME_BASE_URL = "/runtime"; | |
| const BACKEND_DOWNLOADS = { | |
| "custom-lowbit-webgpu": { | |
| title: "Download model assets (3.5 GB)", | |
| status: "Model assets: 3.5 GB", | |
| }, | |
| "onnx-webgpu": { | |
| title: "Download ONNX WebGPU assets (5.2 GB)", | |
| status: "ONNX WebGPU assets: 5.2 GB", | |
| }, | |
| }; | |
| let tokenizerPromise = null; | |
| let transformersModulePromise = null; | |
| let engineReadyPromise = null; | |
| let engineModulePromise = null; | |
| let customRuntimeScriptsPromise = null; | |
| let currentImageUrl = null; | |
| let currentSourceUrl = null; | |
| let backgroundWarmPromise = null; | |
| let backgroundWarmKey = ""; | |
| let backgroundWarmResultKey = ""; | |
| let backgroundWarmResult = null; | |
| let foregroundBusy = false; | |
| let warmScheduleTimer = 0; | |
| let runTimerId = 0; | |
| let runTimerStarted = 0; | |
| let runTimerFinalized = true; | |
| let aspectLockEnabled = true; | |
| let aspectLockRatio = 1; | |
| let aspectSyncing = false; | |
| let generationHistory = []; | |
| let backgroundAssetPreparePromise = null; | |
| let backgroundAssetPrepareKey = ""; | |
| let backgroundTextContextPreparePromise = null; | |
| let backgroundTextContextPrepareKey = ""; | |
| let backgroundTransformerSetupPreparePromise = null; | |
| let backgroundTransformerSetupPrepareKey = ""; | |
| let backgroundVaePreparePromise = null; | |
| let backgroundVaePrepareKey = ""; | |
| let backgroundVaeEncoderPreparePromise = null; | |
| let backgroundVaeEncoderPrepareKey = ""; | |
| let backgroundDeepPrepareTimer = 0; | |
| let backgroundDeepPrepareToken = 0; | |
| let bootStarted = false; | |
| let bootReady = false; | |
| const preparedInitImageCache = new WeakMap(); | |
| const generationResultCache = new Map(); | |
| const tokenizedPromptCache = new Map(); | |
| const nativeFetch = globalThis.fetch.bind(globalThis); | |
| const loadProgressState = { | |
| visible: false, | |
| tracking: false, | |
| completed: false, | |
| startedAt: 0, | |
| title: "Loading", | |
| detail: "Starting", | |
| estimatedTotalBytes: 0, | |
| estimatedFiles: 0, | |
| nextRequestId: 1, | |
| requests: new Map(), | |
| renderQueued: false, | |
| timerId: 0, | |
| hideTimerId: 0, | |
| }; | |
| function formatMiB(bytes) { | |
| const value = Number(bytes || 0); | |
| return `${(value / (1024 * 1024)).toFixed(value >= 100 * 1024 * 1024 ? 0 : 1)} MiB`; | |
| } | |
| function formatBytesCompact(bytes) { | |
| const value = Number(bytes || 0); | |
| if (!Number.isFinite(value) || value <= 0) return "0 B"; | |
| if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(value >= 10 * 1024 ** 3 ? 1 : 2)} GiB`; | |
| if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(value >= 100 * 1024 ** 2 ? 0 : 1)} MiB`; | |
| if (value >= 1024) return `${(value / 1024).toFixed(value >= 100 * 1024 ? 0 : 1)} KiB`; | |
| return `${Math.round(value)} B`; | |
| } | |
| function trimTrailingSlash(value) { | |
| return String(value || "").replace(/\/+$/, ""); | |
| } | |
| function withTrailingSlash(value) { | |
| const text = String(value || ""); | |
| return text.endsWith("/") ? text : `${text}/`; | |
| } | |
| function configString(name) { | |
| const value = globalThis[name]; | |
| return typeof value === "string" && value.trim() ? value.trim() : ""; | |
| } | |
| function currentUrlDefaults() { | |
| return new URLSearchParams(window.location.search); | |
| } | |
| function appAssetVersion() { | |
| return currentUrlDefaults().get("cacheBust") || "20260606-backend-download-gate"; | |
| } | |
| function configuredModelBaseUrl() { | |
| const params = currentUrlDefaults(); | |
| return trimTrailingSlash( | |
| params.get("modelBase") || | |
| params.get("modelBaseUrl") || | |
| configString("FLUX2_MODEL_BASE_URL") || | |
| DEFAULT_LOCAL_MODEL_BASE_URL | |
| ); | |
| } | |
| function configuredRuntimeBaseUrl(modelBaseUrl = configuredModelBaseUrl()) { | |
| const params = currentUrlDefaults(); | |
| return trimTrailingSlash( | |
| params.get("runtimeBase") || | |
| params.get("runtimeBaseUrl") || | |
| configString("FLUX2_RUNTIME_BASE_URL") || | |
| (modelBaseUrl === DEFAULT_LOCAL_MODEL_BASE_URL ? DEFAULT_LOCAL_RUNTIME_BASE_URL : modelBaseUrl) | |
| ); | |
| } | |
| function configuredCustomKernelBaseUrl(modelBaseUrl = configuredModelBaseUrl(), runtimeBaseUrl = configuredRuntimeBaseUrl(modelBaseUrl)) { | |
| const params = currentUrlDefaults(); | |
| return trimTrailingSlash( | |
| params.get("customKernelBase") || | |
| params.get("customKernelBaseUrl") || | |
| configString("FLUX2_CUSTOM_KERNEL_BASE_URL") || | |
| (modelBaseUrl === DEFAULT_LOCAL_MODEL_BASE_URL ? `${runtimeBaseUrl}/custom_lowbit` : `${modelBaseUrl}/custom_lowbit`) | |
| ); | |
| } | |
| function resolveAssetUrl(baseUrl, path) { | |
| return new URL(path, new URL(withTrailingSlash(baseUrl), window.location.href)).toString(); | |
| } | |
| function compactLoadUrlLabel(url) { | |
| try { | |
| const parsed = new URL(url, window.location.href); | |
| const parts = parsed.pathname.split("/").filter(Boolean); | |
| const tail = parts.slice(-3).join("/"); | |
| return tail || parsed.hostname; | |
| } catch { | |
| return String(url || "download"); | |
| } | |
| } | |
| function beginLoadProgress(title = "Loading") { | |
| if (!loadProgressEl) return; | |
| if (loadProgressState.hideTimerId) { | |
| clearTimeout(loadProgressState.hideTimerId); | |
| loadProgressState.hideTimerId = 0; | |
| } | |
| loadProgressState.visible = true; | |
| loadProgressState.tracking = true; | |
| loadProgressState.completed = false; | |
| loadProgressState.startedAt = performance.now(); | |
| loadProgressState.title = title; | |
| loadProgressState.detail = "Starting"; | |
| loadProgressState.estimatedTotalBytes = 0; | |
| loadProgressState.estimatedFiles = 0; | |
| loadProgressState.nextRequestId = 1; | |
| loadProgressState.requests.clear(); | |
| loadProgressEl.hidden = false; | |
| if (loadProgressState.timerId) clearInterval(loadProgressState.timerId); | |
| loadProgressState.timerId = setInterval(renderLoadProgress, 250); | |
| renderLoadProgress(); | |
| } | |
| function setLoadPhase(detail, title = "") { | |
| if (!loadProgressState.visible) return; | |
| if (title) loadProgressState.title = title; | |
| if (detail) loadProgressState.detail = detail; | |
| scheduleLoadProgressRender(); | |
| } | |
| function setLoadEstimate(totalBytes, expectedFiles = 0) { | |
| if (!loadProgressState.visible) return; | |
| const bytes = Number(totalBytes || 0); | |
| if (Number.isFinite(bytes) && bytes > loadProgressState.estimatedTotalBytes) { | |
| loadProgressState.estimatedTotalBytes = bytes; | |
| } | |
| const files = Number(expectedFiles || 0); | |
| if (Number.isFinite(files) && files > loadProgressState.estimatedFiles) { | |
| loadProgressState.estimatedFiles = files; | |
| } | |
| scheduleLoadProgressRender(); | |
| } | |
| function finishLoadProgress(detail = "Ready", success = true) { | |
| if (!loadProgressState.visible) return; | |
| loadProgressState.tracking = false; | |
| loadProgressState.completed = success; | |
| if (!success) loadProgressState.title = detail; | |
| loadProgressState.detail = detail; | |
| if (success) { | |
| const loaded = loadProgressTotals().loaded; | |
| loadProgressState.estimatedTotalBytes = Math.max(loadProgressState.estimatedTotalBytes, loaded); | |
| } | |
| if (loadProgressState.timerId) { | |
| clearInterval(loadProgressState.timerId); | |
| loadProgressState.timerId = 0; | |
| } | |
| renderLoadProgress(); | |
| if (success) { | |
| loadProgressState.hideTimerId = setTimeout(() => { | |
| loadProgressState.visible = false; | |
| if (loadProgressEl) loadProgressEl.hidden = true; | |
| }, 4500); | |
| } | |
| } | |
| function loadProgressTotals() { | |
| let loaded = 0; | |
| let dynamicTotal = 0; | |
| let active = 0; | |
| let completed = 0; | |
| let failed = 0; | |
| let current = ""; | |
| for (const request of loadProgressState.requests.values()) { | |
| loaded += request.loaded || 0; | |
| if (request.total) dynamicTotal += request.total; | |
| if (request.failed) failed += 1; | |
| if (request.done || request.failed) { | |
| completed += 1; | |
| } else { | |
| active += 1; | |
| current = request.label || current; | |
| } | |
| } | |
| return { | |
| loaded, | |
| total: Math.max(loadProgressState.estimatedTotalBytes, dynamicTotal, loaded), | |
| active, | |
| completed, | |
| failed, | |
| current, | |
| }; | |
| } | |
| function renderLoadProgress() { | |
| if (!loadProgressEl || !loadProgressState.visible) return; | |
| loadProgressState.renderQueued = false; | |
| const { loaded, total, active, completed, failed, current } = loadProgressTotals(); | |
| const elapsed = Math.max(0.001, (performance.now() - loadProgressState.startedAt) / 1000); | |
| const rate = loaded / elapsed; | |
| const hasTotal = total > 0; | |
| const percent = hasTotal ? Math.max(0, Math.min(100, (loaded / total) * 100)) : 0; | |
| const displayPercent = hasTotal && loadProgressState.tracking && active > 0 | |
| ? Math.min(percent, 99.5) | |
| : percent; | |
| const files = loadProgressState.estimatedFiles | |
| ? `${Math.min(completed, loadProgressState.estimatedFiles)}/${loadProgressState.estimatedFiles} files` | |
| : `${completed}${active ? ` +${active}` : ""} files`; | |
| const summary = !loadProgressState.tracking && !loadProgressState.completed | |
| ? "Error" | |
| : loadProgressState.completed | |
| ? "Ready" | |
| : hasTotal | |
| ? `${displayPercent.toFixed(displayPercent >= 10 ? 0 : 1)}%` | |
| : "Loading"; | |
| const detailParts = [ | |
| hasTotal ? `${formatBytesCompact(loaded)} / ${formatBytesCompact(total)}` : `${formatBytesCompact(loaded)} downloaded`, | |
| files, | |
| failed ? `${failed} failed` : "", | |
| current || loadProgressState.detail, | |
| ].filter(Boolean); | |
| loadProgressTitleEl.textContent = loadProgressState.title; | |
| loadProgressSummaryEl.textContent = summary; | |
| loadProgressDetailEl.textContent = detailParts.join(" | "); | |
| loadProgressRateEl.textContent = `${formatBytesCompact(rate)}/s | ${elapsed.toFixed(1)}s`; | |
| loadProgressTrackEl.classList.toggle("indeterminate", !hasTotal && loadProgressState.tracking); | |
| loadProgressFillEl.style.width = hasTotal ? `${loadProgressState.completed ? 100 : displayPercent}%` : ""; | |
| } | |
| function scheduleLoadProgressRender() { | |
| if (!loadProgressState.visible || loadProgressState.renderQueued) return; | |
| loadProgressState.renderQueued = true; | |
| requestAnimationFrame(renderLoadProgress); | |
| } | |
| function shouldTrackLoadFetch(input, init, response) { | |
| if (!loadProgressState.tracking || !response || !response.body) return false; | |
| const method = String(init?.method || (input && typeof input === "object" && input.method) || "GET").toUpperCase(); | |
| if (method !== "GET") return false; | |
| return true; | |
| } | |
| function trackLoadFetch(input, response) { | |
| const url = response.url || (typeof input === "string" ? input : input?.url) || ""; | |
| const totalHeader = response.headers.get("content-length"); | |
| const total = totalHeader ? Number(totalHeader) : 0; | |
| const id = loadProgressState.nextRequestId++; | |
| const request = { | |
| id, | |
| url, | |
| label: compactLoadUrlLabel(url), | |
| total: Number.isFinite(total) && total > 0 ? total : 0, | |
| loaded: 0, | |
| done: false, | |
| failed: false, | |
| }; | |
| loadProgressState.requests.set(id, request); | |
| scheduleLoadProgressRender(); | |
| const reader = response.body.getReader(); | |
| const stream = new ReadableStream({ | |
| async pull(controller) { | |
| try { | |
| const { done, value } = await reader.read(); | |
| if (done) { | |
| request.done = true; | |
| scheduleLoadProgressRender(); | |
| controller.close(); | |
| return; | |
| } | |
| request.loaded += value.byteLength || value.length || 0; | |
| scheduleLoadProgressRender(); | |
| controller.enqueue(value); | |
| } catch (err) { | |
| request.failed = true; | |
| scheduleLoadProgressRender(); | |
| controller.error(err); | |
| } | |
| }, | |
| cancel(reason) { | |
| request.done = true; | |
| scheduleLoadProgressRender(); | |
| return reader.cancel(reason); | |
| }, | |
| }); | |
| return new Response(stream, { | |
| status: response.status, | |
| statusText: response.statusText, | |
| headers: response.headers, | |
| }); | |
| } | |
| function installLoadProgressFetch() { | |
| if (globalThis.__flux2LoadProgressFetchInstalled) return; | |
| globalThis.__flux2LoadProgressFetchInstalled = true; | |
| globalThis.fetch = async (input, init) => { | |
| const response = await nativeFetch(input, init); | |
| if (!shouldTrackLoadFetch(input, init, response)) return response; | |
| return trackLoadFetch(input, response); | |
| }; | |
| } | |
| function huggingFaceResolveSpec(baseUrl) { | |
| let url; | |
| try { | |
| url = new URL(baseUrl); | |
| } catch { | |
| return null; | |
| } | |
| if (!/^https?:$/i.test(url.protocol)) return null; | |
| if (!/^(huggingface\.co|hf\.co)$/i.test(url.hostname)) return null; | |
| const parts = url.pathname.split("/").filter(Boolean); | |
| const resolveIndex = parts.indexOf("resolve"); | |
| if (resolveIndex < 2 || resolveIndex + 1 >= parts.length) return null; | |
| return { | |
| repoId: `${parts[0]}/${parts[1]}`, | |
| revision: decodeURIComponent(parts[resolveIndex + 1]), | |
| }; | |
| } | |
| function tokenizerLoadSpec(modelBaseUrl) { | |
| const hfSpec = huggingFaceResolveSpec(modelBaseUrl); | |
| if (hfSpec) { | |
| return { | |
| source: hfSpec.repoId, | |
| remote: true, | |
| options: { | |
| local_files_only: false, | |
| revision: hfSpec.revision || "main", | |
| }, | |
| }; | |
| } | |
| const remote = /^https?:\/\//i.test(modelBaseUrl); | |
| return { | |
| source: remote ? resolveAssetUrl(modelBaseUrl, "tokenizer") : "models/tokenizer", | |
| remote, | |
| options: { | |
| local_files_only: !remote, | |
| }, | |
| }; | |
| } | |
| function setStatus(lines, isError = false) { | |
| const text = Array.isArray(lines) ? lines.filter(Boolean).join("\n") : String(lines); | |
| statusEl.textContent = text; | |
| statusEl.classList.toggle("error", Boolean(isError)); | |
| } | |
| function setButtonDisabled(button, disabled) { | |
| if (button) button.disabled = disabled; | |
| } | |
| function setElapsed(text, active = false) { | |
| if (!elapsedEl) return; | |
| elapsedEl.textContent = text; | |
| elapsedEl.classList.toggle("active", active); | |
| } | |
| function setActionsVisible(visible) { | |
| if (actionsEl) actionsEl.hidden = !visible; | |
| } | |
| function setDownloadGateVisible(visible) { | |
| if (downloadGateEl) downloadGateEl.hidden = !visible; | |
| } | |
| function canRunRuntimeWork() { | |
| return bootStarted; | |
| } | |
| function selectedDownloadInfo() { | |
| return BACKEND_DOWNLOADS[$("backend")?.value] || BACKEND_DOWNLOADS["custom-lowbit-webgpu"]; | |
| } | |
| function updateDownloadGateText() { | |
| const info = selectedDownloadInfo(); | |
| if (downloadGateTitleEl) downloadGateTitleEl.textContent = info.title; | |
| return info; | |
| } | |
| function showDownloadGate({ retry = false, preserveStatus = false } = {}) { | |
| bootReady = false; | |
| const info = updateDownloadGateText(); | |
| setDownloadGateVisible(true); | |
| setActionsVisible(false); | |
| if (startDownloadButton) startDownloadButton.disabled = false; | |
| runButton.disabled = true; | |
| setButtonDisabled(warmButton, true); | |
| abortButton.disabled = true; | |
| setElapsed("0.0s"); | |
| placeholderEl.hidden = false; | |
| placeholderEl.textContent = "Model not loaded"; | |
| if (!preserveStatus) { | |
| setStatus([ | |
| retry ? "Download failed" : "Ready to download", | |
| info.status, | |
| ], retry); | |
| } | |
| } | |
| function showRuntimeLoading() { | |
| updateDownloadGateText(); | |
| setDownloadGateVisible(false); | |
| setActionsVisible(false); | |
| if (startDownloadButton) startDownloadButton.disabled = true; | |
| runButton.disabled = true; | |
| setButtonDisabled(warmButton, true); | |
| abortButton.disabled = true; | |
| setElapsed("0.0s"); | |
| placeholderEl.hidden = false; | |
| placeholderEl.textContent = "Loading"; | |
| } | |
| function showRuntimeReady() { | |
| bootReady = true; | |
| setDownloadGateVisible(false); | |
| setActionsVisible(true); | |
| runButton.disabled = false; | |
| setButtonDisabled(warmButton, false); | |
| abortButton.disabled = true; | |
| } | |
| function updateRunTimer(prefix = "Generating") { | |
| if (!runTimerStarted) { | |
| setElapsed("0.0s"); | |
| return 0; | |
| } | |
| const seconds = (performance.now() - runTimerStarted) / 1000; | |
| setElapsed(`${prefix} ${seconds.toFixed(1)}s`, true); | |
| return seconds; | |
| } | |
| function startRunTimer() { | |
| if (runTimerId) clearInterval(runTimerId); | |
| runTimerStarted = performance.now(); | |
| runTimerFinalized = false; | |
| updateRunTimer(); | |
| runTimerId = setInterval(() => updateRunTimer(), 100); | |
| } | |
| function finishRunTimer(label = "Done") { | |
| if (runTimerFinalized) return; | |
| const seconds = runTimerStarted ? (performance.now() - runTimerStarted) / 1000 : 0; | |
| if (runTimerId) clearInterval(runTimerId); | |
| runTimerId = 0; | |
| runTimerStarted = 0; | |
| runTimerFinalized = true; | |
| setElapsed(`${label} ${seconds.toFixed(2)}s`, false); | |
| } | |
| function setSettingsPopoverOpen(open) { | |
| if (!settingsPopoverEl || !settingsToggleButton) return; | |
| settingsPopoverEl.hidden = !open; | |
| settingsToggleButton.setAttribute("aria-expanded", open ? "true" : "false"); | |
| } | |
| function seedLineForRequest(request) { | |
| if (!request) return ""; | |
| return `Seed: ${request.seed}${Number(request.requestedSeed) < 0 ? " (random from -1)" : ""}`; | |
| } | |
| function seedInputLine() { | |
| const requestedSeed = Number($("seed").value); | |
| return Number.isFinite(requestedSeed) && requestedSeed >= 0 | |
| ? `Seed: ${Math.trunc(requestedSeed)}` | |
| : "Seed: -1 (random)"; | |
| } | |
| function seedLineForResult(result, request) { | |
| const seed = result?.seed ?? request?.seed; | |
| if (seed === undefined || seed === null) return ""; | |
| return `Seed: ${seed}${request && Number(request.requestedSeed) < 0 ? " (random from -1)" : ""}`; | |
| } | |
| function captureDisplayedImageDataUrl() { | |
| try { | |
| if (!canvasEl.hidden && canvasEl.width && canvasEl.height) { | |
| return canvasEl.toDataURL("image/png"); | |
| } | |
| } catch { | |
| } | |
| if (!imageEl.hidden && imageEl.src && !imageEl.src.startsWith("blob:")) return imageEl.src; | |
| return ""; | |
| } | |
| function galleryItemFromResult(result, elapsed, request, heading = "") { | |
| const dataUrl = captureDisplayedImageDataUrl(); | |
| if (!dataUrl || !request) return null; | |
| return { | |
| id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, | |
| imageUrl: dataUrl, | |
| prompt: request.prompt, | |
| seed: result.seed ?? request.seed, | |
| requestedSeed: request.requestedSeed, | |
| width: result.width || request.width, | |
| height: result.height || request.height, | |
| steps: request.numSteps, | |
| backend: request.transformerBackend, | |
| speedProfile: request.speed?.requestedProfile || $("speed-profile").value, | |
| speedLabel: speedProfileLabel(request.speed || {}), | |
| mode: result.mode || (request.hasInitImage ? "image-to-image" : "text-to-image"), | |
| elapsed, | |
| heading, | |
| createdAt: new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit", second: "2-digit" }), | |
| }; | |
| } | |
| function renderGallery() { | |
| if (!galleryPanelEl || !galleryGridEl) return; | |
| galleryPanelEl.hidden = generationHistory.length === 0; | |
| galleryGridEl.innerHTML = ""; | |
| for (const item of generationHistory) { | |
| const card = document.createElement("article"); | |
| card.className = "gallery-card"; | |
| card.dataset.galleryId = item.id; | |
| const image = document.createElement("img"); | |
| image.src = item.imageUrl; | |
| image.alt = `Generated image seed ${item.seed}`; | |
| card.appendChild(image); | |
| const body = document.createElement("div"); | |
| body.className = "gallery-body"; | |
| const meta = document.createElement("div"); | |
| meta.className = "gallery-meta"; | |
| const prompt = document.createElement("div"); | |
| prompt.className = "gallery-prompt"; | |
| prompt.textContent = item.prompt || "(empty prompt)"; | |
| const seed = document.createElement("div"); | |
| seed.textContent = `Seed ${item.seed}${Number(item.requestedSeed) < 0 ? " random" : ""}`; | |
| const details = document.createElement("div"); | |
| details.textContent = `${item.width} x ${item.height} · ${item.steps} steps · ${item.createdAt}`; | |
| meta.append(prompt, seed, details); | |
| const actions = document.createElement("div"); | |
| actions.className = "gallery-actions"; | |
| [ | |
| ["prompt", "Prompt"], | |
| ["seed", "Seed"], | |
| ["reuse", "Reuse"], | |
| ["img2img", "Img2img"], | |
| ].forEach(([action, label]) => { | |
| const button = document.createElement("button"); | |
| button.type = "button"; | |
| button.className = "secondary"; | |
| button.dataset.galleryAction = action; | |
| button.dataset.galleryId = item.id; | |
| button.textContent = label; | |
| actions.appendChild(button); | |
| }); | |
| body.append(meta, actions); | |
| card.appendChild(body); | |
| galleryGridEl.appendChild(card); | |
| } | |
| } | |
| function rememberGalleryResult(result, elapsed, request, heading = "") { | |
| const item = galleryItemFromResult(result, elapsed, request, heading); | |
| if (!item) return; | |
| generationHistory = [item, ...generationHistory].slice(0, GALLERY_LIMIT); | |
| renderGallery(); | |
| } | |
| async function dataUrlToFile(dataUrl, filename) { | |
| const response = await fetch(dataUrl); | |
| const blob = await response.blob(); | |
| return new File([blob], filename, { type: blob.type || "image/png" }); | |
| } | |
| async function setInitImageFromGallery(item) { | |
| const file = await dataUrlToFile(item.imageUrl, `history-seed-${item.seed}.png`); | |
| const transfer = new DataTransfer(); | |
| transfer.items.add(file); | |
| initImageInput.files = transfer.files; | |
| initImageInput.dispatchEvent(new Event("change", { bubbles: true })); | |
| } | |
| function applyGalleryPrompt(item) { | |
| $("prompt").value = item.prompt || ""; | |
| scheduleAutoWarmForCurrentSettings(650); | |
| } | |
| function applyGallerySeed(item) { | |
| $("seed").value = String(item.seed); | |
| scheduleAutoWarmForCurrentSettings(350); | |
| } | |
| function applyGallerySettings(item) { | |
| applyGalleryPrompt(item); | |
| applyGallerySeed(item); | |
| $("steps").value = String(item.steps || 4); | |
| $("width").value = String(item.width || 256); | |
| $("height").value = String(item.height || 256); | |
| syncResolutionPresetFromDimensions({ showAdvancedForCustom: true }); | |
| setSelectValue("backend", item.backend); | |
| setSelectValue("speed-profile", item.speedProfile); | |
| scheduleAutoWarmForCurrentSettings(350); | |
| } | |
| function storedTheme() { | |
| try { | |
| return localStorage.getItem(THEME_STORAGE_KEY) || ""; | |
| } catch { | |
| return ""; | |
| } | |
| } | |
| function persistTheme(theme) { | |
| try { | |
| localStorage.setItem(THEME_STORAGE_KEY, theme); | |
| } catch { | |
| } | |
| } | |
| function applyTheme(theme, options = {}) { | |
| const nextTheme = theme === "light" ? "light" : "dark"; | |
| document.documentElement.dataset.theme = nextTheme; | |
| if (themeIconEl) themeIconEl.textContent = nextTheme === "dark" ? "light_mode" : "dark_mode"; | |
| if (themeToggleButton) { | |
| themeToggleButton.setAttribute("aria-label", nextTheme === "dark" ? "Switch to light mode" : "Switch to dark mode"); | |
| } | |
| if (options.persist !== false) persistTheme(nextTheme); | |
| } | |
| function initTheme() { | |
| const urlTheme = urlDefaults.get("theme"); | |
| const requestedTheme = urlTheme === "light" || urlTheme === "dark" ? urlTheme : storedTheme(); | |
| applyTheme(requestedTheme || "dark", { persist: false }); | |
| } | |
| function isAbortError(err) { | |
| return String(err?.stack || err?.message || err || "").includes("ABORTED_BY_CLIENT"); | |
| } | |
| function loadClassicScript(src) { | |
| return new Promise((resolve, reject) => { | |
| const existing = Array.from(document.scripts).find((script) => script.dataset.flux2Src === src); | |
| if (existing?.dataset.loaded === "true") { | |
| resolve(); | |
| return; | |
| } | |
| if (existing) existing.remove(); | |
| const script = document.createElement("script"); | |
| script.src = src; | |
| script.dataset.flux2Src = src; | |
| script.async = false; | |
| script.onload = () => { | |
| script.dataset.loaded = "true"; | |
| resolve(); | |
| }; | |
| script.onerror = () => reject(new Error(`Failed to load ${src}`)); | |
| if (!existing) document.body.appendChild(script); | |
| }); | |
| } | |
| async function loadCustomRuntimeScripts() { | |
| if (!customRuntimeScriptsPromise) { | |
| customRuntimeScriptsPromise = (async () => { | |
| setLoadPhase("Loading custom WebGPU kernels"); | |
| const version = encodeURIComponent(appAssetVersion()); | |
| await loadClassicScript(`tools/custom_lowbit_webgpu/custom_lowbit_linear.js?v=${version}`); | |
| await loadClassicScript(`tools/custom_lowbit_webgpu/custom_transformer_runtime.js?v=${version}`); | |
| })(); | |
| } | |
| return customRuntimeScriptsPromise; | |
| } | |
| async function loadEngineModule() { | |
| if (!engineModulePromise) { | |
| setLoadPhase("Loading browser engine"); | |
| engineModulePromise = import(`./flux2-engine.js?v=${encodeURIComponent(appAssetVersion())}`); | |
| } | |
| return engineModulePromise; | |
| } | |
| function resetRuntimeLoadPromises() { | |
| engineReadyPromise = null; | |
| tokenizerPromise = null; | |
| if (!window.flux2Engine) engineModulePromise = null; | |
| if (typeof globalThis.createCustomFluxTransformerRuntime !== "function") customRuntimeScriptsPromise = null; | |
| transformersModulePromise = null; | |
| } | |
| function waitForEngine() { | |
| return new Promise((resolve, reject) => { | |
| const deadline = performance.now() + 30000; | |
| const tick = () => { | |
| if (window.flux2Engine && typeof window.flux2Engine.init === "function") { | |
| resolve(window.flux2Engine); | |
| return; | |
| } | |
| if (performance.now() > deadline) { | |
| reject(new Error("FLUX.2 browser engine did not load")); | |
| return; | |
| } | |
| setTimeout(tick, 50); | |
| }; | |
| tick(); | |
| }); | |
| } | |
| async function sha256Hex(value) { | |
| const bytes = new TextEncoder().encode(value); | |
| const digest = await crypto.subtle.digest("SHA-256", bytes); | |
| return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); | |
| } | |
| function buildPromptText(prompt) { | |
| return `<|im_start|>user\n${prompt}<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n`; | |
| } | |
| function staticTextContextUrlForPrompt(prompt) { | |
| if (urlFlag("noStaticPromptContext")) return ""; | |
| if (String(prompt || "") !== DEFAULT_PROMPT) return ""; | |
| return resolveAssetUrl(configuredRuntimeBaseUrl(), "precomputed/default-prompt-context-f16.bin"); | |
| } | |
| function staticTextProjectionUrlForPrompt(prompt, textTokens = 64) { | |
| if (urlFlag("noStaticPromptProjection")) return ""; | |
| if (String(prompt || "") !== DEFAULT_PROMPT) return ""; | |
| const filename = { | |
| 32: "default-prompt-txtproj-32-f32.bin", | |
| 64: "default-prompt-txtproj-64-f32.bin", | |
| }[Number(textTokens)]; | |
| return filename ? resolveAssetUrl(configuredRuntimeBaseUrl(), `precomputed/${filename}`) : ""; | |
| } | |
| function flattenTokenValues(value) { | |
| if (value == null) return []; | |
| if (Array.isArray(value)) { | |
| return value.flat(Infinity).map(Number); | |
| } | |
| if (ArrayBuffer.isView(value)) { | |
| return Array.from(value, Number); | |
| } | |
| if (value.data) { | |
| return flattenTokenValues(value.data); | |
| } | |
| if (value.tolist && typeof value.tolist === "function") { | |
| return flattenTokenValues(value.tolist()); | |
| } | |
| return Array.from(value, Number); | |
| } | |
| function toInt64Padded(values, seqLen, padValue) { | |
| const out = new BigInt64Array(seqLen); | |
| const count = Math.min(values.length, seqLen); | |
| for (let i = 0; i < count; i++) out[i] = BigInt(Math.trunc(Number(values[i]))); | |
| for (let i = count; i < seqLen; i++) out[i] = BigInt(padValue); | |
| return out; | |
| } | |
| function maskFromLength(length, seqLen) { | |
| const out = new BigInt64Array(seqLen); | |
| const count = Math.min(length, seqLen); | |
| for (let i = 0; i < count; i++) out[i] = 1n; | |
| return out; | |
| } | |
| async function loadTokenizer() { | |
| if (tokenizerPromise) return tokenizerPromise; | |
| setLoadPhase("Loading tokenizer"); | |
| const { AutoTokenizer, env } = await loadTransformersModule(); | |
| const modelBaseUrl = configuredModelBaseUrl(); | |
| const tokenizerSpec = tokenizerLoadSpec(modelBaseUrl); | |
| env.allowLocalModels = !tokenizerSpec.remote; | |
| env.allowRemoteModels = tokenizerSpec.remote; | |
| env.localModelPath = "/"; | |
| tokenizerPromise = AutoTokenizer.from_pretrained(tokenizerSpec.source, { | |
| ...tokenizerSpec.options, | |
| progress_callback: (progress) => { | |
| const file = progress?.file ? compactLoadUrlLabel(progress.file) : "tokenizer"; | |
| const status = progress?.status ? ` ${progress.status}` : ""; | |
| setLoadPhase(`Loading ${file}${status}`); | |
| }, | |
| }); | |
| return tokenizerPromise; | |
| } | |
| async function loadTransformersModule() { | |
| if (!transformersModulePromise) { | |
| setLoadPhase("Loading tokenizer runtime"); | |
| transformersModulePromise = import("https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1"); | |
| } | |
| return transformersModulePromise; | |
| } | |
| async function tokenizePrompt(prompt, seqLen) { | |
| const cachePayload = JSON.stringify({ model: "flux.2-klein-4b", text_seq_len: seqLen, prompt }); | |
| const promptCacheKey = await sha256Hex(cachePayload); | |
| const cacheKey = `${seqLen}|${promptCacheKey}`; | |
| if (!urlFlag("noTokenizerCache") && tokenizedPromptCache.has(cacheKey)) { | |
| const cached = tokenizedPromptCache.get(cacheKey); | |
| tokenizedPromptCache.delete(cacheKey); | |
| tokenizedPromptCache.set(cacheKey, cached); | |
| return cached; | |
| } | |
| const tokenizer = await loadTokenizer(); | |
| const text = buildPromptText(prompt); | |
| const encoded = await tokenizer(text, { | |
| truncation: true, | |
| max_length: seqLen, | |
| add_special_tokens: false, | |
| }); | |
| const ids = flattenTokenValues(encoded.input_ids); | |
| const mask = encoded.attention_mask ? flattenTokenValues(encoded.attention_mask) : new Array(ids.length).fill(1); | |
| const padTokenId = Number(tokenizer.pad_token_id ?? 151643); | |
| const tokenized = { | |
| inputIds: toInt64Padded(ids, seqLen, padTokenId), | |
| attentionMask: mask.length ? toInt64Padded(mask.slice(0, ids.length), seqLen, 0) : maskFromLength(ids.length, seqLen), | |
| tokenCount: Math.min(ids.length, seqLen), | |
| promptCacheKey, | |
| }; | |
| if (!urlFlag("noTokenizerCache")) { | |
| tokenizedPromptCache.set(cacheKey, tokenized); | |
| const maxEntries = Math.max(1, Math.trunc(Number(urlDefaults.get("tokenizerCacheEntries") || 8))); | |
| while (tokenizedPromptCache.size > maxEntries) { | |
| tokenizedPromptCache.delete(tokenizedPromptCache.keys().next().value); | |
| } | |
| } | |
| return tokenized; | |
| } | |
| async function initEngine() { | |
| if (engineReadyPromise) return engineReadyPromise; | |
| engineReadyPromise = (async () => { | |
| await Promise.all([loadCustomRuntimeScripts(), loadEngineModule()]); | |
| const engine = await waitForEngine(); | |
| const modelBaseUrl = configuredModelBaseUrl(); | |
| const runtimeBaseUrl = configuredRuntimeBaseUrl(modelBaseUrl); | |
| setLoadPhase("Loading model config and runtime manifest"); | |
| console.log("[flux2] modelBaseUrl", modelBaseUrl); | |
| const result = await engine.init(modelBaseUrl, { | |
| executionProvider: "webgpu", | |
| customKernelBaseUrl: configuredCustomKernelBaseUrl(modelBaseUrl, runtimeBaseUrl), | |
| customTransformerMode: "custom-lowbit-webgpu", | |
| enableGraphCapture: urlFlag("graphCapture"), | |
| graphCaptureStages: urlDefaults.get("graphCaptureStages") ?? urlDefaults.get("captureStages") ?? "", | |
| profileWebGpu: urlFlag("profileWebGpu"), | |
| profileTopK: Number(urlDefaults.get("profileTopK") || 12), | |
| enableFusedTransformer: false, | |
| textContextCacheSize: 4, | |
| vaeEncoderCacheSize: Number(urlDefaults.get("vaeEncoderCacheEntries") || 3), | |
| persistentTextContextCache: !urlFlag("noPromptCache"), | |
| persistentTextContextCacheSize: Number(urlDefaults.get("promptCacheEntries") || 8), | |
| persistentVaeEncoderCache: !urlFlag("noVaeEncoderPersistentCache"), | |
| persistentVaeEncoderCacheSize: Number(urlDefaults.get("vaeEncoderPersistentCacheEntries") || 8), | |
| checkLegacyCustomBundles: urlFlag("legacyStatus"), | |
| }); | |
| return { engine, init: result }; | |
| })(); | |
| return engineReadyPromise; | |
| } | |
| function img2ImgStrengthCurveExponent(curve) { | |
| if (curve === "linear") return 1; | |
| if (curve === "balanced") return 1.25; | |
| if (curve === "strong") return 3; | |
| if (curve === "rewrite") return 6; | |
| if (curve === "edit") return 2.5; | |
| return 2.5; | |
| } | |
| function effectiveImg2ImgStrength(strength, curve = strengthCurveEl?.value) { | |
| const clamped = Math.max(0, Math.min(1, Number(strength ?? 0.65))); | |
| return 1 - Math.pow(1 - clamped, img2ImgStrengthCurveExponent(curve)); | |
| } | |
| function formatStrengthValue(value) { | |
| if (value >= 0.995 && value < 1) return value.toFixed(4); | |
| if (value >= 0.95 && value < 1) return value.toFixed(3); | |
| return value.toFixed(2); | |
| } | |
| function updateStrengthDisplay() { | |
| const strength = Number(strengthEl.value); | |
| strengthValueEl.textContent = `${strength.toFixed(2)} -> ${formatStrengthValue(effectiveImg2ImgStrength(strength))}`; | |
| } | |
| function setInitImageUi(file = null) { | |
| const hasFile = Boolean(file); | |
| if (initImageLabelEl) initImageLabelEl.textContent = hasFile ? file.name : "Choose optional image"; | |
| if (clearInitImageButton) clearInitImageButton.hidden = !hasFile; | |
| if (img2imgControlsEl) img2imgControlsEl.hidden = !hasFile; | |
| if (!hasFile) { | |
| if (sourcePreviewWrap) sourcePreviewWrap.hidden = true; | |
| if (sourcePreviewEl) sourcePreviewEl.removeAttribute("src"); | |
| if (sourceMetaEl) sourceMetaEl.textContent = ""; | |
| } | |
| } | |
| function clearInitImage({ schedule = true } = {}) { | |
| if (currentSourceUrl) URL.revokeObjectURL(currentSourceUrl); | |
| currentSourceUrl = null; | |
| if (initImageInput) initImageInput.value = ""; | |
| setInitImageUi(null); | |
| if (schedule) scheduleAutoWarmForCurrentSettings(0); | |
| } | |
| function setSelectValue(id, value) { | |
| if (!value) return; | |
| const element = $(id); | |
| if (!element) return; | |
| const stringValue = String(value); | |
| if (!element.options) { | |
| element.value = stringValue; | |
| } else if ([...element.options].some((option) => option.value === stringValue)) { | |
| element.value = stringValue; | |
| } else if ((id === "render-max" || id === "decode-max") && Number.isFinite(Number(stringValue)) && Number(stringValue) > 0) { | |
| const option = document.createElement("option"); | |
| option.value = stringValue; | |
| option.textContent = stringValue; | |
| element.appendChild(option); | |
| element.value = stringValue; | |
| } | |
| } | |
| function setInputValue(id, value) { | |
| if (value === null || value === undefined || value === "") return; | |
| const element = $(id); | |
| if (!element) return; | |
| element.value = String(value); | |
| } | |
| function setResolutionAdvancedVisible(visible) { | |
| if (!resolutionAdvancedEl) return; | |
| resolutionAdvancedEl.hidden = !visible; | |
| } | |
| function clampResolutionValue(value, fallback = 256) { | |
| const number = Math.max(16, Math.trunc(Number(value) || fallback)); | |
| return Math.max(16, Math.round(number / 16) * 16); | |
| } | |
| function nearestMultipleOf16(value) { | |
| return clampResolutionValue(value); | |
| } | |
| function dimensionsAreCommonSquare() { | |
| const width = Number($("width").value); | |
| const height = Number($("height").value); | |
| return width === height && COMMON_RESOLUTIONS.includes(width); | |
| } | |
| function setAspectLock(enabled) { | |
| aspectLockEnabled = Boolean(enabled); | |
| if (aspectLockButton) { | |
| aspectLockButton.setAttribute("aria-pressed", aspectLockEnabled ? "true" : "false"); | |
| aspectLockButton.setAttribute("aria-label", aspectLockEnabled ? "Unlock aspect ratio" : "Lock aspect ratio"); | |
| } | |
| if (aspectLockIconEl) aspectLockIconEl.textContent = aspectLockEnabled ? "link" : "link_off"; | |
| if (aspectRatioEl && aspectRatioEl.value === "free" && aspectLockEnabled) { | |
| aspectRatioEl.value = "custom"; | |
| } | |
| } | |
| function currentAspectRatio() { | |
| const width = Number($("width").value) || 1; | |
| const height = Number($("height").value) || 1; | |
| return width / height; | |
| } | |
| function rememberCurrentAspectRatio() { | |
| const ratio = currentAspectRatio(); | |
| if (Number.isFinite(ratio) && ratio > 0) aspectLockRatio = ratio; | |
| } | |
| function syncAspectSelectFromDimensions() { | |
| if (!aspectRatioEl || aspectSyncing) return; | |
| if (!aspectLockEnabled) { | |
| aspectRatioEl.value = "free"; | |
| return; | |
| } | |
| const ratio = currentAspectRatio(); | |
| const matched = Object.entries(ASPECT_RATIOS).find(([, value]) => Math.abs(ratio - value) < 0.015); | |
| aspectRatioEl.value = matched ? matched[0] : "custom"; | |
| } | |
| function applyAspectToHeight() { | |
| if (!aspectLockEnabled || !aspectLockRatio) return; | |
| const width = clampResolutionValue($("width").value); | |
| const nextHeight = nearestMultipleOf16(width / aspectLockRatio); | |
| aspectSyncing = true; | |
| $("width").value = String(width); | |
| $("height").value = String(nextHeight); | |
| aspectSyncing = false; | |
| } | |
| function applyAspectToWidth() { | |
| if (!aspectLockEnabled || !aspectLockRatio) return; | |
| const height = clampResolutionValue($("height").value); | |
| const nextWidth = nearestMultipleOf16(height * aspectLockRatio); | |
| aspectSyncing = true; | |
| $("height").value = String(height); | |
| $("width").value = String(nextWidth); | |
| aspectSyncing = false; | |
| } | |
| function syncResolutionPresetFromDimensions(options = {}) { | |
| if (!resolutionPresetEl) return; | |
| const width = clampResolutionValue($("width").value); | |
| const height = clampResolutionValue($("height").value); | |
| $("width").value = String(width); | |
| $("height").value = String(height); | |
| const commonSquare = dimensionsAreCommonSquare(); | |
| if (options.preserveCustom && resolutionPresetEl.value === "custom") { | |
| setResolutionAdvancedVisible(true); | |
| rememberCurrentAspectRatio(); | |
| syncAspectSelectFromDimensions(); | |
| return; | |
| } | |
| resolutionPresetEl.value = commonSquare ? String(width) : "custom"; | |
| if (commonSquare) { | |
| setResolutionAdvancedVisible(false); | |
| if (aspectRatioEl) aspectRatioEl.value = "1:1"; | |
| aspectLockRatio = 1; | |
| return; | |
| } | |
| if (!commonSquare && options.showAdvancedForCustom) { | |
| setResolutionAdvancedVisible(true); | |
| } | |
| rememberCurrentAspectRatio(); | |
| syncAspectSelectFromDimensions(); | |
| } | |
| function applyResolutionPreset(value) { | |
| if (value === "custom") { | |
| setResolutionAdvancedVisible(true); | |
| rememberCurrentAspectRatio(); | |
| syncAspectSelectFromDimensions(); | |
| return; | |
| } | |
| const size = Number(value); | |
| if (!COMMON_RESOLUTIONS.includes(size)) return; | |
| $("width").value = String(size); | |
| $("height").value = String(size); | |
| aspectLockRatio = 1; | |
| setAspectLock(true); | |
| if (aspectRatioEl) aspectRatioEl.value = "1:1"; | |
| setResolutionAdvancedVisible(false); | |
| } | |
| function applyUrlDefaults() { | |
| const params = urlDefaults; | |
| const resolution = params.get("resolution") ?? params.get("size"); | |
| const resolutionSize = Number(resolution); | |
| if (COMMON_RESOLUTIONS.includes(resolutionSize)) { | |
| setInputValue("width", resolutionSize); | |
| setInputValue("height", resolutionSize); | |
| } | |
| setInputValue("prompt", params.get("prompt")); | |
| setInputValue("width", params.get("w") ?? params.get("width")); | |
| setInputValue("height", params.get("h") ?? params.get("height")); | |
| setInputValue("steps", params.get("steps")); | |
| setInputValue("seed", params.get("seed")); | |
| setInputValue("strength", params.get("strength")); | |
| setSelectValue("backend", params.get("backend")); | |
| setSelectValue("speed-profile", params.get("profile") ?? params.get("speed")); | |
| setSelectValue("strength-curve", params.get("curve") ?? params.get("strengthCurve")); | |
| setSelectValue("fit-mode", params.get("fit")); | |
| syncResolutionPresetFromDimensions({ showAdvancedForCustom: true }); | |
| } | |
| function urlFlag(name) { | |
| const value = urlDefaults.get(name); | |
| return value === "1" || value === "true" || value === "yes" || value === "on"; | |
| } | |
| function selectedSpeedConfig(width, height, requestedProfileOverride = "") { | |
| const requestedProfile = requestedProfileOverride || $("speed-profile").value; | |
| const imageTokens = Math.ceil(width / 16) * Math.ceil(height / 16); | |
| const maxDimension = Math.max(width, height); | |
| const subsecondFinal = requestedProfile === "subsecond-final"; | |
| const subsecondUltra = requestedProfile === "subsecond-ultra"; | |
| const subsecondInstant = requestedProfile === "subsecond-instant"; | |
| const numericUrlDefault = (name, fallback) => { | |
| const value = Number(urlDefaults.get(name) || 0); | |
| return Number.isFinite(value) && value > 0 ? value : fallback; | |
| }; | |
| const approximateCache = requestedProfile === "turbo-cache" || | |
| requestedProfile === "ab-cache" || | |
| requestedProfile === "tail-cache" || | |
| requestedProfile === "tail-cache-lite" || | |
| requestedProfile === "tail-cache-mid" || | |
| requestedProfile === "tail-cache-fast" || | |
| subsecondFinal || | |
| subsecondUltra || | |
| subsecondInstant || | |
| requestedProfile === "subsecond-cache" || | |
| requestedProfile === "subsecond-preview"; | |
| const tailCacheProfile = requestedProfile === "tail-cache" || | |
| requestedProfile === "tail-cache-lite" || | |
| requestedProfile === "tail-cache-mid" || | |
| requestedProfile === "tail-cache-fast"; | |
| const approxReusePredEvery = requestedProfile === "subsecond-cache" || | |
| subsecondInstant || | |
| requestedProfile === "subsecond-preview" | |
| ? 4 | |
| : (requestedProfile === "turbo-cache" || requestedProfile === "ab-cache" ? 2 : 0); | |
| const approxPredictionMode = urlDefaults.get("approxMode") || | |
| urlDefaults.get("approxPredictionMode") || | |
| (requestedProfile === "ab-cache" ? "ab2" : "raw"); | |
| const requestedImg2ImgStepPolicy = urlDefaults.get("img2imgStepPolicy") || urlDefaults.get("img2imgSteps") || ""; | |
| const fastImg2ImgProfile = subsecondFinal || | |
| subsecondUltra || | |
| subsecondInstant || | |
| requestedProfile === "subsecond-preview" || | |
| requestedProfile === "subsecond-cache" || | |
| tailCacheProfile; | |
| const img2imgStepPolicy = requestedImg2ImgStepPolicy === "full" || requestedImg2ImgStepPolicy === "adaptive" | |
| ? requestedImg2ImgStepPolicy | |
| : (fastImg2ImgProfile ? "adaptive" : "full"); | |
| let autoPreviewRenderMaxSize = 0; | |
| if (subsecondInstant && maxDimension > 256) { | |
| autoPreviewRenderMaxSize = maxDimension <= 512 ? 192 : 160; | |
| } else if (subsecondUltra && maxDimension > 256) { | |
| autoPreviewRenderMaxSize = maxDimension <= 512 ? 224 : 192; | |
| } else if (requestedProfile === "subsecond-preview" && maxDimension > 256) { | |
| autoPreviewRenderMaxSize = 256; | |
| } else if (requestedProfile === "tail-cache" && maxDimension > 512) { | |
| autoPreviewRenderMaxSize = 512; | |
| } else if ((requestedProfile === "tail-cache-lite" || (subsecondFinal && maxDimension <= 512)) && maxDimension > 272) { | |
| autoPreviewRenderMaxSize = 272; | |
| } else if (requestedProfile === "tail-cache-mid" && maxDimension > 384) { | |
| autoPreviewRenderMaxSize = 384; | |
| } else if (requestedProfile === "tail-cache-fast" && maxDimension > 320) { | |
| autoPreviewRenderMaxSize = 320; | |
| } else if (subsecondFinal && maxDimension > 512 && maxDimension > 240) { | |
| autoPreviewRenderMaxSize = 240; | |
| } | |
| const autoPreviewDecodeMaxSize = ( | |
| requestedProfile === "subsecond-preview" || | |
| requestedProfile === "tail-cache" || | |
| requestedProfile === "tail-cache-lite" || | |
| requestedProfile === "tail-cache-mid" || | |
| requestedProfile === "tail-cache-fast" || | |
| subsecondFinal || | |
| subsecondUltra || | |
| subsecondInstant | |
| ) && maxDimension > 256 ? 256 : 0; | |
| const forcedKernelProfile = urlDefaults.get("kernelProfile") || urlDefaults.get("kernel"); | |
| const defaultLinearTileCols = maxDimension <= 256 | |
| ? 128 | |
| : (maxDimension <= 1024 ? 64 : undefined); | |
| const customTextTokenLimit = urlFlag("noTextTrim") | |
| ? 0 | |
| : (Number(urlDefaults.get("textTokens") || 0) || (subsecondInstant ? 64 : (requestedProfile === "subsecond-preview" ? 128 : 512))); | |
| const customMinTextTokens = Number(urlDefaults.get("minTextTokens") || 0) || | |
| (requestedProfile === "subsecond-preview" ? 64 : 32); | |
| const profile = forcedKernelProfile || | |
| (requestedProfile === "auto" || requestedProfile === "progressive" || requestedProfile === "progressive-fast" | |
| ? "all-dp4a" | |
| : approximateCache | |
| ? "all-dp4a" | |
| : requestedProfile); | |
| return { | |
| profile, | |
| requestedProfile, | |
| customSingleLinear1Output: "f16", | |
| customSingleLinear1QkvBackend: profile === "all-dp4a" ? "dp4a" : "q4", | |
| customSingleLinear1MlpBackend: profile === "all-dp4a" || profile === "mlp-dp4a" ? "dp4a" : "q4", | |
| customSingleLinear1Q4ActivationScale: "row", | |
| customSingleLinear2Backend: "dp4a", | |
| customLinearTileCols: numericUrlDefault("linearTileCols", defaultLinearTileCols), | |
| customProjTileCols: numericUrlDefault("projTileCols", undefined), | |
| customFinalTileCols: numericUrlDefault("finalTileCols", undefined), | |
| customWChunkCols: numericUrlDefault("wChunkCols", undefined), | |
| customLinearRowBlock: numericUrlDefault("linearRowBlock", numericUrlDefault("linearRows", undefined)), | |
| customSingleQ4TileCols: numericUrlDefault("singleQ4TileCols", undefined), | |
| customSingleQ4KChunk: numericUrlDefault("singleQ4KChunk", undefined), | |
| customSingleQ4Dp4aTileCols: numericUrlDefault("singleQ4Dp4aTileCols", undefined), | |
| customAttentionTileKeys: numericUrlDefault("attentionTileKeys", 8), | |
| customAttentionQueryRows: numericUrlDefault("attentionQueryRows", 16), | |
| customSingleAttentionKernel: urlDefaults.get("singleAttentionKernel") || urlDefaults.get("attentionKernel") || "", | |
| customSingleQkNormStorage: urlDefaults.get("qkNormStorage") || urlDefaults.get("singleQkNormStorage") || "", | |
| customApproxReusePredEvery: approxReusePredEvery, | |
| customApproxPredictionMode: approxPredictionMode, | |
| customApproxAbScale: Number(urlDefaults.get("approxAbScale") || 0) || undefined, | |
| customSkipSingleBlocksFromStep: Number(urlDefaults.get("skipSingleBlocksFromStep") || urlDefaults.get("cacheSingleBlocksFromStep") || 0), | |
| customSkipSingleBlocksFromBlock: Number(urlDefaults.get("skipSingleBlocksFromBlock") || urlDefaults.get("cacheSingleBlocksFromBlock") || 0), | |
| customReuseSingleTailFromStep: Number(urlDefaults.get("reuseSingleTailFromStep") || urlDefaults.get("cacheSingleTailFromStep") || 0) || | |
| ((tailCacheProfile || ((subsecondFinal || subsecondUltra) && maxDimension > 256)) ? 2 : 0), | |
| customReuseSingleTailFromBlock: Number(urlDefaults.get("reuseSingleTailFromBlock") || urlDefaults.get("cacheSingleTailFromBlock") || 0) || | |
| (subsecondUltra && maxDimension > 256 ? 4 : (subsecondFinal && maxDimension > 256 ? 8 : (tailCacheProfile ? 12 : 0))), | |
| customProfilePhases: urlFlag("profilePhases") || urlFlag("phaseProfile"), | |
| customProfileSingleParts: urlFlag("profileSingleParts") || urlFlag("singlePartProfile"), | |
| customStepSubmitFusion: urlFlag("stepFusion") || urlFlag("stepSubmitFusion"), | |
| customSingleComputePass: urlFlag("singlePass"), | |
| customMaxDoubleBlocks: Number(urlDefaults.get("doubleBlocks") || 0) || (subsecondInstant ? 3 : 0), | |
| customMaxSingleBlocks: Number(urlDefaults.get("singleBlocks") || 0) || (subsecondInstant ? 8 : 0), | |
| autoPreviewRenderMaxSize, | |
| autoPreviewDecodeMaxSize, | |
| customTextTokenLimit, | |
| customMinTextTokens, | |
| img2imgStepPolicy, | |
| }; | |
| } | |
| function requestWithSpeedProfile(request, profile, options = {}) { | |
| const speed = selectedSpeedConfig(request.width, request.height, profile); | |
| const useSelectedCaps = options.useSelectedCaps !== false; | |
| const selectedPreviewRenderMaxSize = useSelectedCaps ? Number($("render-max").value) || 0 : 0; | |
| const selectedPreviewDecodeMaxSize = useSelectedCaps ? Number($("decode-max").value) || 0 : 0; | |
| return { | |
| ...request, | |
| speed, | |
| previewRenderMaxSize: selectedPreviewRenderMaxSize || speed.autoPreviewRenderMaxSize || 0, | |
| previewDecodeMaxSize: selectedPreviewDecodeMaxSize || speed.autoPreviewDecodeMaxSize || 0, | |
| }; | |
| } | |
| function currentInitImageKey(width = "", height = "", fitMode = "") { | |
| const file = initImageInput.files?.[0]; | |
| if (!file) return ""; | |
| return [file.name, file.size, file.lastModified || 0, `${width}x${height}`, fitMode].join("|"); | |
| } | |
| function speedProfileLabel(speed) { | |
| if (speed.requestedProfile === "auto") return `auto -> ${speed.profile}`; | |
| if (speed.requestedProfile === "progressive") return "progressive"; | |
| if (speed.requestedProfile === "progressive-fast") return "fast-default exact"; | |
| if (speed.requestedProfile && speed.profile && speed.requestedProfile !== speed.profile) { | |
| return `${speed.requestedProfile} -> ${speed.profile}`; | |
| } | |
| return speed.profile; | |
| } | |
| function progressivePreviewProfileForRequest(request) { | |
| const override = urlDefaults.get("progressivePreview") || urlDefaults.get("previewProfile"); | |
| if (override) return override; | |
| return "subsecond-instant"; | |
| } | |
| function progressiveFinalProfileForRequest(request) { | |
| const override = urlDefaults.get("progressiveFinal") || urlDefaults.get("finalProfile"); | |
| if (override) return override; | |
| if (request.speed.requestedProfile === "progressive-fast") return "subsecond-ultra"; | |
| const maxDimension = Math.max(request.width, request.height); | |
| if (maxDimension > 512) return "tail-cache-mid"; | |
| if (maxDimension === 512) return "tail-cache-lite"; | |
| return "auto"; | |
| } | |
| function speedCacheSignature(speed = {}) { | |
| return { | |
| requestedProfile: speed.requestedProfile, | |
| profile: speed.profile, | |
| reuse: speed.customApproxReusePredEvery, | |
| approxMode: speed.customApproxPredictionMode, | |
| approxAbScale: speed.customApproxAbScale, | |
| textLimit: speed.customTextTokenLimit, | |
| minText: speed.customMinTextTokens, | |
| qkv: speed.customSingleLinear1QkvBackend, | |
| mlp: speed.customSingleLinear1MlpBackend, | |
| linear2: speed.customSingleLinear2Backend, | |
| linearTileCols: speed.customLinearTileCols, | |
| projTileCols: speed.customProjTileCols, | |
| finalTileCols: speed.customFinalTileCols, | |
| wChunkCols: speed.customWChunkCols, | |
| linearRowBlock: speed.customLinearRowBlock, | |
| singleAttentionKernel: speed.customSingleAttentionKernel, | |
| qkNormStorage: speed.customSingleQkNormStorage, | |
| skipSingleBlocksFromStep: speed.customSkipSingleBlocksFromStep, | |
| skipSingleBlocksFromBlock: speed.customSkipSingleBlocksFromBlock, | |
| reuseSingleTailFromStep: speed.customReuseSingleTailFromStep, | |
| reuseSingleTailFromBlock: speed.customReuseSingleTailFromBlock, | |
| profilePhases: speed.customProfilePhases, | |
| profileSingleParts: speed.customProfileSingleParts, | |
| stepSubmitFusion: speed.customStepSubmitFusion, | |
| singlePass: speed.customSingleComputePass, | |
| doubleBlocks: speed.customMaxDoubleBlocks, | |
| singleBlocks: speed.customMaxSingleBlocks, | |
| img2imgStepPolicy: speed.img2imgStepPolicy, | |
| }; | |
| } | |
| function warmPlanForRequest(request) { | |
| const warmProfile = ( | |
| request.speed.requestedProfile === "progressive" | |
| ) && Math.max(request.width, request.height) > 256 | |
| ? progressivePreviewProfileForRequest(request) | |
| : ""; | |
| return { | |
| warmProfile, | |
| warmRequest: warmProfile | |
| ? requestWithSpeedProfile(request, warmProfile, { useSelectedCaps: false }) | |
| : request, | |
| }; | |
| } | |
| function backgroundPrepareRequestForRequest(request) { | |
| if ( | |
| !request.hasInitImage && | |
| request.speed.requestedProfile === "progressive" && | |
| Math.max(request.width, request.height) > 256 | |
| ) { | |
| return requestWithSpeedProfile(request, progressivePreviewProfileForRequest(request), { useSelectedCaps: false }); | |
| } | |
| return request; | |
| } | |
| function progressiveFinalPrepareRequestForRequest(request) { | |
| if ( | |
| !request.hasInitImage && | |
| request.speed.requestedProfile === "progressive" && | |
| Math.max(request.width, request.height) > 256 | |
| ) { | |
| return requestWithSpeedProfile(request, progressiveFinalProfileForRequest(request), { useSelectedCaps: true }); | |
| } | |
| return null; | |
| } | |
| function warmKeyForRequest(request) { | |
| const { warmRequest } = warmPlanForRequest(request); | |
| return JSON.stringify({ | |
| prompt: warmRequest.prompt, | |
| width: warmRequest.width, | |
| height: warmRequest.height, | |
| steps: warmRequest.numSteps, | |
| backend: warmRequest.transformerBackend, | |
| hasInitImage: warmRequest.hasInitImage, | |
| initImageKey: warmRequest.hasInitImage ? warmRequest.initImageKey : undefined, | |
| strength: warmRequest.hasInitImage ? warmRequest.strength : undefined, | |
| strengthCurve: warmRequest.hasInitImage ? warmRequest.strengthCurve : undefined, | |
| effectiveStrength: warmRequest.hasInitImage | |
| ? Number(effectiveImg2ImgStrength(warmRequest.strength, warmRequest.strengthCurve).toFixed(6)) | |
| : undefined, | |
| fitMode: warmRequest.hasInitImage ? warmRequest.fitMode : undefined, | |
| previewRenderMaxSize: warmRequest.previewRenderMaxSize, | |
| previewDecodeMaxSize: warmRequest.previewDecodeMaxSize, | |
| speed: speedCacheSignature(warmRequest.speed), | |
| }); | |
| } | |
| function warmResultKeyForRequest(request) { | |
| const { warmRequest } = warmPlanForRequest(request); | |
| const requestedSeed = Number(warmRequest.requestedSeed); | |
| const fixedSeed = Number.isFinite(requestedSeed) && requestedSeed >= 0 | |
| ? Math.trunc(requestedSeed) | |
| : "random"; | |
| return JSON.stringify({ | |
| warmKey: warmKeyForRequest(request), | |
| seed: fixedSeed, | |
| }); | |
| } | |
| function assetPrepareKeyForRequest(request) { | |
| return JSON.stringify({ | |
| backend: request.transformerBackend, | |
| doubleBlocks: request.speed.customMaxDoubleBlocks || 5, | |
| singleBlocks: request.speed.customMaxSingleBlocks || 20, | |
| }); | |
| } | |
| function shouldAssetPrepareRequest(request) { | |
| if (urlFlag("noAutowarm") || urlFlag("noAssetWarm")) return false; | |
| return request.transformerBackend === "custom-lowbit-webgpu"; | |
| } | |
| function fullManifestLinearById(manifest, id) { | |
| const linears = Array.isArray(manifest?.linears) ? manifest.linears : []; | |
| return linears.find((item) => item.id === id || item.name === id || item.source_node === id) || null; | |
| } | |
| function fullManifestQkNormById(manifest, id) { | |
| const qkNorms = Array.isArray(manifest?.constants?.qk_norms) ? manifest.constants.qk_norms : []; | |
| return qkNorms.find((item) => item.id === id || item.name === id) || null; | |
| } | |
| function estimateLinearAssetBytes(linear) { | |
| if (!linear) return { bytes: 0, files: 0 }; | |
| if (linear.i8_dp4a) { | |
| const packed = Number(linear.i8_dp4a.packed_u32_count || 0) * 4; | |
| const scales = Number(linear.i8_dp4a.scale_count || 0) * 4; | |
| return { bytes: packed + scales, files: 2 }; | |
| } | |
| if (linear.q4) { | |
| const qweight = Number(linear.q4.qweight_u32_count || linear.q4.packed_u32_count || 0) * 4; | |
| const scales = Number(linear.q4.scale_count || 0) * 2; | |
| const zeroPoints = Number(linear.q4.zero_point_count || linear.q4.zero_points_u32_count || 0) * 4; | |
| return { bytes: qweight + scales + zeroPoints, files: 3 }; | |
| } | |
| return { bytes: 0, files: 0 }; | |
| } | |
| function estimateQkNormBytes(entry) { | |
| if (!entry) return { bytes: 0, files: 0 }; | |
| const count = Array.isArray(entry.shape) && Number.isFinite(Number(entry.shape[0])) ? Number(entry.shape[0]) : 128; | |
| return { bytes: count * 2 * 2, files: 2 }; | |
| } | |
| function estimateCustomTransformerAssetSet(manifest, request) { | |
| const doubleBlockCount = Math.max(1, Math.min(5, Math.trunc(Number(request.speed.customMaxDoubleBlocks || 5)))); | |
| const singleBlockCount = Math.max(0, Math.min(20, Math.trunc(Number(request.speed.customMaxSingleBlocks || 20)))); | |
| const linearIds = ["img_in", "txt_in", "final_layer.linear"]; | |
| const qkNormIds = []; | |
| for (let block = 0; block < doubleBlockCount; block += 1) { | |
| linearIds.push( | |
| `double_blocks.${block}.img_attn.qkv`, | |
| `double_blocks.${block}.txt_attn.qkv`, | |
| `double_blocks.${block}.img_attn.proj`, | |
| `double_blocks.${block}.txt_attn.proj`, | |
| `double_blocks.${block}.img_mlp.0`, | |
| `double_blocks.${block}.img_mlp.2`, | |
| `double_blocks.${block}.txt_mlp.0`, | |
| `double_blocks.${block}.txt_mlp.2`, | |
| ); | |
| qkNormIds.push(`double_blocks.${block}.img_attn.norm`, `double_blocks.${block}.txt_attn.norm`); | |
| } | |
| for (let block = 0; block < singleBlockCount; block += 1) { | |
| linearIds.push(`single_blocks.${block}.linear1`, `single_blocks.${block}.linear2`); | |
| qkNormIds.push(`single_blocks.${block}.norm`); | |
| } | |
| let bytes = 0; | |
| let files = 0; | |
| for (const id of linearIds) { | |
| const estimate = estimateLinearAssetBytes(fullManifestLinearById(manifest, id)); | |
| bytes += estimate.bytes; | |
| files += estimate.files; | |
| } | |
| for (const id of qkNormIds) { | |
| const estimate = estimateQkNormBytes(fullManifestQkNormById(manifest, id)); | |
| bytes += estimate.bytes; | |
| files += estimate.files; | |
| } | |
| return { bytes, files, doubleBlockCount, singleBlockCount }; | |
| } | |
| async function prepareCustomAssetLoadEstimate(request) { | |
| if (!loadProgressState.visible || !shouldAssetPrepareRequest(request)) return null; | |
| try { | |
| const modelBaseUrl = configuredModelBaseUrl(); | |
| const runtimeBaseUrl = configuredRuntimeBaseUrl(modelBaseUrl); | |
| const customKernelBaseUrl = configuredCustomKernelBaseUrl(modelBaseUrl, runtimeBaseUrl); | |
| const manifestUrl = resolveAssetUrl(customKernelBaseUrl, "full_transformer/manifest.json"); | |
| setLoadPhase("Reading transformer asset manifest"); | |
| const response = await fetch(manifestUrl); | |
| if (!response.ok) throw new Error(`HTTP ${response.status}`); | |
| const manifest = await response.json(); | |
| const estimate = estimateCustomTransformerAssetSet(manifest, request); | |
| if (estimate.bytes > 0) { | |
| setLoadEstimate(estimate.bytes, estimate.files); | |
| setLoadPhase(`Downloading transformer assets (${formatBytesCompact(estimate.bytes)})`); | |
| } | |
| return estimate; | |
| } catch (err) { | |
| console.warn("[flux2] custom asset byte estimate failed", err); | |
| return null; | |
| } | |
| } | |
| function textContextPrepareKeyForRequest(request) { | |
| return JSON.stringify({ | |
| prompt: request.prompt, | |
| backend: request.transformerBackend, | |
| textLimit: request.speed.customTextTokenLimit, | |
| minText: request.speed.customMinTextTokens, | |
| projectionUrl: staticTextProjectionUrlForPrompt(request.prompt, request.speed.customMinTextTokens || 64), | |
| }); | |
| } | |
| function shouldTextContextPrepareRequest(request) { | |
| if (urlFlag("noAutowarm") || urlFlag("noTextWarm")) return false; | |
| if (shouldBypassSourceOnly(request)) return false; | |
| return request.transformerBackend === "custom-lowbit-webgpu"; | |
| } | |
| function startBackgroundTextContextPrepareForCurrentSettings(request = readRequest()) { | |
| if (!canRunRuntimeWork()) return false; | |
| const prepareRequest = backgroundPrepareRequestForRequest(request); | |
| if (!shouldTextContextPrepareRequest(prepareRequest)) return false; | |
| const key = textContextPrepareKeyForRequest(prepareRequest); | |
| if (backgroundTextContextPreparePromise && backgroundTextContextPrepareKey === key) return true; | |
| backgroundTextContextPrepareKey = key; | |
| backgroundTextContextPreparePromise = initEngine() | |
| .then(async ({ engine, init }) => { | |
| if (typeof engine.encodeText !== "function") return null; | |
| setLoadPhase("Preparing text context"); | |
| const seqLen = Number(init.config.text_seq_len || 512); | |
| const tokenized = await tokenizePrompt(prepareRequest.prompt, seqLen); | |
| const params = buildGenerateParams(prepareRequest, tokenized, null); | |
| if (typeof engine.prepareCustomTransformerTextProjection === "function") { | |
| return await engine.prepareCustomTransformerTextProjection(params); | |
| } | |
| return await engine.encodeText(params); | |
| }) | |
| .then((result) => { | |
| if (result?.shape || result?.textEncoder) { | |
| console.log("[flux2] text/projection prepared", result.shape, result.load || result.textEncoder || null); | |
| } | |
| return result; | |
| }) | |
| .catch((err) => { | |
| console.warn("[flux2] text context prepare failed", err); | |
| return null; | |
| }) | |
| .finally(() => { | |
| backgroundTextContextPreparePromise = null; | |
| backgroundTextContextPrepareKey = ""; | |
| }); | |
| return true; | |
| } | |
| function transformerSetupPrepareKeyForRequest(request) { | |
| return JSON.stringify({ | |
| prompt: request.prompt, | |
| width: request.width, | |
| height: request.height, | |
| steps: request.numSteps, | |
| backend: request.transformerBackend, | |
| hasInitImage: request.hasInitImage, | |
| previewRenderMaxSize: request.previewRenderMaxSize, | |
| speed: speedCacheSignature(request.speed), | |
| }); | |
| } | |
| function shouldTransformerSetupPrepareRequest(request) { | |
| if (urlFlag("noAutowarm") || urlFlag("noStageWarm")) return false; | |
| if (request.transformerBackend !== "custom-lowbit-webgpu") return false; | |
| if (shouldBypassSourceOnly(request)) return false; | |
| if (shouldAutoWarmRequest(request)) return false; | |
| return true; | |
| } | |
| function shouldTransformerDispatchWarmupRequest(request) { | |
| if (urlFlag("noDispatchWarm")) return false; | |
| if (urlFlag("dispatchWarm")) return true; | |
| if (request.hasInitImage) return false; | |
| const renderSize = plannedRenderSizeForRequest(request); | |
| const plannedMaxDimension = Math.max(renderSize.width, renderSize.height); | |
| return plannedMaxDimension <= 512; | |
| } | |
| function startBackgroundTransformerSetupPrepareForCurrentSettings(request = readRequest()) { | |
| if (!canRunRuntimeWork()) return false; | |
| const prepareRequest = backgroundPrepareRequestForRequest(request); | |
| if (!shouldTransformerSetupPrepareRequest(prepareRequest)) return false; | |
| const dispatchWarmup = shouldTransformerDispatchWarmupRequest(prepareRequest); | |
| const key = `${transformerSetupPrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`; | |
| if (backgroundTransformerSetupPreparePromise && backgroundTransformerSetupPrepareKey === key) return true; | |
| backgroundTransformerSetupPrepareKey = key; | |
| backgroundTransformerSetupPreparePromise = initEngine() | |
| .then(async ({ engine, init }) => { | |
| if (typeof engine.prepareCustomTransformerStageSetup !== "function") return null; | |
| setLoadPhase(dispatchWarmup ? "Preparing transformer dispatch warmup" : "Preparing transformer setup"); | |
| const seqLen = Number(init.config.text_seq_len || 512); | |
| const tokenized = await tokenizePrompt(prepareRequest.prompt, seqLen); | |
| const plannedTextTokens = plannedCustomTextTokens(prepareRequest, tokenized.attentionMask, seqLen); | |
| return await engine.prepareCustomTransformerStageSetup({ | |
| width: prepareRequest.width, | |
| height: prepareRequest.height, | |
| hasInitImage: prepareRequest.hasInitImage, | |
| dispatchWarmup, | |
| previewRenderMaxSize: prepareRequest.previewRenderMaxSize, | |
| previewRenderInitImage: shouldPreviewRenderInitImage(prepareRequest), | |
| numSteps: prepareRequest.numSteps, | |
| textTokens: plannedTextTokens, | |
| textProjectionUrl: staticTextProjectionUrlForPrompt(prepareRequest.prompt, plannedTextTokens), | |
| ...prepareRequest.speed, | |
| }); | |
| }) | |
| .then((result) => { | |
| if (result?.load) { | |
| console.log(dispatchWarmup ? "[flux2] custom transformer dispatch warmed" : "[flux2] custom transformer stage prepared", result.load, result.shape); | |
| } | |
| return result; | |
| }) | |
| .catch((err) => { | |
| console.warn("[flux2] custom transformer stage prepare failed", err); | |
| return null; | |
| }) | |
| .finally(() => { | |
| backgroundTransformerSetupPreparePromise = null; | |
| backgroundTransformerSetupPrepareKey = ""; | |
| }); | |
| return true; | |
| } | |
| function defaultVaeChunkSizeForDecodeMax(plannedDecodeMax) { | |
| if (plannedDecodeMax >= 768) return 4096; | |
| if (plannedDecodeMax >= 512) return 2048; | |
| return 0; | |
| } | |
| function plannedDecodeMaxForRequest(request) { | |
| return request.previewDecodeMaxSize && Math.max(request.width, request.height) > request.previewDecodeMaxSize | |
| ? request.previewDecodeMaxSize | |
| : Math.max(request.width, request.height); | |
| } | |
| function shouldAutoWarmRequest(request) { | |
| if (urlFlag("autowarm")) return true; | |
| if (urlFlag("noAutowarm")) return false; | |
| if (request.hasInitImage) return false; | |
| if (urlFlag("hiddenWarm")) return true; | |
| const maxDimension = Math.max(request.width, request.height); | |
| if ( | |
| request.transformerBackend === "custom-lowbit-webgpu" && | |
| !request.speed.customApproxReusePredEvery && | |
| maxDimension <= 256 && | |
| (request.speed.requestedProfile === "auto" || request.speed.requestedProfile === "progressive") | |
| ) { | |
| return false; | |
| } | |
| if ( | |
| request.speed.requestedProfile === "subsecond-preview" || | |
| request.speed.requestedProfile === "subsecond-final" || | |
| request.speed.requestedProfile === "progressive" || | |
| request.speed.requestedProfile === "progressive-fast" || | |
| request.speed.requestedProfile === "tail-cache" || | |
| request.speed.requestedProfile === "tail-cache-lite" || | |
| request.speed.requestedProfile === "tail-cache-mid" || | |
| request.speed.requestedProfile === "tail-cache-fast" | |
| ) return false; | |
| return request.transformerBackend === "custom-lowbit-webgpu" && | |
| request.speed.requestedProfile === "auto" && | |
| !request.hasInitImage && | |
| maxDimension <= 256; | |
| } | |
| function startBackgroundAssetPrepareForCurrentSettings(request = readRequest()) { | |
| if (!canRunRuntimeWork()) return false; | |
| if (!shouldAssetPrepareRequest(request)) return false; | |
| const key = assetPrepareKeyForRequest(request); | |
| if (backgroundAssetPreparePromise && backgroundAssetPrepareKey === key) return true; | |
| backgroundAssetPrepareKey = key; | |
| backgroundAssetPreparePromise = initEngine() | |
| .then(({ engine }) => { | |
| if (typeof engine.prepareCustomTransformerAssets !== "function") return null; | |
| setLoadPhase("Downloading transformer assets"); | |
| return engine.prepareCustomTransformerAssets({ | |
| maxDoubleBlocks: request.speed.customMaxDoubleBlocks || 5, | |
| maxSingleBlocks: request.speed.customMaxSingleBlocks || 20, | |
| }); | |
| }) | |
| .then((result) => { | |
| if (result?.summary) { | |
| console.log("[flux2] custom transformer assets prepared", result.summary); | |
| } | |
| return result; | |
| }) | |
| .catch((err) => { | |
| console.warn("[flux2] custom transformer asset prepare failed", err); | |
| return null; | |
| }) | |
| .finally(() => { | |
| backgroundAssetPreparePromise = null; | |
| backgroundAssetPrepareKey = ""; | |
| }); | |
| return true; | |
| } | |
| function vaePrepareKeyForRequest(request) { | |
| const plannedDecodeMax = plannedDecodeMaxForRequest(request); | |
| return JSON.stringify({ | |
| width: request.width, | |
| height: request.height, | |
| hasInitImage: request.hasInitImage, | |
| previewRenderMaxSize: request.previewRenderMaxSize, | |
| previewDecodeMaxSize: request.previewDecodeMaxSize, | |
| vaeChunk: Number(urlDefaults.get("vaeChunk") || 0) || defaultVaeChunkSizeForDecodeMax(plannedDecodeMax), | |
| maxAutoVaeChunkSeq: Number(urlDefaults.get("maxAutoVaeChunkSeq") || 0), | |
| }); | |
| } | |
| function vaeEncoderPrepareKeyForRequest(request) { | |
| const renderSize = plannedRenderSizeForRequest(request); | |
| return JSON.stringify({ | |
| width: renderSize.width, | |
| height: renderSize.height, | |
| fitMode: request.fitMode, | |
| initImageKey: currentInitImageKey(renderSize.width, renderSize.height, request.fitMode), | |
| previewRenderInitImage: shouldPreviewRenderInitImage(request), | |
| }); | |
| } | |
| function shouldVaeEncoderPrepareRequest(request) { | |
| if (urlFlag("noAutowarm") || urlFlag("noVaeEncoderWarm")) return false; | |
| if (!request.hasInitImage) return false; | |
| if (shouldBypassSourceOnly(request)) return false; | |
| return true; | |
| } | |
| function startBackgroundVaeEncoderPrepareForCurrentSettings(request = readRequest()) { | |
| if (!canRunRuntimeWork()) return false; | |
| if (!shouldVaeEncoderPrepareRequest(request)) return false; | |
| const key = vaeEncoderPrepareKeyForRequest(request); | |
| if (backgroundVaeEncoderPreparePromise && backgroundVaeEncoderPrepareKey === key) return true; | |
| backgroundVaeEncoderPrepareKey = key; | |
| backgroundVaeEncoderPreparePromise = initEngine() | |
| .then(async ({ engine }) => { | |
| if (typeof engine.prepareVaeEncoderLatent !== "function") return null; | |
| setLoadPhase("Preparing source image latent"); | |
| const renderSize = plannedRenderSizeForRequest(request); | |
| const initImage = await prepareInitImage(renderSize.width, renderSize.height, request.fitMode); | |
| if (!initImage) return null; | |
| return await engine.prepareVaeEncoderLatent({ | |
| width: renderSize.width, | |
| height: renderSize.height, | |
| initImage: initImage.data, | |
| initImageCacheKey: initImage.cacheKey || request.initImageKey, | |
| cacheVaeEncoder: !urlFlag("noVaeEncoderCache"), | |
| }); | |
| }) | |
| .then((result) => { | |
| if (result?.verdict) { | |
| console.log("[flux2] VAE encoder latent prepared", result); | |
| } | |
| return result; | |
| }) | |
| .catch((err) => { | |
| console.warn("[flux2] VAE encoder latent prepare failed", err); | |
| return null; | |
| }) | |
| .finally(() => { | |
| backgroundVaeEncoderPreparePromise = null; | |
| backgroundVaeEncoderPrepareKey = ""; | |
| }); | |
| return true; | |
| } | |
| function shouldVaePrepareRequest(request) { | |
| if (urlFlag("noAutowarm") || urlFlag("noVaeWarm")) return false; | |
| if (shouldAutoWarmRequest(request)) return false; | |
| return true; | |
| } | |
| function shouldVaeDispatchWarmupRequest(request) { | |
| if (urlFlag("noVaeDispatchWarm")) return false; | |
| if (urlFlag("vaeDispatchWarm") || urlFlag("dispatchWarm")) return true; | |
| if (request.hasInitImage) return false; | |
| return plannedDecodeMaxForRequest(request) <= 256; | |
| } | |
| async function waitForStartedPreparePromises() { | |
| const textPromise = backgroundTextContextPreparePromise; | |
| const transformerPromise = backgroundTransformerSetupPreparePromise; | |
| const vaeEncoderPromise = backgroundVaeEncoderPreparePromise; | |
| const vaePromise = backgroundVaePreparePromise; | |
| await Promise.all([textPromise, transformerPromise, vaeEncoderPromise, vaePromise].filter(Boolean).map(async (promise) => { | |
| try { | |
| await promise; | |
| } catch { | |
| // Generation can still build the normal path if eager preparation fails. | |
| } | |
| })); | |
| } | |
| function startStageAndVaePrepareForRequest(request) { | |
| if (!canRunRuntimeWork()) return false; | |
| const startedText = startBackgroundTextContextPrepareForCurrentSettings(request); | |
| const startedTransformer = startBackgroundTransformerSetupPrepareForCurrentSettings(request); | |
| if (request.hasInitImage && shouldVaeEncoderPrepareRequest(request)) { | |
| const startedVaeEncoder = startBackgroundVaeEncoderPrepareForCurrentSettings(request); | |
| return startedText || startedTransformer || startedVaeEncoder; | |
| } | |
| const startedVae = startBackgroundVaePrepareForCurrentSettings(request); | |
| return startedText || startedTransformer || startedVae; | |
| } | |
| function startBackgroundVaePrepareForCurrentSettings(request = readRequest()) { | |
| if (!canRunRuntimeWork()) return false; | |
| const prepareRequest = backgroundPrepareRequestForRequest(request); | |
| if (!shouldVaePrepareRequest(prepareRequest)) return false; | |
| const dispatchWarmup = shouldVaeDispatchWarmupRequest(prepareRequest); | |
| const key = `${vaePrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`; | |
| if (backgroundVaePreparePromise && backgroundVaePrepareKey === key) return true; | |
| backgroundVaePrepareKey = key; | |
| const plannedDecodeMax = plannedDecodeMaxForRequest(prepareRequest); | |
| backgroundVaePreparePromise = initEngine() | |
| .then(({ engine }) => { | |
| if (typeof engine.prepareVaeSessions !== "function") return null; | |
| setLoadPhase(dispatchWarmup ? "Preparing VAE dispatch warmup" : "Preparing VAE sessions"); | |
| return engine.prepareVaeSessions({ | |
| width: prepareRequest.width, | |
| height: prepareRequest.height, | |
| hasInitImage: prepareRequest.hasInitImage, | |
| includeEncoder: prepareRequest.hasInitImage && !shouldVaeEncoderPrepareRequest(prepareRequest), | |
| dispatchWarmup, | |
| previewRenderMaxSize: prepareRequest.previewRenderMaxSize, | |
| previewRenderInitImage: shouldPreviewRenderInitImage(prepareRequest), | |
| previewDecodeMaxSize: prepareRequest.previewDecodeMaxSize, | |
| vaeAttentionChunkSize: Number(urlDefaults.get("vaeChunk") || 0) || defaultVaeChunkSizeForDecodeMax(plannedDecodeMax), | |
| maxAutoVaeAttentionChunkSeq: Number(urlDefaults.get("maxAutoVaeChunkSeq") || 0), | |
| }); | |
| }) | |
| .then((result) => { | |
| if (result?.decoder) { | |
| console.log(dispatchWarmup ? "[flux2] VAE dispatch warmed" : "[flux2] VAE sessions prepared", result); | |
| } | |
| return result; | |
| }) | |
| .catch((err) => { | |
| console.warn("[flux2] VAE session prepare failed", err); | |
| return null; | |
| }) | |
| .finally(() => { | |
| backgroundVaePreparePromise = null; | |
| backgroundVaePrepareKey = ""; | |
| }); | |
| return true; | |
| } | |
| function clearWarmSchedule() { | |
| if (warmScheduleTimer) { | |
| clearTimeout(warmScheduleTimer); | |
| warmScheduleTimer = 0; | |
| } | |
| if (backgroundDeepPrepareTimer) { | |
| clearTimeout(backgroundDeepPrepareTimer); | |
| backgroundDeepPrepareTimer = 0; | |
| } | |
| backgroundDeepPrepareToken += 1; | |
| } | |
| function cancelBackgroundWarm(message = "") { | |
| backgroundWarmResult = null; | |
| backgroundWarmResultKey = ""; | |
| if (!backgroundWarmPromise) return false; | |
| backgroundWarmKey = ""; | |
| window.flux2Engine?.abortGeneration?.(); | |
| if (message && !foregroundBusy) setStatus(message); | |
| return true; | |
| } | |
| function startBackgroundWarmForCurrentSettings() { | |
| if (!canRunRuntimeWork()) return false; | |
| if (foregroundBusy) return false; | |
| const request = readRequest(); | |
| if (!shouldAutoWarmRequest(request)) return false; | |
| const key = warmKeyForRequest(request); | |
| const resultKey = warmResultKeyForRequest(request); | |
| if (backgroundWarmResult?.key === key) { | |
| if (backgroundWarmResult.resultKey === resultKey) return true; | |
| backgroundWarmResult = null; | |
| } | |
| if (backgroundWarmPromise) { | |
| if (backgroundWarmKey === key && backgroundWarmResultKey === resultKey) return true; | |
| cancelBackgroundWarm(); | |
| } | |
| backgroundWarmKey = key; | |
| backgroundWarmResultKey = resultKey; | |
| backgroundWarmResult = null; | |
| const warmPromise = warmCurrentSettings({ background: true }).then((warmResult) => { | |
| if (warmResult?.canReuseResult && backgroundWarmKey === warmResult.key && backgroundWarmResultKey === warmResult.resultKey) { | |
| backgroundWarmResult = warmResult; | |
| } | |
| return warmResult; | |
| }).catch((err) => { | |
| if (!foregroundBusy && !isAbortError(err)) setStatus(err?.stack || err?.message || String(err), true); | |
| }).finally(() => { | |
| if (backgroundWarmPromise !== warmPromise) return; | |
| backgroundWarmPromise = null; | |
| backgroundWarmKey = ""; | |
| backgroundWarmResultKey = ""; | |
| if (!foregroundBusy) { | |
| runButton.disabled = false; | |
| setButtonDisabled(warmButton, false); | |
| abortButton.disabled = true; | |
| } | |
| }); | |
| backgroundWarmPromise = warmPromise; | |
| return true; | |
| } | |
| function scheduleBackgroundStageAndVaePrepareForCurrentSettings(request = readRequest(), delayMs = 750) { | |
| if (!canRunRuntimeWork()) return false; | |
| if (foregroundBusy || urlFlag("noAutowarm")) return false; | |
| if (backgroundDeepPrepareTimer) { | |
| clearTimeout(backgroundDeepPrepareTimer); | |
| backgroundDeepPrepareTimer = 0; | |
| } | |
| const token = ++backgroundDeepPrepareToken; | |
| const expectedWarmKey = warmKeyForRequest(request); | |
| const expectedAssetKey = assetPrepareKeyForRequest(request); | |
| backgroundDeepPrepareTimer = setTimeout(async () => { | |
| backgroundDeepPrepareTimer = 0; | |
| if (foregroundBusy || token !== backgroundDeepPrepareToken) return; | |
| const assetPromise = backgroundAssetPreparePromise; | |
| if (assetPromise && backgroundAssetPrepareKey === expectedAssetKey) { | |
| try { | |
| await assetPromise; | |
| } catch { | |
| // Stage preparation can still use the normal generation path if asset warm failed. | |
| } | |
| } | |
| if (foregroundBusy || token !== backgroundDeepPrepareToken) return; | |
| const currentRequest = readRequest(); | |
| if (warmKeyForRequest(currentRequest) !== expectedWarmKey) return; | |
| startStageAndVaePrepareForRequest(currentRequest); | |
| await waitForStartedPreparePromises(); | |
| if (foregroundBusy || token !== backgroundDeepPrepareToken) return; | |
| if (warmKeyForRequest(readRequest()) !== expectedWarmKey) return; | |
| startBackgroundVaePrepareForCurrentSettings(currentRequest); | |
| if (foregroundBusy || token !== backgroundDeepPrepareToken) return; | |
| if (warmKeyForRequest(readRequest()) !== expectedWarmKey) return; | |
| const finalPrepareRequest = progressiveFinalPrepareRequestForRequest(currentRequest); | |
| if (finalPrepareRequest) { | |
| startStageAndVaePrepareForRequest(finalPrepareRequest); | |
| } | |
| }, delayMs); | |
| return true; | |
| } | |
| function scheduleAutoWarmForCurrentSettings(delayMs = 350) { | |
| clearWarmSchedule(); | |
| if (!canRunRuntimeWork()) return; | |
| if (foregroundBusy || urlFlag("noAutowarm")) return; | |
| const request = readRequest(); | |
| startBackgroundAssetPrepareForCurrentSettings(request); | |
| scheduleBackgroundStageAndVaePrepareForCurrentSettings(request, request.hasInitImage ? delayMs : 750); | |
| const key = warmKeyForRequest(request); | |
| const resultKey = warmResultKeyForRequest(request); | |
| if (backgroundWarmResult && backgroundWarmResult.key !== key) { | |
| backgroundWarmResult = null; | |
| } | |
| if (backgroundWarmResult && backgroundWarmResult.resultKey !== resultKey) { | |
| backgroundWarmResult = null; | |
| } | |
| if (backgroundWarmPromise) { | |
| if (backgroundWarmKey && (backgroundWarmKey !== key || backgroundWarmResultKey !== resultKey)) { | |
| cancelBackgroundWarm("Settings changed; restarting background warmup"); | |
| } | |
| } | |
| warmScheduleTimer = setTimeout(() => { | |
| warmScheduleTimer = 0; | |
| startBackgroundWarmForCurrentSettings(); | |
| }, delayMs); | |
| } | |
| function takeReusableWarmResult(request) { | |
| const key = warmKeyForRequest(request); | |
| const resultKey = warmResultKeyForRequest(request); | |
| if (!backgroundWarmResult || backgroundWarmResult.key !== key) return null; | |
| if (backgroundWarmResult.resultKey !== resultKey) { | |
| backgroundWarmResult = null; | |
| return null; | |
| } | |
| const result = backgroundWarmResult; | |
| backgroundWarmResult = null; | |
| return result; | |
| } | |
| function generationResultKeyForRequest(request, initImageCacheKey = "") { | |
| const seed = Number.isFinite(Number(request.seed)) ? Math.trunc(Number(request.seed)) : request.seed; | |
| return JSON.stringify({ | |
| prompt: request.prompt, | |
| width: request.width, | |
| height: request.height, | |
| steps: request.numSteps, | |
| seed, | |
| backend: request.transformerBackend, | |
| hasInitImage: request.hasInitImage, | |
| initImageKey: request.hasInitImage ? (initImageCacheKey || request.initImageKey) : undefined, | |
| strength: request.hasInitImage ? request.strength : undefined, | |
| strengthCurve: request.hasInitImage ? request.strengthCurve : undefined, | |
| effectiveStrength: request.hasInitImage | |
| ? Number(effectiveImg2ImgStrength(request.strength, request.strengthCurve).toFixed(6)) | |
| : undefined, | |
| fitMode: request.hasInitImage ? request.fitMode : undefined, | |
| previewRenderMaxSize: request.previewRenderMaxSize, | |
| previewDecodeMaxSize: request.previewDecodeMaxSize, | |
| speed: speedCacheSignature(request.speed), | |
| }); | |
| } | |
| function cloneImageData(imageData) { | |
| if (!imageData) return null; | |
| return new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height); | |
| } | |
| function cloneGenerationResult(result) { | |
| const copy = { | |
| ...result, | |
| timings: result.timings ? { ...result.timings } : {}, | |
| transformerDetails: result.transformerDetails ? { | |
| ...result.transformerDetails, | |
| config: result.transformerDetails.config ? { ...result.transformerDetails.config } : result.transformerDetails.config, | |
| load: result.transformerDetails.load ? { ...result.transformerDetails.load } : result.transformerDetails.load, | |
| summary: result.transformerDetails.summary ? { ...result.transformerDetails.summary } : result.transformerDetails.summary, | |
| } : result.transformerDetails, | |
| customKernels: result.customKernels ? { ...result.customKernels } : result.customKernels, | |
| img2img: result.img2img ? { ...result.img2img } : result.img2img, | |
| previewRender: result.previewRender ? { ...result.previewRender } : result.previewRender, | |
| previewDecode: result.previewDecode ? { ...result.previewDecode } : result.previewDecode, | |
| }; | |
| if (result.imageData) copy.imageData = cloneImageData(result.imageData); | |
| if (result.imageBlob) copy.imageBlob = result.imageBlob; | |
| return copy; | |
| } | |
| function takeCachedGenerationResult(cacheKey) { | |
| if (!cacheKey || urlFlag("noResultCache")) return null; | |
| const entry = generationResultCache.get(cacheKey); | |
| if (!entry) return null; | |
| generationResultCache.delete(cacheKey); | |
| generationResultCache.set(cacheKey, entry); | |
| const result = cloneGenerationResult(entry.result); | |
| result.resultCacheSource = "memory"; | |
| const originalTimings = entry.result.timings || {}; | |
| result.timings = { | |
| ...(result.timings || {}), | |
| cachedOriginalBrowserSeconds: originalTimings.totalBrowserSeconds, | |
| cachedOriginalTransformerSeconds: originalTimings.transformerSeconds, | |
| cachedOriginalVaeSeconds: originalTimings.vaeSeconds, | |
| totalBrowserSeconds: 0, | |
| transformerSeconds: 0, | |
| textEncoderSeconds: 0, | |
| textEncoderCacheSource: "skipped-result-cache", | |
| textEncoderCacheSeconds: 0, | |
| vaeEncoderSeconds: result.img2img ? 0 : undefined, | |
| vaeEncoderCacheSource: result.img2img ? "skipped-result-cache" : undefined, | |
| vaeSeconds: 0, | |
| }; | |
| result.transformerDetails = null; | |
| return { result, seqLen: entry.seqLen }; | |
| } | |
| function rememberGenerationResult(cacheKey, result, seqLen) { | |
| if (!cacheKey || urlFlag("noResultCache") || !result?.imageData) return; | |
| generationResultCache.set(cacheKey, { | |
| result: cloneGenerationResult(result), | |
| seqLen, | |
| savedAt: performance.now(), | |
| }); | |
| const maxEntries = Math.max(1, Math.trunc(Number(urlDefaults.get("resultCacheEntries") || 4))); | |
| while (generationResultCache.size > maxEntries) { | |
| const oldestKey = generationResultCache.keys().next().value; | |
| generationResultCache.delete(oldestKey); | |
| } | |
| } | |
| function loadImageElement(file) { | |
| return new Promise((resolve, reject) => { | |
| const url = URL.createObjectURL(file); | |
| const image = new Image(); | |
| image.onload = () => resolve({ image, url }); | |
| image.onerror = () => { | |
| URL.revokeObjectURL(url); | |
| reject(new Error("Could not decode input image")); | |
| }; | |
| image.src = url; | |
| }); | |
| } | |
| function drawImageToCanvas(image, width, height, fitMode) { | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = width; | |
| canvas.height = height; | |
| const context = canvas.getContext("2d", { willReadFrequently: true }); | |
| context.fillStyle = "#ffffff"; | |
| context.fillRect(0, 0, width, height); | |
| if (fitMode === "stretch") { | |
| context.drawImage(image, 0, 0, width, height); | |
| return canvas; | |
| } | |
| const scale = fitMode === "contain" | |
| ? Math.min(width / image.naturalWidth, height / image.naturalHeight) | |
| : Math.max(width / image.naturalWidth, height / image.naturalHeight); | |
| const drawWidth = image.naturalWidth * scale; | |
| const drawHeight = image.naturalHeight * scale; | |
| const dx = (width - drawWidth) / 2; | |
| const dy = (height - drawHeight) / 2; | |
| context.drawImage(image, dx, dy, drawWidth, drawHeight); | |
| return canvas; | |
| } | |
| async function prepareInitImage(width, height, fitMode) { | |
| const file = initImageInput.files?.[0]; | |
| if (!file) return null; | |
| const preparedKey = `${width}x${height}|${fitMode}`; | |
| const existingFileCache = preparedInitImageCache.get(file); | |
| if (existingFileCache?.has(preparedKey)) { | |
| const cached = existingFileCache.get(preparedKey); | |
| return { ...cached, sourceCache: "memory" }; | |
| } | |
| const { image, url } = await loadImageElement(file); | |
| URL.revokeObjectURL(url); | |
| const canvas = drawImageToCanvas(image, width, height, fitMode); | |
| const imageData = canvas.getContext("2d", { willReadFrequently: true }).getImageData(0, 0, width, height); | |
| const rgba = imageData.data; | |
| const values = new Float32Array(width * height * 3); | |
| const plane = width * height; | |
| let hash = 2166136261 >>> 0; | |
| for (let pixel = 0; pixel < plane; pixel++) { | |
| const src = pixel * 4; | |
| hash ^= rgba[src]; | |
| hash = Math.imul(hash, 16777619) >>> 0; | |
| hash ^= rgba[src + 1]; | |
| hash = Math.imul(hash, 16777619) >>> 0; | |
| hash ^= rgba[src + 2]; | |
| hash = Math.imul(hash, 16777619) >>> 0; | |
| hash ^= rgba[src + 3]; | |
| hash = Math.imul(hash, 16777619) >>> 0; | |
| values[pixel] = rgba[src] / 127.5 - 1; | |
| values[plane + pixel] = rgba[src + 1] / 127.5 - 1; | |
| values[plane * 2 + pixel] = rgba[src + 2] / 127.5 - 1; | |
| } | |
| const pixelHash = hash.toString(16).padStart(8, "0"); | |
| const prepared = { | |
| data: values, | |
| sourceRgba: new Uint8ClampedArray(rgba), | |
| sourceWidth: image.naturalWidth, | |
| sourceHeight: image.naturalHeight, | |
| sourceCache: "prepared", | |
| cacheKey: [ | |
| file.name, | |
| file.size, | |
| file.lastModified || 0, | |
| `${image.naturalWidth}x${image.naturalHeight}`, | |
| `${width}x${height}`, | |
| fitMode, | |
| pixelHash, | |
| ].join("|"), | |
| }; | |
| const fileCache = existingFileCache || new Map(); | |
| fileCache.set(preparedKey, prepared); | |
| preparedInitImageCache.set(file, fileCache); | |
| return prepared; | |
| } | |
| function shouldBypassSourceOnly(request) { | |
| return request.hasInitImage && | |
| !urlFlag("noSourceBypass") && | |
| effectiveImg2ImgStrength(request.strength, request.strengthCurve) <= 0; | |
| } | |
| function sourceOnlyResult(request, initImage, totalSeconds) { | |
| const effectiveStrength = effectiveImg2ImgStrength(request.strength, request.strengthCurve); | |
| return { | |
| width: request.width, | |
| height: request.height, | |
| imageData: new ImageData(new Uint8ClampedArray(initImage.sourceRgba), request.width, request.height), | |
| imageBytes: request.width * request.height * 4, | |
| mode: "image-to-image", | |
| seed: request.seed, | |
| transformerBackend: request.transformerBackend, | |
| customKernels: { fullImageBackend: "source-bypass" }, | |
| timings: { | |
| totalBrowserSeconds: Number(totalSeconds.toFixed(3)), | |
| customKernelStatusSeconds: 0, | |
| transformerSeconds: 0, | |
| textEncoderSeconds: 0, | |
| textEncoderCacheSource: "skipped-source-only", | |
| textEncoderCacheSeconds: 0, | |
| vaeEncoderSeconds: 0, | |
| vaeEncoderCacheSource: "source-bypass", | |
| vaeSeconds: 0, | |
| }, | |
| img2img: { | |
| strength: request.strength, | |
| effectiveStrength: Number(effectiveStrength.toFixed(6)), | |
| curve: request.strengthCurve, | |
| startT: 0, | |
| denoiseSteps: 0, | |
| requestedSteps: request.numSteps, | |
| stepPolicy: request.speed.img2imgStepPolicy || "full", | |
| }, | |
| }; | |
| } | |
| function shouldPreviewRenderInitImage(request) { | |
| if (!request?.hasInitImage) return false; | |
| if (urlFlag("noImg2ImgPreviewRender")) return false; | |
| if (urlFlag("img2imgPreviewRender") || urlFlag("img2imgPreview")) return true; | |
| const requestedMax = Math.max(0, Math.trunc(Number(request.previewRenderMaxSize || 0))); | |
| return Boolean(requestedMax && Math.max(request.width, request.height) > requestedMax); | |
| } | |
| function shouldPreviewRenderRequest(request) { | |
| return !request?.hasInitImage || shouldPreviewRenderInitImage(request); | |
| } | |
| function plannedRenderSize(width, height, maxSize, enabled = true) { | |
| const requestedMax = Math.max(0, Math.trunc(Number(maxSize || 0))); | |
| if (!enabled || !requestedMax || Math.max(width, height) <= requestedMax) { | |
| return { width, height }; | |
| } | |
| const scale = requestedMax / Math.max(width, height); | |
| return { | |
| width: Math.max(16, Math.floor((width * scale) / 16) * 16), | |
| height: Math.max(16, Math.floor((height * scale) / 16) * 16), | |
| }; | |
| } | |
| function plannedRenderSizeForRequest(request) { | |
| return plannedRenderSize( | |
| request.width, | |
| request.height, | |
| request.previewRenderMaxSize, | |
| shouldPreviewRenderRequest(request), | |
| ); | |
| } | |
| function roundUpToMultiple(value, multiple) { | |
| const safeMultiple = Math.max(1, Math.trunc(Number(multiple || 1))); | |
| return Math.ceil(Number(value || 0) / safeMultiple) * safeMultiple; | |
| } | |
| function attentionMaskActiveTokenCount(values, seqLen) { | |
| if (!values || values.length < seqLen) return seqLen; | |
| for (let index = seqLen - 1; index >= 0; index--) { | |
| if (Number(values[index]) !== 0) return index + 1; | |
| } | |
| return 1; | |
| } | |
| function plannedCustomTextTokens(request, attentionMask, seqLen) { | |
| const requestedLimit = Math.max(0, Math.trunc(Number(request.speed.customTextTokenLimit || 0))); | |
| if (!requestedLimit) return seqLen; | |
| const minTokens = Math.max(1, Math.trunc(Number(request.speed.customMinTextTokens || 64))); | |
| const activeTokens = attentionMaskActiveTokenCount(attentionMask, seqLen); | |
| return Math.min(seqLen, Math.max(minTokens, roundUpToMultiple(Math.min(activeTokens, requestedLimit), 16))); | |
| } | |
| function readRequest() { | |
| const width = Number($("width").value); | |
| const height = Number($("height").value); | |
| const fitMode = $("fit-mode").value; | |
| const requestedSeed = Number($("seed").value); | |
| const seed = Number.isFinite(requestedSeed) && requestedSeed >= 0 | |
| ? Math.trunc(requestedSeed) | |
| : Math.floor(Math.random() * 0x7fffffff); | |
| const speed = selectedSpeedConfig(width, height); | |
| const selectedPreviewRenderMaxSize = Number($("render-max").value) || 0; | |
| const selectedPreviewDecodeMaxSize = Number($("decode-max").value) || 0; | |
| return { | |
| prompt: $("prompt").value, | |
| width, | |
| height, | |
| seed, | |
| requestedSeed, | |
| numSteps: Number($("steps").value), | |
| transformerBackend: $("backend").value, | |
| hasInitImage: Boolean(initImageInput.files?.[0]), | |
| initImageKey: currentInitImageKey(width, height, fitMode), | |
| strength: Number(strengthEl.value), | |
| strengthCurve: strengthCurveEl.value, | |
| fitMode, | |
| previewRenderMaxSize: selectedPreviewRenderMaxSize || speed.autoPreviewRenderMaxSize || 0, | |
| previewDecodeMaxSize: selectedPreviewDecodeMaxSize || speed.autoPreviewDecodeMaxSize || 0, | |
| speed, | |
| }; | |
| } | |
| function buildGenerateParams(request, tokenized, initImage) { | |
| const seqLen = tokenized.inputIds?.length || 512; | |
| const plannedTextTokens = plannedCustomTextTokens(request, tokenized.attentionMask, seqLen); | |
| const plannedDecodeMax = plannedDecodeMaxForRequest(request); | |
| const defaultVaeChunkSize = defaultVaeChunkSizeForDecodeMax(plannedDecodeMax); | |
| return { | |
| inputIds: tokenized.inputIds, | |
| attentionMask: tokenized.attentionMask, | |
| width: request.width, | |
| height: request.height, | |
| seed: request.seed, | |
| numSteps: request.numSteps, | |
| cacheSessions: true, | |
| gpuDenoise: true, | |
| cacheTextContext: true, | |
| persistentTextContextCache: !urlFlag("noPromptCache"), | |
| promptCacheKey: tokenized.promptCacheKey, | |
| textContextUrl: staticTextContextUrlForPrompt(request.prompt), | |
| textProjectionUrl: staticTextProjectionUrlForPrompt(request.prompt, plannedTextTokens), | |
| transformerBackend: request.transformerBackend, | |
| requireCustomTransformer: request.transformerBackend === "custom-lowbit-webgpu", | |
| returnImageData: true, | |
| returnImageBlob: false, | |
| gpuVaeHandoff: urlFlag("gpuVaeHandoff"), | |
| maxGpuHandoffSeq: Number(urlDefaults.get("maxGpuHandoffSeq") || 4096), | |
| vaeAttentionChunkSize: Number(urlDefaults.get("vaeChunk") || 0) || defaultVaeChunkSize, | |
| maxAutoVaeAttentionChunkSeq: Number(urlDefaults.get("maxAutoVaeChunkSeq") || 0), | |
| previewRenderMaxSize: request.previewRenderMaxSize, | |
| previewRenderInitImage: shouldPreviewRenderInitImage(request), | |
| previewDecodeMaxSize: request.previewDecodeMaxSize, | |
| ...(initImage ? { | |
| initImage: initImage.data, | |
| initImageCacheKey: initImage.cacheKey || request.initImageKey, | |
| cacheVaeEncoder: !urlFlag("noVaeEncoderCache"), | |
| strength: request.strength, | |
| strengthCurve: request.strengthCurve, | |
| } : {}), | |
| ...request.speed, | |
| }; | |
| } | |
| async function prepareGenerationInputs(request, init) { | |
| const seqLen = Number(init.config.text_seq_len || 512); | |
| const renderSize = plannedRenderSizeForRequest(request); | |
| const [tokenized, initImage] = await Promise.all([ | |
| tokenizePrompt(request.prompt, seqLen), | |
| request.hasInitImage ? prepareInitImage(renderSize.width, renderSize.height, request.fitMode) : Promise.resolve(null), | |
| ]); | |
| const imageTokens = Math.ceil(renderSize.width / 16) * Math.ceil(renderSize.height / 16); | |
| const textTokens = plannedCustomTextTokens(request, tokenized.attentionMask, seqLen); | |
| if (request.transformerBackend === "custom-lowbit-webgpu" && imageTokens + textTokens > 4608) { | |
| throw new Error(`Custom WebGPU backend supports up to 4608 joint tokens; requested ${imageTokens + textTokens}`); | |
| } | |
| return { tokenized, initImage, seqLen }; | |
| } | |
| function shouldUseCssPreviewUpscale(result, imageDataWidth, imageDataHeight) { | |
| if (!urlFlag("cssPreviewUpscale")) return false; | |
| if (urlFlag("backingUpscale") || urlFlag("noCssPreviewUpscale")) return false; | |
| if (imageDataWidth === result.width && imageDataHeight === result.height) return false; | |
| return Boolean(result.previewRender || result.previewDecode); | |
| } | |
| function displayGenerationResult(result, elapsed, seqLen, heading = "Elapsed", extraLines = [], request = null, options = {}) { | |
| if (currentImageUrl) URL.revokeObjectURL(currentImageUrl); | |
| currentImageUrl = null; | |
| if (result.imageData) { | |
| const imageDataWidth = result.imageDataWidth || result.width; | |
| const imageDataHeight = result.imageDataHeight || result.height; | |
| const cssPreviewUpscale = shouldUseCssPreviewUpscale(result, imageDataWidth, imageDataHeight); | |
| canvasEl.width = cssPreviewUpscale ? imageDataWidth : result.width; | |
| canvasEl.height = cssPreviewUpscale ? imageDataHeight : result.height; | |
| canvasEl.style.width = cssPreviewUpscale ? `${result.width}px` : ""; | |
| canvasEl.style.height = cssPreviewUpscale ? `${result.height}px` : ""; | |
| const context = canvasEl.getContext("2d"); | |
| if (cssPreviewUpscale || (imageDataWidth === result.width && imageDataHeight === result.height)) { | |
| context.putImageData(result.imageData, 0, 0); | |
| } else { | |
| const tempCanvas = document.createElement("canvas"); | |
| tempCanvas.width = imageDataWidth; | |
| tempCanvas.height = imageDataHeight; | |
| tempCanvas.getContext("2d").putImageData(result.imageData, 0, 0); | |
| context.imageSmoothingEnabled = true; | |
| context.imageSmoothingQuality = "high"; | |
| context.clearRect(0, 0, result.width, result.height); | |
| context.drawImage(tempCanvas, 0, 0, result.width, result.height); | |
| } | |
| canvasEl.hidden = false; | |
| imageEl.hidden = true; | |
| } else if (result.imageBlob) { | |
| currentImageUrl = URL.createObjectURL(result.imageBlob); | |
| imageEl.src = currentImageUrl; | |
| imageEl.hidden = false; | |
| canvasEl.hidden = true; | |
| } else { | |
| throw new Error("Generation did not return image pixels"); | |
| } | |
| placeholderEl.hidden = true; | |
| const timings = result.timings || {}; | |
| const transformerConfig = result.transformerDetails?.config || {}; | |
| const transformerLoad = result.transformerDetails?.load || {}; | |
| const transformerSummary = result.transformerDetails?.summary || {}; | |
| const phaseProfile = transformerSummary.phase_profile_ms || {}; | |
| setStatus([ | |
| `${heading}: ${elapsed.toFixed(3)}s`, | |
| ...extraLines, | |
| `Mode: ${result.mode || "text-to-image"}`, | |
| seedLineForResult(result, request), | |
| result.resultCacheSource ? `Result cache: ${result.resultCacheSource}${timings.cachedOriginalBrowserSeconds !== undefined ? ` original=${timings.cachedOriginalBrowserSeconds}s` : ""}${timings.cachedOriginalTransformerSeconds !== undefined ? ` transformer=${timings.cachedOriginalTransformerSeconds}s` : ""}${timings.cachedOriginalVaeSeconds !== undefined ? ` vae=${timings.cachedOriginalVaeSeconds}s` : ""}` : "", | |
| `Browser: ${timings.totalBrowserSeconds ?? "?"}s`, | |
| timings.customKernelStatusSeconds ? `Kernel status: ${timings.customKernelStatusSeconds}s` : "", | |
| `Transformer: ${timings.transformerSeconds ?? "?"}s`, | |
| transformerLoad.total_ms !== undefined ? `Transformer assets: ${(transformerLoad.total_ms / 1000).toFixed(3)}s` : "", | |
| transformerLoad.stage_setup_ms !== undefined ? `Transformer stage setup: ${(transformerLoad.stage_setup_ms / 1000).toFixed(3)}s` : "", | |
| transformerSummary.median_step_ms !== undefined ? `Transformer median step: ${(transformerSummary.median_step_ms / 1000).toFixed(3)}s` : "", | |
| transformerConfig.profilePhases ? `Phase time/input: ${((phaseProfile.time_input_ms || 0) / 1000).toFixed(3)}s` : "", | |
| transformerConfig.profilePhases ? `Phase double: ${((phaseProfile.double_blocks_ms || 0) / 1000).toFixed(3)}s` : "", | |
| transformerConfig.profilePhases ? `Phase single: ${((phaseProfile.single_blocks_ms || 0) / 1000).toFixed(3)}s` : "", | |
| transformerConfig.profileSingleParts ? `Phase single linear1: ${((phaseProfile.single_linear1_ms || 0) / 1000).toFixed(3)}s` : "", | |
| transformerConfig.profileSingleParts ? `Phase single attention: ${((phaseProfile.single_attention_ms || 0) / 1000).toFixed(3)}s` : "", | |
| transformerConfig.profileSingleParts ? `Phase single MLP: ${((phaseProfile.single_mlp_ms || 0) / 1000).toFixed(3)}s` : "", | |
| transformerConfig.profilePhases ? `Phase final/update: ${((phaseProfile.final_update_ms || 0) / 1000).toFixed(3)}s` : "", | |
| transformerLoad.readback_ms !== undefined ? `Transformer readback: ${(transformerLoad.readback_ms / 1000).toFixed(3)}s` : "", | |
| transformerLoad.text_projection_init_ms !== undefined ? `Text projection: ${(transformerLoad.text_projection_init_ms / 1000).toFixed(3)}s cache=${transformerLoad.text_projection_cache_source || (transformerLoad.text_projection_cache_hit ? "hit" : "miss")}${transformerLoad.text_projection_persistent_ms ? ` db=${(transformerLoad.text_projection_persistent_ms / 1000).toFixed(3)}s` : ""}` : "", | |
| transformerConfig.textTokens && transformerConfig.textTokens !== seqLen ? `Transformer text tokens: ${transformerConfig.textTokens}/${seqLen}` : "", | |
| transformerConfig.doubleBlockCount !== undefined && transformerConfig.singleBlockCount !== undefined && ( | |
| transformerConfig.doubleBlockCount !== 5 || transformerConfig.singleBlockCount !== 20 | |
| ) ? `Blocks: double ${transformerConfig.doubleBlockCount}/5, single ${transformerConfig.singleBlockCount}/20` : "", | |
| `Text: ${timings.textEncoderSeconds ?? "?"}s cache=${timings.textEncoderCacheSource || "?"}${timings.textEncoderCacheSeconds ? ` db=${timings.textEncoderCacheSeconds}s` : ""}`, | |
| result.img2img ? `VAE Encode: ${timings.vaeEncoderSeconds ?? "?"}s cache=${timings.vaeEncoderCacheSource || "?"}` : "", | |
| `VAE: ${timings.vaeSeconds ?? "?"}s`, | |
| result.img2img ? `Strength: ${result.img2img.strength} effective=${result.img2img.effectiveStrength} curve=${result.img2img.curve} startT=${result.img2img.startT} steps=${result.img2img.denoiseSteps}${result.img2img.requestedSteps && result.img2img.requestedSteps !== result.img2img.denoiseSteps ? `/${result.img2img.requestedSteps}` : ""}${result.img2img.stepPolicy && result.img2img.stepPolicy !== "full" ? ` policy=${result.img2img.stepPolicy}` : ""}` : "", | |
| result.previewRender ? `Preview render: ${result.previewRender.renderWidth}x${result.previewRender.renderHeight} -> ${result.width}x${result.height}` : "", | |
| result.previewDecode ? `Preview decode: ${result.previewDecode.decodeWidth}x${result.previewDecode.decodeHeight} -> ${result.width}x${result.height}` : "", | |
| shouldUseCssPreviewUpscale(result, result.imageDataWidth || result.width, result.imageDataHeight || result.height) ? `Canvas backing: ${result.imageDataWidth || result.width}x${result.imageDataHeight || result.height} CSS-upscaled` : "", | |
| timings.imageTensorToRgbaSeconds !== undefined ? `Image convert: ${timings.imageTensorToRgbaSeconds}s` : "", | |
| timings.imageDataSeconds !== undefined ? `ImageData: ${timings.imageDataSeconds}s` : "", | |
| transformerConfig.approxReusePredEvery ? `Approx reused steps: ${transformerConfig.approxReusedSteps}/${transformerConfig.steps}` : "", | |
| transformerConfig.approxReusePredEvery && transformerConfig.approxPredictionMode !== "raw" ? `Approx mode: ${transformerConfig.approxPredictionMode} scale=${transformerConfig.approxAbScale}` : "", | |
| transformerConfig.approxCollapsedUpdateDispatches ? `Collapsed update dispatches: ${transformerConfig.approxCollapsedUpdateDispatches}` : "", | |
| transformerConfig.skippedSingleBlocks ? `Skipped single blocks: ${transformerConfig.skippedSingleBlocks}` : "", | |
| transformerConfig.reusedSingleTailBlocks ? `Reused single tail blocks: ${transformerConfig.reusedSingleTailBlocks}` : "", | |
| transformerConfig.stepSubmitFusion ? "Step submit fusion: on" : "", | |
| transformerConfig.skippedOptionalSetupBytes ? `Skipped optional setup: ${formatMiB(transformerConfig.skippedOptionalSetupBytes)}${transformerConfig.skippedOptionalPipelines ? `, ${transformerConfig.skippedOptionalPipelines} pipelines` : ""}` : "", | |
| transformerConfig.linearRowBlock && transformerConfig.linearRowBlock !== 32 ? `Linear row block: ${transformerConfig.linearRowBlock}` : "", | |
| transformerConfig.singleAttentionKernel && transformerConfig.singleAttentionKernel !== "tiled" ? `Single attention kernel: ${transformerConfig.singleAttentionKernel}` : "", | |
| transformerConfig.singleComputePass ? "Compute pass mode: single" : "", | |
| transformerConfig.singleQkNormStorage && transformerConfig.singleQkNormStorage !== "f32" ? `Q/K norm storage: ${transformerConfig.singleQkNormStorage}` : "", | |
| `Backend: ${result.transformerBackend}`, | |
| `Full image backend: ${result.customKernels?.fullImageBackend || "unknown"}`, | |
| `Image: ${result.imageBytes ?? result.pngBytes ?? 0} bytes${result.pngBytes ? " PNG" : " RGBA"}`, | |
| ]); | |
| if (options.addToGallery !== false && request) { | |
| rememberGalleryResult(result, elapsed, request, heading); | |
| } | |
| } | |
| async function generate() { | |
| if (!bootReady) throw new Error("Model assets are not loaded yet."); | |
| const requestedStarted = performance.now(); | |
| const request = readRequest(); | |
| runButton.disabled = true; | |
| setButtonDisabled(warmButton, true); | |
| abortButton.disabled = false; | |
| imageEl.hidden = true; | |
| canvasEl.hidden = true; | |
| placeholderEl.hidden = false; | |
| placeholderEl.textContent = "Running"; | |
| setStatus([ | |
| request.hasInitImage ? "Preparing image" : "Tokenizing", | |
| seedLineForRequest(request), | |
| `Backend: ${request.transformerBackend}`, | |
| ]); | |
| if (shouldBypassSourceOnly(request)) { | |
| cancelBackgroundWarm(); | |
| const prepareStarted = performance.now(); | |
| const initImage = await prepareInitImage(request.width, request.height, request.fitMode); | |
| const prepareSeconds = (performance.now() - prepareStarted) / 1000; | |
| const totalSeconds = (performance.now() - requestedStarted) / 1000; | |
| displayGenerationResult(sourceOnlyResult(request, initImage, totalSeconds), totalSeconds, 512, "Elapsed", [ | |
| `Prepare: ${prepareSeconds.toFixed(3)}s${initImage?.sourceCache ? ` source=${initImage.sourceCache}` : ""}`, | |
| "Source-only bypass: no text encoder, transformer, VAE encoder, or VAE decoder", | |
| ], request); | |
| return; | |
| } | |
| const earlyResultCacheKey = !request.hasInitImage ? generationResultKeyForRequest(request) : ""; | |
| const earlyCachedResult = takeCachedGenerationResult(earlyResultCacheKey); | |
| if (earlyCachedResult) { | |
| cancelBackgroundWarm(); | |
| const elapsed = (performance.now() - requestedStarted) / 1000; | |
| displayGenerationResult(earlyCachedResult.result, elapsed, earlyCachedResult.seqLen, "Ready from result cache", [ | |
| `Click-to-image: ${elapsed.toFixed(3)}s`, | |
| "Exact fixed-seed request reused from memory", | |
| ], request); | |
| return; | |
| } | |
| const warmWait = await waitForBackgroundWarm(request); | |
| const warmWaitSeconds = warmWait.seconds || 0; | |
| const reusableWarmResult = warmWait.result || null; | |
| if (reusableWarmResult && !reusableWarmResult.warmProfile && request.speed.requestedProfile !== "progressive") { | |
| const clickTotal = (performance.now() - requestedStarted) / 1000; | |
| rememberGenerationResult(earlyResultCacheKey || generationResultKeyForRequest(request), reusableWarmResult.result, reusableWarmResult.seqLen); | |
| displayGenerationResult(reusableWarmResult.result, reusableWarmResult.elapsed, reusableWarmResult.seqLen, "Ready from warm", [ | |
| warmWaitSeconds ? `Queued warm wait: ${warmWaitSeconds.toFixed(3)}s` : "", | |
| `Click-to-image: ${clickTotal.toFixed(3)}s`, | |
| "Reused hidden warm result", | |
| ], request); | |
| return; | |
| } | |
| const textContextPrepareWaitSeconds = await waitForBackgroundTextContextPrepare(request); | |
| const transformerSetupPrepareWaitSeconds = await waitForBackgroundTransformerSetupPrepare(request); | |
| const vaeEncoderPrepareWaitSeconds = await waitForBackgroundVaeEncoderPrepare(request); | |
| const vaePrepareWaitSeconds = await waitForBackgroundVaePrepare(request); | |
| const hasPrepareWait = Boolean( | |
| warmWaitSeconds || | |
| textContextPrepareWaitSeconds || | |
| transformerSetupPrepareWaitSeconds || | |
| vaeEncoderPrepareWaitSeconds || | |
| vaePrepareWaitSeconds | |
| ); | |
| const { engine, init } = await initEngine(); | |
| const prepareStarted = performance.now(); | |
| const { tokenized, initImage, seqLen } = await prepareGenerationInputs(request, init); | |
| const prepareSeconds = (performance.now() - prepareStarted) / 1000; | |
| const resultCacheKey = request.hasInitImage | |
| ? generationResultKeyForRequest(request, initImage?.cacheKey) | |
| : earlyResultCacheKey; | |
| const cachedAfterPrepare = takeCachedGenerationResult(resultCacheKey); | |
| if (cachedAfterPrepare) { | |
| const elapsed = (performance.now() - requestedStarted) / 1000; | |
| displayGenerationResult(cachedAfterPrepare.result, elapsed, cachedAfterPrepare.seqLen, "Ready from result cache", [ | |
| `Prepare: ${prepareSeconds.toFixed(3)}s${initImage?.sourceCache ? ` source=${initImage.sourceCache}` : ""}`, | |
| `Click-to-image: ${elapsed.toFixed(3)}s`, | |
| "Exact fixed-seed request reused from memory", | |
| ], request); | |
| return; | |
| } | |
| const plannedTextTokens = plannedCustomTextTokens(request, tokenized.attentionMask, seqLen); | |
| setStatus([ | |
| "Generating", | |
| `Mode: ${initImage ? "Image to image" : "Text to image"}`, | |
| seedLineForRequest(request), | |
| `Tokens: ${tokenized.tokenCount}/${seqLen}`, | |
| plannedTextTokens < seqLen ? `Transformer text tokens: ${plannedTextTokens}/${seqLen}` : "", | |
| initImage ? `Strength: ${request.strength.toFixed(2)} effective=${formatStrengthValue(effectiveImg2ImgStrength(request.strength, request.strengthCurve))} curve=${request.strengthCurve}` : "", | |
| initImage && request.speed.img2imgStepPolicy === "adaptive" ? `Img2img steps: adaptive up to ${request.numSteps}` : "", | |
| `Backend: ${request.transformerBackend}`, | |
| `Speed Profile: ${speedProfileLabel(request.speed)}`, | |
| request.speed.customApproxReusePredEvery ? `Approx cache: every ${request.speed.customApproxReusePredEvery} steps` : "", | |
| request.speed.customApproxReusePredEvery && request.speed.customApproxPredictionMode !== "raw" ? `Approx mode: ${request.speed.customApproxPredictionMode}` : "", | |
| request.previewRenderMaxSize ? `${request.speed.requestedProfile === "progressive" ? "Final preview render" : "Preview render"}: ${request.previewRenderMaxSize}px max` : "", | |
| request.previewDecodeMaxSize ? `${request.speed.requestedProfile === "progressive" ? "Final preview decode" : "Preview decode"}: ${request.previewDecodeMaxSize}px max` : "", | |
| ]); | |
| const started = performance.now(); | |
| const shouldRunProgressivePreview = request.speed.requestedProfile === "progressive" && | |
| !initImage && | |
| Math.max(request.width, request.height) > 256; | |
| if (shouldRunProgressivePreview) { | |
| const progressivePreviewProfile = progressivePreviewProfileForRequest(request); | |
| const previewRequest = requestWithSpeedProfile(request, progressivePreviewProfile, { useSelectedCaps: false }); | |
| const previewStarted = performance.now(); | |
| const previewFromWarm = reusableWarmResult?.warmProfile === progressivePreviewProfile; | |
| const previewResult = previewFromWarm | |
| ? reusableWarmResult.result | |
| : await engine.generateImage(buildGenerateParams(previewRequest, tokenized, initImage)); | |
| const previewComputeElapsed = previewFromWarm ? reusableWarmResult.elapsed : (performance.now() - previewStarted) / 1000; | |
| const previewVisibleElapsed = (performance.now() - previewStarted) / 1000; | |
| displayGenerationResult(previewResult, previewFromWarm ? previewVisibleElapsed : previewComputeElapsed, seqLen, "Preview ready", [ | |
| previewFromWarm ? "Reused hidden warm preview" : "", | |
| previewFromWarm ? `Preview warm compute: ${previewComputeElapsed.toFixed(3)}s` : "", | |
| "Refining final image", | |
| ], previewRequest, { addToGallery: false }); | |
| const progressiveFinalProfile = progressiveFinalProfileForRequest(request); | |
| const finalRequest = requestWithSpeedProfile(request, progressiveFinalProfile, { useSelectedCaps: true }); | |
| setStatus([ | |
| `Preview shown: ${previewVisibleElapsed.toFixed(3)}s`, | |
| previewFromWarm ? `Preview warm compute: ${previewComputeElapsed.toFixed(3)}s` : "", | |
| "Refining final image", | |
| seedLineForRequest(request), | |
| `Backend: ${request.transformerBackend}`, | |
| `Final Profile: ${speedProfileLabel(finalRequest.speed)}`, | |
| finalRequest.previewRenderMaxSize ? `Final preview render: ${finalRequest.previewRenderMaxSize}px max` : "", | |
| finalRequest.previewDecodeMaxSize ? `Final preview decode: ${finalRequest.previewDecodeMaxSize}px max` : "", | |
| ]); | |
| const finalResult = await engine.generateImage(buildGenerateParams(finalRequest, tokenized, initImage)); | |
| const totalElapsed = (performance.now() - started) / 1000; | |
| const clickTotal = (performance.now() - requestedStarted) / 1000; | |
| rememberGenerationResult(resultCacheKey, finalResult, seqLen); | |
| displayGenerationResult(finalResult, totalElapsed, seqLen, "Final ready", [ | |
| `Preview first: ${previewVisibleElapsed.toFixed(3)}s`, | |
| previewFromWarm ? `Preview warm compute: ${previewComputeElapsed.toFixed(3)}s` : "", | |
| `Final Profile: ${speedProfileLabel(finalRequest.speed)}`, | |
| `Prepare: ${prepareSeconds.toFixed(3)}s${initImage?.sourceCache ? ` source=${initImage.sourceCache}` : ""}`, | |
| warmWaitSeconds ? `Queued warm wait: ${warmWaitSeconds.toFixed(3)}s` : "", | |
| textContextPrepareWaitSeconds ? `Text prepare wait: ${textContextPrepareWaitSeconds.toFixed(3)}s` : "", | |
| transformerSetupPrepareWaitSeconds ? `Transformer setup prepare wait: ${transformerSetupPrepareWaitSeconds.toFixed(3)}s` : "", | |
| vaeEncoderPrepareWaitSeconds ? `VAE encoder prepare wait: ${vaeEncoderPrepareWaitSeconds.toFixed(3)}s` : "", | |
| vaePrepareWaitSeconds ? `VAE prepare wait: ${vaePrepareWaitSeconds.toFixed(3)}s` : "", | |
| hasPrepareWait ? `Click-to-final: ${clickTotal.toFixed(3)}s` : "", | |
| ], request); | |
| return; | |
| } | |
| const result = await engine.generateImage(buildGenerateParams(request, tokenized, initImage)); | |
| const elapsed = (performance.now() - started) / 1000; | |
| const clickTotal = (performance.now() - requestedStarted) / 1000; | |
| rememberGenerationResult(resultCacheKey, result, seqLen); | |
| displayGenerationResult(result, elapsed, seqLen, "Elapsed", [ | |
| `Prepare: ${prepareSeconds.toFixed(3)}s${initImage?.sourceCache ? ` source=${initImage.sourceCache}` : ""}`, | |
| warmWaitSeconds ? `Queued warm wait: ${warmWaitSeconds.toFixed(3)}s` : "", | |
| textContextPrepareWaitSeconds ? `Text prepare wait: ${textContextPrepareWaitSeconds.toFixed(3)}s` : "", | |
| transformerSetupPrepareWaitSeconds ? `Transformer setup prepare wait: ${transformerSetupPrepareWaitSeconds.toFixed(3)}s` : "", | |
| vaeEncoderPrepareWaitSeconds ? `VAE encoder prepare wait: ${vaeEncoderPrepareWaitSeconds.toFixed(3)}s` : "", | |
| vaePrepareWaitSeconds ? `VAE prepare wait: ${vaePrepareWaitSeconds.toFixed(3)}s` : "", | |
| hasPrepareWait ? `Click-to-image: ${clickTotal.toFixed(3)}s` : "", | |
| ], request); | |
| } | |
| async function waitForBackgroundWarm(request) { | |
| const pending = backgroundWarmPromise; | |
| if (!pending) return { seconds: 0, result: takeReusableWarmResult(request) }; | |
| const started = performance.now(); | |
| const expectedWarmKey = warmKeyForRequest(request); | |
| const expectedResultKey = warmResultKeyForRequest(request); | |
| if (backgroundWarmKey && (backgroundWarmKey !== expectedWarmKey || backgroundWarmResultKey !== expectedResultKey)) { | |
| setStatus([ | |
| "Stopping stale background warmup", | |
| "Current settings changed; generation will run with the new settings", | |
| ]); | |
| window.flux2Engine?.abortGeneration?.(); | |
| try { | |
| await pending; | |
| } catch { | |
| // Stale warmup errors are expected when it is aborted. | |
| } | |
| return { seconds: 0, result: null }; | |
| } | |
| setStatus([ | |
| "Finishing background warmup", | |
| "Generation will start as soon as the warmed WebGPU state is ready", | |
| ]); | |
| try { | |
| await pending; | |
| } catch { | |
| // The foreground generation path will surface any real runtime issue. | |
| } | |
| return { | |
| seconds: (performance.now() - started) / 1000, | |
| result: takeReusableWarmResult(request), | |
| }; | |
| } | |
| async function waitForBackgroundTextContextPrepare(request) { | |
| const pending = backgroundTextContextPreparePromise; | |
| if (!pending) return 0; | |
| const prepareRequest = backgroundPrepareRequestForRequest(request); | |
| const expectedKey = textContextPrepareKeyForRequest(prepareRequest); | |
| if (!backgroundTextContextPrepareKey || backgroundTextContextPrepareKey !== expectedKey) return 0; | |
| const started = performance.now(); | |
| setStatus([ | |
| "Finishing text context preparation", | |
| "Generation will start as soon as the prompt embedding cache is ready", | |
| ]); | |
| try { | |
| await pending; | |
| } catch { | |
| // The visible generation path will run the text encoder normally if preparation failed. | |
| } | |
| return (performance.now() - started) / 1000; | |
| } | |
| async function waitForBackgroundTransformerSetupPrepare(request) { | |
| const pending = backgroundTransformerSetupPreparePromise; | |
| if (!pending) return 0; | |
| const prepareRequest = backgroundPrepareRequestForRequest(request); | |
| const dispatchWarmup = shouldTransformerDispatchWarmupRequest(prepareRequest); | |
| const expectedKey = `${transformerSetupPrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`; | |
| if (!backgroundTransformerSetupPrepareKey || backgroundTransformerSetupPrepareKey !== expectedKey) return 0; | |
| const started = performance.now(); | |
| setStatus([ | |
| "Finishing transformer setup preparation", | |
| "Generation will start as soon as cached custom WebGPU setup is ready", | |
| ]); | |
| try { | |
| await pending; | |
| } catch { | |
| // The visible generation path will build setup normally if preparation failed. | |
| } | |
| return (performance.now() - started) / 1000; | |
| } | |
| async function waitForBackgroundVaeEncoderPrepare(request) { | |
| const pending = backgroundVaeEncoderPreparePromise; | |
| if (!pending) return 0; | |
| const expectedKey = vaeEncoderPrepareKeyForRequest(request); | |
| if (!backgroundVaeEncoderPrepareKey || backgroundVaeEncoderPrepareKey !== expectedKey) return 0; | |
| const started = performance.now(); | |
| setStatus([ | |
| "Finishing VAE encoder preparation", | |
| "Generation will start as soon as the uploaded source latent is cached", | |
| ]); | |
| try { | |
| await pending; | |
| } catch { | |
| // The visible generation path will encode the source normally if preparation failed. | |
| } | |
| return (performance.now() - started) / 1000; | |
| } | |
| async function waitForBackgroundVaePrepare(request) { | |
| const pending = backgroundVaePreparePromise; | |
| if (!pending) return 0; | |
| const prepareRequest = backgroundPrepareRequestForRequest(request); | |
| const dispatchWarmup = shouldVaeDispatchWarmupRequest(prepareRequest); | |
| const expectedKey = `${vaePrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`; | |
| if (!backgroundVaePrepareKey || backgroundVaePrepareKey !== expectedKey) return 0; | |
| const started = performance.now(); | |
| setStatus([ | |
| "Finishing VAE session preparation", | |
| "Generation will start as soon as cached decoder sessions are ready", | |
| ]); | |
| try { | |
| await pending; | |
| } catch { | |
| // The visible generation path will create sessions normally if preparation failed. | |
| } | |
| return (performance.now() - started) / 1000; | |
| } | |
| async function warmCurrentSettings({ background = false } = {}) { | |
| if (!canRunRuntimeWork()) throw new Error("Model assets are not loaded yet."); | |
| const request = readRequest(); | |
| const { warmRequest, warmProfile } = warmPlanForRequest(request); | |
| const { engine, init } = await initEngine(); | |
| const started = performance.now(); | |
| if (!background) { | |
| runButton.disabled = true; | |
| } else { | |
| runButton.disabled = false; | |
| } | |
| setButtonDisabled(warmButton, true); | |
| abortButton.disabled = !background; | |
| if (!background) { | |
| placeholderEl.hidden = false; | |
| placeholderEl.textContent = "Warming"; | |
| } | |
| setStatus([ | |
| background ? "Preparing fast path in background" : "Warming current settings", | |
| `Seed: ${request.seed}`, | |
| `Backend: ${request.transformerBackend}`, | |
| `Speed Profile: ${warmProfile ? "progressive preview warm" : speedProfileLabel(request.speed)}`, | |
| warmRequest.previewRenderMaxSize ? `Preview render: ${warmRequest.previewRenderMaxSize}px max` : "", | |
| warmRequest.previewDecodeMaxSize ? `Preview decode: ${warmRequest.previewDecodeMaxSize}px max` : "", | |
| ]); | |
| const key = warmKeyForRequest(request); | |
| const resultKey = warmResultKeyForRequest(request); | |
| const { tokenized, initImage, seqLen } = await prepareGenerationInputs(warmRequest, init); | |
| const result = await engine.generateImage(buildGenerateParams(warmRequest, tokenized, initImage)); | |
| const elapsed = (performance.now() - started) / 1000; | |
| const timings = result.timings || {}; | |
| const transformerLoad = result.transformerDetails?.load || {}; | |
| if (!background || !foregroundBusy) { | |
| setStatus([ | |
| `Warm complete: ${elapsed.toFixed(3)}s`, | |
| timings.customKernelStatusSeconds ? `Kernel status: ${timings.customKernelStatusSeconds}s` : "", | |
| `Text: ${timings.textEncoderSeconds ?? "?"}s cache=${timings.textEncoderCacheSource || "?"}${timings.textEncoderCacheSeconds ? ` db=${timings.textEncoderCacheSeconds}s` : ""}`, | |
| `Transformer: ${timings.transformerSeconds ?? "?"}s`, | |
| transformerLoad.total_ms !== undefined ? `Transformer assets: ${(transformerLoad.total_ms / 1000).toFixed(3)}s` : "", | |
| transformerLoad.stage_setup_ms !== undefined ? `Transformer stage setup: ${(transformerLoad.stage_setup_ms / 1000).toFixed(3)}s` : "", | |
| transformerLoad.text_projection_init_ms !== undefined ? `Text projection: ${(transformerLoad.text_projection_init_ms / 1000).toFixed(3)}s cache=${transformerLoad.text_projection_cache_source || (transformerLoad.text_projection_cache_hit ? "hit" : "miss")}${transformerLoad.text_projection_persistent_ms ? ` db=${(transformerLoad.text_projection_persistent_ms / 1000).toFixed(3)}s` : ""}` : "", | |
| `VAE: ${timings.vaeSeconds ?? "?"}s`, | |
| result.previewRender ? `Preview render: ${result.previewRender.renderWidth}x${result.previewRender.renderHeight} -> ${result.width}x${result.height}` : "", | |
| result.previewDecode ? `Preview decode: ${result.previewDecode.decodeWidth}x${result.previewDecode.decodeHeight} -> ${result.width}x${result.height}` : "", | |
| ]); | |
| } | |
| if (!foregroundBusy) { | |
| runButton.disabled = false; | |
| setButtonDisabled(warmButton, false); | |
| abortButton.disabled = true; | |
| } | |
| return { key, resultKey, warmProfile, result, elapsed, seqLen, canReuseResult: background && !warmRequest.hasInitImage }; | |
| } | |
| async function prepareInitialFastPath(request) { | |
| if (urlFlag("noInitialPrepare") || urlFlag("noAutowarm")) return false; | |
| const prepareRequest = backgroundPrepareRequestForRequest(request); | |
| const shouldPrepareAssets = shouldAssetPrepareRequest(prepareRequest); | |
| const shouldPrepareTransformer = shouldTransformerSetupPrepareRequest(prepareRequest); | |
| const shouldPrepareVae = shouldVaePrepareRequest(prepareRequest); | |
| if (!shouldPrepareAssets && !shouldPrepareTransformer && !shouldPrepareVae) return false; | |
| setStatus([ | |
| "Preparing fast path", | |
| `Backend: ${prepareRequest.transformerBackend}`, | |
| `Speed Profile: ${speedProfileLabel(prepareRequest.speed)}`, | |
| prepareRequest.previewRenderMaxSize ? `Preview render: ${prepareRequest.previewRenderMaxSize}px max` : "", | |
| prepareRequest.previewDecodeMaxSize ? `Preview decode: ${prepareRequest.previewDecodeMaxSize}px max` : "", | |
| ]); | |
| setLoadPhase("Preparing fast path"); | |
| if (shouldPrepareAssets) await prepareCustomAssetLoadEstimate(prepareRequest); | |
| startBackgroundAssetPrepareForCurrentSettings(request); | |
| const assetPromise = backgroundAssetPreparePromise; | |
| if (assetPromise) { | |
| try { | |
| await assetPromise; | |
| } catch { | |
| // Generation can still build the normal path if eager preparation fails. | |
| } | |
| } | |
| if (foregroundBusy) return true; | |
| const currentRequest = readRequest(); | |
| if (warmKeyForRequest(currentRequest) !== warmKeyForRequest(request)) { | |
| scheduleBackgroundStageAndVaePrepareForCurrentSettings(currentRequest, 0); | |
| return false; | |
| } | |
| if (shouldPrepareTransformer || shouldPrepareVae) startStageAndVaePrepareForRequest(currentRequest); | |
| await waitForStartedPreparePromises(); | |
| if (foregroundBusy) return true; | |
| const finalPrepareRequest = progressiveFinalPrepareRequestForRequest(currentRequest); | |
| if (finalPrepareRequest && warmKeyForRequest(readRequest()) === warmKeyForRequest(request)) { | |
| setStatus([ | |
| "Preparing refinement path", | |
| `Backend: ${finalPrepareRequest.transformerBackend}`, | |
| `Speed Profile: ${speedProfileLabel(finalPrepareRequest.speed)}`, | |
| finalPrepareRequest.previewRenderMaxSize ? `Preview render: ${finalPrepareRequest.previewRenderMaxSize}px max` : "", | |
| finalPrepareRequest.previewDecodeMaxSize ? `Preview decode: ${finalPrepareRequest.previewDecodeMaxSize}px max` : "", | |
| ]); | |
| setLoadPhase("Preparing refinement path"); | |
| startStageAndVaePrepareForRequest(finalPrepareRequest); | |
| await waitForStartedPreparePromises(); | |
| } | |
| return true; | |
| } | |
| async function boot() { | |
| if (!bootStarted) bootStarted = true; | |
| showRuntimeLoading(); | |
| beginLoadProgress("Downloading assets"); | |
| try { | |
| installLoadProgressFetch(); | |
| const modelBaseUrl = configuredModelBaseUrl(); | |
| setStatus([ | |
| "Loading browser runtime", | |
| selectedDownloadInfo().status, | |
| `Model base: ${modelBaseUrl}`, | |
| ]); | |
| setLoadPhase("Loading browser runtime"); | |
| const [{ init }, tokenizer] = await Promise.all([initEngine(), loadTokenizer()]); | |
| const bootRequest = readRequest(); | |
| const fastPathPrepared = await prepareInitialFastPath(bootRequest); | |
| showRuntimeReady(); | |
| setStatus([ | |
| "Ready", | |
| fastPathPrepared ? "Fast path prepared" : "", | |
| `Model: ${init.config.model_variant || init.config.model_name}`, | |
| seedInputLine(), | |
| `Model base: ${configuredModelBaseUrl()}`, | |
| `Tokenizer: ${tokenizer.constructor?.name || "loaded"}`, | |
| `Full image backend: ${init.customKernels?.fullImageBackend || "unknown"}`, | |
| ]); | |
| if (!fastPathPrepared) { | |
| startBackgroundAssetPrepareForCurrentSettings(bootRequest); | |
| scheduleBackgroundStageAndVaePrepareForCurrentSettings(bootRequest); | |
| } | |
| if (shouldAutoWarmRequest(bootRequest)) startBackgroundWarmForCurrentSettings(); | |
| finishLoadProgress(fastPathPrepared ? "Fast path prepared" : "Ready"); | |
| return true; | |
| } catch (err) { | |
| bootStarted = false; | |
| bootReady = false; | |
| resetRuntimeLoadPromises(); | |
| runButton.disabled = true; | |
| setButtonDisabled(warmButton, true); | |
| abortButton.disabled = true; | |
| setStatus([ | |
| err?.stack || err?.message || String(err), | |
| `Model base: ${configuredModelBaseUrl()}`, | |
| ], true); | |
| finishLoadProgress("Load failed", false); | |
| showDownloadGate({ retry: true, preserveStatus: true }); | |
| return false; | |
| } | |
| } | |
| startDownloadButton?.addEventListener("click", () => { | |
| if (bootStarted) return; | |
| bootStarted = true; | |
| void boot(); | |
| }); | |
| warmButton?.addEventListener("click", () => { | |
| if (!bootReady || foregroundBusy) return; | |
| clearWarmSchedule(); | |
| foregroundBusy = true; | |
| warmCurrentSettings().catch((err) => { | |
| if (isAbortError(err)) return; | |
| placeholderEl.hidden = false; | |
| placeholderEl.textContent = "Error"; | |
| setStatus(err?.stack || err?.message || String(err), true); | |
| }).finally(() => { | |
| foregroundBusy = false; | |
| runButton.disabled = false; | |
| setButtonDisabled(warmButton, false); | |
| abortButton.disabled = true; | |
| }); | |
| }); | |
| runButton.addEventListener("click", () => { | |
| if (!bootReady || foregroundBusy) return; | |
| clearWarmSchedule(); | |
| foregroundBusy = true; | |
| startRunTimer(); | |
| generate().catch((err) => { | |
| if (isAbortError(err)) { | |
| placeholderEl.hidden = false; | |
| placeholderEl.textContent = "Stopped"; | |
| setStatus("Stopped"); | |
| finishRunTimer("Stopped"); | |
| return; | |
| } | |
| imageEl.hidden = true; | |
| canvasEl.hidden = true; | |
| placeholderEl.hidden = false; | |
| placeholderEl.textContent = "Error"; | |
| setStatus(err?.stack || err?.message || String(err), true); | |
| finishRunTimer("Error"); | |
| }).finally(() => { | |
| foregroundBusy = false; | |
| runButton.disabled = false; | |
| setButtonDisabled(warmButton, false); | |
| abortButton.disabled = true; | |
| finishRunTimer("Done"); | |
| }); | |
| }); | |
| abortButton.addEventListener("click", () => { | |
| clearWarmSchedule(); | |
| backgroundWarmKey = ""; | |
| backgroundWarmResultKey = ""; | |
| backgroundWarmResult = null; | |
| window.flux2Engine?.abortGeneration?.(); | |
| abortButton.disabled = true; | |
| if (!runTimerFinalized) updateRunTimer("Stopping"); | |
| }); | |
| strengthEl.addEventListener("input", () => { | |
| updateStrengthDisplay(); | |
| scheduleAutoWarmForCurrentSettings(); | |
| }); | |
| strengthCurveEl.addEventListener("change", () => { | |
| updateStrengthDisplay(); | |
| scheduleAutoWarmForCurrentSettings(); | |
| }); | |
| resolutionPresetEl.addEventListener("change", () => { | |
| applyResolutionPreset(resolutionPresetEl.value); | |
| scheduleAutoWarmForCurrentSettings(); | |
| }); | |
| aspectLockButton?.addEventListener("click", () => { | |
| setAspectLock(!aspectLockEnabled); | |
| if (aspectLockEnabled) { | |
| rememberCurrentAspectRatio(); | |
| } else if (aspectRatioEl) { | |
| aspectRatioEl.value = "free"; | |
| } | |
| }); | |
| aspectRatioEl?.addEventListener("change", () => { | |
| const value = aspectRatioEl.value; | |
| if (value === "free") { | |
| setAspectLock(false); | |
| return; | |
| } | |
| setAspectLock(true); | |
| aspectLockRatio = ASPECT_RATIOS[value] || currentAspectRatio(); | |
| applyAspectToHeight(); | |
| resolutionPresetEl.value = "custom"; | |
| setResolutionAdvancedVisible(true); | |
| scheduleAutoWarmForCurrentSettings(); | |
| }); | |
| settingsToggleButton?.addEventListener("click", (event) => { | |
| event.stopPropagation(); | |
| setSettingsPopoverOpen(settingsPopoverEl?.hidden !== false); | |
| }); | |
| settingsPopoverEl?.addEventListener("click", (event) => { | |
| event.stopPropagation(); | |
| }); | |
| document.addEventListener("click", () => { | |
| setSettingsPopoverOpen(false); | |
| }); | |
| document.addEventListener("keydown", (event) => { | |
| if (event.key === "Escape") setSettingsPopoverOpen(false); | |
| }); | |
| themeToggleButton.addEventListener("click", () => { | |
| const currentTheme = document.documentElement.dataset.theme === "light" ? "light" : "dark"; | |
| applyTheme(currentTheme === "dark" ? "light" : "dark"); | |
| }); | |
| ["width", "height"].forEach((id) => { | |
| const element = $(id); | |
| element.addEventListener("input", () => { | |
| if (aspectSyncing) return; | |
| if (aspectLockEnabled) { | |
| if (id === "width") applyAspectToHeight(); | |
| else applyAspectToWidth(); | |
| } else { | |
| $("width").value = String(clampResolutionValue($("width").value)); | |
| $("height").value = String(clampResolutionValue($("height").value)); | |
| } | |
| syncResolutionPresetFromDimensions({ showAdvancedForCustom: true, preserveCustom: true }); | |
| scheduleAutoWarmForCurrentSettings(); | |
| }); | |
| }); | |
| initImageInput.addEventListener("change", () => { | |
| const file = initImageInput.files?.[0]; | |
| if (!file) { | |
| clearInitImage(); | |
| return; | |
| } | |
| if (currentSourceUrl) URL.revokeObjectURL(currentSourceUrl); | |
| currentSourceUrl = null; | |
| setInitImageUi(file); | |
| clearWarmSchedule(); | |
| const request = readRequest(); | |
| startBackgroundAssetPrepareForCurrentSettings(request); | |
| startStageAndVaePrepareForRequest(request); | |
| scheduleBackgroundStageAndVaePrepareForCurrentSettings(request, 350); | |
| currentSourceUrl = URL.createObjectURL(file); | |
| sourcePreviewEl.src = currentSourceUrl; | |
| const image = new Image(); | |
| image.onload = () => { | |
| sourceMetaEl.textContent = `${file.name}\n${image.naturalWidth}x${image.naturalHeight}`; | |
| }; | |
| image.src = currentSourceUrl; | |
| sourcePreviewWrap.hidden = false; | |
| }); | |
| clearInitImageButton?.addEventListener("click", () => { | |
| clearWarmSchedule(); | |
| clearInitImage(); | |
| }); | |
| clearGalleryButton?.addEventListener("click", () => { | |
| generationHistory = []; | |
| renderGallery(); | |
| }); | |
| galleryGridEl?.addEventListener("click", (event) => { | |
| const button = event.target.closest("button[data-gallery-action]"); | |
| if (!button) return; | |
| const item = generationHistory.find((entry) => entry.id === button.dataset.galleryId); | |
| if (!item) return; | |
| const action = button.dataset.galleryAction; | |
| if (action === "prompt") { | |
| applyGalleryPrompt(item); | |
| } else if (action === "seed") { | |
| applyGallerySeed(item); | |
| } else if (action === "reuse") { | |
| applyGallerySettings(item); | |
| } else if (action === "img2img") { | |
| setInitImageFromGallery(item).catch((err) => setStatus(err?.stack || err?.message || String(err), true)); | |
| } | |
| }); | |
| [ | |
| "prompt", | |
| "steps", | |
| "seed", | |
| "backend", | |
| "speed-profile", | |
| "decode-max", | |
| "render-max", | |
| "fit-mode", | |
| ].forEach((id) => { | |
| const element = $(id); | |
| if (!element) return; | |
| element.addEventListener(id === "prompt" ? "input" : "change", () => { | |
| if (id === "backend" && !bootStarted) showDownloadGate(); | |
| scheduleAutoWarmForCurrentSettings(id === "prompt" ? 650 : 350); | |
| }); | |
| }); | |
| initTheme(); | |
| applyUrlDefaults(); | |
| updateStrengthDisplay(); | |
| showDownloadGate(); | |