edgeinfer 0.0.0-stage → 2.0.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/LICENSE +21 -0
- package/README.md +363 -2
- package/dist/index.d.mts +590 -0
- package/dist/index.d.ts +590 -0
- package/dist/index.js +1827 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1769 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -4
- package/src/download.ts +39 -0
- package/src/edgeinfer.ts +710 -0
- package/src/events.ts +17 -0
- package/src/image-processor.ts +35 -0
- package/src/index.ts +15 -0
- package/src/matryoshka.ts +441 -0
- package/src/model-cache.ts +46 -0
- package/src/onnx-lm.ts +104 -0
- package/src/runtime-manager.ts +338 -0
- package/src/speculative.ts +294 -0
- package/src/tokenizer.ts +419 -0
- package/src/types.ts +19 -0
package/src/tokenizer.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024-2026 Soumya Debnath
|
|
3
|
+
|
|
4
|
+
import { withDownloadDeadline, DownloadOptions } from './download';
|
|
5
|
+
|
|
6
|
+
export interface TokenizerConfig {
|
|
7
|
+
vocab: Record<string, number>;
|
|
8
|
+
/** Keep the legacy uncased default; set false for cased WordPiece vocabularies. */
|
|
9
|
+
lowercase?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* BPE merge rules, either `"a b"` (older HuggingFace tokenizer.json) or
|
|
12
|
+
* `["a", "b"]` (newer tokenizer.json). Accepted and normalised. When present,
|
|
13
|
+
* `encode()` applies these merges and preserves case by default; otherwise it
|
|
14
|
+
* uses WordPiece.
|
|
15
|
+
*/
|
|
16
|
+
merges?: Array<string | [string, string]>;
|
|
17
|
+
specialTokens?: Record<string, number>;
|
|
18
|
+
maxLength?: number;
|
|
19
|
+
padTokenId?: number;
|
|
20
|
+
unkTokenId?: number;
|
|
21
|
+
clsTokenId?: number;
|
|
22
|
+
sepTokenId?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Optional marker prepended to the first BPE piece of each word, e.g. `"▁"`
|
|
25
|
+
* for SentencePiece vocabularies or `"Ġ"` for byte-level BPE vocabularies.
|
|
26
|
+
* Without it, pieces are looked up exactly as produced by the merge table.
|
|
27
|
+
*/
|
|
28
|
+
bpeSpaceMarker?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EncodingResult {
|
|
32
|
+
inputIds: Int32Array;
|
|
33
|
+
attentionMask: Int32Array;
|
|
34
|
+
tokenTypeIds?: Int32Array;
|
|
35
|
+
/** int64 view for ONNX models declared as `tensor(int64)`. */
|
|
36
|
+
inputIdsInt64: BigInt64Array;
|
|
37
|
+
attentionMaskInt64: BigInt64Array;
|
|
38
|
+
tokenTypeIdsInt64?: BigInt64Array;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Production BPE/WordPiece tokenizer for ONNX model inference.
|
|
43
|
+
* Supports loading vocabularies from JSON, encoding text to token IDs,
|
|
44
|
+
* and decoding token IDs back to text.
|
|
45
|
+
*/
|
|
46
|
+
export class Tokenizer {
|
|
47
|
+
private lowercase: boolean;
|
|
48
|
+
private vocab: Map<string, number>;
|
|
49
|
+
private inverseVocab: Map<number, string>;
|
|
50
|
+
private merges: Array<[string, string]>;
|
|
51
|
+
private specialTokens: Map<string, number>;
|
|
52
|
+
private maxLength: number;
|
|
53
|
+
private padTokenId: number;
|
|
54
|
+
private unkTokenId: number;
|
|
55
|
+
private clsTokenId: number;
|
|
56
|
+
private sepTokenId: number;
|
|
57
|
+
private bpeCache: Map<string, string[]>;
|
|
58
|
+
private mergeRanks: Map<string, number>;
|
|
59
|
+
private usesBPE: boolean;
|
|
60
|
+
private bpeSpaceMarker: string;
|
|
61
|
+
|
|
62
|
+
constructor(config: TokenizerConfig) {
|
|
63
|
+
this.vocab = new Map(Object.entries(config.vocab));
|
|
64
|
+
this.inverseVocab = new Map<number, string>();
|
|
65
|
+
for (const [token, id] of this.vocab) {
|
|
66
|
+
this.inverseVocab.set(id, token);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// `merges` may arrive in either HuggingFace form:
|
|
70
|
+
// older tokenizer.json: ["a b", "lo w"] (space-separated string)
|
|
71
|
+
// newer tokenizer.json: [["a","b"], ["lo","w"]] (pre-split pair)
|
|
72
|
+
// Calling .split() on the array form throws "m.split is not a function",
|
|
73
|
+
// so both shapes are handled here.
|
|
74
|
+
this.merges = ((config.merges || []) as Array<string | [string, string]>).map(m => {
|
|
75
|
+
if (Array.isArray(m)) {
|
|
76
|
+
return [m[0], m[1]] as [string, string];
|
|
77
|
+
}
|
|
78
|
+
const parts = String(m).split(' ');
|
|
79
|
+
return [parts[0], parts[1]] as [string, string];
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.specialTokens = new Map(Object.entries(config.specialTokens || {}));
|
|
83
|
+
this.maxLength = config.maxLength || 512;
|
|
84
|
+
this.padTokenId = config.padTokenId ?? 0;
|
|
85
|
+
this.unkTokenId = config.unkTokenId ?? 100;
|
|
86
|
+
this.clsTokenId = config.clsTokenId ?? 101;
|
|
87
|
+
this.sepTokenId = config.sepTokenId ?? 102;
|
|
88
|
+
this.bpeCache = new Map();
|
|
89
|
+
this.usesBPE = this.merges.length > 0;
|
|
90
|
+
this.bpeSpaceMarker = config.bpeSpaceMarker ?? '';
|
|
91
|
+
|
|
92
|
+
this.mergeRanks = new Map();
|
|
93
|
+
this.merges.forEach(([first, second], rank) => {
|
|
94
|
+
this.mergeRanks.set(`${first} ${second}`, rank);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Older WordPiece-only callers relied on an implicit uncased default. A
|
|
98
|
+
// merge table means the vocabulary is BPE (typically cased), so preserving
|
|
99
|
+
// case is the only safe default there.
|
|
100
|
+
this.lowercase = config.lowercase ?? !this.usesBPE;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Load tokenizer configuration from a JSON URL (tokenizer.json or vocab.json).
|
|
105
|
+
*/
|
|
106
|
+
static async fromUrl(url: string, options: DownloadOptions = {}): Promise<Tokenizer> {
|
|
107
|
+
const config = await withDownloadDeadline(async signal => {
|
|
108
|
+
const response = await fetch(url, { signal });
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
await response.body?.cancel();
|
|
111
|
+
throw new Error(`Failed to load tokenizer from ${url}: ${response.status}`);
|
|
112
|
+
}
|
|
113
|
+
return response.json();
|
|
114
|
+
}, options);
|
|
115
|
+
|
|
116
|
+
// Handle HuggingFace tokenizer.json format
|
|
117
|
+
if (config.model && config.model.vocab) {
|
|
118
|
+
const specialTokens: Record<string, number> = {};
|
|
119
|
+
if (config.added_tokens) {
|
|
120
|
+
for (const t of config.added_tokens) {
|
|
121
|
+
specialTokens[t.content] = t.id;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return new Tokenizer({
|
|
125
|
+
vocab: config.model.vocab,
|
|
126
|
+
lowercase: config.normalizer === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: Tokenizer.normalizerLowercase(config.normalizer),
|
|
129
|
+
merges: config.model.merges || [],
|
|
130
|
+
specialTokens,
|
|
131
|
+
maxLength: config.truncation?.max_length || 512,
|
|
132
|
+
padTokenId: specialTokens['[PAD]'] ?? 0,
|
|
133
|
+
unkTokenId: specialTokens['[UNK]'] ?? 100,
|
|
134
|
+
clsTokenId: specialTokens['[CLS]'] ?? 101,
|
|
135
|
+
sepTokenId: specialTokens['[SEP]'] ?? 102,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle simple vocab.json format (key -> id mapping)
|
|
140
|
+
return new Tokenizer({ vocab: config });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private static normalizerLowercase(normalizer: unknown): boolean {
|
|
144
|
+
if (normalizer === undefined) return true; // Legacy default for omitted config.
|
|
145
|
+
if (normalizer === null) return false; // Explicit identity normalizer.
|
|
146
|
+
if (typeof normalizer !== 'object') throw new Error('Unsupported tokenizer normalizer');
|
|
147
|
+
const value = normalizer as Record<string, unknown>;
|
|
148
|
+
if (value.type === 'Lowercase') return true;
|
|
149
|
+
// Other normalization (accent stripping, CJK spacing, Unicode normalization)
|
|
150
|
+
// is not implemented. Reject it instead of silently changing token IDs.
|
|
151
|
+
if (value.type === 'BertNormalizer' && typeof value.lowercase === 'boolean'
|
|
152
|
+
&& value.strip_accents === false && value.clean_text === false && value.handle_chinese_chars === false) {
|
|
153
|
+
return value.lowercase;
|
|
154
|
+
}
|
|
155
|
+
throw new Error('Unsupported tokenizer normalizer; only identity, Lowercase, or casing-only BertNormalizer is supported');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Encode text into token IDs with attention mask.
|
|
160
|
+
* Handles WordPiece tokenization for BERT-like models.
|
|
161
|
+
*/
|
|
162
|
+
encode(text: string, options?: { addSpecialTokens?: boolean; maxLength?: number; padding?: boolean }): EncodingResult {
|
|
163
|
+
const addSpecial = options?.addSpecialTokens !== false;
|
|
164
|
+
const maxLen = options?.maxLength || this.maxLength;
|
|
165
|
+
const padding = options?.padding ?? false;
|
|
166
|
+
|
|
167
|
+
// Pre-tokenize: lowercase, split on whitespace and punctuation
|
|
168
|
+
const words = this.preTokenize(text);
|
|
169
|
+
|
|
170
|
+
// WordPiece / BPE tokenization
|
|
171
|
+
const tokenIds: number[] = [];
|
|
172
|
+
|
|
173
|
+
if (addSpecial) {
|
|
174
|
+
tokenIds.push(this.clsTokenId);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const word of words) {
|
|
178
|
+
const subTokens = this.tokenizeWord(word);
|
|
179
|
+
for (const subToken of subTokens) {
|
|
180
|
+
if (tokenIds.length >= maxLen - (addSpecial ? 1 : 0)) break;
|
|
181
|
+
tokenIds.push(subToken);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (addSpecial) {
|
|
186
|
+
tokenIds.push(this.sepTokenId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Truncate to maxLength
|
|
190
|
+
const truncated = tokenIds.slice(0, maxLen);
|
|
191
|
+
|
|
192
|
+
// Guard against empty encoding producing empty tensors (crashes ONNX)
|
|
193
|
+
if (truncated.length === 0) {
|
|
194
|
+
const fallback = new Int32Array([this.padTokenId]);
|
|
195
|
+
return {
|
|
196
|
+
inputIds: fallback,
|
|
197
|
+
attentionMask: new Int32Array([0]),
|
|
198
|
+
inputIdsInt64: Tokenizer.toInt64(fallback),
|
|
199
|
+
attentionMaskInt64: new BigInt64Array([0n]),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Build attention mask
|
|
204
|
+
const attentionMask = new Int32Array(padding ? maxLen : truncated.length);
|
|
205
|
+
attentionMask.fill(1, 0, truncated.length);
|
|
206
|
+
|
|
207
|
+
// Pad if requested
|
|
208
|
+
const inputIds = new Int32Array(padding ? maxLen : truncated.length);
|
|
209
|
+
inputIds.set(truncated);
|
|
210
|
+
if (padding) {
|
|
211
|
+
inputIds.fill(this.padTokenId, truncated.length);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
inputIds,
|
|
216
|
+
attentionMask,
|
|
217
|
+
inputIdsInt64: Tokenizer.toInt64(inputIds),
|
|
218
|
+
attentionMaskInt64: Tokenizer.toInt64(attentionMask),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Decode token IDs back to text.
|
|
224
|
+
*/
|
|
225
|
+
decode(tokens: Int32Array | number[]): string {
|
|
226
|
+
const tokenArray = tokens instanceof Int32Array ? Array.from(tokens) : tokens;
|
|
227
|
+
const words: string[] = [];
|
|
228
|
+
|
|
229
|
+
for (const id of tokenArray) {
|
|
230
|
+
if (id === this.padTokenId || id === this.clsTokenId || id === this.sepTokenId) continue;
|
|
231
|
+
const token = this.inverseVocab.get(id);
|
|
232
|
+
if (!token) {
|
|
233
|
+
words.push('[UNK]');
|
|
234
|
+
} else if (token.startsWith('##')) {
|
|
235
|
+
// WordPiece continuation token
|
|
236
|
+
if (words.length > 0) {
|
|
237
|
+
words[words.length - 1] += token.slice(2);
|
|
238
|
+
} else {
|
|
239
|
+
words.push(token.slice(2));
|
|
240
|
+
}
|
|
241
|
+
} else if (token.startsWith('\u2581') || token.startsWith('\u0120')) {
|
|
242
|
+
// SentencePiece (`▁`) and byte-level BPE (`Ġ`) word markers.
|
|
243
|
+
words.push(token.slice(1));
|
|
244
|
+
} else {
|
|
245
|
+
words.push(token);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return words.join(' ');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get vocabulary size.
|
|
254
|
+
*/
|
|
255
|
+
get vocabSize(): number {
|
|
256
|
+
return this.vocab.size;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolve a special token (e.g. `</s>`, `<|endoftext|>`) to its ID, whether it
|
|
261
|
+
* was declared in `specialTokens` or is present in the main vocabulary.
|
|
262
|
+
*/
|
|
263
|
+
getSpecialTokenId(token: string): number | undefined {
|
|
264
|
+
return this.specialTokens.get(token) ?? this.vocab.get(token);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Pre-tokenize text: lowercase, split on whitespace and punctuation boundaries.
|
|
269
|
+
*/
|
|
270
|
+
private preTokenize(text: string): string[] {
|
|
271
|
+
const normalized = (this.lowercase ? text.toLowerCase() : text).trim();
|
|
272
|
+
if (!normalized) return [];
|
|
273
|
+
|
|
274
|
+
// Split on whitespace and keep punctuation as separate tokens
|
|
275
|
+
// Uses Unicode-aware patterns to handle emojis, CJK, Arabic, accented chars
|
|
276
|
+
const tokens: string[] = [];
|
|
277
|
+
let current = '';
|
|
278
|
+
|
|
279
|
+
for (const char of normalized) {
|
|
280
|
+
if (/\s/.test(char)) {
|
|
281
|
+
if (current) tokens.push(current);
|
|
282
|
+
current = '';
|
|
283
|
+
} else if (/[.,!?;:'"()\[\]{}\-\/\\@#$%^&*~`|<>]/.test(char) || /\p{P}/u.test(char)) {
|
|
284
|
+
if (current) tokens.push(current);
|
|
285
|
+
tokens.push(char);
|
|
286
|
+
current = '';
|
|
287
|
+
} else {
|
|
288
|
+
current += char;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (current) tokens.push(current);
|
|
292
|
+
return tokens;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Tokenize a single word using the tokenizer's model type: BPE when a merge
|
|
297
|
+
* table is present, WordPiece otherwise.
|
|
298
|
+
*/
|
|
299
|
+
private tokenizeWord(word: string): number[] {
|
|
300
|
+
if (this.usesBPE) return this.tokenizeWordBPE(word);
|
|
301
|
+
|
|
302
|
+
// Check if whole word is in vocabulary
|
|
303
|
+
if (this.vocab.has(word)) {
|
|
304
|
+
return [this.vocab.get(word)!];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// WordPiece: greedily match longest subword from left to right
|
|
308
|
+
const tokens: number[] = [];
|
|
309
|
+
let start = 0;
|
|
310
|
+
|
|
311
|
+
while (start < word.length) {
|
|
312
|
+
let end = word.length;
|
|
313
|
+
let found = false;
|
|
314
|
+
|
|
315
|
+
while (start < end) {
|
|
316
|
+
let substr = word.slice(start, end);
|
|
317
|
+
if (start > 0) {
|
|
318
|
+
substr = '##' + substr;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (this.vocab.has(substr)) {
|
|
322
|
+
tokens.push(this.vocab.get(substr)!);
|
|
323
|
+
found = true;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
end--;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!found) {
|
|
330
|
+
// Character not in vocab, use [UNK]
|
|
331
|
+
tokens.push(this.unkTokenId);
|
|
332
|
+
start++;
|
|
333
|
+
} else {
|
|
334
|
+
start = end;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return tokens;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Apply the merge table to a word, then map the resulting pieces to IDs.
|
|
343
|
+
* Unknown pieces fall back to the unknown token rather than throwing, which
|
|
344
|
+
* matches ONNX Runtime's text-tokenizer behaviour.
|
|
345
|
+
*/
|
|
346
|
+
private tokenizeWordBPE(word: string): number[] {
|
|
347
|
+
const whole = this.vocab.get(
|
|
348
|
+
this.bpeSpaceMarker ? this.bpeSpaceMarker + word : word
|
|
349
|
+
);
|
|
350
|
+
if (whole !== undefined) return [whole];
|
|
351
|
+
|
|
352
|
+
const pieces = this.applyBPE(word);
|
|
353
|
+
const ids: number[] = [];
|
|
354
|
+
|
|
355
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
356
|
+
const piece = i === 0 && this.bpeSpaceMarker
|
|
357
|
+
? this.bpeSpaceMarker + pieces[i]
|
|
358
|
+
: pieces[i];
|
|
359
|
+
const id = this.vocab.get(piece);
|
|
360
|
+
if (id !== undefined) {
|
|
361
|
+
ids.push(id);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
ids.push(this.unkTokenId);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return ids;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Apply BPE merges to a word (for BPE-style tokenizers like GPT-2/Llama).
|
|
372
|
+
* Merge rules are applied in ascending rank order until no adjacent pair is
|
|
373
|
+
* present in the merge table; caching is keyed on the original word.
|
|
374
|
+
*/
|
|
375
|
+
private applyBPE(word: string): string[] {
|
|
376
|
+
const cached = this.bpeCache.get(word);
|
|
377
|
+
if (cached) return cached;
|
|
378
|
+
|
|
379
|
+
let tokens = word.split('');
|
|
380
|
+
if (tokens.length <= 1 || this.mergeRanks.size === 0) {
|
|
381
|
+
this.bpeCache.set(word, tokens);
|
|
382
|
+
return tokens;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
while (tokens.length > 1) {
|
|
386
|
+
let bestRank = Number.POSITIVE_INFINITY;
|
|
387
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
388
|
+
const rank = this.mergeRanks.get(`${tokens[i]} ${tokens[i + 1]}`);
|
|
389
|
+
if (rank !== undefined && rank < bestRank) bestRank = rank;
|
|
390
|
+
}
|
|
391
|
+
if (bestRank === Number.POSITIVE_INFINITY) break;
|
|
392
|
+
|
|
393
|
+
const merged: string[] = [];
|
|
394
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
395
|
+
if (
|
|
396
|
+
i < tokens.length - 1 &&
|
|
397
|
+
this.mergeRanks.get(`${tokens[i]} ${tokens[i + 1]}`) === bestRank
|
|
398
|
+
) {
|
|
399
|
+
merged.push(tokens[i] + tokens[i + 1]);
|
|
400
|
+
i++;
|
|
401
|
+
} else {
|
|
402
|
+
merged.push(tokens[i]);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (merged.length === tokens.length) break;
|
|
407
|
+
tokens = merged;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.bpeCache.set(word, tokens);
|
|
411
|
+
return tokens;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private static toInt64(values: Int32Array): BigInt64Array {
|
|
415
|
+
const out = new BigInt64Array(values.length);
|
|
416
|
+
for (let i = 0; i < values.length; i++) out[i] = BigInt(values[i]);
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024-2026 Soumya Debnath
|
|
3
|
+
|
|
4
|
+
export interface ModelOptions {
|
|
5
|
+
executionProviders?: string[];
|
|
6
|
+
cacheName?: string;
|
|
7
|
+
forceDownload?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ClassificationResult {
|
|
11
|
+
label: string;
|
|
12
|
+
score: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Detection {
|
|
16
|
+
bbox: [number, number, number, number];
|
|
17
|
+
label: string;
|
|
18
|
+
score: number;
|
|
19
|
+
}
|