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