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\n\n\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();