@mauriciobenjamin700/ort-vision-sdk-web 0.3.0 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,101 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-08-03
11
+
12
+ ### Added
13
+
14
+ - **Tasks read their class names off the model, matching the Python SDK.**
15
+ `labels` is now optional on all three tasks: when omitted, the names the
16
+ exporter baked into the model are used (Ultralytics writes them as `names` in
17
+ the metadata map), and only a model carrying none falls back to the COCO
18
+ preset for detection/segmentation or generated `class_<id>` labels for
19
+ classification. Passing `labels` still wins, for a model whose names are
20
+ wrong or absent.
21
+
22
+ ```typescript
23
+ const det = await Detector.create("/models/detect.onnx");
24
+ console.log(det.labels); // ["ocular-mucosa"] — from the model, not a preset
25
+ ```
26
+
27
+ This also fixes a trap: a single-class detector used to **fail** without an
28
+ explicit `labels`, because the 80-name COCO default disagreed with its class
29
+ count.
30
+
31
+ - **`numClasses` is inferred from the declared output shape.** A YOLO head
32
+ declares `(B, 4 + nc, N)` and a classifier `(B, nc)`, so the count no longer
33
+ has to be supplied. Passing it still validates the labels against the model.
34
+
35
+ - **`OrtSession.metadata`** exposes the model's custom metadata map (`names`,
36
+ `task`, `imgsz`, ...). `onnxruntime-web` does not surface that map — unlike
37
+ Python's `custom_metadata_map` — so it is read from the model's own bytes at
38
+ load time by walking `metadata_props` in the ModelProto. A truncated or
39
+ unexpected file yields an empty map instead of an error, and every caller
40
+ falls back to what it was given.
41
+
42
+ - **`OrtSession.outputShapes` / `.outputShape`**, the shapes the graph declares
43
+ for its outputs, with dynamic axes as `null` — the same treatment
44
+ `inputShapes` already got.
45
+
46
+ - **`detectionNumClasses`, `classificationNumClasses`, `readModelMetadata`,
47
+ `modelNames`**: the pure helpers behind the above, exported for anyone
48
+ assembling their own pipeline.
49
+
50
+ ### Changed
51
+
52
+ - **A URL model is fetched by the SDK instead of by ORT**, so its bytes are
53
+ available to read metadata from. It is the same single download, and
54
+ `readMetadata: false` on the session options restores the previous path (ORT
55
+ fetches the URL, `metadata` stays empty). A failed fetch falls back to handing
56
+ ORT the URL, so a model that ORT could load still loads.
57
+
58
+ ## [0.4.0] - 2026-08-03
59
+
60
+ ### Added
61
+
62
+ - **Tasks read their input resolution off the model instead of trusting
63
+ configuration.** `Classifier`, `Detector` and `Segmenter` now ask the ONNX
64
+ graph what shape it declares and preprocess to that. The resolution a session
65
+ must be fed at is a property of the export — feeding a 640x640 tensor to a
66
+ graph exported at 224x224 makes ORT abort mid-run with:
67
+
68
+ ```text
69
+ Inference failed: failed to call OrtRun(). ERROR_CODE: 2, ERROR_MESSAGE: Got
70
+ invalid dimensions for input: images for the following indices index: 2
71
+ Got: 640 Expected: 224
72
+ ```
73
+
74
+ A caller had no way to see that coming: the number lives in the file. So it is
75
+ read from there now.
76
+
77
+ ```typescript
78
+ // An Ultralytics -cls export is 224; nothing to configure, nothing to get wrong
79
+ const clf = await Classifier.create("/models/classify.onnx", { labels: LABELS });
80
+ console.log(clf.inputSize); // [224, 224]
81
+ ```
82
+
83
+ `inputSize` is now a *fallback*, used only when the graph leaves its spatial
84
+ axes dynamic. Passing a size that contradicts a static graph logs a warning
85
+ and the graph wins — honoring the caller there would only turn a fixable
86
+ mismatch into a failed run.
87
+
88
+ - **`inputSize` getter on every task**, so callers can read back the resolution
89
+ inference actually runs at rather than the one they asked for.
90
+
91
+ - **`OrtSession.inputShapes` / `OrtSession.inputShape`** expose what the graph
92
+ declares, with dynamic axes as `null`. Empty when the runtime reports no
93
+ metadata (`onnxruntime-web` older than 1.21).
94
+
95
+ - **`OrtSession.release()`** frees the native session. Needed whenever a session
96
+ is discarded while the page lives on — rebuilding a task at a different input
97
+ size, swapping in a newer model — which previously required reaching into
98
+ `session.raw`.
99
+
100
+ - **`declaredShapesFrom`, `spatialInputSize`, `resolveInputSize`** (plus the
101
+ `DeclaredShape` / `DeclaredDim` types): the pure helpers behind the above,
102
+ exported so callers building their own pipeline can reuse the same precedence
103
+ rules without importing `onnxruntime-web` types themselves.
104
+
10
105
  ## [0.3.0] - 2026-08-02
