nanofable / js /pack.js
Adit Rahman
NanoFable static demo: all 8 packs served same-origin
7b33d33
Raw
History Blame Contribute Delete
3.98 kB
// .tpack (pack format v1 "TCP1") loader + model download/cache helpers.
// Format is defined normatively in src/nanofable/pack.py.
const MAGIC = 0x31504354; // "TCP1" little-endian
const ALIGN = 8;
const CACHE_NAME = "nanofable-models";
// 243 -> 5 trits lookup, built once.
const TRIT_LUT = (() => {
const lut = new Int8Array(243 * 5);
for (let b = 0; b < 243; b++) {
let v = b;
for (let j = 0; j < 5; j++) {
lut[b * 5 + j] = (v % 3) - 1;
v = (v / 3) | 0;
}
}
return lut;
})();
function unpackTrits(bytes, numel) {
const out = new Int8Array(numel);
const full = Math.floor(numel / 5);
let o = 0;
for (let i = 0; i < full; i++) {
const base = bytes[i] * 5;
out[o++] = TRIT_LUT[base];
out[o++] = TRIT_LUT[base + 1];
out[o++] = TRIT_LUT[base + 2];
out[o++] = TRIT_LUT[base + 3];
out[o++] = TRIT_LUT[base + 4];
}
for (let j = 0; o < numel; j++) out[o++] = TRIT_LUT[bytes[full] * 5 + j];
return out;
}
function halfToFloat32(u16) {
const out = new Float32Array(u16.length);
for (let i = 0; i < u16.length; i++) {
const h = u16[i];
const sign = h & 0x8000 ? -1 : 1;
const exp = (h & 0x7c00) >> 10;
const frac = h & 0x03ff;
if (exp === 0) out[i] = sign * 2 ** -14 * (frac / 1024);
else if (exp === 0x1f) out[i] = frac ? NaN : sign * Infinity;
else out[i] = sign * 2 ** (exp - 15) * (1 + frac / 1024);
}
return out;
}
// Parse a pack into { header, tensors }. Tensor values are either
// { f32: Float32Array } or { trits: Int8Array, scale: number } — matmul-ready.
export function parsePack(buffer) {
const view = new DataView(buffer);
if (view.getUint32(0, true) !== MAGIC) throw new Error("not a TCP1 pack");
const headerLen = view.getUint32(4, true);
const header = JSON.parse(
new TextDecoder().decode(new Uint8Array(buffer, 8, headerLen))
);
const payload = Math.ceil((8 + headerLen) / ALIGN) * ALIGN;
const tensors = new Map();
for (const t of header.tensors) {
const numel = t.shape.reduce((a, b) => a * b, 1);
if (t.dtype === "trit5") {
const bytes = new Uint8Array(buffer, payload + t.offset, t.bytes);
tensors.set(t.name, { trits: unpackTrits(bytes, numel), scale: t.scale });
} else {
const u16 = new Uint16Array(buffer.slice(payload + t.offset, payload + t.offset + t.bytes));
tensors.set(t.name, { f32: halfToFloat32(u16) });
}
}
return { header, tensors };
}
export async function loadManifest(url = "manifest.json") {
const res = await fetch(url);
if (!res.ok) throw new Error(`manifest fetch failed: ${res.status}`);
return res.json();
}
const cacheAvailable = () => typeof caches !== "undefined";
export async function isCached(url) {
if (!cacheAvailable()) return false;
const cache = await caches.open(CACHE_NAME);
return (await cache.match(url)) !== undefined;
}
// Fetch a pack with download progress, storing it in the Cache API. Serves from
// cache when present. onProgress(loadedBytes, totalBytes|null) fires per chunk.
export async function fetchModel(url, onProgress) {
const cache = cacheAvailable() ? await caches.open(CACHE_NAME) : null;
const hit = cache && (await cache.match(url));
if (hit) return hit.arrayBuffer();
const res = await fetch(url);
if (!res.ok) throw new Error(`download failed (${res.status})`);
const total = Number(res.headers.get("Content-Length")) || null;
const reader = res.body.getReader();
const chunks = [];
let loaded = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
if (onProgress) onProgress(loaded, total);
}
const data = new Uint8Array(loaded);
let o = 0;
for (const c of chunks) {
data.set(c, o);
o += c.length;
}
if (cache) {
await cache.put(url, new Response(data.buffer.slice(0), {
headers: { "Content-Type": "application/octet-stream" },
}));
}
return data.buffer;
}