ryanhlewis commited on
Commit
5374c62
·
verified ·
1 Parent(s): cb1f0b2

Gate first model download behind start button

Browse files
Files changed (2) hide show
  1. static-app.js +222 -78
  2. static.html +45 -16
static-app.js CHANGED
@@ -1,14 +1,15 @@
1
- import { AutoTokenizer, env } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
2
-
3
- const $ = (id) => document.getElementById(id);
4
 
5
  const statusEl = $("status");
6
- const imageEl = $("image");
7
- const canvasEl = $("image-canvas");
8
- const placeholderEl = $("placeholder");
9
- const runButton = $("run");
10
- const warmButton = $("warm");
11
- const abortButton = $("abort");
 
 
 
12
  const elapsedEl = $("elapsed");
13
  const initImageInput = $("init-image");
14
  const initImageLabelEl = $("init-image-label");
@@ -53,10 +54,15 @@ const THEME_STORAGE_KEY = "flux2-static-theme";
53
  const GALLERY_LIMIT = 24;
54
  const DEFAULT_LOCAL_MODEL_BASE_URL = "/models";
55
  const DEFAULT_LOCAL_RUNTIME_BASE_URL = "/runtime";
56
-
57
- let tokenizerPromise = null;
58
- let engineReadyPromise = null;
59
- let currentImageUrl = null;
 
 
 
 
 
60
  let currentSourceUrl = null;
61
  let backgroundWarmPromise = null;
62
  let backgroundWarmKey = "";
@@ -82,7 +88,9 @@ let backgroundVaePrepareKey = "";
82
  let backgroundVaeEncoderPreparePromise = null;
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();
@@ -422,19 +430,72 @@ function setStatus(lines, isError = false) {
422
  statusEl.classList.toggle("error", Boolean(isError));
423
  }
424
 
