File size: 5,058 Bytes
9c78030
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// The engine is the flux-klein.js package (npm).
// It picks one of two pipelines behind one entry point:
//   desktop — int8 transformer resident on the GPU (~4 GB), fastest
//   mobile  — int4 shards streamed through a small GPU ring (~0.6 GB), built to survive the
//             iPhone's 2 GB tab cap; also fine on any low-memory device
// This module is the page's view of it: which engine to pick, the cache probe that runs before
// anything else is loaded, and one status line per progress event.
import { isIOS, isMobile } from 'flux-klein.js/platform'
import { formatProgress, progressFraction } from 'flux-klein.js/progress'
import { defaultCache } from 'flux-klein.js/cache'
import { MAX_AREA, MAX_AREA_ONE_THING, MAX_SIDE, maxAreaFor } from 'flux-klein.js/limits'
// onnxruntime-web's wasm runtime as Vite assets, so it is served from here rather than from the
// package's default (the jsDelivr CDN). Both files are subpath exports of onnxruntime-web.
import ortMjs from 'onnxruntime-web/ort-wasm-simd-threaded.mjs?url'
import ortWasm from 'onnxruntime-web/ort-wasm-simd-threaded.wasm?url'
import { HF_BASE } from './hf.js'

export const IOS = isIOS()
export const IS_MOBILE = isMobile()

// ?engine=mobile|desktop overrides the detection (e.g. to try the streaming engine on a laptop).
export function pickEngine() {
  const q = new URLSearchParams(location.search).get('engine')
  return q === 'mobile' || q === 'desktop' ? q : IS_MOBILE ? 'mobile' : 'desktop'
}

const ROOT = new URL(import.meta.env.BASE_URL, location.href).href // where public/ is served

// The limits the engine will enforce, known before it loads: each side up to MAX_SIDE, and an
// area budget — the phone build on an iPhone (one heavy thing in memory at a time) caps it at
// MAX_AREA_ONE_THING, everything else at what the GPU can bind in one attention buffer
// (flux-klein.js/limits maxAreaFor), MAX_AREA at most. Knowing them up front keeps the size menu
// from offering presets the first run would refuse; the loaded engine confirms them (loadEngine).
const F16 = () => new URLSearchParams(location.search).get('f16') !== '0'
export const limitsFor = (kind, gpuLimits = null) => ({
  maxSide: MAX_SIDE,
  maxArea: maxAreaFor(gpuLimits, {
    es: F16() ? 2 : 4,
    cap: kind === 'mobile' && IOS ? MAX_AREA_ONE_THING : MAX_AREA,
  }),
})
// The same, refined with the adapter's limits. An adapter is a handle, not a device: no GPU
// memory is taken. Falls back to the static guess where WebGPU is missing.
export async function probeLimits(kind) {
  try {
    const adapter = await navigator.gpu?.requestAdapter()
    return limitsFor(kind, adapter?.limits ?? null)
  } catch {
    return limitsFor(kind)
  }
}

// The weights already in this browser, and wiping them — both without loading the engine
// (flux-klein.js/cache reads OPFS directory metadata only: no weights, no GPU device).
export const cacheInfo = () => defaultCache.cacheInfo()
export const clearCache = () => defaultCache.clearCache()

// `onStatus(text, fraction)`: one status line at a time, with a 0…1 fraction while the line
// carries one (download, upload, denoise) or null. The package reports { stage, detail } events
// (flux-klein.js/progress); the wording and the fraction come from there, not from regexes.
export async function loadEngine(kind, onStatus) {
  // The package and the tokenizer class come in only now: nothing loads before the first Generate.
  const [{ createFluxKlein }, { PreTrainedTokenizer }] = await Promise.all([
    import('flux-klein.js'),
    import('@huggingface/transformers'),
  ])
  const klein = await createFluxKlein({
    mode: kind,
    base: HF_BASE,
    tokenizerUrl: ROOT + 'tokenizer/',
    f16: F16(),
    // Absolute: the mobile build hands these to its ONNX workers, whose base URL is not the page.
    wasmPaths: {
      mjs: new URL(ortMjs, location.href).href,
      wasm: new URL(ortWasm, location.href).href,
    },
    PreTrainedTokenizer,
    // Threads need cross-origin isolation; inside the hub's iframe that is never granted, and the
    // package falls back to one thread on its own.
    onEvent: (ev) => onStatus(formatProgress(ev), progressFraction(ev)),
  })
  addEventListener('pagehide', () => klein.destroy(), { once: true })
  return {
    kind: klein.mode,
    klein,
    f16: klein.f16,
    maxSide: klein.limits.maxSide, // per side 128…maxSide, in steps of 16
    maxArea: klein.limits.maxArea, // the phone build caps the area below maxSide²
    cacheInfo: () => klein.cacheInfo(),
    clearCache: () => klein.clearCache(),
  }
}

// Generate one image. `ref` (ImageBitmap) turns generation into an edit of that image: the package
// cover-fits it to a square, encodes it with the VAE (at 256² unless the output is a 128² or 256²
// square — enc512 overflows ORT-web's shape math) and the prompt describes the change.
export function generate(engine, { prompt, width, height, steps, seed, ref }) {
  return engine.klein.generate({ prompt, width, height, steps, seed, reference: ref ?? null })
}