tempest-react-sdk 0.33.0 → 0.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tabular/assets.cjs +1 -1
- package/dist/tabular/assets.cjs.map +1 -1
- package/dist/tabular/assets.js +11 -9
- package/dist/tabular/assets.js.map +1 -1
- package/dist/tabular/compact.cjs +1 -1
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js +1 -1
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/predictor.cjs +1 -1
- package/dist/tabular/predictor.cjs.map +1 -1
- package/dist/tabular/predictor.js +68 -58
- package/dist/tabular/predictor.js.map +1 -1
- package/dist/tabular.cjs +1 -1
- package/dist/tabular.d.ts +18 -0
- package/dist/tabular.js +8 -8
- package/package.json +1 -1
package/dist/tabular/assets.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
var e=[`ort-wasm-simd-threaded.wasm`,`ort-wasm-simd-threaded.mjs`,`ort-wasm-simd-threaded.jsep.wasm`,`ort-wasm-simd-threaded.jsep.mjs`],t;function n(e){t=e.endsWith(`/`)?e:`${e}/`}function r(){return t}function i(t){let n=t.endsWith(`/`)?t:`${t}/`;return e.map(e=>`${n}${e}`)}exports.ORT_WASM_ASSETS=e,exports.configureOrtAssets=n,exports.configuredOrtAssetPath=r,exports.ortAssetUrls=i;
|
|
2
2
|
//# sourceMappingURL=assets.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assets.cjs","names":[],"sources":["../../src/tabular/assets.ts"],"sourcesContent":["/**\n * The runtime's own assets, which are the other half of working offline.\n *\n * ONNX Runtime Web does **not** embed its WebAssembly binary, not even in\n * the `.bundle` builds. Measured in Chromium: the default entry fetches\n * `ort-wasm-simd-threaded.jsep.wasm` at session creation, and serving the\n * app without that file fails with `Aborted(both async and sync fetching of\n * the wasm failed)` — an error that says nothing about the missing file.\n *\n * So an offline app has to ship those binaries and precache them. This\n * module names them and points the runtime at a local directory.\n
|
|
1
|
+
{"version":3,"file":"assets.cjs","names":[],"sources":["../../src/tabular/assets.ts"],"sourcesContent":["/**\n * The runtime's own assets, which are the other half of working offline.\n *\n * ONNX Runtime Web does **not** embed its WebAssembly binary, not even in\n * the `.bundle` builds. Measured in Chromium: the default entry fetches\n * `ort-wasm-simd-threaded.jsep.wasm` at session creation, and serving the\n * app without that file fails with `Aborted(both async and sync fetching of\n * the wasm failed)` — an error that says nothing about the missing file.\n *\n * So an offline app has to ship those binaries and precache them. This\n * module names them and points the runtime at a local directory.\n *\n * It does **not** import `onnxruntime-web`. An app on the compact route\n * (`CompactPredictor`) has no runtime at all, and a module that imported\n * one at load time would force every consumer of this subpath to install\n * the peer — which is exactly the cost that route exists to avoid. The\n * configured path is remembered here and applied by the ONNX predictor,\n * which imports the runtime only when it actually loads a model.\n */\n\n/**\n * The WebAssembly binaries ONNX Runtime Web may request.\n *\n * Which one is fetched depends on the browser's threading and SIMD support,\n * so an app that must work everywhere ships all of them. Chromium with the\n * default entry point fetched the `jsep` build.\n */\nexport const ORT_WASM_ASSETS: readonly string[] = [\n \"ort-wasm-simd-threaded.wasm\",\n \"ort-wasm-simd-threaded.mjs\",\n \"ort-wasm-simd-threaded.jsep.wasm\",\n \"ort-wasm-simd-threaded.jsep.mjs\",\n];\n\nlet configuredPath: string | undefined;\n\n/**\n * Point ONNX Runtime Web at locally served WebAssembly binaries.\n *\n * Call once, before creating any predictor.\n *\n * @example\n * ```ts\n * configureOrtAssets(\"/ort/\");\n * const predictor = await TabularPredictor.create(\"/models/classifier.onnx\");\n * ```\n *\n * @param basePath Directory the binaries are served from, with a trailing\n * slash. Copy them there at build time — for Vite, from\n * `node_modules/onnxruntime-web/dist/`.\n */\nexport function configureOrtAssets(basePath: string): void {\n configuredPath = basePath.endsWith(\"/\") ? basePath : `${basePath}/`;\n}\n\n/**\n * Where the runtime's binaries were said to live.\n *\n * Read by the ONNX predictor just before it creates a session, which is\n * the first moment the runtime exists to be configured.\n *\n * @returns The configured directory, or `undefined` when none was set.\n */\nexport function configuredOrtAssetPath(): string | undefined {\n return configuredPath;\n}\n\n/**\n * The URLs a service worker should precache for offline inference.\n *\n * The model file is not included: it is cached by\n * {@link fetchModelBytes} on first use, under its own bucket.\n *\n * @example\n * ```ts\n * installPrecache([...ortAssetUrls(\"/ort/\"), \"/index.html\"]);\n * ```\n *\n * @param basePath Directory the binaries are served from.\n * @returns Absolute-from-root URLs for every runtime asset.\n */\nexport function ortAssetUrls(basePath: string): string[] {\n const prefix = basePath.endsWith(\"/\") ? basePath : `${basePath}/`;\n return ORT_WASM_ASSETS.map((asset) => `${prefix}${asset}`);\n}\n"],"mappings":"AA2BA,IAAa,EAAqC,CAC9C,8BACA,6BACA,mCACA,iCACJ,EAEI,EAiBJ,SAAgB,EAAmB,EAAwB,CACvD,EAAiB,EAAS,SAAS,GAAG,EAAI,EAAW,GAAG,EAAS,EACrE,CAUA,SAAgB,GAA6C,CACzD,OAAO,CACX,CAgBA,SAAgB,EAAa,EAA4B,CACrD,IAAM,EAAS,EAAS,SAAS,GAAG,EAAI,EAAW,GAAG,EAAS,GAC/D,OAAO,EAAgB,IAAK,GAAU,GAAG,IAAS,GAAO,CAC7D"}
|
package/dist/tabular/assets.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import * as e from "onnxruntime-web";
|
|
2
1
|
//#region src/tabular/assets.ts
|
|
3
|
-
var
|
|
2
|
+
var e = [
|
|
4
3
|
"ort-wasm-simd-threaded.wasm",
|
|
5
4
|
"ort-wasm-simd-threaded.mjs",
|
|
6
5
|
"ort-wasm-simd-threaded.jsep.wasm",
|
|
7
6
|
"ort-wasm-simd-threaded.jsep.mjs"
|
|
8
|
-
];
|
|
9
|
-
function n(
|
|
10
|
-
|
|
7
|
+
], t;
|
|
8
|
+
function n(e) {
|
|
9
|
+
t = e.endsWith("/") ? e : `${e}/`;
|
|
11
10
|
}
|
|
12
|
-
function r(
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
function r() {
|
|
12
|
+
return t;
|
|
13
|
+
}
|
|
14
|
+
function i(t) {
|
|
15
|
+
let n = t.endsWith("/") ? t : `${t}/`;
|
|
16
|
+
return e.map((e) => `${n}${e}`);
|
|
15
17
|
}
|
|
16
18
|
//#endregion
|
|
17
|
-
export {
|
|
19
|
+
export { e as ORT_WASM_ASSETS, n as configureOrtAssets, r as configuredOrtAssetPath, i as ortAssetUrls };
|
|
18
20
|
|
|
19
21
|
//# sourceMappingURL=assets.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assets.js","names":[],"sources":["../../src/tabular/assets.ts"],"sourcesContent":["/**\n * The runtime's own assets, which are the other half of working offline.\n *\n * ONNX Runtime Web does **not** embed its WebAssembly binary, not even in\n * the `.bundle` builds. Measured in Chromium: the default entry fetches\n * `ort-wasm-simd-threaded.jsep.wasm` at session creation, and serving the\n * app without that file fails with `Aborted(both async and sync fetching of\n * the wasm failed)` — an error that says nothing about the missing file.\n *\n * So an offline app has to ship those binaries and precache them. This\n * module names them and points the runtime at a local directory.\n
|
|
1
|
+
{"version":3,"file":"assets.js","names":[],"sources":["../../src/tabular/assets.ts"],"sourcesContent":["/**\n * The runtime's own assets, which are the other half of working offline.\n *\n * ONNX Runtime Web does **not** embed its WebAssembly binary, not even in\n * the `.bundle` builds. Measured in Chromium: the default entry fetches\n * `ort-wasm-simd-threaded.jsep.wasm` at session creation, and serving the\n * app without that file fails with `Aborted(both async and sync fetching of\n * the wasm failed)` — an error that says nothing about the missing file.\n *\n * So an offline app has to ship those binaries and precache them. This\n * module names them and points the runtime at a local directory.\n *\n * It does **not** import `onnxruntime-web`. An app on the compact route\n * (`CompactPredictor`) has no runtime at all, and a module that imported\n * one at load time would force every consumer of this subpath to install\n * the peer — which is exactly the cost that route exists to avoid. The\n * configured path is remembered here and applied by the ONNX predictor,\n * which imports the runtime only when it actually loads a model.\n */\n\n/**\n * The WebAssembly binaries ONNX Runtime Web may request.\n *\n * Which one is fetched depends on the browser's threading and SIMD support,\n * so an app that must work everywhere ships all of them. Chromium with the\n * default entry point fetched the `jsep` build.\n */\nexport const ORT_WASM_ASSETS: readonly string[] = [\n \"ort-wasm-simd-threaded.wasm\",\n \"ort-wasm-simd-threaded.mjs\",\n \"ort-wasm-simd-threaded.jsep.wasm\",\n \"ort-wasm-simd-threaded.jsep.mjs\",\n];\n\nlet configuredPath: string | undefined;\n\n/**\n * Point ONNX Runtime Web at locally served WebAssembly binaries.\n *\n * Call once, before creating any predictor.\n *\n * @example\n * ```ts\n * configureOrtAssets(\"/ort/\");\n * const predictor = await TabularPredictor.create(\"/models/classifier.onnx\");\n * ```\n *\n * @param basePath Directory the binaries are served from, with a trailing\n * slash. Copy them there at build time — for Vite, from\n * `node_modules/onnxruntime-web/dist/`.\n */\nexport function configureOrtAssets(basePath: string): void {\n configuredPath = basePath.endsWith(\"/\") ? basePath : `${basePath}/`;\n}\n\n/**\n * Where the runtime's binaries were said to live.\n *\n * Read by the ONNX predictor just before it creates a session, which is\n * the first moment the runtime exists to be configured.\n *\n * @returns The configured directory, or `undefined` when none was set.\n */\nexport function configuredOrtAssetPath(): string | undefined {\n return configuredPath;\n}\n\n/**\n * The URLs a service worker should precache for offline inference.\n *\n * The model file is not included: it is cached by\n * {@link fetchModelBytes} on first use, under its own bucket.\n *\n * @example\n * ```ts\n * installPrecache([...ortAssetUrls(\"/ort/\"), \"/index.html\"]);\n * ```\n *\n * @param basePath Directory the binaries are served from.\n * @returns Absolute-from-root URLs for every runtime asset.\n */\nexport function ortAssetUrls(basePath: string): string[] {\n const prefix = basePath.endsWith(\"/\") ? basePath : `${basePath}/`;\n return ORT_WASM_ASSETS.map((asset) => `${prefix}${asset}`);\n}\n"],"mappings":";AA2BA,IAAa,IAAqC;CAC9C;CACA;CACA;CACA;AACJ,GAEI;AAiBJ,SAAgB,EAAmB,GAAwB;CACvD,IAAiB,EAAS,SAAS,GAAG,IAAI,IAAW,GAAG,EAAS;AACrE;AAUA,SAAgB,IAA6C;CACzD,OAAO;AACX;AAgBA,SAAgB,EAAa,GAA4B;CACrD,IAAM,IAAS,EAAS,SAAS,GAAG,IAAI,IAAW,GAAG,EAAS;CAC/D,OAAO,EAAgB,KAAK,MAAU,GAAG,IAAS,GAAO;AAC7D"}
|
package/dist/tabular/compact.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./exceptions.cjs");var t=`TMC1`,n=1,r=class n{header;arrays;info;constructor(e,t,n){this.header=e,this.arrays=t,this.info=n}static async create(r,a){let o=await i(r,a),s=new Uint8Array(o);if(String.fromCharCode(...s.subarray(0,4))!==t)throw new e.CompactFormatError(`This is not a compact model file: it does not start with "${t}". A .onnx file goes through TabularPredictor instead.`);let c=new DataView(o).getUint32(4,!0),l=JSON.parse(new TextDecoder().decode(s.subarray(8,8+c)));if(l.schema_version>1)throw new e.CompactFormatError(`This file uses compact layout ${l.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving it.`);let u={},d=8+c;for(let e of l.sections)u[e.name]=e.dtype===`float32`?new Float32Array(o,d,e.length):new Int32Array(o,d,e.length),d+=e.length*4;return new n(l,u,{kind:l.kind,classes:l.classes,numFeatures:l.n_features,featureNames:l.feature_names,numTrees:l.n_trees??0,isClassifier:l.task===`classification`,estimator:l.estimator})}async predict(e){let t=a(e,this.header.n_features),n=performance.now(),r=[];for(let n of e){let e=this.preprocess(n,t);r.push(this.header.kind===`linear`?this.linearScores(e):this.treeScores(e))}let{labels:i,probabilities:o}=this.finish(r);return{labels:i,probabilities:o,numRows:e.length,ms:performance.now()-n}}async dispose(){}preprocess(e,t){let n=this.header.preprocess;if(n===null)return e;let r=Array(t);for(let i=0;i<t;i+=1)r[i]=(e[i]-n.offset[i])/n.scale[i];return r}linearScores(e){let t=this.arrays.coef,n=this.arrays.intercept,r=this.header.n_features,i=this.header.n_outputs,a=Array(i);for(let o=0;o<i;o+=1){let i=n[o],s=o*r;for(let n=0;n<r;n+=1)i+=t[s+n]*e[n];a[o]=i}return a}treeScores(e){let t=this.arrays.node_feature,n=this.arrays.node_threshold,r=this.arrays.node_left,i=this.arrays.node_right,a=this.arrays.leaf_value,o=this.arrays.tree_offset,s=this.header.n_outputs,c=o.length-1,l=Array(s).fill(0);for(let u=0;u<c;u+=1){let c=o[u],d=t[c];for(;d>=0;)c=e[d]<=n[c]?r[c]:i[c],d=t[c];let f=(-1-d)*s;for(let e=0;e<s;e+=1)l[e]+=a[f+e]}for(let e=0;e<s;e+=1)l[e]=l[e]/c;return l}finish(e){let t=this.header.link,n=this.header.classes;if(t===`identity`)return{labels:e.map(e=>e[0]),probabilities:[]};let r=e.map(e=>{if(t===`sigmoid`){let t=1/(1+Math.exp(-e[0]));return[1-t,t]}if(t===`softmax`){let t=Math.max(...e),n=e.map(e=>Math.exp(e-t)),r=n.reduce((e,t)=>e+t,0);return n.map(e=>e/r)}let n=e.reduce((e,t)=>e+t,0);return n===0?[...e]:e.map(e=>e/n)}),i=this.header.class_type!==`str`;return{labels:r.map(e=>{let t=0;for(let n=1;n<e.length;n+=1)e[n]>e[t]&&(t=n);let r=n[t];return r===void 0?t:i?Number(r):r}),probabilities:r}}};async function i(t,n){if(typeof t==`string`){let r;try{r=await fetch(t,n)}catch(n){throw new e.ModelFetchError(`Could not download the model: ${t}`,{cause:n})}if(!r.ok)throw new e.ModelFetchError(`Could not download the model: ${r.status} ${r.statusText}`);return await r.arrayBuffer()}return t instanceof Uint8Array?t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength):t}function a(t,n){if(!Array.isArray(t)||t.length===0)throw new e.FeatureShapeError(`predict() needs at least one row, shaped [[f1, f2, ...]].`);let r=t[0]?.length??0,i=t.findIndex(e=>e.length!==r);if(i!==-1)throw new e.FeatureShapeError(`All rows must have the same width; row ${i} has ${t[i]?.length} values, expected ${r}.`);if(r!==n)throw new e.FeatureShapeError(`The model expects ${n} features per row, got ${r}.`);return r}exports.CompactPredictor=r,exports.SUPPORTED_COMPACT_SCHEMA=n;
|
|
1
|
+
const e=require("./exceptions.cjs");var t=`TMC1`,n=1,r=class n{header;arrays;info;constructor(e,t,n){this.header=e,this.arrays=t,this.info=n}static async create(r,a){let o=await i(r,a),s=new Uint8Array(o);if(String.fromCharCode(...s.subarray(0,4))!==t)throw new e.CompactFormatError(`This is not a compact model file: it does not start with "${t}". A .onnx file goes through TabularPredictor instead.`);let c=new DataView(o).getUint32(4,!0),l=JSON.parse(new TextDecoder().decode(s.subarray(8,8+c)));if(l.schema_version>1)throw new e.CompactFormatError(`This file uses compact layout ${l.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving it.`);let u={},d=8+c;for(let e of l.sections)u[e.name]=e.dtype===`float32`?new Float32Array(o,d,e.length):new Int32Array(o,d,e.length),d+=e.length*4;return new n(l,u,{kind:l.kind,classes:l.classes,numFeatures:l.n_features,featureNames:l.feature_names,numTrees:l.n_trees??0,isClassifier:l.task===`classification`,estimator:l.estimator})}async predict(e){let t=a(e,this.header.n_features),n=performance.now(),r=[];for(let n of e){let e=this.preprocess(n,t);r.push(this.header.kind===`linear`?this.linearScores(e):this.treeScores(e))}let{labels:i,probabilities:o}=this.finish(r);return{labels:i,probabilities:o,numRows:e.length,ms:performance.now()-n}}async dispose(){}preprocess(e,t){let n=this.header.preprocess;if(n===null)return e;let r=Array(t);for(let i=0;i<t;i+=1)r[i]=(e[i]-n.offset[i])/n.scale[i];return r}linearScores(e){let t=this.arrays.coef,n=this.arrays.intercept,r=this.header.n_features,i=this.header.n_outputs,a=Array(i);for(let o=0;o<i;o+=1){let i=n[o],s=o*r;for(let n=0;n<r;n+=1)i+=t[s+n]*e[n];a[o]=i}return a}treeScores(e){let t=this.arrays.node_feature,n=this.arrays.node_threshold,r=this.arrays.node_left,i=this.arrays.node_right,a=this.arrays.leaf_value,o=this.arrays.tree_offset,s=this.header.n_outputs,c=o.length-1,l=Array(s).fill(0);for(let u=0;u<c;u+=1){let c=o[u],d=t[c];for(;d>=0;)c=Math.fround(e[d])<=n[c]?r[c]:i[c],d=t[c];let f=(-1-d)*s;for(let e=0;e<s;e+=1)l[e]+=a[f+e]}for(let e=0;e<s;e+=1)l[e]=l[e]/c;return l}finish(e){let t=this.header.link,n=this.header.classes;if(t===`identity`)return{labels:e.map(e=>e[0]),probabilities:[]};let r=e.map(e=>{if(t===`sigmoid`){let t=1/(1+Math.exp(-e[0]));return[1-t,t]}if(t===`softmax`){let t=Math.max(...e),n=e.map(e=>Math.exp(e-t)),r=n.reduce((e,t)=>e+t,0);return n.map(e=>e/r)}let n=e.reduce((e,t)=>e+t,0);return n===0?[...e]:e.map(e=>e/n)}),i=this.header.class_type!==`str`;return{labels:r.map(e=>{let t=0;for(let n=1;n<e.length;n+=1)e[n]>e[t]&&(t=n);let r=n[t];return r===void 0?t:i?Number(r):r}),probabilities:r}}};async function i(t,n){if(typeof t==`string`){let r;try{r=await fetch(t,n)}catch(n){throw new e.ModelFetchError(`Could not download the model: ${t}`,{cause:n})}if(!r.ok)throw new e.ModelFetchError(`Could not download the model: ${r.status} ${r.statusText}`);return await r.arrayBuffer()}return t instanceof Uint8Array?t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength):t}function a(t,n){if(!Array.isArray(t)||t.length===0)throw new e.FeatureShapeError(`predict() needs at least one row, shaped [[f1, f2, ...]].`);let r=t[0]?.length??0,i=t.findIndex(e=>e.length!==r);if(i!==-1)throw new e.FeatureShapeError(`All rows must have the same width; row ${i} has ${t[i]?.length} values, expected ${r}.`);if(r!==n)throw new e.FeatureShapeError(`The model expects ${n} features per row, got ${r}.`);return r}exports.CompactPredictor=r,exports.SUPPORTED_COMPACT_SCHEMA=n;
|
|
2
2
|
//# sourceMappingURL=compact.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compact.cjs","names":[],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n (row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"compact.cjs","names":[],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
|
package/dist/tabular/compact.js
CHANGED
|
@@ -59,7 +59,7 @@ var r = "TMC1", i = 1, a = class t {
|
|
|
59
59
|
let t = this.arrays.node_feature, n = this.arrays.node_threshold, r = this.arrays.node_left, i = this.arrays.node_right, a = this.arrays.leaf_value, o = this.arrays.tree_offset, s = this.header.n_outputs, c = o.length - 1, l = Array(s).fill(0);
|
|
60
60
|
for (let u = 0; u < c; u += 1) {
|
|
61
61
|
let c = o[u], d = t[c];
|
|
62
|
-
for (; d >= 0;) c = e[d] <= n[c] ? r[c] : i[c], d = t[c];
|
|
62
|
+
for (; d >= 0;) c = Math.fround(e[d]) <= n[c] ? r[c] : i[c], d = t[c];
|
|
63
63
|
let f = (-1 - d) * s;
|
|
64
64
|
for (let e = 0; e < s; e += 1) l[e] += a[f + e];
|
|
65
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compact.js","names":[],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n (row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"compact.js","names":[],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("
|
|
1
|
+
const e=require("./assets.cjs"),t=require("./exceptions.cjs");async function n(){try{return await import(`onnxruntime-web`)}catch(e){throw new t.ModelLoadError(`The ONNX route needs the optional peer dependency: npm install onnxruntime-web. For a model with no runtime at all, export it with edge_pipeline(compact=True) and load it through CompactPredictor.`,{cause:e})}}var r=[`wasm`],i=[`label`,`class`,`variable`,`output`],a=[`probabilit`,`score`],o=1e6;function s(e,t){for(let n of t){let t=e.find(e=>e.toLowerCase().includes(n));if(t!==void 0)return t}return null}function c(e){let t=e.inputMetadata?.[0];if(t===void 0||t.isTensor!==!0)return null;let n=t.shape[1];return typeof n!=`number`||!Number.isInteger(n)||n<=0||n>o?null:n}function l(e){return typeof e==`bigint`?Number(e):typeof e==`number`?e:String(e)}function u(e){let n=e instanceof Error?e.message:String(e);return n.includes(`No Op registered`)?new t.UnsupportedGraphError(`This runtime build has no kernels for the model's operators. scikit-learn exports use the ai.onnx.ml domain, which is missing from the WebGPU build: import "onnxruntime-web", not "onnxruntime-web/webgpu". Original error: `+n,{cause:e}):new t.ModelLoadError(`Failed to load the model: ${n}`,{cause:e})}function d(e){let n=e instanceof Error?e.message:String(e);return n.includes(`non-tensor typed value`)||n.includes(`Can't access output tensor data`)?new t.InferenceError(`The model has a non-tensor output, which ONNX Runtime Web cannot read. A scikit-learn export made with ZipMap enabled returns a sequence of maps per row — re-export with export_sklearn_to_onnx, which disables it. Original error: ${n}`,{cause:e}):new t.InferenceError(`Inference failed: ${n}`,{cause:e})}var f=class i{runtime;session;info;constructor(e,t,n){this.runtime=e,this.session=t,this.info=n}static async create(t,o={}){let l=o.providers??r,d=await n(),f=e.configuredOrtAssetPath();f!==void 0&&(d.env.wasm.wasmPaths=f);let m;try{m=await d.InferenceSession.create(t,{...o.sessionOptions??{},executionProviders:l})}catch(e){throw u(e)}let h=[...m.outputNames],g=s(h,a),_=h.find(e=>e!==g&&p(e))??h.find(e=>e!==g)??h[0],v=new i(d,m,{inputName:m.inputNames[0],numFeatures:c(m),outputNames:h,labelOutput:_,probabilityOutput:g,isClassifier:g!==null,providers:l});return o.warmup!==!1&&await v.warmUp(),v}async warmUp(){let e=this.info.numFeatures;if(e!==null)try{await this.predict([Array(e).fill(0)])}catch{}}async predict(e){if(!Array.isArray(e)||e.length===0)throw new t.FeatureShapeError(`predict() needs at least one row, shaped [[f1, f2, ...]].`);let n=e[0]?.length??0;if(n===0)throw new t.FeatureShapeError(`The first row has no feature values.`);let r=e.findIndex(e=>e.length!==n);if(r!==-1)throw new t.FeatureShapeError(`All rows must have the same width; row ${r} has ${e[r]?.length} values, expected ${n}.`);let i=this.info.numFeatures;if(i!==null&&n!==i)throw new t.FeatureShapeError(`The model expects ${i} features per row, got ${n}.`);let a=new Float32Array(e.length*n);for(let t=0;t<e.length;t+=1)a.set(e[t],t*n);let o=new this.runtime.Tensor(`float32`,a,[e.length,n]),s=performance.now(),c;try{c=await this.session.run({[this.info.inputName]:o})}catch(e){throw d(e)}let u=performance.now()-s,f=c[this.info.labelOutput];if(f?.data===void 0)throw new t.InferenceError(`The model produced no readable "${this.info.labelOutput}" output.`);let p=Array.from(f.data,l),m=[];if(this.info.probabilityOutput!==null){let t=c[this.info.probabilityOutput];if(t?.data!==void 0){let n=Array.from(t.data,Number),r=n.length/e.length;for(let t=0;t<e.length;t+=1)m.push(n.slice(t*r,(t+1)*r))}}return{labels:p,probabilities:m,numRows:e.length,ms:u}}async dispose(){await this.session.release?.()}};function p(e){let t=e.toLowerCase();return i.some(e=>t.includes(e))}exports.DEFAULT_TABULAR_PROVIDERS=r,exports.TabularPredictor=f;
|
|
2
2
|
//# sourceMappingURL=predictor.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"predictor.cjs","names":[],"sources":["../../src/tabular/predictor.ts"],"sourcesContent":["/**\n * Running a scikit-learn model in the browser, offline.\n *\n * The model file is produced by `tempest-fastapi-sdk`'s\n * `export_sklearn_to_onnx`. This is everything between that file and an\n * answer — the same glue the Python `OnnxPredictor` provides on a device,\n * with the browser's own traps handled:\n *\n * - **int64 labels arrive as `bigint`.** ONNX Runtime Web surfaces the\n * label tensor as a `BigInt64Array`, so a caller comparing `label === 1`\n * silently gets `false` and `JSON.stringify` throws. Labels are converted.\n * - **`ai.onnx.ml` needs the right build.** Measured: importing\n * `onnxruntime-web/webgpu` loads a WebAssembly binary without those\n * operators, and session creation fails with `No Op registered for\n * TreeEnsembleClassifier`. That failure is translated into an error that\n * names the import.\n * - **Which output is which.** A classifier returns `label` and\n * `probabilities`; a regressor returns a single `variable`. Indexing by\n * position works until the day you deploy the other kind.\n */\n\nimport * as ort from \"onnxruntime-web\";\n\nimport {\n FeatureShapeError,\n InferenceError,\n ModelLoadError,\n UnsupportedGraphError,\n} from \"./exceptions\";\nimport type {\n FeatureRow,\n PredictedLabel,\n TabularModelSource,\n TabularPrediction,\n TabularPredictorInfo,\n TabularPredictorOptions,\n} from \"./types\";\n\n/**\n * Execution providers used when the caller does not choose.\n *\n * WebAssembly only, and deliberately: scikit-learn graphs are `ai.onnx.ml`\n * operators, which the WebGPU backend does not implement. There is no\n * speed left on the table here — a 10-tree forest predicts a row in about\n * 0.05 ms in Chromium.\n */\nexport const DEFAULT_TABULAR_PROVIDERS: readonly string[] = [\"wasm\"];\n\n/** Output names that indicate predicted classes rather than scores. */\nconst LABEL_HINTS = [\"label\", \"class\", \"variable\", \"output\"] as const;\n\n/** Output names that indicate class scores. */\nconst PROBABILITY_HINTS = [\"probabilit\", \"score\"] as const;\n\n/** Largest plausible feature count; anything above is a dynamic-dim sentinel. */\nconst MAX_DECLARED_FEATURES = 1_000_000;\n\n/**\n * Pick the first output whose name contains one of `hints`.\n *\n * @param names Graph output names.\n * @param hints Lowercase substrings to look for.\n * @returns The matching name, or `null`.\n */\nfunction matchOutput(names: readonly string[], hints: readonly string[]): string | null {\n for (const hint of hints) {\n const found = names.find((name) => name.toLowerCase().includes(hint));\n if (found !== undefined) return found;\n }\n return null;\n}\n\n/**\n * Read the declared feature count from the input metadata.\n *\n * A dynamic batch dimension is reported as a symbolic string or as an\n * out-of-range number (`4294967295` — an unsigned `-1`), so only a sane\n * positive integer in the second position is trusted.\n *\n * @param session The loaded session.\n * @returns The feature count, or `null` when the graph does not declare one.\n */\nfunction declaredFeatures(session: ort.InferenceSession): number | null {\n const metadata = session.inputMetadata?.[0];\n if (metadata === undefined || metadata.isTensor !== true) return null;\n const dimension = metadata.shape[1];\n if (typeof dimension !== \"number\") return null;\n if (!Number.isInteger(dimension) || dimension <= 0) return null;\n return dimension > MAX_DECLARED_FEATURES ? null : dimension;\n}\n\n/**\n * Convert one raw label value into a JS-friendly label.\n *\n * @param value A tensor element: `bigint` for int64, `number` for float,\n * `string` for a string-labelled classifier.\n * @returns The label as a number or string.\n */\nfunction toLabel(value: unknown): PredictedLabel {\n if (typeof value === \"bigint\") return Number(value);\n if (typeof value === \"number\") return value;\n return String(value);\n}\n\n/**\n * Translate a session-creation failure into an error naming its cause.\n *\n * @param error Whatever ONNX Runtime threw.\n * @returns The error to surface.\n */\nfunction asLoadError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes(\"No Op registered\")) {\n return new UnsupportedGraphError(\n \"This runtime build has no kernels for the model's operators. \" +\n \"scikit-learn exports use the ai.onnx.ml domain, which is missing \" +\n 'from the WebGPU build: import \"onnxruntime-web\", not ' +\n '\"onnxruntime-web/webgpu\". Original error: ' +\n message,\n { cause: error },\n );\n }\n return new ModelLoadError(`Failed to load the model: ${message}`, { cause: error });\n}\n\n/**\n * Translate a run failure into an error naming its cause.\n *\n * Measured: an export made with skl2onnx's default (ZipMap enabled) has a\n * probability output that is a sequence of maps, and ONNX Runtime Web\n * refuses to read non-tensor values — `Reading data from non-tensor typed\n * value is not supported`. That message describes the runtime's limitation,\n * not the fix, so it is replaced by one that names the export flag.\n *\n * @param error Whatever ONNX Runtime threw.\n * @returns The error to surface.\n */\nfunction asRunError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n if (\n message.includes(\"non-tensor typed value\") ||\n message.includes(\"Can't access output tensor data\")\n ) {\n return new InferenceError(\n \"The model has a non-tensor output, which ONNX Runtime Web cannot \" +\n \"read. A scikit-learn export made with ZipMap enabled returns a \" +\n \"sequence of maps per row — re-export with export_sklearn_to_onnx, \" +\n `which disables it. Original error: ${message}`,\n { cause: error },\n );\n }\n return new InferenceError(`Inference failed: ${message}`, { cause: error });\n}\n\n/**\n * A loaded tabular model, ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await TabularPredictor.create(\"/models/classifier.onnx\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class TabularPredictor {\n private constructor(\n private readonly session: ort.InferenceSession,\n /** What is loaded and how it is configured. */\n public readonly info: TabularPredictorInfo,\n ) {}\n\n /**\n * Load a model and describe its graph.\n *\n * @param source A URL string, or the model bytes (which is what an\n * offline app passes, having read them from the cache).\n * @param options Providers, warm-up and pass-through session options.\n * @throws {@link UnsupportedGraphError} when the runtime build lacks the\n * `ai.onnx.ml` operators — the WebGPU entry point does.\n * @throws {@link ModelLoadError} for any other load failure.\n */\n static async create(\n source: TabularModelSource,\n options: TabularPredictorOptions = {},\n ): Promise<TabularPredictor> {\n const providers = options.providers ?? DEFAULT_TABULAR_PROVIDERS;\n let session: ort.InferenceSession;\n try {\n session = await ort.InferenceSession.create(source as never, {\n ...(options.sessionOptions ?? {}),\n executionProviders:\n providers as ort.InferenceSession.SessionOptions[\"executionProviders\"],\n });\n } catch (error) {\n throw asLoadError(error);\n }\n\n const outputNames = [...session.outputNames];\n const probabilityOutput = matchOutput(outputNames, PROBABILITY_HINTS);\n const labelOutput =\n outputNames.find((name) => name !== probabilityOutput && isLabelName(name)) ??\n outputNames.find((name) => name !== probabilityOutput) ??\n (outputNames[0] as string);\n\n const predictor = new TabularPredictor(session, {\n inputName: session.inputNames[0] as string,\n numFeatures: declaredFeatures(session),\n outputNames,\n labelOutput,\n probabilityOutput,\n isClassifier: probabilityOutput !== null,\n providers,\n });\n\n if (options.warmup !== false) await predictor.warmUp();\n return predictor;\n }\n\n /**\n * Run one throwaway inference so the first real call is not the slow one.\n *\n * Skipped when the graph does not declare a feature count, since there\n * is no shape to synthesise. Failures are swallowed: a warm-up that\n * cannot run is not a reason to refuse to serve.\n */\n async warmUp(): Promise<void> {\n const features = this.info.numFeatures;\n if (features === null) return;\n try {\n await this.predict([new Array<number>(features).fill(0)]);\n } catch {\n /* a failed warm-up must not prevent serving */\n }\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model produces them, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width — checked here so the failure names the mismatch\n * instead of surfacing as an opaque runtime error.\n * @throws {@link InferenceError} when the session runs but its outputs\n * cannot be read.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\n \"predict() needs at least one row, shaped [[f1, f2, ...]].\",\n );\n }\n const width = rows[0]?.length ?? 0;\n if (width === 0) {\n throw new FeatureShapeError(\"The first row has no feature values.\");\n }\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n const expected = this.info.numFeatures;\n if (expected !== null && width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n\n const flat = new Float32Array(rows.length * width);\n for (let index = 0; index < rows.length; index += 1) {\n flat.set(rows[index] as number[], index * width);\n }\n const tensor = new ort.Tensor(\"float32\", flat, [rows.length, width]);\n\n const started = performance.now();\n let outputs: ort.InferenceSession.OnnxValueMapType;\n try {\n outputs = await this.session.run({ [this.info.inputName]: tensor });\n } catch (error) {\n throw asRunError(error);\n }\n const ms = performance.now() - started;\n\n const labelTensor = outputs[this.info.labelOutput];\n if (labelTensor?.data === undefined) {\n throw new InferenceError(\n `The model produced no readable \"${this.info.labelOutput}\" output.`,\n );\n }\n\n const labels: PredictedLabel[] = Array.from(\n labelTensor.data as ArrayLike<unknown>,\n toLabel,\n );\n\n const probabilities: number[][] = [];\n if (this.info.probabilityOutput !== null) {\n const scores = outputs[this.info.probabilityOutput];\n if (scores?.data !== undefined) {\n const values = Array.from(scores.data as ArrayLike<number>, Number);\n const classes = values.length / rows.length;\n for (let index = 0; index < rows.length; index += 1) {\n probabilities.push(values.slice(index * classes, (index + 1) * classes));\n }\n }\n }\n\n return { labels, probabilities, numRows: rows.length, ms };\n }\n\n /**\n * Release the session's memory.\n *\n * Worth calling on a route that swaps models: the WebAssembly heap does\n * not shrink on garbage collection alone.\n */\n async dispose(): Promise<void> {\n await this.session.release?.();\n }\n}\n\n/**\n * Whether an output name looks like a label rather than a score.\n *\n * @param name The graph output name.\n * @returns `true` when the name matches a known label convention.\n */\nfunction isLabelName(name: string): boolean {\n const lowered = name.toLowerCase();\n return LABEL_HINTS.some((hint) => lowered.includes(hint));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"predictor.cjs","names":[],"sources":["../../src/tabular/predictor.ts"],"sourcesContent":["/**\n * Running a scikit-learn model in the browser, offline.\n *\n * The model file is produced by `tempest-fastapi-sdk`'s\n * `export_sklearn_to_onnx`. This is everything between that file and an\n * answer — the same glue the Python `OnnxPredictor` provides on a device,\n * with the browser's own traps handled:\n *\n * - **int64 labels arrive as `bigint`.** ONNX Runtime Web surfaces the\n * label tensor as a `BigInt64Array`, so a caller comparing `label === 1`\n * silently gets `false` and `JSON.stringify` throws. Labels are converted.\n * - **`ai.onnx.ml` needs the right build.** Measured: importing\n * `onnxruntime-web/webgpu` loads a WebAssembly binary without those\n * operators, and session creation fails with `No Op registered for\n * TreeEnsembleClassifier`. That failure is translated into an error that\n * names the import.\n * - **Which output is which.** A classifier returns `label` and\n * `probabilities`; a regressor returns a single `variable`. Indexing by\n * position works until the day you deploy the other kind.\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { configuredOrtAssetPath } from \"./assets\";\nimport {\n FeatureShapeError,\n InferenceError,\n ModelLoadError,\n UnsupportedGraphError,\n} from \"./exceptions\";\nimport type {\n FeatureRow,\n PredictedLabel,\n TabularModelSource,\n TabularPrediction,\n TabularPredictorInfo,\n TabularPredictorOptions,\n} from \"./types\";\n\n/**\n * Import ONNX Runtime Web, only when a model is actually being loaded.\n *\n * Static import would make every consumer of this subpath install the peer,\n * including apps that only ever touch `CompactPredictor` — whose whole\n * point is not needing a runtime. Found by installing the published\n * package into an empty project, which is the only place the difference\n * shows.\n *\n * @returns The runtime module.\n * @throws {@link ModelLoadError} when the peer is not installed, naming it.\n */\nasync function loadRuntime(): Promise<typeof ort> {\n try {\n return (await import(\"onnxruntime-web\")) as typeof ort;\n } catch (error) {\n throw new ModelLoadError(\n \"The ONNX route needs the optional peer dependency: \" +\n \"npm install onnxruntime-web. For a model with no runtime at \" +\n \"all, export it with edge_pipeline(compact=True) and load it \" +\n \"through CompactPredictor.\",\n { cause: error },\n );\n }\n}\n\n/**\n * Execution providers used when the caller does not choose.\n *\n * WebAssembly only, and deliberately: scikit-learn graphs are `ai.onnx.ml`\n * operators, which the WebGPU backend does not implement. There is no\n * speed left on the table here — a 10-tree forest predicts a row in about\n * 0.05 ms in Chromium.\n */\nexport const DEFAULT_TABULAR_PROVIDERS: readonly string[] = [\"wasm\"];\n\n/** Output names that indicate predicted classes rather than scores. */\nconst LABEL_HINTS = [\"label\", \"class\", \"variable\", \"output\"] as const;\n\n/** Output names that indicate class scores. */\nconst PROBABILITY_HINTS = [\"probabilit\", \"score\"] as const;\n\n/** Largest plausible feature count; anything above is a dynamic-dim sentinel. */\nconst MAX_DECLARED_FEATURES = 1_000_000;\n\n/**\n * Pick the first output whose name contains one of `hints`.\n *\n * @param names Graph output names.\n * @param hints Lowercase substrings to look for.\n * @returns The matching name, or `null`.\n */\nfunction matchOutput(names: readonly string[], hints: readonly string[]): string | null {\n for (const hint of hints) {\n const found = names.find((name) => name.toLowerCase().includes(hint));\n if (found !== undefined) return found;\n }\n return null;\n}\n\n/**\n * Read the declared feature count from the input metadata.\n *\n * A dynamic batch dimension is reported as a symbolic string or as an\n * out-of-range number (`4294967295` — an unsigned `-1`), so only a sane\n * positive integer in the second position is trusted.\n *\n * @param session The loaded session.\n * @returns The feature count, or `null` when the graph does not declare one.\n */\nfunction declaredFeatures(session: ort.InferenceSession): number | null {\n const metadata = session.inputMetadata?.[0];\n if (metadata === undefined || metadata.isTensor !== true) return null;\n const dimension = metadata.shape[1];\n if (typeof dimension !== \"number\") return null;\n if (!Number.isInteger(dimension) || dimension <= 0) return null;\n return dimension > MAX_DECLARED_FEATURES ? null : dimension;\n}\n\n/**\n * Convert one raw label value into a JS-friendly label.\n *\n * @param value A tensor element: `bigint` for int64, `number` for float,\n * `string` for a string-labelled classifier.\n * @returns The label as a number or string.\n */\nfunction toLabel(value: unknown): PredictedLabel {\n if (typeof value === \"bigint\") return Number(value);\n if (typeof value === \"number\") return value;\n return String(value);\n}\n\n/**\n * Translate a session-creation failure into an error naming its cause.\n *\n * @param error Whatever ONNX Runtime threw.\n * @returns The error to surface.\n */\nfunction asLoadError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes(\"No Op registered\")) {\n return new UnsupportedGraphError(\n \"This runtime build has no kernels for the model's operators. \" +\n \"scikit-learn exports use the ai.onnx.ml domain, which is missing \" +\n 'from the WebGPU build: import \"onnxruntime-web\", not ' +\n '\"onnxruntime-web/webgpu\". Original error: ' +\n message,\n { cause: error },\n );\n }\n return new ModelLoadError(`Failed to load the model: ${message}`, { cause: error });\n}\n\n/**\n * Translate a run failure into an error naming its cause.\n *\n * Measured: an export made with skl2onnx's default (ZipMap enabled) has a\n * probability output that is a sequence of maps, and ONNX Runtime Web\n * refuses to read non-tensor values — `Reading data from non-tensor typed\n * value is not supported`. That message describes the runtime's limitation,\n * not the fix, so it is replaced by one that names the export flag.\n *\n * @param error Whatever ONNX Runtime threw.\n * @returns The error to surface.\n */\nfunction asRunError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n if (\n message.includes(\"non-tensor typed value\") ||\n message.includes(\"Can't access output tensor data\")\n ) {\n return new InferenceError(\n \"The model has a non-tensor output, which ONNX Runtime Web cannot \" +\n \"read. A scikit-learn export made with ZipMap enabled returns a \" +\n \"sequence of maps per row — re-export with export_sklearn_to_onnx, \" +\n `which disables it. Original error: ${message}`,\n { cause: error },\n );\n }\n return new InferenceError(`Inference failed: ${message}`, { cause: error });\n}\n\n/**\n * A loaded tabular model, ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await TabularPredictor.create(\"/models/classifier.onnx\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class TabularPredictor {\n private constructor(\n private readonly runtime: typeof ort,\n private readonly session: ort.InferenceSession,\n /** What is loaded and how it is configured. */\n public readonly info: TabularPredictorInfo,\n ) {}\n\n /**\n * Load a model and describe its graph.\n *\n * @param source A URL string, or the model bytes (which is what an\n * offline app passes, having read them from the cache).\n * @param options Providers, warm-up and pass-through session options.\n * @throws {@link UnsupportedGraphError} when the runtime build lacks the\n * `ai.onnx.ml` operators — the WebGPU entry point does.\n * @throws {@link ModelLoadError} for any other load failure.\n */\n static async create(\n source: TabularModelSource,\n options: TabularPredictorOptions = {},\n ): Promise<TabularPredictor> {\n const providers = options.providers ?? DEFAULT_TABULAR_PROVIDERS;\n const runtime = await loadRuntime();\n const assets = configuredOrtAssetPath();\n if (assets !== undefined) runtime.env.wasm.wasmPaths = assets;\n\n let session: ort.InferenceSession;\n try {\n session = await runtime.InferenceSession.create(source as never, {\n ...(options.sessionOptions ?? {}),\n executionProviders:\n providers as ort.InferenceSession.SessionOptions[\"executionProviders\"],\n });\n } catch (error) {\n throw asLoadError(error);\n }\n\n const outputNames = [...session.outputNames];\n const probabilityOutput = matchOutput(outputNames, PROBABILITY_HINTS);\n const labelOutput =\n outputNames.find((name) => name !== probabilityOutput && isLabelName(name)) ??\n outputNames.find((name) => name !== probabilityOutput) ??\n (outputNames[0] as string);\n\n const predictor = new TabularPredictor(runtime, session, {\n inputName: session.inputNames[0] as string,\n numFeatures: declaredFeatures(session),\n outputNames,\n labelOutput,\n probabilityOutput,\n isClassifier: probabilityOutput !== null,\n providers,\n });\n\n if (options.warmup !== false) await predictor.warmUp();\n return predictor;\n }\n\n /**\n * Run one throwaway inference so the first real call is not the slow one.\n *\n * Skipped when the graph does not declare a feature count, since there\n * is no shape to synthesise. Failures are swallowed: a warm-up that\n * cannot run is not a reason to refuse to serve.\n */\n async warmUp(): Promise<void> {\n const features = this.info.numFeatures;\n if (features === null) return;\n try {\n await this.predict([new Array<number>(features).fill(0)]);\n } catch {\n /* a failed warm-up must not prevent serving */\n }\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model produces them, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width — checked here so the failure names the mismatch\n * instead of surfacing as an opaque runtime error.\n * @throws {@link InferenceError} when the session runs but its outputs\n * cannot be read.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\n \"predict() needs at least one row, shaped [[f1, f2, ...]].\",\n );\n }\n const width = rows[0]?.length ?? 0;\n if (width === 0) {\n throw new FeatureShapeError(\"The first row has no feature values.\");\n }\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n const expected = this.info.numFeatures;\n if (expected !== null && width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n\n const flat = new Float32Array(rows.length * width);\n for (let index = 0; index < rows.length; index += 1) {\n flat.set(rows[index] as number[], index * width);\n }\n const tensor = new this.runtime.Tensor(\"float32\", flat, [rows.length, width]);\n\n const started = performance.now();\n let outputs: ort.InferenceSession.OnnxValueMapType;\n try {\n outputs = await this.session.run({ [this.info.inputName]: tensor });\n } catch (error) {\n throw asRunError(error);\n }\n const ms = performance.now() - started;\n\n const labelTensor = outputs[this.info.labelOutput];\n if (labelTensor?.data === undefined) {\n throw new InferenceError(\n `The model produced no readable \"${this.info.labelOutput}\" output.`,\n );\n }\n\n const labels: PredictedLabel[] = Array.from(\n labelTensor.data as ArrayLike<unknown>,\n toLabel,\n );\n\n const probabilities: number[][] = [];\n if (this.info.probabilityOutput !== null) {\n const scores = outputs[this.info.probabilityOutput];\n if (scores?.data !== undefined) {\n const values = Array.from(scores.data as ArrayLike<number>, Number);\n const classes = values.length / rows.length;\n for (let index = 0; index < rows.length; index += 1) {\n probabilities.push(values.slice(index * classes, (index + 1) * classes));\n }\n }\n }\n\n return { labels, probabilities, numRows: rows.length, ms };\n }\n\n /**\n * Release the session's memory.\n *\n * Worth calling on a route that swaps models: the WebAssembly heap does\n * not shrink on garbage collection alone.\n */\n async dispose(): Promise<void> {\n await this.session.release?.();\n }\n}\n\n/**\n * Whether an output name looks like a label rather than a score.\n *\n * @param name The graph output name.\n * @returns `true` when the name matches a known label convention.\n */\nfunction isLabelName(name: string): boolean {\n const lowered = name.toLowerCase();\n return LABEL_HINTS.some((hint) => lowered.includes(hint));\n}\n"],"mappings":"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"}
|
|
@@ -1,62 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { configuredOrtAssetPath as e } from "./assets.js";
|
|
2
|
+
import { FeatureShapeError as t, InferenceError as n, ModelLoadError as r, UnsupportedGraphError as i } from "./exceptions.js";
|
|
3
3
|
//#region src/tabular/predictor.ts
|
|
4
|
-
|
|
4
|
+
async function a() {
|
|
5
|
+
try {
|
|
6
|
+
return await import("onnxruntime-web");
|
|
7
|
+
} catch (e) {
|
|
8
|
+
throw new r("The ONNX route needs the optional peer dependency: npm install onnxruntime-web. For a model with no runtime at all, export it with edge_pipeline(compact=True) and load it through CompactPredictor.", { cause: e });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var o = ["wasm"], s = [
|
|
5
12
|
"label",
|
|
6
13
|
"class",
|
|
7
14
|
"variable",
|
|
8
15
|
"output"
|
|
9
|
-
],
|
|
10
|
-
function
|
|
16
|
+
], c = ["probabilit", "score"], l = 1e6;
|
|
17
|
+
function u(e, t) {
|
|
11
18
|
for (let n of t) {
|
|
12
19
|
let t = e.find((e) => e.toLowerCase().includes(n));
|
|
13
20
|
if (t !== void 0) return t;
|
|
14
21
|
}
|
|
15
22
|
return null;
|
|
16
23
|
}
|
|
17
|
-
function
|
|
24
|
+
function d(e) {
|
|
18
25
|
let t = e.inputMetadata?.[0];
|
|
19
26
|
if (t === void 0 || t.isTensor !== !0) return null;
|
|
20
27
|
let n = t.shape[1];
|
|
21
|
-
return typeof n != "number" || !Number.isInteger(n) || n <= 0 || n >
|
|
28
|
+
return typeof n != "number" || !Number.isInteger(n) || n <= 0 || n > l ? null : n;
|
|
22
29
|
}
|
|
23
|
-
function
|
|
30
|
+
function f(e) {
|
|
24
31
|
return typeof e == "bigint" ? Number(e) : typeof e == "number" ? e : String(e);
|
|
25
32
|
}
|
|
26
|
-
function
|
|
33
|
+
function p(e) {
|
|
27
34
|
let t = e instanceof Error ? e.message : String(e);
|
|
28
|
-
return t.includes("No Op registered") ? new
|
|
35
|
+
return t.includes("No Op registered") ? new i("This runtime build has no kernels for the model's operators. scikit-learn exports use the ai.onnx.ml domain, which is missing from the WebGPU build: import \"onnxruntime-web\", not \"onnxruntime-web/webgpu\". Original error: " + t, { cause: e }) : new r(`Failed to load the model: ${t}`, { cause: e });
|
|
29
36
|
}
|
|
30
|
-
function
|
|
31
|
-
let
|
|
32
|
-
return
|
|
37
|
+
function m(e) {
|
|
38
|
+
let t = e instanceof Error ? e.message : String(e);
|
|
39
|
+
return t.includes("non-tensor typed value") || t.includes("Can't access output tensor data") ? new n(`The model has a non-tensor output, which ONNX Runtime Web cannot read. A scikit-learn export made with ZipMap enabled returns a sequence of maps per row — re-export with export_sklearn_to_onnx, which disables it. Original error: ${t}`, { cause: e }) : new n(`Inference failed: ${t}`, { cause: e });
|
|
33
40
|
}
|
|
34
|
-
var
|
|
41
|
+
var h = class r {
|
|
42
|
+
runtime;
|
|
35
43
|
session;
|
|
36
44
|
info;
|
|
37
|
-
constructor(e, t) {
|
|
38
|
-
this.
|
|
45
|
+
constructor(e, t, n) {
|
|
46
|
+
this.runtime = e, this.session = t, this.info = n;
|
|
39
47
|
}
|
|
40
|
-
static async create(
|
|
41
|
-
let
|
|
48
|
+
static async create(t, n = {}) {
|
|
49
|
+
let i = n.providers ?? o, s = await a(), l = e();
|
|
50
|
+
l !== void 0 && (s.env.wasm.wasmPaths = l);
|
|
51
|
+
let f;
|
|
42
52
|
try {
|
|
43
|
-
|
|
44
|
-
...
|
|
45
|
-
executionProviders:
|
|
53
|
+
f = await s.InferenceSession.create(t, {
|
|
54
|
+
...n.sessionOptions ?? {},
|
|
55
|
+
executionProviders: i
|
|
46
56
|
});
|
|
47
57
|
} catch (e) {
|
|
48
|
-
throw
|
|
58
|
+
throw p(e);
|
|
49
59
|
}
|
|
50
|
-
let
|
|
51
|
-
inputName:
|
|
52
|
-
numFeatures:
|
|
53
|
-
outputNames:
|
|
54
|
-
labelOutput:
|
|
55
|
-
probabilityOutput:
|
|
56
|
-
isClassifier:
|
|
57
|
-
providers:
|
|
60
|
+
let m = [...f.outputNames], h = u(m, c), _ = m.find((e) => e !== h && g(e)) ?? m.find((e) => e !== h) ?? m[0], v = new r(s, f, {
|
|
61
|
+
inputName: f.inputNames[0],
|
|
62
|
+
numFeatures: d(f),
|
|
63
|
+
outputNames: m,
|
|
64
|
+
labelOutput: _,
|
|
65
|
+
probabilityOutput: h,
|
|
66
|
+
isClassifier: h !== null,
|
|
67
|
+
providers: i
|
|
58
68
|
});
|
|
59
|
-
return
|
|
69
|
+
return n.warmup !== !1 && await v.warmUp(), v;
|
|
60
70
|
}
|
|
61
71
|
async warmUp() {
|
|
62
72
|
let e = this.info.numFeatures;
|
|
@@ -64,48 +74,48 @@ var m = class n {
|
|
|
64
74
|
await this.predict([Array(e).fill(0)]);
|
|
65
75
|
} catch {}
|
|
66
76
|
}
|
|
67
|
-
async predict(
|
|
68
|
-
if (!Array.isArray(
|
|
69
|
-
let r =
|
|
70
|
-
if (r === 0) throw new
|
|
71
|
-
let
|
|
72
|
-
if (
|
|
73
|
-
let
|
|
74
|
-
if (
|
|
75
|
-
let
|
|
76
|
-
for (let
|
|
77
|
-
let
|
|
77
|
+
async predict(e) {
|
|
78
|
+
if (!Array.isArray(e) || e.length === 0) throw new t("predict() needs at least one row, shaped [[f1, f2, ...]].");
|
|
79
|
+
let r = e[0]?.length ?? 0;
|
|
80
|
+
if (r === 0) throw new t("The first row has no feature values.");
|
|
81
|
+
let i = e.findIndex((e) => e.length !== r);
|
|
82
|
+
if (i !== -1) throw new t(`All rows must have the same width; row ${i} has ${e[i]?.length} values, expected ${r}.`);
|
|
83
|
+
let a = this.info.numFeatures;
|
|
84
|
+
if (a !== null && r !== a) throw new t(`The model expects ${a} features per row, got ${r}.`);
|
|
85
|
+
let o = new Float32Array(e.length * r);
|
|
86
|
+
for (let t = 0; t < e.length; t += 1) o.set(e[t], t * r);
|
|
87
|
+
let s = new this.runtime.Tensor("float32", o, [e.length, r]), c = performance.now(), l;
|
|
78
88
|
try {
|
|
79
|
-
|
|
89
|
+
l = await this.session.run({ [this.info.inputName]: s });
|
|
80
90
|
} catch (e) {
|
|
81
|
-
throw
|
|
91
|
+
throw m(e);
|
|
82
92
|
}
|
|
83
|
-
let
|
|
84
|
-
if (
|
|
85
|
-
let
|
|
93
|
+
let u = performance.now() - c, d = l[this.info.labelOutput];
|
|
94
|
+
if (d?.data === void 0) throw new n(`The model produced no readable "${this.info.labelOutput}" output.`);
|
|
95
|
+
let p = Array.from(d.data, f), h = [];
|
|
86
96
|
if (this.info.probabilityOutput !== null) {
|
|
87
|
-
let
|
|
88
|
-
if (
|
|
89
|
-
let
|
|
90
|
-
for (let
|
|
97
|
+
let t = l[this.info.probabilityOutput];
|
|
98
|
+
if (t?.data !== void 0) {
|
|
99
|
+
let n = Array.from(t.data, Number), r = n.length / e.length;
|
|
100
|
+
for (let t = 0; t < e.length; t += 1) h.push(n.slice(t * r, (t + 1) * r));
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
return {
|
|
94
|
-
labels:
|
|
95
|
-
probabilities:
|
|
96
|
-
numRows:
|
|
97
|
-
ms:
|
|
104
|
+
labels: p,
|
|
105
|
+
probabilities: h,
|
|
106
|
+
numRows: e.length,
|
|
107
|
+
ms: u
|
|
98
108
|
};
|
|
99
109
|
}
|
|
100
110
|
async dispose() {
|
|
101
111
|
await this.session.release?.();
|
|
102
112
|
}
|
|
103
113
|
};
|
|
104
|
-
function
|
|
114
|
+
function g(e) {
|
|
105
115
|
let t = e.toLowerCase();
|
|
106
|
-
return
|
|
116
|
+
return s.some((e) => t.includes(e));
|
|
107
117
|
}
|
|
108
118
|
//#endregion
|
|
109
|
-
export {
|
|
119
|
+
export { o as DEFAULT_TABULAR_PROVIDERS, h as TabularPredictor };
|
|
110
120
|
|
|
111
121
|
//# sourceMappingURL=predictor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"predictor.js","names":[],"sources":["../../src/tabular/predictor.ts"],"sourcesContent":["/**\n * Running a scikit-learn model in the browser, offline.\n *\n * The model file is produced by `tempest-fastapi-sdk`'s\n * `export_sklearn_to_onnx`. This is everything between that file and an\n * answer — the same glue the Python `OnnxPredictor` provides on a device,\n * with the browser's own traps handled:\n *\n * - **int64 labels arrive as `bigint`.** ONNX Runtime Web surfaces the\n * label tensor as a `BigInt64Array`, so a caller comparing `label === 1`\n * silently gets `false` and `JSON.stringify` throws. Labels are converted.\n * - **`ai.onnx.ml` needs the right build.** Measured: importing\n * `onnxruntime-web/webgpu` loads a WebAssembly binary without those\n * operators, and session creation fails with `No Op registered for\n * TreeEnsembleClassifier`. That failure is translated into an error that\n * names the import.\n * - **Which output is which.** A classifier returns `label` and\n * `probabilities`; a regressor returns a single `variable`. Indexing by\n * position works until the day you deploy the other kind.\n */\n\nimport * as ort from \"onnxruntime-web\";\n\nimport {\n FeatureShapeError,\n InferenceError,\n ModelLoadError,\n UnsupportedGraphError,\n} from \"./exceptions\";\nimport type {\n FeatureRow,\n PredictedLabel,\n TabularModelSource,\n TabularPrediction,\n TabularPredictorInfo,\n TabularPredictorOptions,\n} from \"./types\";\n\n/**\n * Execution providers used when the caller does not choose.\n *\n * WebAssembly only, and deliberately: scikit-learn graphs are `ai.onnx.ml`\n * operators, which the WebGPU backend does not implement. There is no\n * speed left on the table here — a 10-tree forest predicts a row in about\n * 0.05 ms in Chromium.\n */\nexport const DEFAULT_TABULAR_PROVIDERS: readonly string[] = [\"wasm\"];\n\n/** Output names that indicate predicted classes rather than scores. */\nconst LABEL_HINTS = [\"label\", \"class\", \"variable\", \"output\"] as const;\n\n/** Output names that indicate class scores. */\nconst PROBABILITY_HINTS = [\"probabilit\", \"score\"] as const;\n\n/** Largest plausible feature count; anything above is a dynamic-dim sentinel. */\nconst MAX_DECLARED_FEATURES = 1_000_000;\n\n/**\n * Pick the first output whose name contains one of `hints`.\n *\n * @param names Graph output names.\n * @param hints Lowercase substrings to look for.\n * @returns The matching name, or `null`.\n */\nfunction matchOutput(names: readonly string[], hints: readonly string[]): string | null {\n for (const hint of hints) {\n const found = names.find((name) => name.toLowerCase().includes(hint));\n if (found !== undefined) return found;\n }\n return null;\n}\n\n/**\n * Read the declared feature count from the input metadata.\n *\n * A dynamic batch dimension is reported as a symbolic string or as an\n * out-of-range number (`4294967295` — an unsigned `-1`), so only a sane\n * positive integer in the second position is trusted.\n *\n * @param session The loaded session.\n * @returns The feature count, or `null` when the graph does not declare one.\n */\nfunction declaredFeatures(session: ort.InferenceSession): number | null {\n const metadata = session.inputMetadata?.[0];\n if (metadata === undefined || metadata.isTensor !== true) return null;\n const dimension = metadata.shape[1];\n if (typeof dimension !== \"number\") return null;\n if (!Number.isInteger(dimension) || dimension <= 0) return null;\n return dimension > MAX_DECLARED_FEATURES ? null : dimension;\n}\n\n/**\n * Convert one raw label value into a JS-friendly label.\n *\n * @param value A tensor element: `bigint` for int64, `number` for float,\n * `string` for a string-labelled classifier.\n * @returns The label as a number or string.\n */\nfunction toLabel(value: unknown): PredictedLabel {\n if (typeof value === \"bigint\") return Number(value);\n if (typeof value === \"number\") return value;\n return String(value);\n}\n\n/**\n * Translate a session-creation failure into an error naming its cause.\n *\n * @param error Whatever ONNX Runtime threw.\n * @returns The error to surface.\n */\nfunction asLoadError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes(\"No Op registered\")) {\n return new UnsupportedGraphError(\n \"This runtime build has no kernels for the model's operators. \" +\n \"scikit-learn exports use the ai.onnx.ml domain, which is missing \" +\n 'from the WebGPU build: import \"onnxruntime-web\", not ' +\n '\"onnxruntime-web/webgpu\". Original error: ' +\n message,\n { cause: error },\n );\n }\n return new ModelLoadError(`Failed to load the model: ${message}`, { cause: error });\n}\n\n/**\n * Translate a run failure into an error naming its cause.\n *\n * Measured: an export made with skl2onnx's default (ZipMap enabled) has a\n * probability output that is a sequence of maps, and ONNX Runtime Web\n * refuses to read non-tensor values — `Reading data from non-tensor typed\n * value is not supported`. That message describes the runtime's limitation,\n * not the fix, so it is replaced by one that names the export flag.\n *\n * @param error Whatever ONNX Runtime threw.\n * @returns The error to surface.\n */\nfunction asRunError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n if (\n message.includes(\"non-tensor typed value\") ||\n message.includes(\"Can't access output tensor data\")\n ) {\n return new InferenceError(\n \"The model has a non-tensor output, which ONNX Runtime Web cannot \" +\n \"read. A scikit-learn export made with ZipMap enabled returns a \" +\n \"sequence of maps per row — re-export with export_sklearn_to_onnx, \" +\n `which disables it. Original error: ${message}`,\n { cause: error },\n );\n }\n return new InferenceError(`Inference failed: ${message}`, { cause: error });\n}\n\n/**\n * A loaded tabular model, ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await TabularPredictor.create(\"/models/classifier.onnx\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class TabularPredictor {\n private constructor(\n private readonly session: ort.InferenceSession,\n /** What is loaded and how it is configured. */\n public readonly info: TabularPredictorInfo,\n ) {}\n\n /**\n * Load a model and describe its graph.\n *\n * @param source A URL string, or the model bytes (which is what an\n * offline app passes, having read them from the cache).\n * @param options Providers, warm-up and pass-through session options.\n * @throws {@link UnsupportedGraphError} when the runtime build lacks the\n * `ai.onnx.ml` operators — the WebGPU entry point does.\n * @throws {@link ModelLoadError} for any other load failure.\n */\n static async create(\n source: TabularModelSource,\n options: TabularPredictorOptions = {},\n ): Promise<TabularPredictor> {\n const providers = options.providers ?? DEFAULT_TABULAR_PROVIDERS;\n let session: ort.InferenceSession;\n try {\n session = await ort.InferenceSession.create(source as never, {\n ...(options.sessionOptions ?? {}),\n executionProviders:\n providers as ort.InferenceSession.SessionOptions[\"executionProviders\"],\n });\n } catch (error) {\n throw asLoadError(error);\n }\n\n const outputNames = [...session.outputNames];\n const probabilityOutput = matchOutput(outputNames, PROBABILITY_HINTS);\n const labelOutput =\n outputNames.find((name) => name !== probabilityOutput && isLabelName(name)) ??\n outputNames.find((name) => name !== probabilityOutput) ??\n (outputNames[0] as string);\n\n const predictor = new TabularPredictor(session, {\n inputName: session.inputNames[0] as string,\n numFeatures: declaredFeatures(session),\n outputNames,\n labelOutput,\n probabilityOutput,\n isClassifier: probabilityOutput !== null,\n providers,\n });\n\n if (options.warmup !== false) await predictor.warmUp();\n return predictor;\n }\n\n /**\n * Run one throwaway inference so the first real call is not the slow one.\n *\n * Skipped when the graph does not declare a feature count, since there\n * is no shape to synthesise. Failures are swallowed: a warm-up that\n * cannot run is not a reason to refuse to serve.\n */\n async warmUp(): Promise<void> {\n const features = this.info.numFeatures;\n if (features === null) return;\n try {\n await this.predict([new Array<number>(features).fill(0)]);\n } catch {\n /* a failed warm-up must not prevent serving */\n }\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model produces them, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width — checked here so the failure names the mismatch\n * instead of surfacing as an opaque runtime error.\n * @throws {@link InferenceError} when the session runs but its outputs\n * cannot be read.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\n \"predict() needs at least one row, shaped [[f1, f2, ...]].\",\n );\n }\n const width = rows[0]?.length ?? 0;\n if (width === 0) {\n throw new FeatureShapeError(\"The first row has no feature values.\");\n }\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n const expected = this.info.numFeatures;\n if (expected !== null && width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n\n const flat = new Float32Array(rows.length * width);\n for (let index = 0; index < rows.length; index += 1) {\n flat.set(rows[index] as number[], index * width);\n }\n const tensor = new ort.Tensor(\"float32\", flat, [rows.length, width]);\n\n const started = performance.now();\n let outputs: ort.InferenceSession.OnnxValueMapType;\n try {\n outputs = await this.session.run({ [this.info.inputName]: tensor });\n } catch (error) {\n throw asRunError(error);\n }\n const ms = performance.now() - started;\n\n const labelTensor = outputs[this.info.labelOutput];\n if (labelTensor?.data === undefined) {\n throw new InferenceError(\n `The model produced no readable \"${this.info.labelOutput}\" output.`,\n );\n }\n\n const labels: PredictedLabel[] = Array.from(\n labelTensor.data as ArrayLike<unknown>,\n toLabel,\n );\n\n const probabilities: number[][] = [];\n if (this.info.probabilityOutput !== null) {\n const scores = outputs[this.info.probabilityOutput];\n if (scores?.data !== undefined) {\n const values = Array.from(scores.data as ArrayLike<number>, Number);\n const classes = values.length / rows.length;\n for (let index = 0; index < rows.length; index += 1) {\n probabilities.push(values.slice(index * classes, (index + 1) * classes));\n }\n }\n }\n\n return { labels, probabilities, numRows: rows.length, ms };\n }\n\n /**\n * Release the session's memory.\n *\n * Worth calling on a route that swaps models: the WebAssembly heap does\n * not shrink on garbage collection alone.\n */\n async dispose(): Promise<void> {\n await this.session.release?.();\n }\n}\n\n/**\n * Whether an output name looks like a label rather than a score.\n *\n * @param name The graph output name.\n * @returns `true` when the name matches a known label convention.\n */\nfunction isLabelName(name: string): boolean {\n const lowered = name.toLowerCase();\n return LABEL_HINTS.some((hint) => lowered.includes(hint));\n}\n"],"mappings":";;;AA8CA,IAAa,IAA+C,CAAC,MAAM,GAG7D,IAAc;CAAC;CAAS;CAAS;CAAY;AAAQ,GAGrD,IAAoB,CAAC,cAAc,OAAO,GAG1C,IAAwB;AAS9B,SAAS,EAAY,GAA0B,GAAyC;CACpF,KAAK,IAAM,KAAQ,GAAO;EACtB,IAAM,IAAQ,EAAM,MAAM,MAAS,EAAK,YAAY,CAAC,CAAC,SAAS,CAAI,CAAC;EACpE,IAAI,MAAU,KAAA,GAAW,OAAO;CACpC;CACA,OAAO;AACX;AAYA,SAAS,EAAiB,GAA8C;CACpE,IAAM,IAAW,EAAQ,gBAAgB;CACzC,IAAI,MAAa,KAAA,KAAa,EAAS,aAAa,IAAM,OAAO;CACjE,IAAM,IAAY,EAAS,MAAM;CAGjC,OAFI,OAAO,KAAc,YACrB,CAAC,OAAO,UAAU,CAAS,KAAK,KAAa,KAC1C,IAAY,IADwC,OACT;AACtD;AASA,SAAS,EAAQ,GAAgC;CAG7C,OAFI,OAAO,KAAU,WAAiB,OAAO,CAAK,IAC9C,OAAO,KAAU,WAAiB,IAC/B,OAAO,CAAK;AACvB;AAQA,SAAS,EAAY,GAAuB;CACxC,IAAM,IAAU,aAAiB,QAAQ,EAAM,UAAU,OAAO,CAAK;CAWrE,OAVI,EAAQ,SAAS,kBAAkB,IAC5B,IAAI,EACP,sOAII,GACJ,EAAE,OAAO,EAAM,CACnB,IAEG,IAAI,EAAe,6BAA6B,KAAW,EAAE,OAAO,EAAM,CAAC;AACtF;AAcA,SAAS,EAAW,GAAuB;CACvC,IAAM,IAAU,aAAiB,QAAQ,EAAM,UAAU,OAAO,CAAK;CAarE,OAXI,EAAQ,SAAS,wBAAwB,KACzC,EAAQ,SAAS,iCAAiC,IAE3C,IAAI,EACP,wOAG0C,KAC1C,EAAE,OAAO,EAAM,CACnB,IAEG,IAAI,EAAe,qBAAqB,KAAW,EAAE,OAAO,EAAM,CAAC;AAC9E;AAWA,IAAa,IAAb,MAAa,EAAiB;CAEL;CAED;CAHpB,YACI,GAEA,GACF;EADkB,AAFC,KAAA,UAAA,GAED,KAAA,OAAA;CACjB;CAYH,aAAa,OACT,GACA,IAAmC,CAAC,GACX;EACzB,IAAM,IAAY,EAAQ,aAAa,GACnC;EACJ,IAAI;GACA,IAAU,MAAM,EAAI,iBAAiB,OAAO,GAAiB;IACzD,GAAI,EAAQ,kBAAkB,CAAC;IAC/B,oBACI;GACR,CAAC;EACL,SAAS,GAAO;GACZ,MAAM,EAAY,CAAK;EAC3B;EAEA,IAAM,IAAc,CAAC,GAAG,EAAQ,WAAW,GACrC,IAAoB,EAAY,GAAa,CAAiB,GAC9D,IACF,EAAY,MAAM,MAAS,MAAS,KAAqB,EAAY,CAAI,CAAC,KAC1E,EAAY,MAAM,MAAS,MAAS,CAAiB,KACpD,EAAY,IAEX,IAAY,IAAI,EAAiB,GAAS;GAC5C,WAAW,EAAQ,WAAW;GAC9B,aAAa,EAAiB,CAAO;GACrC;GACA;GACA;GACA,cAAc,MAAsB;GACpC;EACJ,CAAC;EAGD,OADI,EAAQ,WAAW,MAAO,MAAM,EAAU,OAAO,GAC9C;CACX;CASA,MAAM,SAAwB;EAC1B,IAAM,IAAW,KAAK,KAAK;EACvB,UAAa,MACjB,IAAI;GACA,MAAM,KAAK,QAAQ,CAAK,MAAc,CAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;EAC5D,QAAQ,CAER;CACJ;CAeA,MAAM,QAAQ,GAAyD;EACnE,IAAI,CAAC,MAAM,QAAQ,CAAI,KAAK,EAAK,WAAW,GACxC,MAAM,IAAI,EACN,2DACJ;EAEJ,IAAM,IAAQ,EAAK,EAAE,EAAE,UAAU;EACjC,IAAI,MAAU,GACV,MAAM,IAAI,EAAkB,sCAAsC;EAEtE,IAAM,IAAS,EAAK,WAAW,MAAQ,EAAI,WAAW,CAAK;EAC3D,IAAI,MAAW,IACX,MAAM,IAAI,EACN,0CAA0C,EAAO,OAC1C,EAAK,EAAO,EAAE,OAAO,oBAAoB,EAAM,EAC1D;EAEJ,IAAM,IAAW,KAAK,KAAK;EAC3B,IAAI,MAAa,QAAQ,MAAU,GAC/B,MAAM,IAAI,EACN,qBAAqB,EAAS,yBAAyB,EAAM,EACjE;EAGJ,IAAM,IAAO,IAAI,aAAa,EAAK,SAAS,CAAK;EACjD,KAAK,IAAI,IAAQ,GAAG,IAAQ,EAAK,QAAQ,KAAS,GAC9C,EAAK,IAAI,EAAK,IAAoB,IAAQ,CAAK;EAEnD,IAAM,IAAS,IAAI,EAAI,OAAO,WAAW,GAAM,CAAC,EAAK,QAAQ,CAAK,CAAC,GAE7D,IAAU,YAAY,IAAI,GAC5B;EACJ,IAAI;GACA,IAAU,MAAM,KAAK,QAAQ,IAAI,GAAG,KAAK,KAAK,YAAY,EAAO,CAAC;EACtE,SAAS,GAAO;GACZ,MAAM,EAAW,CAAK;EAC1B;EACA,IAAM,IAAK,YAAY,IAAI,IAAI,GAEzB,IAAc,EAAQ,KAAK,KAAK;EACtC,IAAI,GAAa,SAAS,KAAA,GACtB,MAAM,IAAI,EACN,mCAAmC,KAAK,KAAK,YAAY,UAC7D;EAGJ,IAAM,IAA2B,MAAM,KACnC,EAAY,MACZ,CACJ,GAEM,IAA4B,CAAC;EACnC,IAAI,KAAK,KAAK,sBAAsB,MAAM;GACtC,IAAM,IAAS,EAAQ,KAAK,KAAK;GACjC,IAAI,GAAQ,SAAS,KAAA,GAAW;IAC5B,IAAM,IAAS,MAAM,KAAK,EAAO,MAA2B,MAAM,GAC5D,IAAU,EAAO,SAAS,EAAK;IACrC,KAAK,IAAI,IAAQ,GAAG,IAAQ,EAAK,QAAQ,KAAS,GAC9C,EAAc,KAAK,EAAO,MAAM,IAAQ,IAAU,IAAQ,KAAK,CAAO,CAAC;GAE/E;EACJ;EAEA,OAAO;GAAE;GAAQ;GAAe,SAAS,EAAK;GAAQ;EAAG;CAC7D;CAQA,MAAM,UAAyB;EAC3B,MAAM,KAAK,QAAQ,UAAU;CACjC;AACJ;AAQA,SAAS,EAAY,GAAuB;CACxC,IAAM,IAAU,EAAK,YAAY;CACjC,OAAO,EAAY,MAAM,MAAS,EAAQ,SAAS,CAAI,CAAC;AAC5D"}
|
|
1
|
+
{"version":3,"file":"predictor.js","names":[],"sources":["../../src/tabular/predictor.ts"],"sourcesContent":["/**\n * Running a scikit-learn model in the browser, offline.\n *\n * The model file is produced by `tempest-fastapi-sdk`'s\n * `export_sklearn_to_onnx`. This is everything between that file and an\n * answer — the same glue the Python `OnnxPredictor` provides on a device,\n * with the browser's own traps handled:\n *\n * - **int64 labels arrive as `bigint`.** ONNX Runtime Web surfaces the\n * label tensor as a `BigInt64Array`, so a caller comparing `label === 1`\n * silently gets `false` and `JSON.stringify` throws. Labels are converted.\n * - **`ai.onnx.ml` needs the right build.** Measured: importing\n * `onnxruntime-web/webgpu` loads a WebAssembly binary without those\n * operators, and session creation fails with `No Op registered for\n * TreeEnsembleClassifier`. That failure is translated into an error that\n * names the import.\n * - **Which output is which.** A classifier returns `label` and\n * `probabilities`; a regressor returns a single `variable`. Indexing by\n * position works until the day you deploy the other kind.\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { configuredOrtAssetPath } from \"./assets\";\nimport {\n FeatureShapeError,\n InferenceError,\n ModelLoadError,\n UnsupportedGraphError,\n} from \"./exceptions\";\nimport type {\n FeatureRow,\n PredictedLabel,\n TabularModelSource,\n TabularPrediction,\n TabularPredictorInfo,\n TabularPredictorOptions,\n} from \"./types\";\n\n/**\n * Import ONNX Runtime Web, only when a model is actually being loaded.\n *\n * Static import would make every consumer of this subpath install the peer,\n * including apps that only ever touch `CompactPredictor` — whose whole\n * point is not needing a runtime. Found by installing the published\n * package into an empty project, which is the only place the difference\n * shows.\n *\n * @returns The runtime module.\n * @throws {@link ModelLoadError} when the peer is not installed, naming it.\n */\nasync function loadRuntime(): Promise<typeof ort> {\n try {\n return (await import(\"onnxruntime-web\")) as typeof ort;\n } catch (error) {\n throw new ModelLoadError(\n \"The ONNX route needs the optional peer dependency: \" +\n \"npm install onnxruntime-web. For a model with no runtime at \" +\n \"all, export it with edge_pipeline(compact=True) and load it \" +\n \"through CompactPredictor.\",\n { cause: error },\n );\n }\n}\n\n/**\n * Execution providers used when the caller does not choose.\n *\n * WebAssembly only, and deliberately: scikit-learn graphs are `ai.onnx.ml`\n * operators, which the WebGPU backend does not implement. There is no\n * speed left on the table here — a 10-tree forest predicts a row in about\n * 0.05 ms in Chromium.\n */\nexport const DEFAULT_TABULAR_PROVIDERS: readonly string[] = [\"wasm\"];\n\n/** Output names that indicate predicted classes rather than scores. */\nconst LABEL_HINTS = [\"label\", \"class\", \"variable\", \"output\"] as const;\n\n/** Output names that indicate class scores. */\nconst PROBABILITY_HINTS = [\"probabilit\", \"score\"] as const;\n\n/** Largest plausible feature count; anything above is a dynamic-dim sentinel. */\nconst MAX_DECLARED_FEATURES = 1_000_000;\n\n/**\n * Pick the first output whose name contains one of `hints`.\n *\n * @param names Graph output names.\n * @param hints Lowercase substrings to look for.\n * @returns The matching name, or `null`.\n */\nfunction matchOutput(names: readonly string[], hints: readonly string[]): string | null {\n for (const hint of hints) {\n const found = names.find((name) => name.toLowerCase().includes(hint));\n if (found !== undefined) return found;\n }\n return null;\n}\n\n/**\n * Read the declared feature count from the input metadata.\n *\n * A dynamic batch dimension is reported as a symbolic string or as an\n * out-of-range number (`4294967295` — an unsigned `-1`), so only a sane\n * positive integer in the second position is trusted.\n *\n * @param session The loaded session.\n * @returns The feature count, or `null` when the graph does not declare one.\n */\nfunction declaredFeatures(session: ort.InferenceSession): number | null {\n const metadata = session.inputMetadata?.[0];\n if (metadata === undefined || metadata.isTensor !== true) return null;\n const dimension = metadata.shape[1];\n if (typeof dimension !== \"number\") return null;\n if (!Number.isInteger(dimension) || dimension <= 0) return null;\n return dimension > MAX_DECLARED_FEATURES ? null : dimension;\n}\n\n/**\n * Convert one raw label value into a JS-friendly label.\n *\n * @param value A tensor element: `bigint` for int64, `number` for float,\n * `string` for a string-labelled classifier.\n * @returns The label as a number or string.\n */\nfunction toLabel(value: unknown): PredictedLabel {\n if (typeof value === \"bigint\") return Number(value);\n if (typeof value === \"number\") return value;\n return String(value);\n}\n\n/**\n * Translate a session-creation failure into an error naming its cause.\n *\n * @param error Whatever ONNX Runtime threw.\n * @returns The error to surface.\n */\nfunction asLoadError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes(\"No Op registered\")) {\n return new UnsupportedGraphError(\n \"This runtime build has no kernels for the model's operators. \" +\n \"scikit-learn exports use the ai.onnx.ml domain, which is missing \" +\n 'from the WebGPU build: import \"onnxruntime-web\", not ' +\n '\"onnxruntime-web/webgpu\". Original error: ' +\n message,\n { cause: error },\n );\n }\n return new ModelLoadError(`Failed to load the model: ${message}`, { cause: error });\n}\n\n/**\n * Translate a run failure into an error naming its cause.\n *\n * Measured: an export made with skl2onnx's default (ZipMap enabled) has a\n * probability output that is a sequence of maps, and ONNX Runtime Web\n * refuses to read non-tensor values — `Reading data from non-tensor typed\n * value is not supported`. That message describes the runtime's limitation,\n * not the fix, so it is replaced by one that names the export flag.\n *\n * @param error Whatever ONNX Runtime threw.\n * @returns The error to surface.\n */\nfunction asRunError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error);\n if (\n message.includes(\"non-tensor typed value\") ||\n message.includes(\"Can't access output tensor data\")\n ) {\n return new InferenceError(\n \"The model has a non-tensor output, which ONNX Runtime Web cannot \" +\n \"read. A scikit-learn export made with ZipMap enabled returns a \" +\n \"sequence of maps per row — re-export with export_sklearn_to_onnx, \" +\n `which disables it. Original error: ${message}`,\n { cause: error },\n );\n }\n return new InferenceError(`Inference failed: ${message}`, { cause: error });\n}\n\n/**\n * A loaded tabular model, ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await TabularPredictor.create(\"/models/classifier.onnx\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class TabularPredictor {\n private constructor(\n private readonly runtime: typeof ort,\n private readonly session: ort.InferenceSession,\n /** What is loaded and how it is configured. */\n public readonly info: TabularPredictorInfo,\n ) {}\n\n /**\n * Load a model and describe its graph.\n *\n * @param source A URL string, or the model bytes (which is what an\n * offline app passes, having read them from the cache).\n * @param options Providers, warm-up and pass-through session options.\n * @throws {@link UnsupportedGraphError} when the runtime build lacks the\n * `ai.onnx.ml` operators — the WebGPU entry point does.\n * @throws {@link ModelLoadError} for any other load failure.\n */\n static async create(\n source: TabularModelSource,\n options: TabularPredictorOptions = {},\n ): Promise<TabularPredictor> {\n const providers = options.providers ?? DEFAULT_TABULAR_PROVIDERS;\n const runtime = await loadRuntime();\n const assets = configuredOrtAssetPath();\n if (assets !== undefined) runtime.env.wasm.wasmPaths = assets;\n\n let session: ort.InferenceSession;\n try {\n session = await runtime.InferenceSession.create(source as never, {\n ...(options.sessionOptions ?? {}),\n executionProviders:\n providers as ort.InferenceSession.SessionOptions[\"executionProviders\"],\n });\n } catch (error) {\n throw asLoadError(error);\n }\n\n const outputNames = [...session.outputNames];\n const probabilityOutput = matchOutput(outputNames, PROBABILITY_HINTS);\n const labelOutput =\n outputNames.find((name) => name !== probabilityOutput && isLabelName(name)) ??\n outputNames.find((name) => name !== probabilityOutput) ??\n (outputNames[0] as string);\n\n const predictor = new TabularPredictor(runtime, session, {\n inputName: session.inputNames[0] as string,\n numFeatures: declaredFeatures(session),\n outputNames,\n labelOutput,\n probabilityOutput,\n isClassifier: probabilityOutput !== null,\n providers,\n });\n\n if (options.warmup !== false) await predictor.warmUp();\n return predictor;\n }\n\n /**\n * Run one throwaway inference so the first real call is not the slow one.\n *\n * Skipped when the graph does not declare a feature count, since there\n * is no shape to synthesise. Failures are swallowed: a warm-up that\n * cannot run is not a reason to refuse to serve.\n */\n async warmUp(): Promise<void> {\n const features = this.info.numFeatures;\n if (features === null) return;\n try {\n await this.predict([new Array<number>(features).fill(0)]);\n } catch {\n /* a failed warm-up must not prevent serving */\n }\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model produces them, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width — checked here so the failure names the mismatch\n * instead of surfacing as an opaque runtime error.\n * @throws {@link InferenceError} when the session runs but its outputs\n * cannot be read.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\n \"predict() needs at least one row, shaped [[f1, f2, ...]].\",\n );\n }\n const width = rows[0]?.length ?? 0;\n if (width === 0) {\n throw new FeatureShapeError(\"The first row has no feature values.\");\n }\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n const expected = this.info.numFeatures;\n if (expected !== null && width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n\n const flat = new Float32Array(rows.length * width);\n for (let index = 0; index < rows.length; index += 1) {\n flat.set(rows[index] as number[], index * width);\n }\n const tensor = new this.runtime.Tensor(\"float32\", flat, [rows.length, width]);\n\n const started = performance.now();\n let outputs: ort.InferenceSession.OnnxValueMapType;\n try {\n outputs = await this.session.run({ [this.info.inputName]: tensor });\n } catch (error) {\n throw asRunError(error);\n }\n const ms = performance.now() - started;\n\n const labelTensor = outputs[this.info.labelOutput];\n if (labelTensor?.data === undefined) {\n throw new InferenceError(\n `The model produced no readable \"${this.info.labelOutput}\" output.`,\n );\n }\n\n const labels: PredictedLabel[] = Array.from(\n labelTensor.data as ArrayLike<unknown>,\n toLabel,\n );\n\n const probabilities: number[][] = [];\n if (this.info.probabilityOutput !== null) {\n const scores = outputs[this.info.probabilityOutput];\n if (scores?.data !== undefined) {\n const values = Array.from(scores.data as ArrayLike<number>, Number);\n const classes = values.length / rows.length;\n for (let index = 0; index < rows.length; index += 1) {\n probabilities.push(values.slice(index * classes, (index + 1) * classes));\n }\n }\n }\n\n return { labels, probabilities, numRows: rows.length, ms };\n }\n\n /**\n * Release the session's memory.\n *\n * Worth calling on a route that swaps models: the WebAssembly heap does\n * not shrink on garbage collection alone.\n */\n async dispose(): Promise<void> {\n await this.session.release?.();\n }\n}\n\n/**\n * Whether an output name looks like a label rather than a score.\n *\n * @param name The graph output name.\n * @returns `true` when the name matches a known label convention.\n */\nfunction isLabelName(name: string): boolean {\n const lowered = name.toLowerCase();\n return LABEL_HINTS.some((hint) => lowered.includes(hint));\n}\n"],"mappings":"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"}
|
package/dist/tabular.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./tabular/assets.cjs"),t=require("./tabular/exceptions.cjs"),n=require("./tabular/cache.cjs"),r=require("./tabular/compact.cjs"),i=require("./tabular/predictor.cjs"),a=require("./tabular/manifest.cjs"),o=require("./tabular/use-tabular-predictor.cjs");exports.CompactFormatError=t.CompactFormatError,exports.CompactPredictor=r.CompactPredictor,exports.DEFAULT_MODEL_CACHE=n.DEFAULT_MODEL_CACHE,exports.DEFAULT_TABULAR_PROVIDERS=i.DEFAULT_TABULAR_PROVIDERS,exports.FeatureShapeError=t.FeatureShapeError,exports.InferenceError=t.InferenceError,exports.MANIFEST_FILENAME=a.MANIFEST_FILENAME,exports.ModelFetchError=t.ModelFetchError,exports.ModelLoadError=t.ModelLoadError,exports.ORT_WASM_ASSETS=e.ORT_WASM_ASSETS,exports.SUPPORTED_COMPACT_SCHEMA=r.SUPPORTED_COMPACT_SCHEMA,exports.SUPPORTED_MANIFEST_SCHEMA=a.SUPPORTED_MANIFEST_SCHEMA,exports.TabularError=t.TabularError,exports.TabularPredictor=i.TabularPredictor,exports.UnsupportedGraphError=t.UnsupportedGraphError,exports.cacheModelBytes=n.cacheModelBytes,exports.clearModelCache=n.clearModelCache,exports.configureOrtAssets=e.configureOrtAssets,exports.fetchEdgeManifest=a.fetchEdgeManifest,exports.fetchModelBytes=n.fetchModelBytes,exports.isModelCached=n.isModelCached,exports.loadEdgePackage=a.loadEdgePackage,exports.ortAssetUrls=e.ortAssetUrls,exports.useTabularPredictor=o.useTabularPredictor;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./tabular/assets.cjs"),t=require("./tabular/exceptions.cjs"),n=require("./tabular/cache.cjs"),r=require("./tabular/compact.cjs"),i=require("./tabular/predictor.cjs"),a=require("./tabular/manifest.cjs"),o=require("./tabular/use-tabular-predictor.cjs");exports.CompactFormatError=t.CompactFormatError,exports.CompactPredictor=r.CompactPredictor,exports.DEFAULT_MODEL_CACHE=n.DEFAULT_MODEL_CACHE,exports.DEFAULT_TABULAR_PROVIDERS=i.DEFAULT_TABULAR_PROVIDERS,exports.FeatureShapeError=t.FeatureShapeError,exports.InferenceError=t.InferenceError,exports.MANIFEST_FILENAME=a.MANIFEST_FILENAME,exports.ModelFetchError=t.ModelFetchError,exports.ModelLoadError=t.ModelLoadError,exports.ORT_WASM_ASSETS=e.ORT_WASM_ASSETS,exports.SUPPORTED_COMPACT_SCHEMA=r.SUPPORTED_COMPACT_SCHEMA,exports.SUPPORTED_MANIFEST_SCHEMA=a.SUPPORTED_MANIFEST_SCHEMA,exports.TabularError=t.TabularError,exports.TabularPredictor=i.TabularPredictor,exports.UnsupportedGraphError=t.UnsupportedGraphError,exports.cacheModelBytes=n.cacheModelBytes,exports.clearModelCache=n.clearModelCache,exports.configureOrtAssets=e.configureOrtAssets,exports.configuredOrtAssetPath=e.configuredOrtAssetPath,exports.fetchEdgeManifest=a.fetchEdgeManifest,exports.fetchModelBytes=n.fetchModelBytes,exports.isModelCached=n.isModelCached,exports.loadEdgePackage=a.loadEdgePackage,exports.ortAssetUrls=e.ortAssetUrls,exports.useTabularPredictor=o.useTabularPredictor;
|
package/dist/tabular.d.ts
CHANGED
|
@@ -95,6 +95,13 @@ export declare class CompactPredictor {
|
|
|
95
95
|
* its slot in the value array — so the walk needs no second lookup and
|
|
96
96
|
* the file stores values only for leaves.
|
|
97
97
|
*
|
|
98
|
+
* **The comparison runs in float32** (`Math.fround`), because that is
|
|
99
|
+
* what scikit-learn does: it casts its input to float32 before
|
|
100
|
+
* traversing, so a threshold like 5.099999904632568 — a float32 value
|
|
101
|
+
* widened for storage — and an input of 5.1 compare *equal* and go
|
|
102
|
+
* left. Comparing in float64 sends that row right instead, which on an
|
|
103
|
+
* iris forest changed one tree's vote and moved a probability by 0.05.
|
|
104
|
+
*
|
|
98
105
|
* @param row The prepared feature values.
|
|
99
106
|
* @returns One averaged score per output.
|
|
100
107
|
*/
|
|
@@ -131,6 +138,16 @@ export declare class CompactPredictor {
|
|
|
131
138
|
readonly estimator: string;
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Where the runtime's binaries were said to live.
|
|
143
|
+
*
|
|
144
|
+
* Read by the ONNX predictor just before it creates a session, which is
|
|
145
|
+
* the first moment the runtime exists to be configured.
|
|
146
|
+
*
|
|
147
|
+
* @returns The configured directory, or `undefined` when none was set.
|
|
148
|
+
*/
|
|
149
|
+
export declare function configuredOrtAssetPath(): string | undefined;
|
|
150
|
+
|
|
134
151
|
/**
|
|
135
152
|
* Point ONNX Runtime Web at locally served WebAssembly binaries.
|
|
136
153
|
*
|
|
@@ -503,6 +520,7 @@ export declare class CompactPredictor {
|
|
|
503
520
|
* ```
|
|
504
521
|
*/
|
|
505
522
|
export declare class TabularPredictor {
|
|
523
|
+
private readonly runtime;
|
|
506
524
|
private readonly session;
|
|
507
525
|
/** What is loaded and how it is configured. */
|
|
508
526
|
readonly info: TabularPredictorInfo;
|
package/dist/tabular.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ORT_WASM_ASSETS as e, configureOrtAssets as t,
|
|
2
|
-
import { CompactFormatError as
|
|
3
|
-
import { DEFAULT_MODEL_CACHE as
|
|
4
|
-
import { CompactPredictor as
|
|
5
|
-
import { DEFAULT_TABULAR_PROVIDERS as
|
|
6
|
-
import { MANIFEST_FILENAME as
|
|
7
|
-
import { useTabularPredictor as
|
|
8
|
-
export {
|
|
1
|
+
import { ORT_WASM_ASSETS as e, configureOrtAssets as t, configuredOrtAssetPath as n, ortAssetUrls as r } from "./tabular/assets.js";
|
|
2
|
+
import { CompactFormatError as i, FeatureShapeError as a, InferenceError as o, ModelFetchError as s, ModelLoadError as c, TabularError as l, UnsupportedGraphError as u } from "./tabular/exceptions.js";
|
|
3
|
+
import { DEFAULT_MODEL_CACHE as d, cacheModelBytes as f, clearModelCache as p, fetchModelBytes as m, isModelCached as h } from "./tabular/cache.js";
|
|
4
|
+
import { CompactPredictor as g, SUPPORTED_COMPACT_SCHEMA as _ } from "./tabular/compact.js";
|
|
5
|
+
import { DEFAULT_TABULAR_PROVIDERS as v, TabularPredictor as y } from "./tabular/predictor.js";
|
|
6
|
+
import { MANIFEST_FILENAME as b, SUPPORTED_MANIFEST_SCHEMA as x, fetchEdgeManifest as S, loadEdgePackage as C } from "./tabular/manifest.js";
|
|
7
|
+
import { useTabularPredictor as w } from "./tabular/use-tabular-predictor.js";
|
|
8
|
+
export { i as CompactFormatError, g as CompactPredictor, d as DEFAULT_MODEL_CACHE, v as DEFAULT_TABULAR_PROVIDERS, a as FeatureShapeError, o as InferenceError, b as MANIFEST_FILENAME, s as ModelFetchError, c as ModelLoadError, e as ORT_WASM_ASSETS, _ as SUPPORTED_COMPACT_SCHEMA, x as SUPPORTED_MANIFEST_SCHEMA, l as TabularError, y as TabularPredictor, u as UnsupportedGraphError, f as cacheModelBytes, p as clearModelCache, t as configureOrtAssets, n as configuredOrtAssetPath, S as fetchEdgeManifest, m as fetchModelBytes, h as isModelCached, C as loadEdgePackage, r as ortAssetUrls, w as useTabularPredictor };
|