11
106
 
12
107
  ### Added
@@ -46,6 +141,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
46
141
  would break `tempest-react-sdk`, which re-exports all three from its vendored
47
142
  copy. The warning now points at `0.4.0`.
48
143
 
144
+ ## [0.2.2] - 2026-05-31
145
+
146
+ ### Changed
147
+
148
+ - README now links to the bilingual (PT-BR + EN-US) documentation site on
149
+ GitHub Pages (<https://mauriciobenjamin700.github.io/ort-vision-sdk/>) so the
150
+ npm package page points readers to the full guide and API reference.
151
+ Documentation-only release; no public API changes.
152
+
49
153
  ## [0.2.1] - 2026-05-03
50
154
 
51
155
  First **published** release on npm. The previous `0.2.0` tag never produced a
package/README.md CHANGED
@@ -37,8 +37,17 @@ const result = await clf.predict("/images/dog.jpg", { topK: 5 });
37
37
  console.log(result.className, result.confidence);
38
38
  console.log(result.probabilities);
39
39
  // result.image is an RGBImage (HWC RGB Uint8Array) — the original input.
40
+ console.log(clf.inputSize);
41
+ // [224, 224] — read from the .onnx graph, not configured.
40
42
  ```
41
43
 
44
+ > **The model decides its input size.** `inputSize` is optional: the resolution
45
+ > the graph declares always wins, because it is the only shape ONNX Runtime will
46
+ > accept. An Ultralytics `-cls` export is 224x224 while a detector is 640x640 —
47
+ > get it wrong and ORT aborts with `Got invalid dimensions for input: images`.
48
+ > Read it back with `task.inputSize`, or inspect `session.inputShape`. See
49
+ > [O modelo manda / The model decides](https://mauriciobenjamin700.github.io/ort-vision-sdk/en/guia/modelo/).
50
+
42
51
  ### Object detection
43
52
 
44
53
  ```typescript
@@ -0,0 +1,81 @@
1
+ /**
2
+ * What the ONNX graph itself says about its inputs.
3
+ *
4
+ * The resolution a session must be fed at is a property of the exported model,
5
+ * not of the configuration around it. Feeding a 640x640 tensor to a graph
6
+ * exported at 224x224 makes ORT abort the run with
7
+ * `Got invalid dimensions for input: images ... Got: 640 Expected: 224`, and the
8
+ * caller has no way to see that coming from the outside — the number lives in
9
+ * the file. So the SDK reads it from the graph and treats any configured size as
10
+ * a fallback for when the graph leaves it open.
11
+ */
12
+ import type * as ort from "onnxruntime-web";
13
+ /**
14
+ * One declared dimension: a number when the graph pins it, `null` when the
15
+ * dimension is symbolic (dynamic).
16
+ */
17
+ export type DeclaredDim = number | null;
18
+ /** A declared input/output shape, dynamic axes appearing as `null`. */
19
+ export type DeclaredShape = readonly DeclaredDim[];
20
+ /**
21
+ * Convert ORT value metadata into declared shapes.
22
+ *
23
+ * @param metadata Metadata as reported by `InferenceSession.inputMetadata`, or
24
+ * `undefined` on ORT builds that predate it (added in onnxruntime 1.21).
25
+ * @returns One shape per value, in declaration order. Non-tensor values and
26
+ * builds without metadata yield empty shapes, which read as "nothing
27
+ * declared" everywhere downstream.
28
+ */
29
+ export declare function declaredShapesFrom(metadata: readonly ort.InferenceSession.ValueMetadata[] | undefined): readonly DeclaredShape[];
30
+ /**
31
+ * Read the spatial input size out of a declared NCHW shape.
32
+ *
33
+ * @param shape The declared shape of the model's image input.
34
+ * @returns `[width, height]` in pixels, or `null` when the shape is not 4D or
35
+ * leaves either spatial axis dynamic — in which case the model accepts more
36
+ * than one resolution and there is nothing to correct.
37
+ */
38
+ export declare function spatialInputSize(shape: DeclaredShape): readonly [number, number] | null;
39
+ /**
40
+ * Infer how many classes a YOLO detection/segmentation head emits.
41
+ *
42
+ * Such a head declares `(B, 4 + nc, N)` — four box coordinates stacked above one
43
+ * score per class, over `N` candidate anchors. `N` is in the thousands and the
44
+ * batch is 1, so the channel axis is the smallest static axis above 1.
45
+ *
46
+ * @param shape Declared shape of the model's first output.
47
+ * @returns The class count, or `null` when the shape leaves it undeterminable —
48
+ * fully dynamic, or too small to hold boxes plus at least one class.
49
+ */
50
+ export declare function detectionNumClasses(shape: DeclaredShape): number | null;
51
+ /**
52
+ * Infer how many classes a classification head emits.
53
+ *
54
+ * A classifier declares `(B, nc)`, so the count is the last static axis.
55
+ *
56
+ * @param shape Declared shape of the model's first output.
57
+ * @returns The class count, or `null` when the last axis is dynamic or absent.
58
+ */
59
+ export declare function classificationNumClasses(shape: DeclaredShape): number | null;
60
+ export interface ResolveInputSizeOptions {
61
+ /** Declared shape of the model's image input, from {@link declaredShapesFrom}. */
62
+ readonly graphShape?: DeclaredShape;
63
+ /** Size the caller asked for, if any. */
64
+ readonly requested?: readonly [number, number];
65
+ /** Size to use when neither the graph nor the caller pins one. */
66
+ readonly fallback: readonly [number, number];
67
+ }
68
+ /**
69
+ * Decide the input size a task will preprocess to.
70
+ *
71
+ * Precedence is graph → caller → fallback. The graph wins over an explicit
72
+ * `inputSize` because a static shape is not a preference, it is what ORT will
73
+ * accept: honoring the caller there would only turn a fixable mismatch into a
74
+ * failed run. A disagreement is a configuration bug in the caller, so it is
75
+ * reported through `console.warn` instead of being swallowed.
76
+ *
77
+ * @param options Graph shape, requested size and per-task fallback.
78
+ * @returns The `[width, height]` to preprocess to.
79
+ */
80
+ export declare function resolveInputSize(options: ResolveInputSizeOptions): readonly [number, number];
81
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/core/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAE5C;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,IAAI,CAAC;AAExC,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG,SAAS,WAAW,EAAE,CAAC;AAEnD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,GAAG,SAAS,GAClE,SAAS,aAAa,EAAE,CAO1B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAMvF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAMvE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAI5E;AAED,MAAM,WAAW,uBAAuB;IACtC,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC;IACpC,yCAAyC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAW5F"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * What the ONNX graph itself says about its inputs.
3
+ *
4
+ * The resolution a session must be fed at is a property of the exported model,
5
+ * not of the configuration around it. Feeding a 640x640 tensor to a graph
6
+ * exported at 224x224 makes ORT abort the run with
7
+ * `Got invalid dimensions for input: images ... Got: 640 Expected: 224`, and the
8
+ * caller has no way to see that coming from the outside — the number lives in
9
+ * the file. So the SDK reads it from the graph and treats any configured size as
10
+ * a fallback for when the graph leaves it open.
11
+ */
12
+ /**
13
+ * Convert ORT value metadata into declared shapes.
14
+ *
15
+ * @param metadata Metadata as reported by `InferenceSession.inputMetadata`, or
16
+ * `undefined` on ORT builds that predate it (added in onnxruntime 1.21).
17
+ * @returns One shape per value, in declaration order. Non-tensor values and
18
+ * builds without metadata yield empty shapes, which read as "nothing
19
+ * declared" everywhere downstream.
20
+ */
21
+ export function declaredShapesFrom(metadata) {
22
+ if (metadata === undefined)
23
+ return [];
24
+ return metadata.map((value) => value.isTensor
25
+ ? value.shape.map((dim) => (typeof dim === "number" && Number.isInteger(dim) && dim > 0 ? dim : null))
26
+ : []);
27
+ }
28
+ /**
29
+ * Read the spatial input size out of a declared NCHW shape.
30
+ *
31
+ * @param shape The declared shape of the model's image input.
32
+ * @returns `[width, height]` in pixels, or `null` when the shape is not 4D or
33
+ * leaves either spatial axis dynamic — in which case the model accepts more
34
+ * than one resolution and there is nothing to correct.
35
+ */
36
+ export function spatialInputSize(shape) {
37
+ if (shape.length !== 4)
38
+ return null;
39
+ const height = shape[2];
40
+ const width = shape[3];
41
+ if (height === null || height === undefined || width === null || width === undefined)
42
+ return null;
43
+ return [width, height];
44
+ }
45
+ /**
46
+ * Infer how many classes a YOLO detection/segmentation head emits.
47
+ *
48
+ * Such a head declares `(B, 4 + nc, N)` — four box coordinates stacked above one
49
+ * score per class, over `N` candidate anchors. `N` is in the thousands and the
50
+ * batch is 1, so the channel axis is the smallest static axis above 1.
51
+ *
52
+ * @param shape Declared shape of the model's first output.
53
+ * @returns The class count, or `null` when the shape leaves it undeterminable —
54
+ * fully dynamic, or too small to hold boxes plus at least one class.
55
+ */
56
+ export function detectionNumClasses(shape) {
57
+ const staticDims = shape.filter((dim) => dim !== null && dim > 1);
58
+ if (staticDims.length === 0)
59
+ return null;
60
+ const channels = Math.min(...staticDims);
61
+ if (channels < 5)
62
+ return null;
63
+ return channels - 4;
64
+ }
65
+ /**
66
+ * Infer how many classes a classification head emits.
67
+ *
68
+ * A classifier declares `(B, nc)`, so the count is the last static axis.
69
+ *
70
+ * @param shape Declared shape of the model's first output.
71
+ * @returns The class count, or `null` when the last axis is dynamic or absent.
72
+ */
73
+ export function classificationNumClasses(shape) {
74
+ const last = shape[shape.length - 1];
75
+ if (last === null || last === undefined || last < 1)
76
+ return null;
77
+ return last;
78
+ }
79
+ /**
80
+ * Decide the input size a task will preprocess to.
81
+ *
82
+ * Precedence is graph → caller → fallback. The graph wins over an explicit
83
+ * `inputSize` because a static shape is not a preference, it is what ORT will
84
+ * accept: honoring the caller there would only turn a fixable mismatch into a
85
+ * failed run. A disagreement is a configuration bug in the caller, so it is
86
+ * reported through `console.warn` instead of being swallowed.
87
+ *
88
+ * @param options Graph shape, requested size and per-task fallback.
89
+ * @returns The `[width, height]` to preprocess to.
90
+ */
91
+ export function resolveInputSize(options) {
92
+ const graph = options.graphShape === undefined ? null : spatialInputSize(options.graphShape);
93
+ const requested = options.requested;
94
+ if (graph === null)
95
+ return requested ?? options.fallback;
96
+ if (requested !== undefined && (requested[0] !== graph[0] || requested[1] !== graph[1])) {
97
+ console.warn(`[ort-vision-sdk] The model declares a ${graph[0]}x${graph[1]} input; ` +
98
+ `ignoring the requested ${requested[0]}x${requested[1]}, which ONNX Runtime would reject.`);
99
+ }
100
+ return graph;
101
+ }
102
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/core/graph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Read the metadata an exporter baked into a `.onnx` file.
3
+ *
4
+ * `onnxruntime-web` exposes input/output metadata but **not** the model's
5
+ * custom metadata map, which is where Ultralytics writes `names`, `task` and
6
+ * `imgsz`. The Python SDK gets it for free from
7
+ * `InferenceSession.get_modelmeta().custom_metadata_map`; in the browser the
8
+ * only way to the same information is to read it out of the file, so this
9
+ * module walks just enough of the ModelProto wire format to collect
10
+ * `metadata_props`.
11
+ *
12
+ * It never throws and never allocates unbounded: a truncated, hostile or
13
+ * simply unexpected file yields an empty map, and every caller treats that as
14
+ * "the model says nothing", falling back to what it was given.
15
+ */
16
+ /**
17
+ * Collect a model's custom metadata map straight out of its bytes.
18
+ *
19
+ * @param model The `.onnx` file contents.
20
+ * @returns Key/value metadata — `names`, `task`, `imgsz`, ... for an
21
+ * Ultralytics export — or an empty object when the file carries none or
22
+ * cannot be walked.
23
+ */
24
+ export declare function readModelMetadata(model: Uint8Array | ArrayBufferLike): Readonly<Record<string, string>>;
25
+ /**
26
+ * Read the class names an export baked into the model metadata.
27
+ *
28
+ * Ultralytics writes `names` as the Python `repr` of a `dict[int, str]` — e.g.
29
+ * `"{0: 'deworm', 1: 'not_deworm'}"`. The value is parsed structurally (never
30
+ * evaluated), and anything unparseable, non-`dict`, or not keyed by contiguous
31
+ * integers from zero is rejected whole rather than half-applied: a partial name
32
+ * map would silently mislabel predictions.
33
+ *
34
+ * @param metadata A model's custom metadata map.
35
+ * @returns Class names in class-id order, or `null` when the model carries no
36
+ * usable `names` entry.
37
+ */
38
+ export declare function modelNames(metadata: Readonly<Record<string, string>> | undefined): readonly string[] | null;
39
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/core/metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAyIH;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,GAAG,eAAe,GAClC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqBlC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,GACrD,SAAS,MAAM,EAAE,GAAG,IAAI,CA6B1B"}
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Read the metadata an exporter baked into a `.onnx` file.
3
+ *
4
+ * `onnxruntime-web` exposes input/output metadata but **not** the model's
5
+ * custom metadata map, which is where Ultralytics writes `names`, `task` and
6
+ * `imgsz`. The Python SDK gets it for free from
7
+ * `InferenceSession.get_modelmeta().custom_metadata_map`; in the browser the
8
+ * only way to the same information is to read it out of the file, so this
9
+ * module walks just enough of the ModelProto wire format to collect
10
+ * `metadata_props`.
11
+ *
12
+ * It never throws and never allocates unbounded: a truncated, hostile or
13
+ * simply unexpected file yields an empty map, and every caller treats that as
14
+ * "the model says nothing", falling back to what it was given.
15
+ */
16
+ /** Field number of `metadata_props` in `ModelProto` (repeated StringStringEntryProto). */
17
+ const MODEL_METADATA_PROPS_FIELD = 14;
18
+ /** Field numbers of `key` and `value` in `StringStringEntryProto`. */
19
+ const ENTRY_KEY_FIELD = 1;
20
+ const ENTRY_VALUE_FIELD = 2;
21
+ /** Protobuf wire types this reader understands. */
22
+ const WIRE_VARINT = 0;
23
+ const WIRE_FIXED64 = 1;
24
+ const WIRE_LENGTH_DELIMITED = 2;
25
+ const WIRE_FIXED32 = 5;
26
+ /**
27
+ * Hard ceiling on a single length-delimited field, as a guard against a corrupt
28
+ * length turning into a huge slice. Model metadata values are strings — a class
29
+ * name map for thousands of classes still fits well inside this.
30
+ */
31
+ const MAX_FIELD_BYTES = 1 << 20;
32
+ /**
33
+ * Read a base-128 varint.
34
+ *
35
+ * @param cursor Cursor to advance.
36
+ * @returns The value, or `null` when the varint is truncated or overlong
37
+ * (beyond the 64-bit range protobuf allows).
38
+ */
39
+ function readVarint(cursor) {
40
+ let result = 0;
41
+ let shift = 0;
42
+ while (cursor.pos < cursor.end) {
43
+ const byte = cursor.bytes[cursor.pos];
44
+ cursor.pos += 1;
45
+ result += (byte & 0x7f) * 2 ** shift;
46
+ if ((byte & 0x80) === 0)
47
+ return result;
48
+ shift += 7;
49
+ if (shift > 63)
50
+ return null;
51
+ }
52
+ return null;
53
+ }
54
+ /**
55
+ * Skip a field whose contents are not needed.
56
+ *
57
+ * @param cursor Cursor to advance past the field's payload.
58
+ * @param wireType Wire type read from the field's tag.
59
+ * @returns `true` when the field was skipped, `false` when the stream is
60
+ * unreadable from here (unknown wire type or truncated payload).
61
+ */
62
+ function skipField(cursor, wireType) {
63
+ switch (wireType) {
64
+ case WIRE_VARINT:
65
+ return readVarint(cursor) !== null;
66
+ case WIRE_FIXED64:
67
+ cursor.pos += 8;
68
+ return cursor.pos <= cursor.end;
69
+ case WIRE_LENGTH_DELIMITED: {
70
+ const length = readVarint(cursor);
71
+ if (length === null)
72
+ return false;
73
+ cursor.pos += length;
74
+ return cursor.pos <= cursor.end;
75
+ }
76
+ case WIRE_FIXED32:
77
+ cursor.pos += 4;
78
+ return cursor.pos <= cursor.end;
79
+ default:
80
+ return false;
81
+ }
82
+ }
83
+ /**
84
+ * Read a length-delimited payload as a byte range.
85
+ *
86
+ * @param cursor Cursor to advance past the payload.
87
+ * @returns Start and end offsets of the payload, or `null` when the length is
88
+ * truncated, overruns the buffer, or exceeds {@link MAX_FIELD_BYTES}.
89
+ */
90
+ function readLengthDelimited(cursor) {
91
+ const length = readVarint(cursor);
92
+ if (length === null || length > MAX_FIELD_BYTES)
93
+ return null;
94
+ const start = cursor.pos;
95
+ const end = start + length;
96
+ if (end > cursor.end)
97
+ return null;
98
+ cursor.pos = end;
99
+ return { start, end };
100
+ }
101
+ /**
102
+ * Decode one `StringStringEntryProto` into a key/value pair.
103
+ *
104
+ * @param bytes The model buffer.
105
+ * @param start Offset the entry's payload starts at.
106
+ * @param end Offset the entry's payload ends at.
107
+ * @returns The pair, or `null` when either half is missing or undecodable.
108
+ */
109
+ function readEntry(bytes, start, end) {
110
+ const cursor = { bytes, end, pos: start };
111
+ const decoder = new TextDecoder("utf-8", { fatal: false });
112
+ let key = null;
113
+ let value = null;
114
+ while (cursor.pos < end) {
115
+ const tag = readVarint(cursor);
116
+ if (tag === null)
117
+ return null;
118
+ const field = tag >>> 3;
119
+ const wireType = tag & 0x07;
120
+ if (wireType === WIRE_LENGTH_DELIMITED &&
121
+ (field === ENTRY_KEY_FIELD || field === ENTRY_VALUE_FIELD)) {
122
+ const range = readLengthDelimited(cursor);
123
+ if (range === null)
124
+ return null;
125
+ const text = decoder.decode(bytes.subarray(range.start, range.end));
126
+ if (field === ENTRY_KEY_FIELD)
127
+ key = text;
128
+ else
129
+ value = text;
130
+ continue;
131
+ }
132
+ if (!skipField(cursor, wireType))
133
+ return null;
134
+ }
135
+ if (key === null || value === null)
136
+ return null;
137
+ return [key, value];
138
+ }
139
+ /**
140
+ * Collect a model's custom metadata map straight out of its bytes.
141
+ *
142
+ * @param model The `.onnx` file contents.
143
+ * @returns Key/value metadata — `names`, `task`, `imgsz`, ... for an
144
+ * Ultralytics export — or an empty object when the file carries none or
145
+ * cannot be walked.
146
+ */
147
+ export function readModelMetadata(model) {
148
+ const bytes = model instanceof Uint8Array ? model : new Uint8Array(model);
149
+ const cursor = { bytes, end: bytes.length, pos: 0 };
150
+ const metadata = {};
151
+ while (cursor.pos < cursor.end) {
152
+ const tag = readVarint(cursor);
153
+ if (tag === null)
154
+ break;
155
+ const field = tag >>> 3;
156
+ const wireType = tag & 0x07;
157
+ if (field === MODEL_METADATA_PROPS_FIELD && wireType === WIRE_LENGTH_DELIMITED) {
158
+ const range = readLengthDelimited(cursor);
159
+ if (range === null)
160
+ break;
161
+ const entry = readEntry(bytes, range.start, range.end);
162
+ if (entry)
163
+ metadata[entry[0]] = entry[1];
164
+ continue;
165
+ }
166
+ if (!skipField(cursor, wireType))
167
+ break;
168
+ }
169
+ return metadata;
170
+ }
171
+ /**
172
+ * Read the class names an export baked into the model metadata.
173
+ *
174
+ * Ultralytics writes `names` as the Python `repr` of a `dict[int, str]` — e.g.
175
+ * `"{0: 'deworm', 1: 'not_deworm'}"`. The value is parsed structurally (never
176
+ * evaluated), and anything unparseable, non-`dict`, or not keyed by contiguous
177
+ * integers from zero is rejected whole rather than half-applied: a partial name
178
+ * map would silently mislabel predictions.
179
+ *
180
+ * @param metadata A model's custom metadata map.
181
+ * @returns Class names in class-id order, or `null` when the model carries no
182
+ * usable `names` entry.
183
+ */
184
+ export function modelNames(metadata) {
185
+ const raw = metadata?.names?.trim();
186
+ if (!raw || !raw.startsWith("{") || !raw.endsWith("}"))
187
+ return null;
188
+ const body = raw.slice(1, -1).trim();
189
+ if (!body)
190
+ return null;
191
+ const names = new Map();
192
+ const entryPattern = /(-?\d+)\s*:\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")/g;
193
+ let consumed = 0;
194
+ for (const match of body.matchAll(entryPattern)) {
195
+ const id = Number(match[1]);
196
+ const text = match[2] ?? match[3];
197
+ if (!Number.isInteger(id) || text === undefined)
198
+ return null;
199
+ names.set(id, unescapeQuoted(text));
200
+ consumed += match[0].length;
201
+ }
202
+ if (names.size === 0)
203
+ return null;
204
+ const separators = body.length - consumed;
205
+ if (separators > names.size * 3)
206
+ return null;
207
+ const ordered = [];
208
+ for (let id = 0; id < names.size; id += 1) {
209
+ const name = names.get(id);
210
+ if (name === undefined)
211
+ return null;
212
+ ordered.push(name);
213
+ }
214
+ return ordered;
215
+ }
216
+ /**
217
+ * Resolve the backslash escapes Python's `repr` emits inside a quoted string.
218
+ *
219
+ * @param text The quoted string's contents, escapes intact.
220
+ * @returns The same text with `\\`, `\'`, `\"`, `\n`, `\r` and `\t` resolved.
221
+ */
222
+ function unescapeQuoted(text) {
223
+ return text.replace(/\\(.)/g, (_, char) => {
224
+ if (char === "n")
225
+ return "\n";
226
+ if (char === "r")
227
+ return "\r";
228
+ if (char === "t")
229
+ return "\t";
230
+ return char;
231
+ });
232
+ }
233
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../src/core/metadata.ts"],"names":[],"mappings":"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"}