ryanhlewis commited on
Commit
cb1f0b2
·
verified ·
1 Parent(s): 97524a3

Add first-load download progress indicator

Browse files
Files changed (2) hide show
  1. static-app.js +440 -80
  2. static.html +99 -16
static-app.js CHANGED
@@ -27,14 +27,21 @@ const aspectLockIconEl = $("aspect-lock-icon");
27
  const aspectRatioEl = $("aspect-ratio");
28
  const settingsToggleButton = $("settings-toggle");
29
  const settingsPopoverEl = $("settings-popover");
30
- const themeToggleButton = $("theme-toggle");
31
- const themeIconEl = $("theme-icon");
32
- const galleryPanelEl = $("gallery-panel");
33
- const galleryGridEl = $("gallery-grid");
34
- const clearGalleryButton = $("clear-gallery");
35
- const urlDefaults = new URLSearchParams(window.location.search);
36
- const DEFAULT_PROMPT = "A small ceramic robot on a workbench, product photo lighting";
37
- const COMMON_RESOLUTIONS = [256, 512, 768, 1024];
 
 
 
 
 
 
 
38
  const ASPECT_RATIOS = {
39
  "1:1": 1,
40
  "4:3": 4 / 3,
@@ -76,15 +83,41 @@ let backgroundVaeEncoderPreparePromise = null;
76
  let backgroundVaeEncoderPrepareKey = "";
77
  let backgroundDeepPrepareTimer = 0;
78
  let backgroundDeepPrepareToken = 0;
79
- const preparedInitImageCache = new WeakMap();
80
- const generationResultCache = new Map();
81
- const tokenizedPromptCache = new Map();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  function formatMiB(bytes) {
84
  const value = Number(bytes || 0);
85
  return `${(value / (1024 * 1024)).toFixed(value >= 100 * 1024 * 1024 ? 0 : 1)} MiB`;
86
  }
87
 
 
 
 
 
 
 
 
 
 
88
  function trimTrailingSlash(value) {
89
  return String(value || "").replace(/\/+$/, "");
90
  }
@@ -130,6 +163,219 @@ function resolveAssetUrl(baseUrl, path) {
130
  return new URL(path, new URL(withTrailingSlash(baseUrl), window.location.href)).toString();
131
  }
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  function huggingFaceResolveSpec(baseUrl) {
134
  let url;
135
  try {
@@ -477,16 +723,24 @@ function maskFromLength(length, seqLen) {
477
 
478
  async function loadTokenizer() {
479
  if (tokenizerPromise) return tokenizerPromise;
 
480
  const modelBaseUrl = configuredModelBaseUrl();
481
  const tokenizerSpec = tokenizerLoadSpec(modelBaseUrl);
482
  env.allowLocalModels = !tokenizerSpec.remote;
483
  env.allowRemoteModels = tokenizerSpec.remote;
484
  env.localModelPath = "/";
485
- tokenizerPromise = AutoTokenizer.from_pretrained(tokenizerSpec.source, tokenizerSpec.options);
 
 
 
 
 
 
 
486
  return tokenizerPromise;
487
  }
488
-
489
- async function tokenizePrompt(prompt, seqLen) {
490
  const cachePayload = JSON.stringify({ model: "flux.2-klein-4b", text_seq_len: seqLen, prompt });
491
  const promptCacheKey = await sha256Hex(cachePayload);
492
  const cacheKey = `${seqLen}|${promptCacheKey}`;
@@ -528,6 +782,7 @@ async function initEngine() {
528
  const engine = await waitForEngine();
529
  const modelBaseUrl = configuredModelBaseUrl();
530
  const runtimeBaseUrl = configuredRuntimeBaseUrl(modelBaseUrl);
 
531
  const result = await engine.init(modelBaseUrl, {
532
  executionProvider: "webgpu",
533
  customKernelBaseUrl: configuredCustomKernelBaseUrl(modelBaseUrl, runtimeBaseUrl),
@@ -1050,14 +1305,106 @@ function assetPrepareKeyForRequest(request) {
1050
  });
1051
  }
1052
 
1053
- function shouldAssetPrepareRequest(request) {
1054
- if (urlFlag("noAutowarm") || urlFlag("noAssetWarm")) return false;
1055
- return request.transformerBackend === "custom-lowbit-webgpu";
1056
- }
1057
-
1058
- function textContextPrepareKeyForRequest(request) {
1059
- return JSON.stringify({
1060
- prompt: request.prompt,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1061
  backend: request.transformerBackend,
1062
  textLimit: request.speed.customTextTokenLimit,
1063
  minText: request.speed.customMinTextTokens,
@@ -1077,10 +1424,11 @@ function startBackgroundTextContextPrepareForCurrentSettings(request = readReque
1077
  const key = textContextPrepareKeyForRequest(prepareRequest);
1078
  if (backgroundTextContextPreparePromise && backgroundTextContextPrepareKey === key) return true;
1079
  backgroundTextContextPrepareKey = key;
1080
- backgroundTextContextPreparePromise = initEngine()
1081
- .then(async ({ engine, init }) => {
1082
- if (typeof engine.encodeText !== "function") return null;
1083
- const seqLen = Number(init.config.text_seq_len || 512);
 
1084
  const tokenized = await tokenizePrompt(prepareRequest.prompt, seqLen);
1085
  const params = buildGenerateParams(prepareRequest, tokenized, null);
1086
  if (typeof engine.prepareCustomTransformerTextProjection === "function") {
@@ -1142,11 +1490,12 @@ function startBackgroundTransformerSetupPrepareForCurrentSettings(request = read
1142
  const key = `${transformerSetupPrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`;
1143
  if (backgroundTransformerSetupPreparePromise && backgroundTransformerSetupPrepareKey === key) return true;
1144
  backgroundTransformerSetupPrepareKey = key;
1145
- backgroundTransformerSetupPreparePromise = initEngine()
1146
- .then(async ({ engine, init }) => {
1147
- if (typeof engine.prepareCustomTransformerStageSetup !== "function") return null;
1148
- const seqLen = Number(init.config.text_seq_len || 512);
1149
- const tokenized = await tokenizePrompt(prepareRequest.prompt, seqLen);
 
1150
  const plannedTextTokens = plannedCustomTextTokens(prepareRequest, tokenized.attentionMask, seqLen);
1151
  return await engine.prepareCustomTransformerStageSetup({
1152
  width: prepareRequest.width,
@@ -1225,12 +1574,13 @@ function startBackgroundAssetPrepareForCurrentSettings(request = readRequest())
1225
  const key = assetPrepareKeyForRequest(request);
1226
  if (backgroundAssetPreparePromise && backgroundAssetPrepareKey === key) return true;
1227
  backgroundAssetPrepareKey = key;
1228
- backgroundAssetPreparePromise = initEngine()
1229
- .then(({ engine }) => {
1230
- if (typeof engine.prepareCustomTransformerAssets !== "function") return null;
1231
- return engine.prepareCustomTransformerAssets({
1232
- maxDoubleBlocks: request.speed.customMaxDoubleBlocks || 5,
1233
- maxSingleBlocks: request.speed.customMaxSingleBlocks || 20,
 
1234
  });
1235
  })
1236
  .then((result) => {
@@ -1286,10 +1636,11 @@ function startBackgroundVaeEncoderPrepareForCurrentSettings(request = readReques
1286
  const key = vaeEncoderPrepareKeyForRequest(request);
1287
  if (backgroundVaeEncoderPreparePromise && backgroundVaeEncoderPrepareKey === key) return true;
1288
  backgroundVaeEncoderPrepareKey = key;
1289
- backgroundVaeEncoderPreparePromise = initEngine()
1290
- .then(async ({ engine }) => {
1291
- if (typeof engine.prepareVaeEncoderLatent !== "function") return null;
1292
- const renderSize = plannedRenderSizeForRequest(request);
 
1293
  const initImage = await prepareInitImage(renderSize.width, renderSize.height, request.fitMode);
1294
  if (!initImage) return null;
1295
  return await engine.prepareVaeEncoderLatent({
@@ -1363,10 +1714,11 @@ function startBackgroundVaePrepareForCurrentSettings(request = readRequest()) {
1363
  if (backgroundVaePreparePromise && backgroundVaePrepareKey === key) return true;
1364
  backgroundVaePrepareKey = key;
1365
  const plannedDecodeMax = plannedDecodeMaxForRequest(prepareRequest);
1366
- backgroundVaePreparePromise = initEngine()
1367
- .then(({ engine }) => {
1368
- if (typeof engine.prepareVaeSessions !== "function") return null;
1369
- return engine.prepareVaeSessions({
 
1370
  width: prepareRequest.width,
1371
  height: prepareRequest.height,
1372
  hasInitImage: prepareRequest.hasInitImage,
@@ -2330,16 +2682,18 @@ async function prepareInitialFastPath(request) {
2330
  const shouldPrepareVae = shouldVaePrepareRequest(prepareRequest);
2331
  if (!shouldPrepareAssets && !shouldPrepareTransformer && !shouldPrepareVae) return false;
2332
 
2333
- setStatus([
2334
- "Preparing fast path",
2335
- `Backend: ${prepareRequest.transformerBackend}`,
2336
- `Speed Profile: ${speedProfileLabel(prepareRequest.speed)}`,
2337
- prepareRequest.previewRenderMaxSize ? `Preview render: ${prepareRequest.previewRenderMaxSize}px max` : "",
2338
- prepareRequest.previewDecodeMaxSize ? `Preview decode: ${prepareRequest.previewDecodeMaxSize}px max` : "",
2339
- ]);
2340
-
2341
- startBackgroundAssetPrepareForCurrentSettings(request);
2342
- const assetPromise = backgroundAssetPreparePromise;
 
 
2343
  if (assetPromise) {
2344
  try {
2345
  await assetPromise;
@@ -2360,25 +2714,29 @@ async function prepareInitialFastPath(request) {
2360
 
2361
  if (foregroundBusy) return true;
2362
  const finalPrepareRequest = progressiveFinalPrepareRequestForRequest(currentRequest);
2363
- if (finalPrepareRequest && warmKeyForRequest(readRequest()) === warmKeyForRequest(request)) {
2364
- setStatus([
2365
- "Preparing refinement path",
2366
  `Backend: ${finalPrepareRequest.transformerBackend}`,
2367
  `Speed Profile: ${speedProfileLabel(finalPrepareRequest.speed)}`,
2368
  finalPrepareRequest.previewRenderMaxSize ? `Preview render: ${finalPrepareRequest.previewRenderMaxSize}px max` : "",
2369
- finalPrepareRequest.previewDecodeMaxSize ? `Preview decode: ${finalPrepareRequest.previewDecodeMaxSize}px max` : "",
2370
- ]);
2371
- startStageAndVaePrepareForRequest(finalPrepareRequest);
2372
- await waitForStartedPreparePromises();
2373
- }
 
2374
  return true;
2375
  }
2376
 
2377
- async function boot() {
2378
- try {
2379
- setStatus("Loading browser runtime");
2380
- const [{ init }, tokenizer] = await Promise.all([initEngine(), loadTokenizer()]);
2381
- const bootRequest = readRequest();
 
 
 
2382
  const fastPathPrepared = await prepareInitialFastPath(bootRequest);
2383
  runButton.disabled = false;
2384
  setButtonDisabled(warmButton, false);
@@ -2389,18 +2747,20 @@ async function boot() {
2389
  seedInputLine(),
2390
  `Tokenizer: ${tokenizer.constructor?.name || "loaded"}`,
2391
  `Full image backend: ${init.customKernels?.fullImageBackend || "unknown"}`,
2392
- ]);
2393
- if (!fastPathPrepared) {
2394
- startBackgroundAssetPrepareForCurrentSettings(bootRequest);
2395
- scheduleBackgroundStageAndVaePrepareForCurrentSettings(bootRequest);
2396
- }
2397
- if (shouldAutoWarmRequest(bootRequest)) startBackgroundWarmForCurrentSettings();
2398
- } catch (err) {
2399
- runButton.disabled = true;
2400
- setButtonDisabled(warmButton, true);
2401
- setStatus(err?.stack || err?.message || String(err), true);
2402
- }
2403
- }
 
 
2404
 
2405
  warmButton?.addEventListener("click", () => {
2406
  if (foregroundBusy) return;
 
27
  const aspectRatioEl = $("aspect-ratio");
28
  const settingsToggleButton = $("settings-toggle");
29
  const settingsPopoverEl = $("settings-popover");
30
+ const themeToggleButton = $("theme-toggle");
31
+ const themeIconEl = $("theme-icon");
32
+ const galleryPanelEl = $("gallery-panel");
33
+ const galleryGridEl = $("gallery-grid");
34
+ const clearGalleryButton = $("clear-gallery");
35
+ const loadProgressEl = $("load-progress");
36
+ const loadProgressTitleEl = $("load-progress-title");
37
+ const loadProgressSummaryEl = $("load-progress-summary");
38
+ const loadProgressTrackEl = $("load-progress-track");
39
+ const loadProgressFillEl = $("load-progress-fill");
40
+ const loadProgressDetailEl = $("load-progress-detail");
41
+ const loadProgressRateEl = $("load-progress-rate");
42
+ const urlDefaults = new URLSearchParams(window.location.search);
43
+ const DEFAULT_PROMPT = "A small ceramic robot on a workbench, product photo lighting";
44
+ const COMMON_RESOLUTIONS = [256, 512, 768, 1024];
45
  const ASPECT_RATIOS = {
46
  "1:1": 1,
47
  "4:3": 4 / 3,
 
83
  let backgroundVaeEncoderPrepareKey = "";
84
  let backgroundDeepPrepareTimer = 0;
85
  let backgroundDeepPrepareToken = 0;
86
+ const preparedInitImageCache = new WeakMap();
87
+ const generationResultCache = new Map();
88
+ const tokenizedPromptCache = new Map();
89
+ const nativeFetch = globalThis.fetch.bind(globalThis);
90
+
91
+ const loadProgressState = {
92
+ visible: false,
93
+ tracking: false,
94
+ completed: false,
95
+ startedAt: 0,
96
+ title: "Loading",
97
+ detail: "Starting",
98
+ estimatedTotalBytes: 0,
99
+ estimatedFiles: 0,
100
+ nextRequestId: 1,
101
+ requests: new Map(),
102
+ renderQueued: false,
103
+ timerId: 0,
104
+ hideTimerId: 0,
105
+ };
106
 
107
  function formatMiB(bytes) {
108
  const value = Number(bytes || 0);
109
  return `${(value / (1024 * 1024)).toFixed(value >= 100 * 1024 * 1024 ? 0 : 1)} MiB`;
110
  }
111
 
112
+ function formatBytesCompact(bytes) {
113
+ const value = Number(bytes || 0);
114
+ if (!Number.isFinite(value) || value <= 0) return "0 B";
115
+ if (value >= 1024 ** 3) return `${(value / 1024 ** 3).toFixed(value >= 10 * 1024 ** 3 ? 1 : 2)} GiB`;
116
+ if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(value >= 100 * 1024 ** 2 ? 0 : 1)} MiB`;
117
+ if (value >= 1024) return `${(value / 1024).toFixed(value >= 100 * 1024 ? 0 : 1)} KiB`;
118
+ return `${Math.round(value)} B`;
119
+ }
120
+
121
  function trimTrailingSlash(value) {
122
  return String(value || "").replace(/\/+$/, "");
123
  }
 
163
  return new URL(path, new URL(withTrailingSlash(baseUrl), window.location.href)).toString();
164
  }
165
 
166
+ function compactLoadUrlLabel(url) {
167
+ try {
168
+ const parsed = new URL(url, window.location.href);
169
+ const parts = parsed.pathname.split("/").filter(Boolean);
170
+ const tail = parts.slice(-3).join("/");
171
+ return tail || parsed.hostname;
172
+ } catch {
173
+ return String(url || "download");
174
+ }
175
+ }
176
+
177
+ function beginLoadProgress(title = "Loading") {
178
+ if (!loadProgressEl) return;
179
+ if (loadProgressState.hideTimerId) {
180
+ clearTimeout(loadProgressState.hideTimerId);
181
+ loadProgressState.hideTimerId = 0;
182
+ }
183
+ loadProgressState.visible = true;
184
+ loadProgressState.tracking = true;
185
+ loadProgressState.completed = false;
186
+ loadProgressState.startedAt = performance.now();
187
+ loadProgressState.title = title;
188
+ loadProgressState.detail = "Starting";
189
+ loadProgressState.estimatedTotalBytes = 0;
190
+ loadProgressState.estimatedFiles = 0;
191
+ loadProgressState.nextRequestId = 1;
192
+ loadProgressState.requests.clear();
193
+ loadProgressEl.hidden = false;
194
+ if (loadProgressState.timerId) clearInterval(loadProgressState.timerId);
195
+ loadProgressState.timerId = setInterval(renderLoadProgress, 250);
196
+ renderLoadProgress();
197
+ }
198
+
199
+ function setLoadPhase(detail, title = "") {
200
+ if (!loadProgressState.visible) return;
201
+ if (title) loadProgressState.title = title;
202
+ if (detail) loadProgressState.detail = detail;
203
+ scheduleLoadProgressRender();
204
+ }
205
+
206
+ function setLoadEstimate(totalBytes, expectedFiles = 0) {
207
+ if (!loadProgressState.visible) return;
208
+ const bytes = Number(totalBytes || 0);
209
+ if (Number.isFinite(bytes) && bytes > loadProgressState.estimatedTotalBytes) {
210
+ loadProgressState.estimatedTotalBytes = bytes;
211
+ }
212
+ const files = Number(expectedFiles || 0);
213
+ if (Number.isFinite(files) && files > loadProgressState.estimatedFiles) {
214
+ loadProgressState.estimatedFiles = files;
215
+ }
216
+ scheduleLoadProgressRender();
217
+ }
218
+
219
+ function finishLoadProgress(detail = "Ready", success = true) {
220
+ if (!loadProgressState.visible) return;
221
+ loadProgressState.tracking = false;
222
+ loadProgressState.completed = success;
223
+ if (!success) loadProgressState.title = detail;
224
+ loadProgressState.detail = detail;
225
+ if (success) {
226
+ const loaded = loadProgressTotals().loaded;
227
+ loadProgressState.estimatedTotalBytes = Math.max(loadProgressState.estimatedTotalBytes, loaded);
228
+ }
229
+ if (loadProgressState.timerId) {
230
+ clearInterval(loadProgressState.timerId);
231
+ loadProgressState.timerId = 0;
232
+ }
233
+ renderLoadProgress();
234
+ if (success) {
235
+ loadProgressState.hideTimerId = setTimeout(() => {
236
+ loadProgressState.visible = false;
237
+ if (loadProgressEl) loadProgressEl.hidden = true;
238
+ }, 4500);
239
+ }
240
+ }
241
+
242
+ function loadProgressTotals() {
243
+ let loaded = 0;
244
+ let dynamicTotal = 0;
245
+ let active = 0;
246
+ let completed = 0;
247
+ let failed = 0;
248
+ let current = "";
249
+ for (const request of loadProgressState.requests.values()) {
250
+ loaded += request.loaded || 0;
251
+ if (request.total) dynamicTotal += request.total;
252
+ if (request.failed) failed += 1;
253
+ if (request.done || request.failed) {
254
+ completed += 1;
255
+ } else {
256
+ active += 1;
257
+ current = request.label || current;
258
+ }
259
+ }
260
+ return {
261
+ loaded,
262
+ total: Math.max(loadProgressState.estimatedTotalBytes, dynamicTotal, loaded),
263
+ active,
264
+ completed,
265
+ failed,
266
+ current,
267
+ };
268
+ }
269
+
270
+ function renderLoadProgress() {
271
+ if (!loadProgressEl || !loadProgressState.visible) return;
272
+ loadProgressState.renderQueued = false;
273
+ const { loaded, total, active, completed, failed, current } = loadProgressTotals();
274
+ const elapsed = Math.max(0.001, (performance.now() - loadProgressState.startedAt) / 1000);
275
+ const rate = loaded / elapsed;
276
+ const hasTotal = total > 0;
277
+ const percent = hasTotal ? Math.max(0, Math.min(100, (loaded / total) * 100)) : 0;
278
+ const displayPercent = hasTotal && loadProgressState.tracking && active > 0
279
+ ? Math.min(percent, 99.5)
280
+ : percent;
281
+ const files = loadProgressState.estimatedFiles
282
+ ? `${Math.min(completed, loadProgressState.estimatedFiles)}/${loadProgressState.estimatedFiles} files`
283
+ : `${completed}${active ? ` +${active}` : ""} files`;
284
+ const summary = !loadProgressState.tracking && !loadProgressState.completed
285
+ ? "Error"
286
+ : loadProgressState.completed
287
+ ? "Ready"
288
+ : hasTotal
289
+ ? `${displayPercent.toFixed(displayPercent >= 10 ? 0 : 1)}%`
290
+ : "Loading";
291
+ const detailParts = [
292
+ hasTotal ? `${formatBytesCompact(loaded)} / ${formatBytesCompact(total)}` : `${formatBytesCompact(loaded)} downloaded`,
293
+ files,
294
+ failed ? `${failed} failed` : "",
295
+ current || loadProgressState.detail,
296
+ ].filter(Boolean);
297
+ loadProgressTitleEl.textContent = loadProgressState.title;
298
+ loadProgressSummaryEl.textContent = summary;
299
+ loadProgressDetailEl.textContent = detailParts.join(" | ");
300
+ loadProgressRateEl.textContent = `${formatBytesCompact(rate)}/s | ${elapsed.toFixed(1)}s`;
301
+ loadProgressTrackEl.classList.toggle("indeterminate", !hasTotal && loadProgressState.tracking);
302
+ loadProgressFillEl.style.width = hasTotal ? `${loadProgressState.completed ? 100 : displayPercent}%` : "";
303
+ }
304
+
305
+ function scheduleLoadProgressRender() {
306
+ if (!loadProgressState.visible || loadProgressState.renderQueued) return;
307
+ loadProgressState.renderQueued = true;
308
+ requestAnimationFrame(renderLoadProgress);
309
+ }
310
+
311
+ function shouldTrackLoadFetch(input, init, response) {
312
+ if (!loadProgressState.tracking || !response || !response.body) return false;
313
+ const method = String(init?.method || (input && typeof input === "object" && input.method) || "GET").toUpperCase();
314
+ if (method !== "GET") return false;
315
+ return true;
316
+ }
317
+
318
+ function trackLoadFetch(input, response) {
319
+ const url = response.url || (typeof input === "string" ? input : input?.url) || "";
320
+ const totalHeader = response.headers.get("content-length");
321
+ const total = totalHeader ? Number(totalHeader) : 0;
322
+ const id = loadProgressState.nextRequestId++;
323
+ const request = {
324
+ id,
325
+ url,
326
+ label: compactLoadUrlLabel(url),
327
+ total: Number.isFinite(total) && total > 0 ? total : 0,
328
+ loaded: 0,
329
+ done: false,
330
+ failed: false,
331
+ };
332
+ loadProgressState.requests.set(id, request);
333
+ scheduleLoadProgressRender();
334
+
335
+ const reader = response.body.getReader();
336
+ const stream = new ReadableStream({
337
+ async pull(controller) {
338
+ try {
339
+ const { done, value } = await reader.read();
340
+ if (done) {
341
+ request.done = true;
342
+ scheduleLoadProgressRender();
343
+ controller.close();
344
+ return;
345
+ }
346
+ request.loaded += value.byteLength || value.length || 0;
347
+ scheduleLoadProgressRender();
348
+ controller.enqueue(value);
349
+ } catch (err) {
350
+ request.failed = true;
351
+ scheduleLoadProgressRender();
352
+ controller.error(err);
353
+ }
354
+ },
355
+ cancel(reason) {
356
+ request.done = true;
357
+ scheduleLoadProgressRender();
358
+ return reader.cancel(reason);
359
+ },
360
+ });
361
+
362
+ return new Response(stream, {
363
+ status: response.status,
364
+ statusText: response.statusText,
365
+ headers: response.headers,
366
+ });
367
+ }
368
+
369
+ function installLoadProgressFetch() {
370
+ if (globalThis.__flux2LoadProgressFetchInstalled) return;
371
+ globalThis.__flux2LoadProgressFetchInstalled = true;
372
+ globalThis.fetch = async (input, init) => {
373
+ const response = await nativeFetch(input, init);
374
+ if (!shouldTrackLoadFetch(input, init, response)) return response;
375
+ return trackLoadFetch(input, response);
376
+ };
377
+ }
378
+
379
  function huggingFaceResolveSpec(baseUrl) {
380
  let url;
381
  try {
 
723
 
724
  async function loadTokenizer() {
725
  if (tokenizerPromise) return tokenizerPromise;
726
+ setLoadPhase("Loading tokenizer");
727
  const modelBaseUrl = configuredModelBaseUrl();
728
  const tokenizerSpec = tokenizerLoadSpec(modelBaseUrl);
729
  env.allowLocalModels = !tokenizerSpec.remote;
730
  env.allowRemoteModels = tokenizerSpec.remote;
731
  env.localModelPath = "/";
732
+ tokenizerPromise = AutoTokenizer.from_pretrained(tokenizerSpec.source, {
733
+ ...tokenizerSpec.options,
734
+ progress_callback: (progress) => {
735
+ const file = progress?.file ? compactLoadUrlLabel(progress.file) : "tokenizer";
736
+ const status = progress?.status ? ` ${progress.status}` : "";
737
+ setLoadPhase(`Loading ${file}${status}`);
738
+ },
739
+ });
740
  return tokenizerPromise;
741
  }
742
+
743
+ async function tokenizePrompt(prompt, seqLen) {
744
  const cachePayload = JSON.stringify({ model: "flux.2-klein-4b", text_seq_len: seqLen, prompt });
745
  const promptCacheKey = await sha256Hex(cachePayload);
746
  const cacheKey = `${seqLen}|${promptCacheKey}`;
 
782
  const engine = await waitForEngine();
783
  const modelBaseUrl = configuredModelBaseUrl();
784
  const runtimeBaseUrl = configuredRuntimeBaseUrl(modelBaseUrl);
785
+ setLoadPhase("Loading model config and runtime manifest");
786
  const result = await engine.init(modelBaseUrl, {
787
  executionProvider: "webgpu",
788
  customKernelBaseUrl: configuredCustomKernelBaseUrl(modelBaseUrl, runtimeBaseUrl),
 
1305
  });
1306
  }
1307
 
1308
+ function shouldAssetPrepareRequest(request) {
1309
+ if (urlFlag("noAutowarm") || urlFlag("noAssetWarm")) return false;
1310
+ return request.transformerBackend === "custom-lowbit-webgpu";
1311
+ }
1312
+
1313
+ function fullManifestLinearById(manifest, id) {
1314
+ const linears = Array.isArray(manifest?.linears) ? manifest.linears : [];
1315
+ return linears.find((item) => item.id === id || item.name === id || item.source_node === id) || null;
1316
+ }
1317
+
1318
+ function fullManifestQkNormById(manifest, id) {
1319
+ const qkNorms = Array.isArray(manifest?.constants?.qk_norms) ? manifest.constants.qk_norms : [];
1320
+ return qkNorms.find((item) => item.id === id || item.name === id) || null;
1321
+ }
1322
+
1323
+ function estimateLinearAssetBytes(linear) {
1324
+ if (!linear) return { bytes: 0, files: 0 };
1325
+ if (linear.i8_dp4a) {
1326
+ const packed = Number(linear.i8_dp4a.packed_u32_count || 0) * 4;
1327
+ const scales = Number(linear.i8_dp4a.scale_count || 0) * 4;
1328
+ return { bytes: packed + scales, files: 2 };
1329
+ }
1330
+ if (linear.q4) {
1331
+ const qweight = Number(linear.q4.qweight_u32_count || linear.q4.packed_u32_count || 0) * 4;
1332
+ const scales = Number(linear.q4.scale_count || 0) * 2;
1333
+ const zeroPoints = Number(linear.q4.zero_point_count || linear.q4.zero_points_u32_count || 0) * 4;
1334
+ return { bytes: qweight + scales + zeroPoints, files: 3 };
1335
+ }
1336
+ return { bytes: 0, files: 0 };
1337
+ }
1338
+
1339
+ function estimateQkNormBytes(entry) {
1340
+ if (!entry) return { bytes: 0, files: 0 };
1341
+ const count = Array.isArray(entry.shape) && Number.isFinite(Number(entry.shape[0])) ? Number(entry.shape[0]) : 128;
1342
+ return { bytes: count * 2 * 2, files: 2 };
1343
+ }
1344
+
1345
+ function estimateCustomTransformerAssetSet(manifest, request) {
1346
+ const doubleBlockCount = Math.max(1, Math.min(5, Math.trunc(Number(request.speed.customMaxDoubleBlocks || 5))));
1347
+ const singleBlockCount = Math.max(0, Math.min(20, Math.trunc(Number(request.speed.customMaxSingleBlocks || 20))));
1348
+ const linearIds = ["img_in", "txt_in", "final_layer.linear"];
1349
+ const qkNormIds = [];
1350
+ for (let block = 0; block < doubleBlockCount; block += 1) {
1351
+ linearIds.push(
1352
+ `double_blocks.${block}.img_attn.qkv`,
1353
+ `double_blocks.${block}.txt_attn.qkv`,
1354
+ `double_blocks.${block}.img_attn.proj`,
1355
+ `double_blocks.${block}.txt_attn.proj`,
1356
+ `double_blocks.${block}.img_mlp.0`,
1357
+ `double_blocks.${block}.img_mlp.2`,
1358
+ `double_blocks.${block}.txt_mlp.0`,
1359
+ `double_blocks.${block}.txt_mlp.2`,
1360
+ );
1361
+ qkNormIds.push(`double_blocks.${block}.img_attn.norm`, `double_blocks.${block}.txt_attn.norm`);
1362
+ }
1363
+ for (let block = 0; block < singleBlockCount; block += 1) {
1364
+ linearIds.push(`single_blocks.${block}.linear1`, `single_blocks.${block}.linear2`);
1365
+ qkNormIds.push(`single_blocks.${block}.norm`);
1366
+ }
1367
+ let bytes = 0;
1368
+ let files = 0;
1369
+ for (const id of linearIds) {
1370
+ const estimate = estimateLinearAssetBytes(fullManifestLinearById(manifest, id));
1371
+ bytes += estimate.bytes;
1372
+ files += estimate.files;
1373
+ }
1374
+ for (const id of qkNormIds) {
1375
+ const estimate = estimateQkNormBytes(fullManifestQkNormById(manifest, id));
1376
+ bytes += estimate.bytes;
1377
+ files += estimate.files;
1378
+ }
1379
+ return { bytes, files, doubleBlockCount, singleBlockCount };
1380
+ }
1381
+
1382
+ async function prepareCustomAssetLoadEstimate(request) {
1383
+ if (!loadProgressState.visible || !shouldAssetPrepareRequest(request)) return null;
1384
+ try {
1385
+ const modelBaseUrl = configuredModelBaseUrl();
1386
+ const runtimeBaseUrl = configuredRuntimeBaseUrl(modelBaseUrl);
1387
+ const customKernelBaseUrl = configuredCustomKernelBaseUrl(modelBaseUrl, runtimeBaseUrl);
1388
+ const manifestUrl = resolveAssetUrl(customKernelBaseUrl, "full_transformer/manifest.json");
1389
+ setLoadPhase("Reading transformer asset manifest");
1390
+ const response = await fetch(manifestUrl);
1391
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1392
+ const manifest = await response.json();
1393
+ const estimate = estimateCustomTransformerAssetSet(manifest, request);
1394
+ if (estimate.bytes > 0) {
1395
+ setLoadEstimate(estimate.bytes, estimate.files);
1396
+ setLoadPhase(`Downloading transformer assets (${formatBytesCompact(estimate.bytes)})`);
1397
+ }
1398
+ return estimate;
1399
+ } catch (err) {
1400
+ console.warn("[flux2] custom asset byte estimate failed", err);
1401
+ return null;
1402
+ }
1403
+ }
1404
+
1405
+ function textContextPrepareKeyForRequest(request) {
1406
+ return JSON.stringify({
1407
+ prompt: request.prompt,
1408
  backend: request.transformerBackend,
1409
  textLimit: request.speed.customTextTokenLimit,
1410
  minText: request.speed.customMinTextTokens,
 
1424
  const key = textContextPrepareKeyForRequest(prepareRequest);
1425
  if (backgroundTextContextPreparePromise && backgroundTextContextPrepareKey === key) return true;
1426
  backgroundTextContextPrepareKey = key;
1427
+ backgroundTextContextPreparePromise = initEngine()
1428
+ .then(async ({ engine, init }) => {
1429
+ if (typeof engine.encodeText !== "function") return null;
1430
+ setLoadPhase("Preparing text context");
1431
+ const seqLen = Number(init.config.text_seq_len || 512);
1432
  const tokenized = await tokenizePrompt(prepareRequest.prompt, seqLen);
1433
  const params = buildGenerateParams(prepareRequest, tokenized, null);
1434
  if (typeof engine.prepareCustomTransformerTextProjection === "function") {
 
1490
  const key = `${transformerSetupPrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`;
1491
  if (backgroundTransformerSetupPreparePromise && backgroundTransformerSetupPrepareKey === key) return true;
1492
  backgroundTransformerSetupPrepareKey = key;
1493
+ backgroundTransformerSetupPreparePromise = initEngine()
1494
+ .then(async ({ engine, init }) => {
1495
+ if (typeof engine.prepareCustomTransformerStageSetup !== "function") return null;
1496
+ setLoadPhase(dispatchWarmup ? "Preparing transformer dispatch warmup" : "Preparing transformer setup");
1497
+ const seqLen = Number(init.config.text_seq_len || 512);
1498
+ const tokenized = await tokenizePrompt(prepareRequest.prompt, seqLen);
1499
  const plannedTextTokens = plannedCustomTextTokens(prepareRequest, tokenized.attentionMask, seqLen);
1500
  return await engine.prepareCustomTransformerStageSetup({
1501
  width: prepareRequest.width,
 
1574
  const key = assetPrepareKeyForRequest(request);
1575
  if (backgroundAssetPreparePromise && backgroundAssetPrepareKey === key) return true;
1576
  backgroundAssetPrepareKey = key;
1577
+ backgroundAssetPreparePromise = initEngine()
1578
+ .then(({ engine }) => {
1579
+ if (typeof engine.prepareCustomTransformerAssets !== "function") return null;
1580
+ setLoadPhase("Downloading transformer assets");
1581
+ return engine.prepareCustomTransformerAssets({
1582
+ maxDoubleBlocks: request.speed.customMaxDoubleBlocks || 5,
1583
+ maxSingleBlocks: request.speed.customMaxSingleBlocks || 20,
1584
  });
1585
  })
1586
  .then((result) => {
 
1636
  const key = vaeEncoderPrepareKeyForRequest(request);
1637
  if (backgroundVaeEncoderPreparePromise && backgroundVaeEncoderPrepareKey === key) return true;
1638
  backgroundVaeEncoderPrepareKey = key;
1639
+ backgroundVaeEncoderPreparePromise = initEngine()
1640
+ .then(async ({ engine }) => {
1641
+ if (typeof engine.prepareVaeEncoderLatent !== "function") return null;
1642
+ setLoadPhase("Preparing source image latent");
1643
+ const renderSize = plannedRenderSizeForRequest(request);
1644
  const initImage = await prepareInitImage(renderSize.width, renderSize.height, request.fitMode);
1645
  if (!initImage) return null;
1646
  return await engine.prepareVaeEncoderLatent({
 
1714
  if (backgroundVaePreparePromise && backgroundVaePrepareKey === key) return true;
1715
  backgroundVaePrepareKey = key;
1716
  const plannedDecodeMax = plannedDecodeMaxForRequest(prepareRequest);
1717
+ backgroundVaePreparePromise = initEngine()
1718
+ .then(({ engine }) => {
1719
+ if (typeof engine.prepareVaeSessions !== "function") return null;
1720
+ setLoadPhase(dispatchWarmup ? "Preparing VAE dispatch warmup" : "Preparing VAE sessions");
1721
+ return engine.prepareVaeSessions({
1722
  width: prepareRequest.width,
1723
  height: prepareRequest.height,
1724
  hasInitImage: prepareRequest.hasInitImage,
 
2682
  const shouldPrepareVae = shouldVaePrepareRequest(prepareRequest);
2683
  if (!shouldPrepareAssets && !shouldPrepareTransformer && !shouldPrepareVae) return false;
2684
 
2685
+ setStatus([
2686
+ "Preparing fast path",
2687
+ `Backend: ${prepareRequest.transformerBackend}`,
2688
+ `Speed Profile: ${speedProfileLabel(prepareRequest.speed)}`,
2689
+ prepareRequest.previewRenderMaxSize ? `Preview render: ${prepareRequest.previewRenderMaxSize}px max` : "",
2690
+ prepareRequest.previewDecodeMaxSize ? `Preview decode: ${prepareRequest.previewDecodeMaxSize}px max` : "",
2691
+ ]);
2692
+ setLoadPhase("Preparing fast path");
2693
+
2694
+ if (shouldPrepareAssets) await prepareCustomAssetLoadEstimate(prepareRequest);
2695
+ startBackgroundAssetPrepareForCurrentSettings(request);
2696
+ const assetPromise = backgroundAssetPreparePromise;
2697
  if (assetPromise) {
2698
  try {
2699
  await assetPromise;
 
2714
 
2715
  if (foregroundBusy) return true;
2716
  const finalPrepareRequest = progressiveFinalPrepareRequestForRequest(currentRequest);
2717
+ if (finalPrepareRequest && warmKeyForRequest(readRequest()) === warmKeyForRequest(request)) {
2718
+ setStatus([
2719
+ "Preparing refinement path",
2720
  `Backend: ${finalPrepareRequest.transformerBackend}`,
2721
  `Speed Profile: ${speedProfileLabel(finalPrepareRequest.speed)}`,
2722
  finalPrepareRequest.previewRenderMaxSize ? `Preview render: ${finalPrepareRequest.previewRenderMaxSize}px max` : "",
2723
+ finalPrepareRequest.previewDecodeMaxSize ? `Preview decode: ${finalPrepareRequest.previewDecodeMaxSize}px max` : "",
2724
+ ]);
2725
+ setLoadPhase("Preparing refinement path");
2726
+ startStageAndVaePrepareForRequest(finalPrepareRequest);
2727
+ await waitForStartedPreparePromises();
2728
+ }
2729
  return true;
2730
  }
2731
 
2732
+ async function boot() {
2733
+ beginLoadProgress("First load");
2734
+ try {
2735
+ installLoadProgressFetch();
2736
+ setStatus("Loading browser runtime");
2737
+ setLoadPhase("Loading browser runtime");
2738
+ const [{ init }, tokenizer] = await Promise.all([initEngine(), loadTokenizer()]);
2739
+ const bootRequest = readRequest();
2740
  const fastPathPrepared = await prepareInitialFastPath(bootRequest);
2741
  runButton.disabled = false;
2742
  setButtonDisabled(warmButton, false);
 
2747
  seedInputLine(),
2748
  `Tokenizer: ${tokenizer.constructor?.name || "loaded"}`,
2749
  `Full image backend: ${init.customKernels?.fullImageBackend || "unknown"}`,
2750
+ ]);
2751
+ if (!fastPathPrepared) {
2752
+ startBackgroundAssetPrepareForCurrentSettings(bootRequest);
2753
+ scheduleBackgroundStageAndVaePrepareForCurrentSettings(bootRequest);
2754
+ }
2755
+ if (shouldAutoWarmRequest(bootRequest)) startBackgroundWarmForCurrentSettings();
2756
+ finishLoadProgress(fastPathPrepared ? "Fast path prepared" : "Ready");
2757
+ } catch (err) {
2758
+ runButton.disabled = true;
2759
+ setButtonDisabled(warmButton, true);
2760
+ setStatus(err?.stack || err?.message || String(err), true);
2761
+ finishLoadProgress("Load failed", false);
2762
+ }
2763
+ }
2764
 
2765
  warmButton?.addEventListener("click", () => {
2766
  if (foregroundBusy) return;
static.html CHANGED
@@ -387,15 +387,85 @@
387
  font-weight: 750;
388
  min-width: 96px;
389
  }
390
- .elapsed-meter.active {
391
- border-color: var(--accent);
392
- color: var(--accent-strong);
393
- background: var(--accent-soft);
394
- }
395
- pre {
396
- margin: 16px 0 0;
397
- padding: 12px;
398
- min-height: 132px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  border: 1px solid var(--line);
400
  border-radius: var(--radius);
401
  background: var(--panel-2);
@@ -628,13 +698,26 @@
628
  <input id="decode-max" type="hidden" value="0">
629
  <input id="render-max" type="hidden" value="0">
630
  </div>
631
- <div class="actions">
632
- <button id="run" type="button" disabled><span class="icon">auto_awesome</span>Generate</button>
633
- <button id="abort" class="secondary" type="button" disabled><span class="icon">stop</span>Stop</button>
634
- <div id="elapsed" class="elapsed-meter" aria-live="polite">0.0s</div>
635
- </div>
636
- <pre id="status">Loading</pre>
637
- </aside>
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  <section>
639
  <div class="stage">
640
  <img id="image" alt="Generated image" hidden>
 
387
  font-weight: 750;
388
  min-width: 96px;
389
  }
390
+ .elapsed-meter.active {
391
+ border-color: var(--accent);
392
+ color: var(--accent-strong);
393
+ background: var(--accent-soft);
394
+ }
395
+ .load-progress {
396
+ margin: 14px 0 0;
397
+ padding: 12px;
398
+ border: 1px solid var(--line);
399
+ border-radius: var(--radius);
400
+ background: var(--panel-2);
401
+ }
402
+ .load-progress[hidden] {
403
+ display: none;
404
+ }
405
+ .load-progress-head,
406
+ .load-progress-meta {
407
+ display: flex;
408
+ align-items: center;
409
+ justify-content: space-between;
410
+ gap: 12px;
411
+ }
412
+ .load-progress-title {
413
+ min-width: 0;
414
+ overflow: hidden;
415
+ text-overflow: ellipsis;
416
+ white-space: nowrap;
417
+ font-size: 12px;
418
+ font-weight: 800;
419
+ color: var(--text);
420
+ }
421
+ .load-progress-summary {
422
+ flex: 0 0 auto;
423
+ color: var(--accent-strong);
424
+ font-size: 12px;
425
+ font-weight: 800;
426
+ }
427
+ .load-progress-track {
428
+ position: relative;
429
+ height: 9px;
430
+ margin: 10px 0 8px;
431
+ overflow: hidden;
432
+ border-radius: 999px;
433
+ background: color-mix(in srgb, var(--line) 55%, transparent);
434
+ }
435
+ .load-progress-fill {
436
+ width: 0%;
437
+ height: 100%;
438
+ border-radius: inherit;
439
+ background: var(--accent);
440
+ transition: width 160ms ease;
441
+ }
442
+ .load-progress-track.indeterminate .load-progress-fill {
443
+ width: 42%;
444
+ animation: load-progress-sweep 1.15s ease-in-out infinite;
445
+ }
446
+ @keyframes load-progress-sweep {
447
+ 0% { transform: translateX(-110%); }
448
+ 100% { transform: translateX(245%); }
449
+ }
450
+ .load-progress-meta {
451
+ color: var(--muted);
452
+ font: 11px/1.35 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
453
+ }
454
+ .load-progress-detail {
455
+ min-width: 0;
456
+ overflow: hidden;
457
+ text-overflow: ellipsis;
458
+ white-space: nowrap;
459
+ }
460
+ .load-progress-rate {
461
+ flex: 0 0 auto;
462
+ color: var(--muted);
463
+ white-space: nowrap;
464
+ }
465
+ pre {
466
+ margin: 16px 0 0;
467
+ padding: 12px;
468
+ min-height: 132px;
469
  border: 1px solid var(--line);
470
  border-radius: var(--radius);
471
  background: var(--panel-2);
 
698
  <input id="decode-max" type="hidden" value="0">
699
  <input id="render-max" type="hidden" value="0">
700
  </div>
701
+ <div class="actions">
702
+ <button id="run" type="button" disabled><span class="icon">auto_awesome</span>Generate</button>
703
+ <button id="abort" class="secondary" type="button" disabled><span class="icon">stop</span>Stop</button>
704
+ <div id="elapsed" class="elapsed-meter" aria-live="polite">0.0s</div>
705
+ </div>
706
+ <div id="load-progress" class="load-progress" aria-live="polite" hidden>
707
+ <div class="load-progress-head">
708
+ <div id="load-progress-title" class="load-progress-title">Loading</div>
709
+ <div id="load-progress-summary" class="load-progress-summary">0%</div>
710
+ </div>
711
+ <div id="load-progress-track" class="load-progress-track">
712
+ <div id="load-progress-fill" class="load-progress-fill"></div>
713
+ </div>
714
+ <div class="load-progress-meta">
715
+ <div id="load-progress-detail" class="load-progress-detail">Starting</div>
716
+ <div id="load-progress-rate" class="load-progress-rate">0 MiB/s</div>
717
+ </div>
718
+ </div>
719
+ <pre id="status">Loading</pre>
720
+ </aside>
721
  <section>
722
  <div class="stage">
723
  <img id="image" alt="Generated image" hidden>