@swiftbrowser/cli 0.1.0 → 0.2.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/README.md +330 -64
- package/dist/assetSymbols.d.ts +25 -0
- package/dist/assetSymbols.js +218 -0
- package/dist/assetSymbols.js.map +1 -0
- package/dist/build.d.ts +17 -2
- package/dist/build.js +40 -13
- package/dist/build.js.map +1 -1
- package/dist/buildSettings.d.ts +15 -0
- package/dist/buildSettings.js +74 -0
- package/dist/buildSettings.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +60 -11
- package/dist/cli.js.map +1 -1
- package/dist/commands/build.js +6 -5
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/check.js +2 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/dev.d.ts +5 -2
- package/dist/commands/dev.js +19 -13
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts +18 -0
- package/dist/commands/init.js +57 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/kit.d.ts +3 -0
- package/dist/commands/kit.js +21 -0
- package/dist/commands/kit.js.map +1 -0
- package/dist/commands/shared.d.ts +25 -4
- package/dist/commands/shared.js +104 -10
- package/dist/commands/shared.js.map +1 -1
- package/dist/compat.d.ts +44 -6
- package/dist/compat.js +64 -36
- package/dist/compat.js.map +1 -1
- package/dist/config.d.ts +48 -10
- package/dist/config.js +64 -8
- package/dist/config.js.map +1 -1
- package/dist/coreml.d.ts +194 -0
- package/dist/coreml.js +626 -0
- package/dist/coreml.js.map +1 -0
- package/dist/icu.d.ts +17 -4
- package/dist/icu.js +32 -18
- package/dist/icu.js.map +1 -1
- package/dist/kit.d.ts +133 -0
- package/dist/kit.js +403 -0
- package/dist/kit.js.map +1 -0
- package/dist/objc.d.ts +31 -0
- package/dist/objc.js +319 -0
- package/dist/objc.js.map +1 -0
- package/dist/packagePatches.d.ts +24 -0
- package/dist/packagePatches.js +107 -0
- package/dist/packagePatches.js.map +1 -0
- package/dist/packages.d.ts +59 -0
- package/dist/packages.js +149 -0
- package/dist/packages.js.map +1 -0
- package/dist/project.d.ts +45 -4
- package/dist/project.js +91 -18
- package/dist/project.js.map +1 -1
- package/dist/protobuf.d.ts +62 -0
- package/dist/protobuf.js +233 -0
- package/dist/protobuf.js.map +1 -0
- package/dist/stage.d.ts +155 -27
- package/dist/stage.js +483 -79
- package/dist/stage.js.map +1 -1
- package/dist/strings.d.ts +44 -0
- package/dist/strings.js +386 -0
- package/dist/strings.js.map +1 -0
- package/dist/template.d.ts +29 -0
- package/dist/template.js +438 -0
- package/dist/template.js.map +1 -0
- package/dist/threadStacks.d.ts +14 -0
- package/dist/threadStacks.js +78 -0
- package/dist/threadStacks.js.map +1 -0
- package/dist/toolchain.d.ts +7 -1
- package/dist/toolchain.js +8 -2
- package/dist/toolchain.js.map +1 -1
- package/dist/urlSchemes.d.ts +11 -0
- package/dist/urlSchemes.js +58 -0
- package/dist/urlSchemes.js.map +1 -0
- package/dist/vendor.d.ts +109 -0
- package/dist/vendor.js +440 -0
- package/dist/vendor.js.map +1 -0
- package/dist/wasm.d.ts +28 -0
- package/dist/wasm.js +84 -0
- package/dist/wasm.js.map +1 -0
- package/package.json +7 -6
package/dist/coreml.js
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
// Core ML models, compiled the way Xcode compiles them: a `.mlmodel` in the
|
|
2
|
+
// target's Sources becomes a Swift class (`SleepCalculator`, with its
|
|
3
|
+
// `SleepCalculatorInput` and `SleepCalculatorOutput`) the app calls. Xcode
|
|
4
|
+
// also compiles the model to an `.mlmodelc` the class loads at run time; here
|
|
5
|
+
// the model is read from its protobuf (Model.proto in apple/coremltools),
|
|
6
|
+
// checked against what the shim's CoreML module can evaluate, and embedded in
|
|
7
|
+
// the generated class as a JSON description of its math (docs/ops-protocol.md,
|
|
8
|
+
// "Core ML models").
|
|
9
|
+
//
|
|
10
|
+
// Supported: what Create ML's tabular regressors and classifiers are made of.
|
|
11
|
+
// Linear and logistic models (GLM), boosted trees and random forests (tree
|
|
12
|
+
// ensembles), and pipelines of them with feature vectorizers, one-hot
|
|
13
|
+
// encoders, imputers, normalizers, scalers, dictionary vectorizers, array
|
|
14
|
+
// feature extractors, categorical mappings and identities. Anything else
|
|
15
|
+
// (neural networks, ML programs, Vision and Natural Language models) is a
|
|
16
|
+
// CliError naming the model type.
|
|
17
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { CliError } from './log.js';
|
|
20
|
+
import { ProtobufError, decodeMessage } from './protobuf.js';
|
|
21
|
+
// --- Reading Model.proto ---
|
|
22
|
+
/** Model.proto's `oneof Type`: field number → name, for the supported types and for naming the others. */
|
|
23
|
+
const MODEL_TYPES = {
|
|
24
|
+
200: 'pipelineClassifier',
|
|
25
|
+
201: 'pipelineRegressor',
|
|
26
|
+
202: 'pipeline',
|
|
27
|
+
300: 'glmRegressor',
|
|
28
|
+
301: 'supportVectorRegressor',
|
|
29
|
+
302: 'treeEnsembleRegressor',
|
|
30
|
+
303: 'neuralNetworkRegressor',
|
|
31
|
+
304: 'bayesianProbitRegressor',
|
|
32
|
+
400: 'glmClassifier',
|
|
33
|
+
401: 'supportVectorClassifier',
|
|
34
|
+
402: 'treeEnsembleClassifier',
|
|
35
|
+
403: 'neuralNetworkClassifier',
|
|
36
|
+
404: 'kNearestNeighborsClassifier',
|
|
37
|
+
500: 'neuralNetwork',
|
|
38
|
+
501: 'itemSimilarityRecommender',
|
|
39
|
+
502: 'mlProgram',
|
|
40
|
+
555: 'customModel',
|
|
41
|
+
556: 'linkedModel',
|
|
42
|
+
560: 'classConfidenceThresholding',
|
|
43
|
+
600: 'oneHotEncoder',
|
|
44
|
+
601: 'imputer',
|
|
45
|
+
602: 'featureVectorizer',
|
|
46
|
+
603: 'dictVectorizer',
|
|
47
|
+
604: 'scaler',
|
|
48
|
+
606: 'categoricalMapping',
|
|
49
|
+
607: 'normalizer',
|
|
50
|
+
609: 'arrayFeatureExtractor',
|
|
51
|
+
610: 'nonMaximumSuppression',
|
|
52
|
+
900: 'identity',
|
|
53
|
+
2000: 'textClassifier',
|
|
54
|
+
2001: 'wordTagger',
|
|
55
|
+
2002: 'visionFeaturePrint',
|
|
56
|
+
2003: 'soundAnalysisPreprocessing',
|
|
57
|
+
2004: 'gazetteer',
|
|
58
|
+
2005: 'wordEmbedding',
|
|
59
|
+
2006: 'audioFeaturePrint',
|
|
60
|
+
3000: 'serializedModel',
|
|
61
|
+
};
|
|
62
|
+
/** What the unsupported types are, in words, for the error message. */
|
|
63
|
+
const UNSUPPORTED_TYPES = {
|
|
64
|
+
supportVectorRegressor: 'a support vector machine regressor',
|
|
65
|
+
supportVectorClassifier: 'a support vector machine classifier',
|
|
66
|
+
neuralNetworkRegressor: 'a neural network regressor',
|
|
67
|
+
neuralNetworkClassifier: 'a neural network classifier',
|
|
68
|
+
neuralNetwork: 'a neural network',
|
|
69
|
+
bayesianProbitRegressor: 'a Bayesian probit regressor',
|
|
70
|
+
kNearestNeighborsClassifier: 'a k-nearest neighbors classifier',
|
|
71
|
+
itemSimilarityRecommender: 'a recommender',
|
|
72
|
+
mlProgram: 'an ML program (a converted neural network)',
|
|
73
|
+
customModel: 'a custom model',
|
|
74
|
+
linkedModel: 'a linked model',
|
|
75
|
+
classConfidenceThresholding: 'a class confidence thresholding model',
|
|
76
|
+
nonMaximumSuppression: 'a non-maximum suppression model (object detection)',
|
|
77
|
+
textClassifier: 'a Natural Language text classifier',
|
|
78
|
+
wordTagger: 'a Natural Language word tagger',
|
|
79
|
+
visionFeaturePrint: 'a Vision feature print (image models)',
|
|
80
|
+
soundAnalysisPreprocessing: 'a sound analysis model',
|
|
81
|
+
gazetteer: 'a Natural Language gazetteer',
|
|
82
|
+
wordEmbedding: 'a Natural Language word embedding',
|
|
83
|
+
audioFeaturePrint: 'an audio feature print',
|
|
84
|
+
serializedModel: 'a serialized model',
|
|
85
|
+
};
|
|
86
|
+
export const SUPPORTED_SUMMARY = 'linear and logistic regression (GLM), boosted trees and random forests (tree ensembles), and pipelines of them with feature vectorizers, one-hot encoders, imputers, normalizers, scalers and the other feature-engineering models';
|
|
87
|
+
/** A model the CLI cannot compile: names the file and what is wrong. */
|
|
88
|
+
export class ModelError extends CliError {
|
|
89
|
+
}
|
|
90
|
+
class Reader {
|
|
91
|
+
file;
|
|
92
|
+
constructor(file) {
|
|
93
|
+
this.file = file;
|
|
94
|
+
}
|
|
95
|
+
fail(message) {
|
|
96
|
+
throw new ModelError(`${path.basename(this.file)}: ${message}`);
|
|
97
|
+
}
|
|
98
|
+
model(message, where) {
|
|
99
|
+
const specificationVersion = message.int(1);
|
|
100
|
+
const descriptionMessage = message.message(2);
|
|
101
|
+
if (!descriptionMessage)
|
|
102
|
+
this.fail(`${where} has no description`);
|
|
103
|
+
const description = this.description(descriptionMessage, where);
|
|
104
|
+
const typeFields = message.numbers().filter((n) => n >= 200);
|
|
105
|
+
if (typeFields.length === 0)
|
|
106
|
+
this.fail(`${where} has no model type`);
|
|
107
|
+
const number = typeFields[0];
|
|
108
|
+
const typeName = MODEL_TYPES[number] ?? `type ${number}`;
|
|
109
|
+
const body = message.message(number) ?? decodeMessage(new Uint8Array());
|
|
110
|
+
return { specificationVersion, description, model: this.kind(typeName, body, where, description) };
|
|
111
|
+
}
|
|
112
|
+
description(message, where) {
|
|
113
|
+
if (message.has(20) && !message.has(1))
|
|
114
|
+
this.fail(`${where} declares several functions (a multifunction model), which is not supported`);
|
|
115
|
+
const description = {
|
|
116
|
+
inputs: message.messages(1).map((feature) => this.feature(feature, where)),
|
|
117
|
+
outputs: message.messages(10).map((feature) => this.feature(feature, where)),
|
|
118
|
+
};
|
|
119
|
+
const predicted = message.string(11);
|
|
120
|
+
if (predicted)
|
|
121
|
+
description.predictedFeatureName = predicted;
|
|
122
|
+
const probabilities = message.string(12);
|
|
123
|
+
if (probabilities)
|
|
124
|
+
description.predictedProbabilitiesName = probabilities;
|
|
125
|
+
const metadataMessage = message.message(100);
|
|
126
|
+
if (metadataMessage) {
|
|
127
|
+
const metadata = {};
|
|
128
|
+
const fields = [['shortDescription', 1], ['versionString', 2], ['author', 3], ['license', 4]];
|
|
129
|
+
for (const [key, number] of fields) {
|
|
130
|
+
const value = metadataMessage.string(number);
|
|
131
|
+
if (value)
|
|
132
|
+
metadata[key] = value;
|
|
133
|
+
}
|
|
134
|
+
const userDefined = metadataMessage.messages(100);
|
|
135
|
+
if (userDefined.length > 0)
|
|
136
|
+
metadata.creatorDefined = Object.fromEntries(userDefined.map((entry) => [entry.string(1), entry.string(2)]));
|
|
137
|
+
if (Object.keys(metadata).length > 0)
|
|
138
|
+
description.metadata = metadata;
|
|
139
|
+
}
|
|
140
|
+
return description;
|
|
141
|
+
}
|
|
142
|
+
feature(message, where) {
|
|
143
|
+
const name = message.string(1);
|
|
144
|
+
const typeMessage = message.message(3);
|
|
145
|
+
if (!typeMessage)
|
|
146
|
+
this.fail(`feature "${name}" of ${where} has no type`);
|
|
147
|
+
const type = this.featureType(typeMessage, `feature "${name}" of ${where}`);
|
|
148
|
+
const feature = { name, ...type };
|
|
149
|
+
if (typeMessage.bool(1000))
|
|
150
|
+
feature.optional = true;
|
|
151
|
+
const shortDescription = message.string(2);
|
|
152
|
+
if (shortDescription)
|
|
153
|
+
feature.shortDescription = shortDescription;
|
|
154
|
+
return feature;
|
|
155
|
+
}
|
|
156
|
+
featureType(message, what) {
|
|
157
|
+
if (message.has(1))
|
|
158
|
+
return { type: 'int64' };
|
|
159
|
+
if (message.has(2))
|
|
160
|
+
return { type: 'double' };
|
|
161
|
+
if (message.has(3))
|
|
162
|
+
return { type: 'string' };
|
|
163
|
+
if (message.has(5)) {
|
|
164
|
+
const array = message.message(5);
|
|
165
|
+
const dataTypes = { 65600: 'double', 65568: 'float32', 65552: 'float16', 131104: 'int32', 131080: 'int8' };
|
|
166
|
+
const dataType = dataTypes[array.int(2)] ?? 'double';
|
|
167
|
+
return { type: 'multiArray', shape: array.ints(1), dataType };
|
|
168
|
+
}
|
|
169
|
+
if (message.has(6)) {
|
|
170
|
+
const dictionary = message.message(6);
|
|
171
|
+
return { type: 'dictionary', keyType: dictionary.has(1) ? 'int64' : 'string' };
|
|
172
|
+
}
|
|
173
|
+
const kinds = { 4: 'an image', 7: 'a sequence', 8: 'a state' };
|
|
174
|
+
const kind = message.numbers().map((n) => kinds[n]).find((k) => k !== undefined);
|
|
175
|
+
this.fail(`${what} is ${kind ?? 'of an unknown type'}, which is not supported (Int64, Double, String, MLMultiArray and dictionaries are)`);
|
|
176
|
+
}
|
|
177
|
+
labels(message, what) {
|
|
178
|
+
if (message.has(100))
|
|
179
|
+
return { strings: message.message(100).strings(1) };
|
|
180
|
+
if (message.has(101))
|
|
181
|
+
return { int64s: message.message(101).ints(1) };
|
|
182
|
+
this.fail(`${what} has no class labels`);
|
|
183
|
+
}
|
|
184
|
+
kind(typeName, body, where, description) {
|
|
185
|
+
switch (typeName) {
|
|
186
|
+
case 'pipeline':
|
|
187
|
+
return { kind: 'pipeline', models: this.pipeline(body, where) };
|
|
188
|
+
case 'pipelineClassifier':
|
|
189
|
+
case 'pipelineRegressor':
|
|
190
|
+
return { kind: typeName, models: this.pipeline(body.message(1) ?? decodeMessage(new Uint8Array()), where) };
|
|
191
|
+
case 'glmRegressor': {
|
|
192
|
+
const transforms = ['none', 'logit', 'probit'];
|
|
193
|
+
const weights = body.messages(1).map((row) => row.doubles(1));
|
|
194
|
+
const offsets = body.doubles(2);
|
|
195
|
+
this.checkGLM(weights, offsets, where);
|
|
196
|
+
return { kind: 'glmRegressor', weights, offsets, transform: transforms[body.int(3)] ?? this.fail(`${where}: unknown GLM transform ${body.int(3)}`) };
|
|
197
|
+
}
|
|
198
|
+
case 'glmClassifier': {
|
|
199
|
+
const transforms = ['logit', 'probit'];
|
|
200
|
+
const encodings = ['referenceClass', 'oneVsRest'];
|
|
201
|
+
const weights = body.messages(1).map((row) => row.doubles(1));
|
|
202
|
+
const offsets = body.doubles(2);
|
|
203
|
+
this.checkGLM(weights, offsets, where);
|
|
204
|
+
const labels = this.labels(body, where);
|
|
205
|
+
const encoding = encodings[body.int(4)] ?? this.fail(`${where}: unknown class encoding ${body.int(4)}`);
|
|
206
|
+
const classes = labelCount(labels);
|
|
207
|
+
const rows = weights.length;
|
|
208
|
+
// One row is binary (the second label's probability); otherwise one row
|
|
209
|
+
// per class (one vs. rest) or per class but the first (reference class).
|
|
210
|
+
if (!(rows === 1 && classes === 2) && !(encoding === 'oneVsRest' && rows === classes) && !(encoding === 'referenceClass' && rows === classes - 1)) {
|
|
211
|
+
this.fail(`${where}: a GLM classifier with ${rows} weight rows for ${classes} classes (${encoding}) is not supported`);
|
|
212
|
+
}
|
|
213
|
+
return { kind: 'glmClassifier', weights, offsets, transform: transforms[body.int(3)] ?? this.fail(`${where}: unknown GLM transform ${body.int(3)}`), encoding, labels };
|
|
214
|
+
}
|
|
215
|
+
case 'treeEnsembleRegressor':
|
|
216
|
+
return { kind: 'treeEnsembleRegressor', ...this.treeEnsemble(body, where) };
|
|
217
|
+
case 'treeEnsembleClassifier': {
|
|
218
|
+
const ensemble = this.treeEnsemble(body, where);
|
|
219
|
+
const labels = this.labels(body, where);
|
|
220
|
+
const classes = labelCount(labels);
|
|
221
|
+
const d = ensemble.dimensions;
|
|
222
|
+
const fits = ensemble.transform === 'softmaxWithZeroClassReference' ? d === classes - 1 : d === classes || (d === 1 && classes === 2);
|
|
223
|
+
if (!fits)
|
|
224
|
+
this.fail(`${where}: a tree ensemble classifier with ${d} prediction dimensions for ${classes} classes (${ensemble.transform}) is not supported`);
|
|
225
|
+
return { kind: 'treeEnsembleClassifier', ...ensemble, labels };
|
|
226
|
+
}
|
|
227
|
+
case 'featureVectorizer':
|
|
228
|
+
return { kind: 'featureVectorizer', columns: body.messages(1).map((column) => ({ name: column.string(1), dimensions: column.int(2) })) };
|
|
229
|
+
case 'oneHotEncoder': {
|
|
230
|
+
let categories;
|
|
231
|
+
if (body.has(1))
|
|
232
|
+
categories = { strings: body.message(1).strings(1) };
|
|
233
|
+
else if (body.has(2))
|
|
234
|
+
categories = { int64s: body.message(2).ints(1) };
|
|
235
|
+
else
|
|
236
|
+
this.fail(`${where}: a one-hot encoder without categories`);
|
|
237
|
+
return { kind: 'oneHotEncoder', categories, sparse: body.bool(10), ignoreUnknown: body.int(11) === 1 };
|
|
238
|
+
}
|
|
239
|
+
case 'normalizer': {
|
|
240
|
+
const norms = ['max', 'l1', 'l2'];
|
|
241
|
+
return { kind: 'normalizer', norm: norms[body.int(1)] ?? this.fail(`${where}: unknown norm ${body.int(1)}`) };
|
|
242
|
+
}
|
|
243
|
+
case 'imputer':
|
|
244
|
+
return this.imputer(body, where);
|
|
245
|
+
case 'scaler':
|
|
246
|
+
return { kind: 'scaler', shift: body.doubles(1), scale: body.doubles(2) };
|
|
247
|
+
case 'dictVectorizer': {
|
|
248
|
+
if (body.has(1))
|
|
249
|
+
return { kind: 'dictVectorizer', keys: { strings: body.message(1).strings(1) } };
|
|
250
|
+
if (body.has(2))
|
|
251
|
+
return { kind: 'dictVectorizer', keys: { int64s: body.message(2).ints(1) } };
|
|
252
|
+
return this.fail(`${where}: a dictionary vectorizer without keys`);
|
|
253
|
+
}
|
|
254
|
+
case 'arrayFeatureExtractor':
|
|
255
|
+
return { kind: 'arrayFeatureExtractor', indices: body.ints(1) };
|
|
256
|
+
case 'categoricalMapping': {
|
|
257
|
+
const mapping = { kind: 'categoricalMapping' };
|
|
258
|
+
if (body.has(1))
|
|
259
|
+
mapping.stringToInt64 = Object.fromEntries(body.message(1).messages(1).map((entry) => [entry.string(1), entry.int(2)]));
|
|
260
|
+
else if (body.has(2))
|
|
261
|
+
mapping.int64ToString = Object.fromEntries(body.message(2).messages(1).map((entry) => [String(entry.int(1)), entry.string(2)]));
|
|
262
|
+
else
|
|
263
|
+
this.fail(`${where}: a categorical mapping without a map`);
|
|
264
|
+
if (body.has(101))
|
|
265
|
+
mapping.defaultString = body.string(101);
|
|
266
|
+
if (body.has(102))
|
|
267
|
+
mapping.defaultInt64 = body.int(102);
|
|
268
|
+
return mapping;
|
|
269
|
+
}
|
|
270
|
+
case 'identity':
|
|
271
|
+
if (description.inputs.length !== description.outputs.length)
|
|
272
|
+
this.fail(`${where}: an identity model needs as many outputs as inputs`);
|
|
273
|
+
return { kind: 'identity' };
|
|
274
|
+
default: {
|
|
275
|
+
const what = UNSUPPORTED_TYPES[typeName];
|
|
276
|
+
this.fail(`${where} is ${what ? `${what} (${typeName})` : `a model of type ${typeName}`}, which the browser cannot run. Supported: ${SUPPORTED_SUMMARY}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
pipeline(message, where) {
|
|
281
|
+
const models = message.messages(1);
|
|
282
|
+
if (models.length === 0)
|
|
283
|
+
this.fail(`${where} is an empty pipeline`);
|
|
284
|
+
return models.map((model, index) => this.model(model, `${where === 'the model' ? 'pipeline' : where} step ${index + 1}`));
|
|
285
|
+
}
|
|
286
|
+
checkGLM(weights, offsets, where) {
|
|
287
|
+
if (weights.length === 0)
|
|
288
|
+
this.fail(`${where}: a GLM without weights`);
|
|
289
|
+
if (offsets.length !== 0 && offsets.length !== weights.length)
|
|
290
|
+
this.fail(`${where}: ${weights.length} weight rows but ${offsets.length} offsets`);
|
|
291
|
+
}
|
|
292
|
+
treeEnsemble(body, where) {
|
|
293
|
+
const parameters = body.message(1);
|
|
294
|
+
if (!parameters)
|
|
295
|
+
this.fail(`${where}: a tree ensemble without parameters`);
|
|
296
|
+
const transforms = ['none', 'softmax', 'logistic', 'softmaxWithZeroClassReference'];
|
|
297
|
+
const transform = transforms[body.int(2)] ?? this.fail(`${where}: unknown tree ensemble transform ${body.int(2)}`);
|
|
298
|
+
const dimensions = Math.max(1, parameters.int(2));
|
|
299
|
+
const base = parameters.doubles(3);
|
|
300
|
+
if (base.length !== 0 && base.length !== dimensions)
|
|
301
|
+
this.fail(`${where}: ${base.length} base prediction values for ${dimensions} dimensions`);
|
|
302
|
+
// Nodes arrive as one list across trees; group them by tree id.
|
|
303
|
+
const byTree = new Map();
|
|
304
|
+
for (const node of parameters.messages(1)) {
|
|
305
|
+
const treeId = node.int(1);
|
|
306
|
+
let list = byTree.get(treeId);
|
|
307
|
+
if (!list) {
|
|
308
|
+
list = [];
|
|
309
|
+
byTree.set(treeId, list);
|
|
310
|
+
}
|
|
311
|
+
list.push(node);
|
|
312
|
+
}
|
|
313
|
+
const trees = [...byTree.keys()].sort((a, b) => a - b).map((treeId) => this.tree(treeId, byTree.get(treeId), dimensions, where));
|
|
314
|
+
return { dimensions, base: base.length === 0 ? new Array(dimensions).fill(0) : base, trees, transform };
|
|
315
|
+
}
|
|
316
|
+
/** One tree in columns, root first, children as positions; checks that it is a tree. */
|
|
317
|
+
tree(treeId, nodes, dimensions, where) {
|
|
318
|
+
const what = `${where}, tree ${treeId}`;
|
|
319
|
+
const byId = new Map();
|
|
320
|
+
for (const node of nodes) {
|
|
321
|
+
const id = node.int(2);
|
|
322
|
+
if (byId.has(id))
|
|
323
|
+
this.fail(`${what}: node ${id} appears twice`);
|
|
324
|
+
byId.set(id, node);
|
|
325
|
+
}
|
|
326
|
+
const children = new Set();
|
|
327
|
+
for (const node of nodes) {
|
|
328
|
+
if (node.int(3) === 6)
|
|
329
|
+
continue;
|
|
330
|
+
children.add(node.int(12));
|
|
331
|
+
children.add(node.int(13));
|
|
332
|
+
}
|
|
333
|
+
const roots = [...byId.keys()].filter((id) => !children.has(id));
|
|
334
|
+
if (roots.length !== 1)
|
|
335
|
+
this.fail(`${what} has ${roots.length} root nodes`);
|
|
336
|
+
// Breadth first from the root: positions in the order nodes are reached.
|
|
337
|
+
const order = [roots[0]];
|
|
338
|
+
const position = new Map([[roots[0], 0]]);
|
|
339
|
+
for (let i = 0; i < order.length; i += 1) {
|
|
340
|
+
const node = byId.get(order[i]);
|
|
341
|
+
if (node.int(3) === 6)
|
|
342
|
+
continue;
|
|
343
|
+
for (const child of [node.int(12), node.int(13)]) {
|
|
344
|
+
if (!byId.has(child))
|
|
345
|
+
this.fail(`${what}: node ${order[i]} branches to node ${child}, which does not exist`);
|
|
346
|
+
if (position.has(child))
|
|
347
|
+
this.fail(`${what}: node ${child} is reached twice (not a tree)`);
|
|
348
|
+
position.set(child, order.length);
|
|
349
|
+
order.push(child);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (order.length !== byId.size)
|
|
353
|
+
this.fail(`${what}: ${byId.size - order.length} nodes are unreachable from the root`);
|
|
354
|
+
const tree = { behaviors: [], features: [], thresholds: [], trueChildren: [], falseChildren: [], missingTrue: [], leafStarts: [], leafIndices: [], leafValues: [] };
|
|
355
|
+
for (const id of order) {
|
|
356
|
+
const node = byId.get(id);
|
|
357
|
+
const behavior = node.int(3);
|
|
358
|
+
if (behavior < 0 || behavior > 6)
|
|
359
|
+
this.fail(`${what}: node ${id} has an unknown behavior ${behavior}`);
|
|
360
|
+
const leaf = behavior === 6;
|
|
361
|
+
tree.behaviors.push(behavior);
|
|
362
|
+
tree.features.push(leaf ? 0 : node.int(10));
|
|
363
|
+
tree.thresholds.push(leaf ? 0 : node.double(11));
|
|
364
|
+
tree.trueChildren.push(leaf ? -1 : position.get(node.int(12)));
|
|
365
|
+
tree.falseChildren.push(leaf ? -1 : position.get(node.int(13)));
|
|
366
|
+
tree.missingTrue.push(node.bool(14));
|
|
367
|
+
tree.leafStarts.push(tree.leafIndices.length);
|
|
368
|
+
if (leaf) {
|
|
369
|
+
for (const info of node.messages(20)) {
|
|
370
|
+
const index = info.int(1);
|
|
371
|
+
if (index >= dimensions)
|
|
372
|
+
this.fail(`${what}: leaf ${id} adds to dimension ${index} of ${dimensions}`);
|
|
373
|
+
tree.leafIndices.push(index);
|
|
374
|
+
tree.leafValues.push(info.double(2));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
tree.leafStarts.push(tree.leafIndices.length);
|
|
379
|
+
return tree;
|
|
380
|
+
}
|
|
381
|
+
imputer(body, where) {
|
|
382
|
+
let imputed;
|
|
383
|
+
if (body.has(1))
|
|
384
|
+
imputed = { double: body.double(1) };
|
|
385
|
+
else if (body.has(2))
|
|
386
|
+
imputed = { int64: body.int(2) };
|
|
387
|
+
else if (body.has(3))
|
|
388
|
+
imputed = { string: body.string(3) };
|
|
389
|
+
else if (body.has(4))
|
|
390
|
+
imputed = { doubles: body.message(4).doubles(1) };
|
|
391
|
+
else if (body.has(5))
|
|
392
|
+
imputed = { int64s: body.message(5).ints(1) };
|
|
393
|
+
else if (body.has(6))
|
|
394
|
+
imputed = { stringDictionary: Object.fromEntries(body.message(6).messages(1).map((entry) => [entry.string(1), entry.double(2)])) };
|
|
395
|
+
else if (body.has(7))
|
|
396
|
+
imputed = { int64Dictionary: Object.fromEntries(body.message(7).messages(1).map((entry) => [String(entry.int(1)), entry.double(2)])) };
|
|
397
|
+
else
|
|
398
|
+
this.fail(`${where}: an imputer without an imputed value`);
|
|
399
|
+
const imputer = { kind: 'imputer', imputed };
|
|
400
|
+
if (body.has(11))
|
|
401
|
+
imputer.replace = { double: body.double(11) };
|
|
402
|
+
else if (body.has(12))
|
|
403
|
+
imputer.replace = { int64: body.int(12) };
|
|
404
|
+
else if (body.has(13))
|
|
405
|
+
imputer.replace = { string: body.string(13) };
|
|
406
|
+
return imputer;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function labelCount(labels) {
|
|
410
|
+
return 'strings' in labels ? labels.strings.length : labels.int64s.length;
|
|
411
|
+
}
|
|
412
|
+
/** Parses a model's protobuf bytes; `file` names it in errors. */
|
|
413
|
+
export function parseModel(bytes, file) {
|
|
414
|
+
const reader = new Reader(file);
|
|
415
|
+
try {
|
|
416
|
+
return reader.model(decodeMessage(bytes), 'the model');
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
if (error instanceof ProtobufError)
|
|
420
|
+
reader.fail(`not a readable Core ML model (${error.message})`);
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The protobuf of an `.mlmodel` file, or of the root model of an `.mlpackage`
|
|
426
|
+
* (its Manifest.json names the specification file under Data/).
|
|
427
|
+
*/
|
|
428
|
+
export function readModelBytes(file) {
|
|
429
|
+
if (!existsSync(file))
|
|
430
|
+
throw new ModelError(`Core ML model not found: ${file}`);
|
|
431
|
+
if (!statSync(file).isDirectory())
|
|
432
|
+
return readFileSync(file);
|
|
433
|
+
const manifestPath = path.join(file, 'Manifest.json');
|
|
434
|
+
if (!existsSync(manifestPath))
|
|
435
|
+
throw new ModelError(`${path.basename(file)}: no Manifest.json in the package`);
|
|
436
|
+
let specPath;
|
|
437
|
+
try {
|
|
438
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
439
|
+
const root = manifest.rootModelIdentifier ? manifest.itemInfoEntries?.[manifest.rootModelIdentifier]?.path : undefined;
|
|
440
|
+
if (root)
|
|
441
|
+
specPath = path.join(file, 'Data', root);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
throw new ModelError(`${path.basename(file)}: Manifest.json is not valid JSON`);
|
|
445
|
+
}
|
|
446
|
+
if (!specPath || !existsSync(specPath))
|
|
447
|
+
throw new ModelError(`${path.basename(file)}: the package's Manifest.json names no model specification`);
|
|
448
|
+
return readFileSync(specPath);
|
|
449
|
+
}
|
|
450
|
+
// --- Checking what the top-level model offers the app ---
|
|
451
|
+
/** The class Xcode generates for `file`: its base name as a Swift identifier. */
|
|
452
|
+
export function modelClassName(file) {
|
|
453
|
+
const base = path.basename(file).replace(/\.(mlmodel|mlpackage)$/, '');
|
|
454
|
+
const cleaned = base.replace(/[^A-Za-z0-9_]/g, '_');
|
|
455
|
+
return /^[0-9]/.test(cleaned) || cleaned === '' ? `_${cleaned}` : cleaned;
|
|
456
|
+
}
|
|
457
|
+
// --- Generating the Swift ---
|
|
458
|
+
const SWIFT_KEYWORDS = new Set([
|
|
459
|
+
'associatedtype', 'class', 'deinit', 'enum', 'extension', 'fileprivate', 'func', 'import', 'init', 'inout', 'internal', 'let', 'open', 'operator',
|
|
460
|
+
'private', 'precedencegroup', 'protocol', 'public', 'rethrows', 'static', 'struct', 'subscript', 'typealias', 'var', 'break', 'case', 'catch',
|
|
461
|
+
'continue', 'default', 'defer', 'do', 'else', 'fallthrough', 'for', 'guard', 'if', 'in', 'repeat', 'return', 'throw', 'switch', 'where', 'while',
|
|
462
|
+
'Any', 'as', 'await', 'false', 'is', 'nil', 'self', 'Self', 'super', 'throws', 'true', 'try', 'Type', 'Protocol',
|
|
463
|
+
]);
|
|
464
|
+
/** A feature name as a Swift identifier, as Xcode spells it: other characters become `_`, keywords get backticks. */
|
|
465
|
+
export function swiftIdentifier(name) {
|
|
466
|
+
let cleaned = name.replace(/[^A-Za-z0-9_]/g, '_');
|
|
467
|
+
if (cleaned === '' || /^[0-9]/.test(cleaned))
|
|
468
|
+
cleaned = `_${cleaned}`;
|
|
469
|
+
return SWIFT_KEYWORDS.has(cleaned) ? `\`${cleaned}\`` : cleaned;
|
|
470
|
+
}
|
|
471
|
+
/** The Swift type Xcode gives a feature. */
|
|
472
|
+
export function swiftType(feature) {
|
|
473
|
+
switch (feature.type) {
|
|
474
|
+
case 'int64':
|
|
475
|
+
return 'Int64';
|
|
476
|
+
case 'double':
|
|
477
|
+
return 'Double';
|
|
478
|
+
case 'string':
|
|
479
|
+
return 'String';
|
|
480
|
+
case 'multiArray':
|
|
481
|
+
return 'MLMultiArray';
|
|
482
|
+
case 'dictionary':
|
|
483
|
+
return feature.keyType === 'string' ? '[String : Double]' : '[Int64 : Double]';
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** `MLFeatureValue` for a stored property of the input class. */
|
|
487
|
+
function featureValueExpression(feature, variable) {
|
|
488
|
+
switch (feature.type) {
|
|
489
|
+
case 'int64':
|
|
490
|
+
return `MLFeatureValue(int64: ${variable})`;
|
|
491
|
+
case 'double':
|
|
492
|
+
return `MLFeatureValue(double: ${variable})`;
|
|
493
|
+
case 'string':
|
|
494
|
+
return `MLFeatureValue(string: ${variable})`;
|
|
495
|
+
case 'multiArray':
|
|
496
|
+
return `MLFeatureValue(multiArray: ${variable})`;
|
|
497
|
+
case 'dictionary':
|
|
498
|
+
return `MLFeatureValue(_doubles: ${variable})`;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/** Reading an output property back from its `MLFeatureValue`. */
|
|
502
|
+
function outputAccessor(feature) {
|
|
503
|
+
const value = `provider.featureValue(for: ${JSON.stringify(feature.name)})!`;
|
|
504
|
+
switch (feature.type) {
|
|
505
|
+
case 'int64':
|
|
506
|
+
return `${value}.int64Value`;
|
|
507
|
+
case 'double':
|
|
508
|
+
return `${value}.doubleValue`;
|
|
509
|
+
case 'string':
|
|
510
|
+
return `${value}.stringValue`;
|
|
511
|
+
case 'multiArray':
|
|
512
|
+
return `${value}.multiArrayValue!`;
|
|
513
|
+
case 'dictionary':
|
|
514
|
+
return `${value}._doubles(keyedBy: ${feature.keyType === 'string' ? 'String' : 'Int64'}.self)`;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function typeWords(feature) {
|
|
518
|
+
switch (feature.type) {
|
|
519
|
+
case 'int64':
|
|
520
|
+
return 'integer value';
|
|
521
|
+
case 'double':
|
|
522
|
+
return 'double value';
|
|
523
|
+
case 'string':
|
|
524
|
+
return 'string value';
|
|
525
|
+
case 'multiArray':
|
|
526
|
+
return `${feature.shape.length > 0 ? `${feature.shape.join(' by ')} ` : ''}${{ double: 'Double', float32: 'Float32', float16: 'Float16', int32: 'Int32', int8: 'Int8' }[feature.dataType]} MLMultiArray`;
|
|
527
|
+
case 'dictionary':
|
|
528
|
+
return `dictionary of ${feature.keyType === 'string' ? 'String' : 'Int64'} to Double values`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function docComment(feature, indent) {
|
|
532
|
+
const text = feature.shortDescription ? `${feature.shortDescription.replace(/\s+/g, ' ')} as ${typeWords(feature)}` : `${feature.name} as ${typeWords(feature)}`;
|
|
533
|
+
return `${indent}/// ${text}`;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* The spec as JSON for the generated file: objects one key per line (a short
|
|
537
|
+
* one whose values are all numbers, strings or lists of them on one line),
|
|
538
|
+
* lists of numbers and strings on one line, NaN and infinities as the strings
|
|
539
|
+
* the CoreML module's decoder converts back ("nan", "inf", "-inf").
|
|
540
|
+
*/
|
|
541
|
+
export function specJSON(value, indent = '') {
|
|
542
|
+
if (typeof value === 'number') {
|
|
543
|
+
if (Number.isNaN(value))
|
|
544
|
+
return '"nan"';
|
|
545
|
+
if (value === Infinity)
|
|
546
|
+
return '"inf"';
|
|
547
|
+
if (value === -Infinity)
|
|
548
|
+
return '"-inf"';
|
|
549
|
+
return Object.is(value, -0) ? '-0.0' : String(value);
|
|
550
|
+
}
|
|
551
|
+
if (typeof value === 'string' || typeof value === 'boolean' || value === null)
|
|
552
|
+
return JSON.stringify(value);
|
|
553
|
+
const inner = `${indent} `;
|
|
554
|
+
if (Array.isArray(value)) {
|
|
555
|
+
if (value.every((item) => typeof item !== 'object' || item === null))
|
|
556
|
+
return `[${value.map((item) => specJSON(item)).join(', ')}]`;
|
|
557
|
+
if (value.length === 0)
|
|
558
|
+
return '[]';
|
|
559
|
+
return `[\n${value.map((item) => `${inner}${specJSON(item, inner)}`).join(',\n')}\n${indent}]`;
|
|
560
|
+
}
|
|
561
|
+
if (typeof value === 'object') {
|
|
562
|
+
const entries = Object.entries(value).filter(([, item]) => item !== undefined);
|
|
563
|
+
if (entries.length === 0)
|
|
564
|
+
return '{}';
|
|
565
|
+
const flat = entries.every(([, item]) => typeof item !== 'object' || item === null || (Array.isArray(item) && item.every((x) => typeof x !== 'object' || x === null)));
|
|
566
|
+
if (flat) {
|
|
567
|
+
const line = `{${entries.map(([key, item]) => `${JSON.stringify(key)}: ${specJSON(item)}`).join(', ')}}`;
|
|
568
|
+
if (line.length <= 100)
|
|
569
|
+
return line;
|
|
570
|
+
}
|
|
571
|
+
return `{\n${entries.map(([key, item]) => `${inner}${JSON.stringify(key)}: ${specJSON(item, inner)}`).join(',\n')}\n${indent}}`;
|
|
572
|
+
}
|
|
573
|
+
throw new Error(`cannot write ${typeof value} as JSON`);
|
|
574
|
+
}
|
|
575
|
+
/** A raw string literal whose delimiter cannot occur in `text`. */
|
|
576
|
+
function rawStringLiteral(text) {
|
|
577
|
+
let hashes = '#';
|
|
578
|
+
while (text.includes(`"${hashes}`))
|
|
579
|
+
hashes += '#';
|
|
580
|
+
return `${hashes}"""\n${text}\n"""${hashes}`;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* The Swift Xcode would generate for the model, with the model itself
|
|
584
|
+
* embedded: the class with `model: MLModel`, `urlOfModelInThisBundle`, the
|
|
585
|
+
* initializers and `load` functions, `prediction` with one argument per input
|
|
586
|
+
* and with an `Input`, `predictions(inputs:)`; the Input and Output feature
|
|
587
|
+
* providers. `file` is the model's path (for the header comment).
|
|
588
|
+
*/
|
|
589
|
+
export function generateModelSource(className, spec, file) {
|
|
590
|
+
const { inputs, outputs } = spec.description;
|
|
591
|
+
const input = `${className}Input`;
|
|
592
|
+
const output = `${className}Output`;
|
|
593
|
+
const ids = (features) => features.map((feature) => swiftIdentifier(feature.name));
|
|
594
|
+
const inputIds = ids(inputs);
|
|
595
|
+
const outputIds = ids(outputs);
|
|
596
|
+
const parameters = inputs.map((feature, i) => `${inputIds[i]}: ${swiftType(feature)}`).join(', ');
|
|
597
|
+
const forwarded = inputIds.map((id) => `${id}: ${id}`).join(', ');
|
|
598
|
+
const description = spec.description.metadata?.shortDescription;
|
|
599
|
+
const lines = [];
|
|
600
|
+
const push = (...more) => {
|
|
601
|
+
lines.push(...more);
|
|
602
|
+
};
|
|
603
|
+
push(`// Generated by @swiftbrowser/cli from ${path.basename(file)}; rewritten on every build.`, '// What Xcode generates for a Core ML model, with the model compiled into the', '// class: the JSON below is its specification, which the CoreML module', '// evaluates (docs/ops-protocol.md, "Core ML models").', 'import CoreML', 'import Foundation', '', `private let _${className}Specification = ${rawStringLiteral(specJSON(stripDescriptions(spec)))}`, '', `/// Model Prediction Input Type`, `class ${input} : MLFeatureProvider {`);
|
|
604
|
+
inputs.forEach((feature, i) => push('', docComment(feature, ' '), ` var ${inputIds[i]}: ${swiftType(feature)}`));
|
|
605
|
+
push('', ` var featureNames: Set<String> { [${inputs.map((f) => JSON.stringify(f.name)).join(', ')}] }`, '', ' func featureValue(for featureName: String) -> MLFeatureValue? {', ...inputs.map((feature, i) => ` if featureName == ${JSON.stringify(feature.name)} { return ${featureValueExpression(feature, inputIds[i])} }`), ' return nil', ' }', '', ` init(${parameters}) {`, ...inputIds.map((id) => ` self.${id.replace(/`/g, '')} = ${id}`), ' }', '}', '', `/// Model Prediction Output Type`, `class ${output} : MLFeatureProvider {`, '', ' /// Source provided by CoreML', ' private let provider : MLFeatureProvider');
|
|
606
|
+
outputs.forEach((feature, i) => push('', docComment(feature, ' '), ` var ${outputIds[i]}: ${swiftType(feature)} {`, ` ${outputAccessor(feature)}`, ' }'));
|
|
607
|
+
push('', ' var featureNames: Set<String> {', ' provider.featureNames', ' }', '', ' func featureValue(for featureName: String) -> MLFeatureValue? {', ' provider.featureValue(for: featureName)', ' }', '', ` init(${outputs.map((feature, i) => `${outputIds[i]}: ${swiftType(feature)}`).join(', ')}) {`, ` self.provider = try! MLDictionaryFeatureProvider(dictionary: [${outputs.length === 0 ? ':' : outputs.map((feature, i) => `${JSON.stringify(feature.name)} : ${featureValueExpression(feature, outputIds[i])}`).join(', ')}])`, ' }', '', ' init(features: MLFeatureProvider) {', ' self.provider = features', ' }', '}', '', '/// Class for model loading and prediction', ...(description ? [`/// ${description.replace(/\s+/g, ' ')}`] : []), `class ${className} {`, ' let model: MLModel', '', ' /// URL of model assuming it was installed in the same bundle as this class', ' class var urlOfModelInThisBundle : URL {', ` MLModel._compiledModelURL(named: ${JSON.stringify(className)}, specification: _${className}Specification)`, ' }', '', ' /// Construct a model with explicit path to mlmodelc file', ' init(model: MLModel) {', ' self.model = model', ' }', '', ' /// Construct a model that automatically loads the model from the app\'s bundle.', ' @available(*, deprecated, message: "Use init(configuration:) instead and handle errors appropriately.")', ' convenience init() {', ' try! self.init(contentsOf: type(of:self).urlOfModelInThisBundle)', ' }', '', ' /// Construct a model that automatically loads the model from the app\'s bundle.', ' convenience init(configuration: MLModelConfiguration) throws {', ' try self.init(contentsOf: type(of:self).urlOfModelInThisBundle, configuration: configuration)', ' }', '', ' /// Construct a model with explicit path to mlmodelc file', ' convenience init(contentsOf modelURL: URL) throws {', ' try self.init(model: MLModel(contentsOf: modelURL))', ' }', '', ' /// Construct a model with explicit path to mlmodelc file and configuration.', ' convenience init(contentsOf modelURL: URL, configuration: MLModelConfiguration) throws {', ' try self.init(model: MLModel(contentsOf: modelURL, configuration: configuration))', ' }', '', ' /// Construct and load the model asynchronously.', ` class func load(configuration: MLModelConfiguration = MLModelConfiguration(), completionHandler handler: @escaping (Swift.Result<${className}, Error>) -> Void) {`, ` load(contentsOf: self.urlOfModelInThisBundle, configuration: configuration, completionHandler: handler)`, ' }', '', ' /// Construct and load the model asynchronously.', ` class func load(configuration: MLModelConfiguration = MLModelConfiguration()) async throws -> ${className} {`, ` try await load(contentsOf: self.urlOfModelInThisBundle, configuration: configuration)`, ' }', '', ' /// Construct and load the model from a URL asynchronously.', ` class func load(contentsOf modelURL: URL, configuration: MLModelConfiguration = MLModelConfiguration(), completionHandler handler: @escaping (Swift.Result<${className}, Error>) -> Void) {`, ' MLModel.load(contentsOf: modelURL, configuration: configuration) { result in', ' switch result {', ' case .failure(let error):', ' handler(.failure(error))', ' case .success(let model):', ` handler(.success(${className}(model: model)))`, ' }', ' }', ' }', '', ' /// Construct and load the model from a URL asynchronously.', ` class func load(contentsOf modelURL: URL, configuration: MLModelConfiguration = MLModelConfiguration()) async throws -> ${className} {`, ' let model = try await MLModel.load(contentsOf: modelURL, configuration: configuration)', ` return ${className}(model: model)`, ' }', '', ' /// Make a prediction using the structured interface', ` func prediction(input: ${input}) throws -> ${output} {`, ' try prediction(input: input, options: MLPredictionOptions())', ' }', '', ' /// Make a prediction using the structured interface', ` func prediction(input: ${input}, options: MLPredictionOptions) throws -> ${output} {`, ' let outFeatures = try model.prediction(from: input, options: options)', ` return ${output}(features: outFeatures)`, ' }', '', ' /// Make a prediction using the convenience interface', ` func prediction(${parameters}) throws -> ${output} {`, ` let input_ = ${input}(${forwarded})`, ' return try prediction(input: input_)', ' }', '', ' /// Make a batch prediction using the structured interface', ` func predictions(inputs: [${input}], options: MLPredictionOptions = MLPredictionOptions()) throws -> [${output}] {`, ' let batchIn = MLArrayBatchProvider(array: inputs)', ' let batchOut = try model.predictions(from: batchIn, options: options)', ` var results : [${output}] = []`, ' results.reserveCapacity(inputs.count)', ' for i in 0..<batchOut.count {', ' let outProvider = batchOut.features(at: i)', ` results.append(${output}(features: outProvider))`, ' }', ' return results', ' }', '}', '');
|
|
608
|
+
return lines.join('\n');
|
|
609
|
+
}
|
|
610
|
+
/** The spec without the feature descriptions' prose (it goes into doc comments, not the JSON). */
|
|
611
|
+
function stripDescriptions(spec) {
|
|
612
|
+
const strip = (features) => features.map((feature) => {
|
|
613
|
+
const { shortDescription: _ignored, ...rest } = feature;
|
|
614
|
+
return rest;
|
|
615
|
+
});
|
|
616
|
+
const model = spec.model;
|
|
617
|
+
const strippedModel = 'models' in model ? { ...model, models: model.models.map(stripDescriptions) } : model;
|
|
618
|
+
return { ...spec, description: { ...spec.description, inputs: strip(spec.description.inputs), outputs: strip(spec.description.outputs) }, model: strippedModel };
|
|
619
|
+
}
|
|
620
|
+
/** Reads, checks and generates the Swift for one model. */
|
|
621
|
+
export function compileModel(file) {
|
|
622
|
+
const spec = parseModel(readModelBytes(file), file);
|
|
623
|
+
const className = modelClassName(file);
|
|
624
|
+
return { file, className, spec, source: generateModelSource(className, spec, file) };
|
|
625
|
+
}
|
|
626
|
+
//# sourceMappingURL=coreml.js.map
|