Spaces:
Running
Running
Load the decision fine-tune (ONNX served from the Space), trained prompt format and scale, Egg check tab
d562f50 verified Download worker.js from FINAL-Bench/Decision-AI-Web: direct link, hf CLI and curl.
- Browser
- Download file 9.07 kB
-
https://huggingface.co/spaces/FINAL-Bench/Decision-AI-Web/resolve/main/worker.js
- Command line
-
hf download hf://spaces/FINAL-Bench/Decision-AI-Web/worker.js
-
curl -L -o worker.js https://huggingface.co/spaces/FINAL-Bench/Decision-AI-Web/resolve/main/worker.js
9.07 kB
| // SPDX-License-Identifier: Apache-2.0 | |
| // Embedding worker: loads a decision-tuned EmbeddingGemma 2 (ONNX, same graph layout as | |
| // onnx-community/embeddinggemma-2-ONNX) with transformers.js and returns L2-normalized | |
| // 768-d embeddings (mean pooling over the attention mask, then L2 normalization). | |
| // The model files are served next to this page (./models/...), so nothing is fetched from elsewhere. | |
| import { | |
| AutoConfig, | |
| AutoModel, | |
| AutoProcessor, | |
| RawImage, | |
| RawVideo, | |
| RawVideoFrame, | |
| env, | |
| } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.1/dist/transformers.min.js"; | |
| const MODEL_ID = "egemma2-decide"; | |
| env.allowLocalModels = true; | |
| env.allowRemoteModels = false; | |
| env.localModelPath = new URL("./models/", self.location.href).pathname; // same-origin path, e.g. "/models/" | |
| if (env.backends?.onnx?.wasm) env.backends.onnx.wasm.numThreads = 1; // static hosting is not cross-origin isolated | |
| let processor = null; | |
| let model = null; | |
| let loaded = null; // { device, dtype, audio } | |
| let decision = null; // { scale } from decision.json | |
| let queue = Promise.resolve(); | |
| const post = (msg, transfer) => self.postMessage(msg, transfer || []); | |
| const state = (phase, label, extra = {}) => post({ type: "state", phase, label, ...extra }); | |
| let gpuF16 = false; | |
| async function hasWebGPU() { | |
| try { | |
| if (!("gpu" in navigator)) return false; | |
| const adapter = await navigator.gpu.requestAdapter(); | |
| gpuF16 = !!adapter?.features?.has("shader-f16"); | |
| return !!adapter; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| async function loadModel({ device = "auto", dtype = "q4", audio = false }) { | |
| if (loaded && (device === "auto" || device === loaded.device) && dtype === loaded.requested && (!audio || loaded.audio)) { | |
| return loaded; | |
| } | |
| if (model) { | |
| try { await model.dispose(); } catch {} | |
| model = null; | |
| loaded = null; | |
| } | |
| let candidates; | |
| if (device === "auto") candidates = (await hasWebGPU()) ? ["webgpu", "wasm"] : ["wasm"]; | |
| else candidates = [device]; | |
| if (!decision) { | |
| try { decision = await (await fetch(`${env.localModelPath}${MODEL_ID}/decision.json`)).json(); } catch { decision = {}; } | |
| } | |
| if (!processor) { | |
| state("loading", "Loading processor"); | |
| processor = await AutoProcessor.from_pretrained(MODEL_ID); | |
| } | |
| let lastErr = null; | |
| const requested = dtype; | |
| if (candidates.includes("webgpu") && device !== "auto") await hasWebGPU(); | |
| for (const dev of candidates) { | |
| // Files shipped with this page (Space storage is limited to 1 GB): | |
| // q4: 4-bit weights with plain 8-bit gathers for the embedding and position tables, so the same files | |
| // run on WebGPU and on the WASM (CPU) runtime, which lacks GatherBlockQuantized; | |
| // q4f16: 4-bit weights with fp16 math (WebGPU with shader-f16 only); | |
| // audio: one 4-bit audio encoder used with both. | |
| const d = dev === "wasm" ? "q4" : requested === "auto" ? (gpuF16 ? "q4f16" : "q4") : requested; | |
| dtype = audio ? { model: d, vision_encoder: d, audio_encoder: "q4" } : d; | |
| try { | |
| const config = await AutoConfig.from_pretrained(MODEL_ID); | |
| if (!audio) config.audio_config = null; // text + vision only (video uses the vision encoder) | |
| state("loading", `Downloading model (${dev}${audio ? ", with audio encoder" : ""})`, { progress: 0, device: dev }); | |
| const t0 = performance.now(); | |
| model = await AutoModel.from_pretrained(MODEL_ID, { | |
| config, | |
| device: dev, | |
| dtype, | |
| progress_callback: (p) => { | |
| if (p.status === "progress_total") state("loading", `Downloading model (${dev})`, { progress: p.progress, device: dev, loadedBytes: p.loaded, totalBytes: p.total }); | |
| }, | |
| }); | |
| const sessionMs = performance.now() - t0; | |
| state("loading", "Warming up", { progress: 100, device: dev }); | |
| const tw = performance.now(); | |
| const warm = await processor(["task: classification | query: warm up"]); | |
| const out = await model(warm); | |
| out.sentence_embedding.dispose?.(); | |
| const warmMs = performance.now() - tw; | |
| const label = typeof dtype === "string" ? dtype : Object.entries(dtype).map(([k, v]) => `${k.replace("_encoder", "")} ${v}`).join(", "); | |
| loaded = { device: dev, dtype: label, requested, audio, loadMs: performance.now() - t0, sessionMs, warmMs, scale: decision?.scale || null }; | |
| state("ready", `Ready on ${dev.toUpperCase()} (${label})`, { device: dev, audio }); | |
| return loaded; | |
| } catch (e) { | |
| lastErr = e; | |
| console.warn(`Loading on ${dev} failed`, e); | |
| if (model) { try { await model.dispose(); } catch {} model = null; } | |
| state("loading", `Loading on ${dev} failed, trying next backend`, { device: dev }); | |
| } | |
| } | |
| state("error", `Could not load the model: ${lastErr?.message || lastErr}`); | |
| throw lastErr; | |
| } | |
| function rows(tensor) { | |
| const [n, d] = tensor.dims; | |
| const data = tensor.data; | |
| const out = []; | |
| for (let i = 0; i < n; i++) { | |
| const v = new Float32Array(d); | |
| let s = 0; | |
| for (let j = 0; j < d; j++) { v[j] = data[i * d + j]; s += v[j] * v[j]; } | |
| s = Math.sqrt(s) || 1; | |
| for (let j = 0; j < d; j++) v[j] /= s; | |
| out.push(v); | |
| } | |
| return out; | |
| } | |
| let lastModelMs = 0; | |
| async function run(inputs) { | |
| const tm = performance.now(); | |
| const out = await model(inputs); | |
| lastModelMs = performance.now() - tm; | |
| const emb = rows(out.sentence_embedding); | |
| try { out.sentence_embedding.dispose?.(); } catch {} | |
| return emb; | |
| } | |
| // texts: array of strings, embedded in small batches (right padding; padded tokens are masked out) | |
| async function embedTexts(texts) { | |
| const res = []; | |
| const B = 8; | |
| for (let i = 0; i < texts.length; i += B) { | |
| const batch = texts.slice(i, i + B); | |
| res.push(...(await run(await processor(batch)))); | |
| } | |
| return res; | |
| } | |
| // One input that may interleave text with an image, audio clip or video. | |
| // If the text already holds a placeholder (<|image|>, <|audio|>, <|video|>) it is used as is; | |
| // otherwise the placeholder is put first, as in the trained prompt format. Empty text embeds the media alone. | |
| async function embedInput(input) { | |
| const { kind } = input; | |
| const text = (input.text || "").trim(); | |
| const withPh = (ph) => (text.includes(ph) ? text : `${ph}${text}`); | |
| if (kind === "text" || !kind) { | |
| return (await run(await processor([text])))[0]; | |
| } | |
| if (kind === "image" || kind === "rgba") { | |
| if (processor.image_processor) processor.image_processor.max_soft_tokens = input.budget || 280; | |
| const img = kind === "rgba" | |
| ? new RawImage(new Uint8ClampedArray(input.data), input.width, input.height, 4) | |
| : await RawImage.fromBlob(input.blob); | |
| if (!text) return (await run(await processor(null, img)))[0]; | |
| return (await run(await processor([withPh("<|image|>")], [[img]])))[0]; | |
| } | |
| if (kind === "audio") { | |
| if (!loaded?.audio) throw new Error("The audio encoder is not loaded."); | |
| const audio = input.audio instanceof Float32Array ? input.audio : new Float32Array(input.audio); | |
| if (!text) return (await run(await processor(null, null, audio)))[0]; | |
| return (await run(await processor([withPh("<|audio|>")], null, [audio])))[0]; | |
| } | |
| if (kind === "video") { | |
| if (processor.image_processor) processor.image_processor.max_soft_tokens = input.budget || 280; | |
| const frames = input.frames.map((f) => new RawVideoFrame(new RawImage(new Uint8ClampedArray(f.data), f.width, f.height, 4), f.timestamp)); | |
| const video = new RawVideo(frames, input.duration); | |
| if (!text) return (await run(await processor(null, null, null, video)))[0]; | |
| return (await run(await processor([withPh("<|video|>")], null, null, [video])))[0]; | |
| } | |
| throw new Error(`Unknown input kind: ${kind}`); | |
| } | |
| async function handle(msg) { | |
| const { id, op } = msg; | |
| try { | |
| if (op === "load") { | |
| const info = await loadModel(msg.options || {}); | |
| post({ type: "result", id, result: info }); | |
| return; | |
| } | |
| if (!model) throw new Error("Model is not loaded yet."); | |
| const t0 = performance.now(); | |
| if (op === "embedTexts") { | |
| const embs = await embedTexts(msg.texts); | |
| post({ type: "result", id, result: { embeddings: embs, ms: performance.now() - t0 } }, embs.map((e) => e.buffer)); | |
| } else if (op === "embedInput") { | |
| const emb = await embedInput(msg.input); | |
| post({ type: "result", id, result: { embedding: emb, ms: performance.now() - t0, modelMs: lastModelMs } }, [emb.buffer]); | |
| } else if (op === "embedInputs") { | |
| // several prompts over the same media (one pass each); used by live cards in the trained format | |
| const embs = []; | |
| for (const inp of msg.inputs) embs.push(await embedInput(inp)); | |
| post({ type: "result", id, result: { embeddings: embs, ms: performance.now() - t0 } }, embs.map((e) => e.buffer)); | |
| } else { | |
| throw new Error(`Unknown op ${op}`); | |
| } | |
| } catch (e) { | |
| post({ type: "error", id, message: e?.message || String(e) }); | |
| } | |
| } | |
| self.addEventListener("message", ({ data }) => { | |
| // serialize all requests: one model, one queue | |
| queue = queue.then(() => handle(data)); | |
| }); | |