Download browseml.js from PYTHAI/bankml: direct link, hf CLI and curl.
- Browser
- Download file 6.83 kB
-
https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/browseml.js
- Command line
-
hf download hf://spaces/PYTHAI/bankml/browseml.js
-
curl -L -o browseml.js https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/browseml.js
6.83 kB
| // SPDX-License-Identifier: MIT OR Apache-2.0 | |
| // browseML — bankML in this browser. The model is downloaded once from its Hugging Face repository at a pinned | |
| // revision and kept in this browser's Cache Storage; bankML's own engine, compiled to WebAssembly, verifies it | |
| // against its FORK.json (the guard, then the sha256 pin) before it answers, and puts a receipt on every answer — | |
| // the same receipt `bankml serve` gives. The visitor's CPU does the work: no server, no account, no quota. | |
| export const MODEL = { | |
| name: "Bonsai-1.7B-Q1_0.gguf", | |
| title: "Bonsai-1.7B · 1-bit (Q1_0)", | |
| bytes: 248302272, | |
| url: "https://huggingface.co/prism-ml/Bonsai-1.7B-gguf/resolve/210a9e99f79cb184909d49595906526eb2b3dd9a/Bonsai-1.7B-Q1_0.gguf", | |
| fork: "browseML/Bonsai-1.7B-Q1_0.gguf.FORK.json", | |
| }; | |
| const CACHE = "browseml-models"; | |
| let worker = null, ready = null, busy = null; | |
| /** The engine's WebAssembly memory after its last open or answer, in bytes (0 when not loaded). */ | |
| export let memoryBytes = 0; | |
| /** The most threads the engine takes, however many cores the browser reports. */ | |
| export const MAX_THREADS = 8; | |
| /** Threads for the engine: every core this browser reports (up to MAX_THREADS), when the page is cross-origin | |
| * isolated (the Space sends COOP and COEP); otherwise one. Also the most a visitor can choose. */ | |
| export const threads = () => (globalThis.crossOriginIsolated && typeof SharedArrayBuffer !== "undefined" ? Math.max(1, Math.min(MAX_THREADS, navigator.hardwareConcurrency || 1)) : 1); | |
| /** The model in this browser's cache, or null. */ | |
| async function cached() { | |
| try { const c = await caches.open(CACHE); const r = await c.match(MODEL.url); return r ? await r.arrayBuffer() : null; } catch { return null; } | |
| } | |
| /** Download the model (or take it from the cache), with progress as (bytes, total, phase). */ | |
| async function fetchModel(progress) { | |
| const kept = await cached(); | |
| if (kept && kept.byteLength === MODEL.bytes) { progress(kept.byteLength, MODEL.bytes, "cache"); return kept; } | |
| const r = await fetch(MODEL.url); | |
| if (!r.ok) throw new Error(`the model could not be downloaded: HTTP ${r.status}`); | |
| const total = +r.headers.get("Content-Length") || MODEL.bytes; | |
| const buf = new Uint8Array(total); | |
| const reader = r.body.getReader(); | |
| let got = 0; | |
| for (;;) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| if (got + value.length > buf.length) throw new Error("the download is longer than the model"); | |
| buf.set(value, got); got += value.length; | |
| progress(got, total, "download"); | |
| } | |
| if (got !== total) throw new Error(`the download stopped at ${got} of ${total} bytes`); | |
| // kept for the next visit (put finishes reading the buffer before it is handed to the engine); a private window or | |
| // a full disk just means downloading again | |
| try { const c = await caches.open(CACHE); await c.put(MODEL.url, new Response(buf, { headers: { "Content-Type": "application/octet-stream" } })); } catch {} | |
| return buf.buffer; | |
| } | |
| export const isReady = () => !!ready; | |
| export const isBusy = () => !!busy; | |
| /** Stop the engine and free its memory (the model stays in the browser's cache): new settings apply on the next load. */ | |
| export function unload() { | |
| if (busy) throw new Error("browseML is writing an answer: wait for it to finish"); | |
| if (worker) worker.terminate(); | |
| worker = null; ready = null; memoryBytes = 0; | |
| } | |
| /** The model's bytes in this browser's cache, or 0. */ | |
| export async function cachedBytes() { try { const r = await (await caches.open(CACHE)).match(MODEL.url); return r ? +(r.headers.get("Content-Length") || MODEL.bytes) : 0; } catch { return 0; } } | |
| export async function isCached() { try { return !!(await (await caches.open(CACHE)).match(MODEL.url)); } catch { return false; } } | |
| export async function forget() { try { await caches.delete(CACHE); } catch {} } | |
| /** | |
| * Load bankML in this browser: the model (downloaded or cached), the engine, and bankML's verification. | |
| * @param {(bytes:number, total:number, phase:string) => void} progress | |
| * @param {(level:number, text:string) => void} log | |
| * @returns {Promise<{model, sha256, guard, engine, arch, version, threads}>} what bankML verified, and its threads | |
| */ | |
| let loading = null; | |
| /** A load in progress (a download, the verification), or null. */ | |
| export const isLoading = () => !!loading; | |
| export const whenLoaded = () => loading; | |
| export function load(progress, log, opts = {}) { | |
| if (ready) return Promise.resolve(ready); | |
| if (loading) return loading; // one download at a time: a second caller waits for the first | |
| loading = loadOnce(progress, log, opts).finally(() => { loading = null; }); | |
| return loading; | |
| } | |
| async function loadOnce(progress, log, { threads: want, ctx = 4096 } = {}) { | |
| const [bytes, fork] = await Promise.all([fetchModel(progress), fetch(MODEL.fork).then((r) => { if (!r.ok) throw new Error("the model's FORK.json is missing"); return r.text(); })]); | |
| progress(MODEL.bytes, MODEL.bytes, "verify"); | |
| worker = new Worker(new URL("./browseml-worker.js", import.meta.url), { type: "module" }); | |
| const verified = await new Promise((resolve, reject) => { | |
| worker.onmessage = (e) => { | |
| const m = e.data; | |
| if (m.type === "log") log(m.level, m.text); | |
| else if (m.type === "opened") { memoryBytes = m.memory || 0; m.rc === 0 ? resolve({ ...JSON.parse(m.out), threads: m.threads, ctx, build: m.build }) : reject(new Error(m.out)); } | |
| }; | |
| worker.onerror = (e) => reject(new Error(e.message || "the engine could not start")); | |
| worker.postMessage({ type: "open", name: MODEL.name, bytes, fork, threads: Math.max(1, Math.min(threads(), want || threads())), ctx }, [bytes]); | |
| }); | |
| ready = verified; | |
| return verified; | |
| } | |
| /** | |
| * One answer: an OpenAI-shaped request (messages, max_tokens, …), the pieces as they are written, and the | |
| * `/v1/chat/completions` object at the end (usage, timings, bankml_receipt). | |
| */ | |
| export function chat(request, onPiece, log) { | |
| if (!ready) return Promise.reject(new Error("browseML is not loaded")); | |
| if (busy) return Promise.reject(new Error("browseML is still writing the last answer")); | |
| busy = new Promise((resolve, reject) => { | |
| worker.onmessage = (e) => { | |
| const m = e.data; | |
| if (m.type === "piece") onPiece(m.text); | |
| else if (m.type === "log") log(m.level, m.text); | |
| else if (m.type === "done") { | |
| busy = null; | |
| memoryBytes = m.memory || memoryBytes; | |
| if (m.rc === -99) { ready = null; worker.terminate(); worker = null; } // the engine trapped: load it again | |
| const j = (() => { try { return JSON.parse(m.out); } catch { return { error: { message: m.out } }; } })(); | |
| m.rc === 0 ? resolve(j) : reject(new Error(j.error ? j.error.message : m.out)); | |
| } | |
| }; | |
| worker.postMessage({ type: "chat", request }); | |
| }); | |
| return busy; | |
| } | |