| |
| |
|
|
| const MAGIC = 0x31504354; |
| const ALIGN = 8; |
| const CACHE_NAME = "nanofable-models"; |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| 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; |
| } |
|
|
| |
| |
| 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; |
| } |
|
|