Spaces:
Running
Running
File size: 6,518 Bytes
9c78030 c12f0b0 9c78030 c12f0b0 9c78030 c12f0b0 9c78030 5be91a0 c12f0b0 9c78030 8ff29b4 5be91a0 8ff29b4 5be91a0 8ff29b4 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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | // 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, SPRINT_LORA_DIR, SPRINT_LORA_LOCAL_BASE } from './hf.js'
export const IOS = isIOS()
export const IS_MOBILE = isMobile()
// Dev only: send just the LoRA's requests to a local server (SPRINT_LORA_LOCAL_BASE), leaving the
// base transformer / text encoder / VAE on the hub. Keyed on the directory name, so the cache key
// stays the hub URL and the browser's existing OPFS cache still hits for everything else.
function loraFetchImpl() {
if (!SPRINT_LORA_LOCAL_BASE) return undefined
const localRoot = SPRINT_LORA_LOCAL_BASE.replace(/\/+$/, '')
return (url, opts) => {
const s = typeof url === 'string' ? url : url.url
const i = s.indexOf(`/${SPRINT_LORA_DIR}/`)
return fetch(i >= 0 ? localRoot + s.slice(i) : url, opts)
}
}
// ?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 fetchImpl = loraFetchImpl()
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,
// The SANA-Sprint LoRA (the few-step distillation), applied at any step count; the engine
// downloads it (once, cached) the first time a run asks for it. Each generate() says whether
// it runs (`lora: true`) or the stock weights do (`lora: false`) — the switch in the prompt bar.
lora: SPRINT_LORA_DIR,
...(fetchImpl ? { fetchImpl } : {}),
// 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.
// `decoder` picks the VAE decoder per call: 'full' (default) or 'tiny' — fal's 2.9M
// FLUX.2 Tiny AutoEncoder, ~20× less decode work, slightly softer textures. `lora` picks the
// model: true = the SANA-Sprint LoRA on the base, false = the stock distilled weights.
export function generate(engine, { prompt, width, height, steps, seed, ref, decoder, lora }) {
return engine.klein.generate({
prompt,
width,
height,
steps,
seed,
reference: ref ?? null,
...(decoder ? { decoder } : {}),
...(lora !== undefined ? { lora } : {}),
})
}
|