425
- function setButtonDisabled(button, disabled) {
426
- if (button) button.disabled = disabled;
427
- }
428
-
429
- function setElapsed(text, active = false) {
430
- if (!elapsedEl) return;
431
- elapsedEl.textContent = text;
432
- elapsedEl.classList.toggle("active", active);
433
- }
434
-
435
- function updateRunTimer(prefix = "Generating") {
436
- if (!runTimerStarted) {
437
- setElapsed("0.0s");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  return 0;
439
  }
440
  const seconds = (performance.now() - runTimerStarted) / 1000;
@@ -641,12 +702,60 @@ function initTheme() {
641
  applyTheme(requestedTheme || "dark", { persist: false });
642
  }
643
 
644
- function isAbortError(err) {
645
- return String(err?.stack || err?.message || err || "").includes("ABORTED_BY_CLIENT");
646
- }
647
-
648
- function waitForEngine() {
649
- return new Promise((resolve, reject) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
650
  const deadline = performance.now() + 30000;
651
  const tick = () => {
652
  if (window.flux2Engine && typeof window.flux2Engine.init === "function") {
@@ -724,6 +833,7 @@ function maskFromLength(length, seqLen) {
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;
@@ -740,6 +850,14 @@ async function loadTokenizer() {
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);
@@ -779,6 +897,7 @@ async function tokenizePrompt(prompt, seqLen) {
779
  async function initEngine() {
780
  if (engineReadyPromise) return engineReadyPromise;
781
  engineReadyPromise = (async () => {
 
782
  const engine = await waitForEngine();
783
  const modelBaseUrl = configuredModelBaseUrl();
784
  const runtimeBaseUrl = configuredRuntimeBaseUrl(modelBaseUrl);
@@ -1418,9 +1537,10 @@ function shouldTextContextPrepareRequest(request) {
1418
  return request.transformerBackend === "custom-lowbit-webgpu";
1419
  }
1420
 
1421
- function startBackgroundTextContextPrepareForCurrentSettings(request = readRequest()) {
1422
- const prepareRequest = backgroundPrepareRequestForRequest(request);
1423
- if (!shouldTextContextPrepareRequest(prepareRequest)) return false;
 
1424
  const key = textContextPrepareKeyForRequest(prepareRequest);
1425
  if (backgroundTextContextPreparePromise && backgroundTextContextPrepareKey === key) return true;
1426
  backgroundTextContextPrepareKey = key;
@@ -1483,9 +1603,10 @@ function shouldTransformerDispatchWarmupRequest(request) {
1483
  return plannedMaxDimension <= 512;
1484
  }
1485
 
1486
- function startBackgroundTransformerSetupPrepareForCurrentSettings(request = readRequest()) {
1487
- const prepareRequest = backgroundPrepareRequestForRequest(request);
1488
- if (!shouldTransformerSetupPrepareRequest(prepareRequest)) return false;
 
1489
  const dispatchWarmup = shouldTransformerDispatchWarmupRequest(prepareRequest);
1490
  const key = `${transformerSetupPrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`;
1491
  if (backgroundTransformerSetupPreparePromise && backgroundTransformerSetupPrepareKey === key) return true;
@@ -1569,8 +1690,9 @@ function shouldAutoWarmRequest(request) {
1569
  maxDimension <= 256;
1570
  }
1571
 
1572
- function startBackgroundAssetPrepareForCurrentSettings(request = readRequest()) {
1573
- if (!shouldAssetPrepareRequest(request)) return false;
 
1574
  const key = assetPrepareKeyForRequest(request);
1575
  if (backgroundAssetPreparePromise && backgroundAssetPrepareKey === key) return true;
1576
  backgroundAssetPrepareKey = key;
@@ -1631,8 +1753,9 @@ function shouldVaeEncoderPrepareRequest(request) {
1631
  return true;
1632
  }
1633
 
1634
- function startBackgroundVaeEncoderPrepareForCurrentSettings(request = readRequest()) {
1635
- if (!shouldVaeEncoderPrepareRequest(request)) return false;
 
1636
  const key = vaeEncoderPrepareKeyForRequest(request);
1637
  if (backgroundVaeEncoderPreparePromise && backgroundVaeEncoderPrepareKey === key) return true;
1638
  backgroundVaeEncoderPrepareKey = key;
@@ -1695,8 +1818,9 @@ async function waitForStartedPreparePromises() {
1695
  }));
1696
  }
1697
 
1698
- function startStageAndVaePrepareForRequest(request) {
1699
- const startedText = startBackgroundTextContextPrepareForCurrentSettings(request);
 
1700
  const startedTransformer = startBackgroundTransformerSetupPrepareForCurrentSettings(request);
1701
  if (request.hasInitImage && shouldVaeEncoderPrepareRequest(request)) {
1702
  const startedVaeEncoder = startBackgroundVaeEncoderPrepareForCurrentSettings(request);
@@ -1706,8 +1830,9 @@ function startStageAndVaePrepareForRequest(request) {
1706
  return startedText || startedTransformer || startedVae;
1707
  }
1708
 
1709
- function startBackgroundVaePrepareForCurrentSettings(request = readRequest()) {
1710
- const prepareRequest = backgroundPrepareRequestForRequest(request);
 
1711
  if (!shouldVaePrepareRequest(prepareRequest)) return false;
1712
  const dispatchWarmup = shouldVaeDispatchWarmupRequest(prepareRequest);
1713
  const key = `${vaePrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`;
@@ -1770,8 +1895,9 @@ function cancelBackgroundWarm(message = "") {
1770
  return true;
1771
  }
1772
 
1773
- function startBackgroundWarmForCurrentSettings() {
1774
- if (foregroundBusy) return false;
 
1775
  const request = readRequest();
1776
  if (!shouldAutoWarmRequest(request)) return false;
1777
  const key = warmKeyForRequest(request);
@@ -1809,8 +1935,9 @@ function startBackgroundWarmForCurrentSettings() {
1809
  return true;
1810
  }
1811
 
1812
- function scheduleBackgroundStageAndVaePrepareForCurrentSettings(request = readRequest(), delayMs = 750) {
1813
- if (foregroundBusy || urlFlag("noAutowarm")) return false;
 
1814
  if (backgroundDeepPrepareTimer) {
1815
  clearTimeout(backgroundDeepPrepareTimer);
1816
  backgroundDeepPrepareTimer = 0;
@@ -1847,9 +1974,10 @@ function scheduleBackgroundStageAndVaePrepareForCurrentSettings(request = readRe
1847
  return true;
1848
  }
1849
 
1850
- function scheduleAutoWarmForCurrentSettings(delayMs = 350) {
1851
- clearWarmSchedule();
1852
- if (foregroundBusy || urlFlag("noAutowarm")) return;
 
1853
  const request = readRequest();
1854
  startBackgroundAssetPrepareForCurrentSettings(request);
1855
  scheduleBackgroundStageAndVaePrepareForCurrentSettings(request, request.hasInitImage ? delayMs : 750);
@@ -2344,9 +2472,10 @@ function displayGenerationResult(result, elapsed, seqLen, heading = "Elapsed", e
2344
  }
2345
  }
2346
 
2347
- async function generate() {
2348
- const requestedStarted = performance.now();
2349
- const request = readRequest();
 
2350
  runButton.disabled = true;
2351
  setButtonDisabled(warmButton, true);
2352
  abortButton.disabled = false;
@@ -2621,8 +2750,9 @@ async function waitForBackgroundVaePrepare(request) {
2621
  return (performance.now() - started) / 1000;
2622
  }
2623
 
2624
- async function warmCurrentSettings({ background = false } = {}) {
2625
- const request = readRequest();
 
2626
  const { warmRequest, warmProfile } = warmPlanForRequest(request);
2627
  const { engine, init } = await initEngine();
2628
  const started = performance.now();
@@ -2730,19 +2860,20 @@ async function prepareInitialFastPath(request) {
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);
2743
- setStatus([
2744
- "Ready",
2745
- fastPathPrepared ? "Fast path prepared" : "",
2746
  `Model: ${init.config.model_variant || init.config.model_name}`,
2747
  seedInputLine(),
2748
  `Tokenizer: ${tokenizer.constructor?.name || "loaded"}`,
@@ -2754,18 +2885,31 @@ async function boot() {
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;
2767
- clearWarmSchedule();
2768
- foregroundBusy = true;
 
 
 
 
 
 
2769
  warmCurrentSettings().catch((err) => {
2770
  if (isAbortError(err)) return;
2771
  placeholderEl.hidden = false;
@@ -2779,9 +2923,9 @@ warmButton?.addEventListener("click", () => {
2779
  });
2780
  });
2781
 
2782
- runButton.addEventListener("click", () => {
2783
- if (foregroundBusy) return;
2784
- clearWarmSchedule();
2785
  foregroundBusy = true;
2786
  startRunTimer();
2787
  generate().catch((err) => {
@@ -2961,7 +3105,7 @@ galleryGridEl?.addEventListener("click", (event) => {
2961
  });
2962
  });
2963
 
2964
- initTheme();
2965
- applyUrlDefaults();
2966
- updateStrengthDisplay();
2967
- boot();
 
1
+ const $ = (id) => document.getElementById(id);
 
 
2
 
3
  const statusEl = $("status");
4
+ const imageEl = $("image");
5
+ const canvasEl = $("image-canvas");
6
+ const placeholderEl = $("placeholder");
7
+ const downloadGateEl = $("download-gate");
8
+ const startDownloadButton = $("start-download");
9
+ const actionsEl = $("actions");
10
+ const runButton = $("run");
11
+ const warmButton = $("warm");
12
+ const abortButton = $("abort");
13
  const elapsedEl = $("elapsed");
14
  const initImageInput = $("init-image");
15
  const initImageLabelEl = $("init-image-label");
 
54
  const GALLERY_LIMIT = 24;
55
  const DEFAULT_LOCAL_MODEL_BASE_URL = "/models";
56
  const DEFAULT_LOCAL_RUNTIME_BASE_URL = "/runtime";
57
+ const DEFAULT_FIRST_LOAD_DOWNLOAD_LABEL = "about 3.8 GB / 3.5 GiB";
58
+ const FULL_MODEL_REPO_DOWNLOAD_LABEL = "12.6 GB";
59
+
60
+ let tokenizerPromise = null;
61
+ let transformersModulePromise = null;
62
+ let engineReadyPromise = null;
63
+ let engineModulePromise = null;
64
+ let customRuntimeScriptsPromise = null;
65
+ let currentImageUrl = null;
66
  let currentSourceUrl = null;
67
  let backgroundWarmPromise = null;
68
  let backgroundWarmKey = "";
 
88
  let backgroundVaeEncoderPreparePromise = null;
89
  let backgroundVaeEncoderPrepareKey = "";
90
  let backgroundDeepPrepareTimer = 0;
91
+ let backgroundDeepPrepareToken = 0;
92
+ let bootStarted = false;
93
+ let bootReady = false;
94
  const preparedInitImageCache = new WeakMap();
95
  const generationResultCache = new Map();
96
  const tokenizedPromptCache = new Map();
 
430
  statusEl.classList.toggle("error", Boolean(isError));
431
  }
432
 
433
+ function setButtonDisabled(button, disabled) {
434
+ if (button) button.disabled = disabled;
435
+ }
436
+
437
+ function setElapsed(text, active = false) {
438
+ if (!elapsedEl) return;
439
+ elapsedEl.textContent = text;
440
+ elapsedEl.classList.toggle("active", active);
441
+ }
442
+
443
+ function setActionsVisible(visible) {
444
+ if (actionsEl) actionsEl.hidden = !visible;
445
+ }
446
+
447
+ function setDownloadGateVisible(visible) {
448
+ if (downloadGateEl) downloadGateEl.hidden = !visible;
449
+ }
450
+
451
+ function canRunRuntimeWork() {
452
+ return bootStarted;
453
+ }
454
+
455
+ function showDownloadGate({ retry = false, preserveStatus = false } = {}) {
456
+ bootReady = false;
457
+ setDownloadGateVisible(true);
458
+ setActionsVisible(false);
459
+ if (startDownloadButton) startDownloadButton.disabled = false;
460
+ runButton.disabled = true;
461
+ setButtonDisabled(warmButton, true);
462
+ abortButton.disabled = true;
463
+ setElapsed("0.0s");
464
+ placeholderEl.hidden = false;
465
+ placeholderEl.textContent = "Model not loaded";
466
+ if (!preserveStatus) {
467
+ setStatus([
468
+ retry ? "Download failed" : "Ready to download",
469
+ `Default fast path: ${DEFAULT_FIRST_LOAD_DOWNLOAD_LABEL}`,
470
+ `Full model repo: ${FULL_MODEL_REPO_DOWNLOAD_LABEL}`,
471
+ ], retry);
472
+ }
473
+ }
474
+
475
+ function showRuntimeLoading() {
476
+ setDownloadGateVisible(false);
477
+ setActionsVisible(false);
478
+ if (startDownloadButton) startDownloadButton.disabled = true;
479
+ runButton.disabled = true;
480
+ setButtonDisabled(warmButton, true);
481
+ abortButton.disabled = true;
482
+ setElapsed("0.0s");
483
+ placeholderEl.hidden = false;
484
+ placeholderEl.textContent = "Loading";
485
+ }
486
+
487
+ function showRuntimeReady() {
488
+ bootReady = true;
489
+ setDownloadGateVisible(false);
490
+ setActionsVisible(true);
491
+ runButton.disabled = false;
492
+ setButtonDisabled(warmButton, false);
493
+ abortButton.disabled = true;
494
+ }
495
+
496
+ function updateRunTimer(prefix = "Generating") {
497
+ if (!runTimerStarted) {
498
+ setElapsed("0.0s");
499
  return 0;
500
  }
501
  const seconds = (performance.now() - runTimerStarted) / 1000;
 
702
  applyTheme(requestedTheme || "dark", { persist: false });
703
  }
704
 
705
+ function isAbortError(err) {
706
+ return String(err?.stack || err?.message || err || "").includes("ABORTED_BY_CLIENT");
707
+ }
708
+
709
+ function loadClassicScript(src) {
710
+ return new Promise((resolve, reject) => {
711
+ const existing = Array.from(document.scripts).find((script) => script.dataset.flux2Src === src);
712
+ if (existing?.dataset.loaded === "true") {
713
+ resolve();
714
+ return;
715
+ }
716
+ if (existing) existing.remove();
717
+ const script = document.createElement("script");
718
+ script.src = src;
719
+ script.dataset.flux2Src = src;
720
+ script.async = false;
721
+ script.onload = () => {
722
+ script.dataset.loaded = "true";
723
+ resolve();
724
+ };
725
+ script.onerror = () => reject(new Error(`Failed to load ${src}`));
726
+ if (!existing) document.body.appendChild(script);
727
+ });
728
+ }
729
+
730
+ async function loadCustomRuntimeScripts() {
731
+ if (!customRuntimeScriptsPromise) {
732
+ customRuntimeScriptsPromise = (async () => {
733
+ setLoadPhase("Loading custom WebGPU kernels");
734
+ await loadClassicScript("tools/custom_lowbit_webgpu/custom_lowbit_linear.js");
735
+ await loadClassicScript("tools/custom_lowbit_webgpu/custom_transformer_runtime.js");
736
+ })();
737
+ }
738
+ return customRuntimeScriptsPromise;
739
+ }
740
+
741
+ async function loadEngineModule() {
742
+ if (!engineModulePromise) {
743
+ setLoadPhase("Loading browser engine");
744
+ engineModulePromise = import("./flux2-engine.js");
745
+ }
746
+ return engineModulePromise;
747
+ }
748
+
749
+ function resetRuntimeLoadPromises() {
750
+ engineReadyPromise = null;
751
+ tokenizerPromise = null;
752
+ if (!window.flux2Engine) engineModulePromise = null;
753
+ if (typeof globalThis.createCustomFluxTransformerRuntime !== "function") customRuntimeScriptsPromise = null;
754
+ transformersModulePromise = null;
755
+ }
756
+
757
+ function waitForEngine() {
758
+ return new Promise((resolve, reject) => {
759
  const deadline = performance.now() + 30000;
760
  const tick = () => {
761
  if (window.flux2Engine && typeof window.flux2Engine.init === "function") {
 
833
  async function loadTokenizer() {
834
  if (tokenizerPromise) return tokenizerPromise;
835
  setLoadPhase("Loading tokenizer");
836
+ const { AutoTokenizer, env } = await loadTransformersModule();
837
  const modelBaseUrl = configuredModelBaseUrl();
838
  const tokenizerSpec = tokenizerLoadSpec(modelBaseUrl);
839
  env.allowLocalModels = !tokenizerSpec.remote;
 
850
  return tokenizerPromise;
851
  }
852
 
853
+ async function loadTransformersModule() {
854
+ if (!transformersModulePromise) {
855
+ setLoadPhase("Loading tokenizer runtime");
856
+ transformersModulePromise = import("https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1");
857
+ }
858
+ return transformersModulePromise;
859
+ }
860
+
861
  async function tokenizePrompt(prompt, seqLen) {
862
  const cachePayload = JSON.stringify({ model: "flux.2-klein-4b", text_seq_len: seqLen, prompt });
863
  const promptCacheKey = await sha256Hex(cachePayload);
 
897
  async function initEngine() {
898
  if (engineReadyPromise) return engineReadyPromise;
899
  engineReadyPromise = (async () => {
900
+ await Promise.all([loadCustomRuntimeScripts(), loadEngineModule()]);
901
  const engine = await waitForEngine();
902
  const modelBaseUrl = configuredModelBaseUrl();
903
  const runtimeBaseUrl = configuredRuntimeBaseUrl(modelBaseUrl);
 
1537
  return request.transformerBackend === "custom-lowbit-webgpu";
1538
  }
1539
 
1540
+ function startBackgroundTextContextPrepareForCurrentSettings(request = readRequest()) {
1541
+ if (!canRunRuntimeWork()) return false;
1542
+ const prepareRequest = backgroundPrepareRequestForRequest(request);
1543
+ if (!shouldTextContextPrepareRequest(prepareRequest)) return false;
1544
  const key = textContextPrepareKeyForRequest(prepareRequest);
1545
  if (backgroundTextContextPreparePromise && backgroundTextContextPrepareKey === key) return true;
1546
  backgroundTextContextPrepareKey = key;
 
1603
  return plannedMaxDimension <= 512;
1604
  }
1605
 
1606
+ function startBackgroundTransformerSetupPrepareForCurrentSettings(request = readRequest()) {
1607
+ if (!canRunRuntimeWork()) return false;
1608
+ const prepareRequest = backgroundPrepareRequestForRequest(request);
1609
+ if (!shouldTransformerSetupPrepareRequest(prepareRequest)) return false;
1610
  const dispatchWarmup = shouldTransformerDispatchWarmupRequest(prepareRequest);
1611
  const key = `${transformerSetupPrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`;
1612
  if (backgroundTransformerSetupPreparePromise && backgroundTransformerSetupPrepareKey === key) return true;
 
1690
  maxDimension <= 256;
1691
  }
1692
 
1693
+ function startBackgroundAssetPrepareForCurrentSettings(request = readRequest()) {
1694
+ if (!canRunRuntimeWork()) return false;
1695
+ if (!shouldAssetPrepareRequest(request)) return false;
1696
  const key = assetPrepareKeyForRequest(request);
1697
  if (backgroundAssetPreparePromise && backgroundAssetPrepareKey === key) return true;
1698
  backgroundAssetPrepareKey = key;
 
1753
  return true;
1754
  }
1755
 
1756
+ function startBackgroundVaeEncoderPrepareForCurrentSettings(request = readRequest()) {
1757
+ if (!canRunRuntimeWork()) return false;
1758
+ if (!shouldVaeEncoderPrepareRequest(request)) return false;
1759
  const key = vaeEncoderPrepareKeyForRequest(request);
1760
  if (backgroundVaeEncoderPreparePromise && backgroundVaeEncoderPrepareKey === key) return true;
1761
  backgroundVaeEncoderPrepareKey = key;
 
1818
  }));
1819
  }
1820
 
1821
+ function startStageAndVaePrepareForRequest(request) {
1822
+ if (!canRunRuntimeWork()) return false;
1823
+ const startedText = startBackgroundTextContextPrepareForCurrentSettings(request);
1824
  const startedTransformer = startBackgroundTransformerSetupPrepareForCurrentSettings(request);
1825
  if (request.hasInitImage && shouldVaeEncoderPrepareRequest(request)) {
1826
  const startedVaeEncoder = startBackgroundVaeEncoderPrepareForCurrentSettings(request);
 
1830
  return startedText || startedTransformer || startedVae;
1831
  }
1832
 
1833
+ function startBackgroundVaePrepareForCurrentSettings(request = readRequest()) {
1834
+ if (!canRunRuntimeWork()) return false;
1835
+ const prepareRequest = backgroundPrepareRequestForRequest(request);
1836
  if (!shouldVaePrepareRequest(prepareRequest)) return false;
1837
  const dispatchWarmup = shouldVaeDispatchWarmupRequest(prepareRequest);
1838
  const key = `${vaePrepareKeyForRequest(prepareRequest)}|dispatch=${dispatchWarmup ? 1 : 0}`;
 
1895
  return true;
1896
  }
1897
 
1898
+ function startBackgroundWarmForCurrentSettings() {
1899
+ if (!canRunRuntimeWork()) return false;
1900
+ if (foregroundBusy) return false;
1901
  const request = readRequest();
1902
  if (!shouldAutoWarmRequest(request)) return false;
1903
  const key = warmKeyForRequest(request);
 
1935
  return true;
1936
  }
1937
 
1938
+ function scheduleBackgroundStageAndVaePrepareForCurrentSettings(request = readRequest(), delayMs = 750) {
1939
+ if (!canRunRuntimeWork()) return false;
1940
+ if (foregroundBusy || urlFlag("noAutowarm")) return false;
1941
  if (backgroundDeepPrepareTimer) {
1942
  clearTimeout(backgroundDeepPrepareTimer);
1943
  backgroundDeepPrepareTimer = 0;
 
1974
  return true;
1975
  }
1976
 
1977
+ function scheduleAutoWarmForCurrentSettings(delayMs = 350) {
1978
+ clearWarmSchedule();
1979
+ if (!canRunRuntimeWork()) return;
1980
+ if (foregroundBusy || urlFlag("noAutowarm")) return;
1981
  const request = readRequest();
1982
  startBackgroundAssetPrepareForCurrentSettings(request);
1983
  scheduleBackgroundStageAndVaePrepareForCurrentSettings(request, request.hasInitImage ? delayMs : 750);
 
2472
  }
2473
  }
2474
 
2475
+ async function generate() {
2476
+ if (!bootReady) throw new Error("Model assets are not loaded yet.");
2477
+ const requestedStarted = performance.now();
2478
+ const request = readRequest();
2479
  runButton.disabled = true;
2480
  setButtonDisabled(warmButton, true);
2481
  abortButton.disabled = false;
 
2750
  return (performance.now() - started) / 1000;
2751
  }
2752
 
2753
+ async function warmCurrentSettings({ background = false } = {}) {
2754
+ if (!canRunRuntimeWork()) throw new Error("Model assets are not loaded yet.");
2755
+ const request = readRequest();
2756
  const { warmRequest, warmProfile } = warmPlanForRequest(request);
2757
  const { engine, init } = await initEngine();
2758
  const started = performance.now();
 
2860
  }
2861
 
2862
  async function boot() {
2863
+ if (!bootStarted) bootStarted = true;
2864
+ showRuntimeLoading();
2865
+ beginLoadProgress("Downloading assets");
2866
  try {
2867
  installLoadProgressFetch();
2868
  setStatus("Loading browser runtime");
2869
  setLoadPhase("Loading browser runtime");
2870
  const [{ init }, tokenizer] = await Promise.all([initEngine(), loadTokenizer()]);
2871
  const bootRequest = readRequest();
2872
+ const fastPathPrepared = await prepareInitialFastPath(bootRequest);
2873
+ showRuntimeReady();
2874
+ setStatus([
2875
+ "Ready",
2876
+ fastPathPrepared ? "Fast path prepared" : "",
 
2877
  `Model: ${init.config.model_variant || init.config.model_name}`,
2878
  seedInputLine(),
2879
  `Tokenizer: ${tokenizer.constructor?.name || "loaded"}`,
 
2885
  }
2886
  if (shouldAutoWarmRequest(bootRequest)) startBackgroundWarmForCurrentSettings();
2887
  finishLoadProgress(fastPathPrepared ? "Fast path prepared" : "Ready");
2888
+ return true;
2889
  } catch (err) {
2890
+ bootStarted = false;
2891
+ bootReady = false;
2892
+ resetRuntimeLoadPromises();
2893
  runButton.disabled = true;
2894
  setButtonDisabled(warmButton, true);
2895
+ abortButton.disabled = true;
2896
  setStatus(err?.stack || err?.message || String(err), true);
2897
  finishLoadProgress("Load failed", false);
2898
+ showDownloadGate({ retry: true, preserveStatus: true });
2899
+ return false;
2900
  }
2901
  }
2902
+
2903
+ startDownloadButton?.addEventListener("click", () => {
2904
+ if (bootStarted) return;
2905
+ bootStarted = true;
2906
+ void boot();
2907
+ });
2908
+
2909
+ warmButton?.addEventListener("click", () => {
2910
+ if (!bootReady || foregroundBusy) return;
2911
+ clearWarmSchedule();
2912
+ foregroundBusy = true;
2913
  warmCurrentSettings().catch((err) => {
2914
  if (isAbortError(err)) return;
2915
  placeholderEl.hidden = false;
 
2923
  });
2924
  });
2925
 
2926
+ runButton.addEventListener("click", () => {
2927
+ if (!bootReady || foregroundBusy) return;
2928
+ clearWarmSchedule();
2929
  foregroundBusy = true;
2930
  startRunTimer();
2931
  generate().catch((err) => {
 
3105
  });
3106
  });
3107
 
3108
+ initTheme();
3109
+ applyUrlDefaults();
3110
+ updateStrengthDisplay();
3111
+ showDownloadGate();
static.html CHANGED
@@ -337,17 +337,44 @@
337
  gap: 10px;
338
  }
339
  .wide { grid-column: 1 / -1; }
340
- .actions {
341
- display: flex;
342
- gap: 10px;
343
- margin-top: 18px;
344
- align-items: center;
345
- flex-wrap: wrap;
346
- }
347
- button {
348
- min-height: 42px;
349
- border: 0;
350
- border-radius: var(--radius);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  padding: 0 14px;
352
  background: var(--accent);
353
  color: #061715;
@@ -698,7 +725,12 @@
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>
@@ -732,11 +764,8 @@
732
  <div id="gallery-grid" class="gallery-grid"></div>
733
  </div>
734
  </section>
735
- </main>
736
  <script src="space-config.js"></script>
737
- <script src="tools/custom_lowbit_webgpu/custom_lowbit_linear.js"></script>
738
- <script src="tools/custom_lowbit_webgpu/custom_transformer_runtime.js"></script>
739
- <script type="module" src="flux2-engine.js"></script>
740
  <script type="module" src="static-app.js"></script>
741
  </body>
742
  </html>
 
337
  gap: 10px;
338
  }
339
  .wide { grid-column: 1 / -1; }
340
+ .actions {
341
+ display: flex;
342
+ gap: 10px;
343
+ margin-top: 18px;
344
+ align-items: center;
345
+ flex-wrap: wrap;
346
+ }
347
+ .actions[hidden] {
348
+ display: none;
349
+ }
350
+ .download-gate {
351
+ margin-top: 18px;
352
+ padding: 14px;
353
+ border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line));
354
+ border-radius: var(--radius);
355
+ background: color-mix(in srgb, var(--accent-soft) 74%, var(--panel-2));
356
+ }
357
+ .download-gate[hidden] {
358
+ display: none;
359
+ }
360
+ .download-gate-title {
361
+ font-size: 13px;
362
+ font-weight: 850;
363
+ color: var(--text);
364
+ }
365
+ .download-gate-copy {
366
+ margin: 7px 0 12px;
367
+ color: var(--muted);
368
+ font-size: 12px;
369
+ line-height: 1.45;
370
+ }
371
+ .download-gate button {
372
+ width: 100%;
373
+ }
374
+ button {
375
+ min-height: 42px;
376
+ border: 0;
377
+ border-radius: var(--radius);
378
  padding: 0 14px;
379
  background: var(--accent);
380
  color: #061715;
 
725
  <input id="decode-max" type="hidden" value="0">
726
  <input id="render-max" type="hidden" value="0">
727
  </div>
728
+ <div id="download-gate" class="download-gate">
729
+ <div class="download-gate-title">Download model assets</div>
730
+ <div class="download-gate-copy">Default fast path downloads about 3.8 GB / 3.5 GiB before the first generation. The full public model repo is 12.6 GB; ONNX fallback modes can fetch more later.</div>
731
+ <button id="start-download" type="button"><span class="icon">download</span>Start download</button>
732
+ </div>
733
+ <div id="actions" class="actions" hidden>
734
  <button id="run" type="button" disabled><span class="icon">auto_awesome</span>Generate</button>
735
  <button id="abort" class="secondary" type="button" disabled><span class="icon">stop</span>Stop</button>
736
  <div id="elapsed" class="elapsed-meter" aria-live="polite">0.0s</div>
 
764
  <div id="gallery-grid" class="gallery-grid"></div>
765
  </div>
766
  </section>
767
+ </main>
768
  <script src="space-config.js"></script>
 
 
 
769
  <script type="module" src="static-app.js"></script>
770
  </body>
771
  </html>