@jseeio/jsee 0.2.8 → 0.3.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 +13 -3
- package/README.md +42 -11
- package/dist/jsee.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/jest.config.js +1 -0
- package/package.json +28 -25
- package/src/app.js +26 -16
- package/src/main.js +678 -0
- package/src/utils.js +91 -12
- package/src/worker.js +56 -60
- package/templates/bulma-app.vue +32 -7
- package/templates/bulma-input.vue +11 -5
- package/templates/common-inputs.js +3 -0
- package/test/class.html +22 -0
- package/test/importw.html +28 -0
- package/test/minimal.html +1 -1
- package/test/minimal4.html +1 -1
- package/test/pipeline.html +52 -0
- package/test/python.html +41 -0
- package/{test.js → test/test-basic.test.js} +94 -17
- package/test/test-python.test.js +26 -0
- package/webpack.config.js +2 -1
- package/main.js +0 -621
- package/test/example-async-function.js +0 -0
- package/test/example-async-init.js +0 -0
package/src/main.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { createVueApp } from './app'
|
|
2
|
+
import Worker from './worker.js'
|
|
3
|
+
|
|
4
|
+
const utils = require('./utils')
|
|
5
|
+
const isObject = utils.isObject
|
|
6
|
+
|
|
7
|
+
const { Notyf } = require('notyf')
|
|
8
|
+
const notyf = new Notyf({
|
|
9
|
+
types: [
|
|
10
|
+
{
|
|
11
|
+
type: 'success',
|
|
12
|
+
background: '#00d1b2',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
type: 'error',
|
|
16
|
+
background: '#f14668',
|
|
17
|
+
duration: 2000,
|
|
18
|
+
dismissible: true
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const Overlay = require('./overlay')
|
|
24
|
+
|
|
25
|
+
require('notyf/notyf.min.css')
|
|
26
|
+
|
|
27
|
+
const fetch = window['fetch']
|
|
28
|
+
const Blob = window['Blob']
|
|
29
|
+
|
|
30
|
+
let verbose = true
|
|
31
|
+
function log () {
|
|
32
|
+
if (verbose) {
|
|
33
|
+
console.log(`[JSEE v${VERSION}]`, ...arguments)
|
|
34
|
+
const logElement = document.querySelector('#log')
|
|
35
|
+
if (logElement) {
|
|
36
|
+
logElement.innerHTML += `\n${[...arguments].join(' ')}`
|
|
37
|
+
logElement.scrollTop = logElement.scrollHeight // auto scroll to bottom
|
|
38
|
+
if (logElement.innerHTML.length > 10000) {
|
|
39
|
+
logElement.innerHTML = logElement.innerHTML.slice(-10000)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// const Worker = window['Worker']
|
|
46
|
+
|
|
47
|
+
// Deep clone a simple object
|
|
48
|
+
function clone (obj) {
|
|
49
|
+
// return JSON.parse(JSON.stringify(obj))
|
|
50
|
+
return Object.assign({}, obj)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
function getName (code) {
|
|
56
|
+
switch (typeof code) {
|
|
57
|
+
case 'function':
|
|
58
|
+
return code.name
|
|
59
|
+
case 'string':
|
|
60
|
+
const words = code.split(' ')
|
|
61
|
+
const functionIndex = words.findIndex((word) => word == 'function')
|
|
62
|
+
const name = words[functionIndex + 1]
|
|
63
|
+
return name.includes('(') ? undefined : name
|
|
64
|
+
default:
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Return input value
|
|
70
|
+
function getValue (input) {
|
|
71
|
+
if (input.type === 'group') {
|
|
72
|
+
const value = {}
|
|
73
|
+
input.elements.forEach(el => {
|
|
74
|
+
value[el.name] = getValue(el)
|
|
75
|
+
})
|
|
76
|
+
return value
|
|
77
|
+
} else {
|
|
78
|
+
return input.value
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getModelType (model) {
|
|
83
|
+
if (model.code && typeof model.code === 'string' && model.code.split(' ').map(v => v.trim()).includes('def')) {
|
|
84
|
+
return 'py'
|
|
85
|
+
} else if (model.url) {
|
|
86
|
+
return 'post'
|
|
87
|
+
}
|
|
88
|
+
return 'function'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Nice trick to get a function parameters by Jack Allan
|
|
92
|
+
// From: https://stackoverflow.com/a/9924463/2998960
|
|
93
|
+
const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg
|
|
94
|
+
const ARGUMENT_NAMES = /([^\s,]+)/g
|
|
95
|
+
function getParamNames (func) {
|
|
96
|
+
const fnStr = func.toString().replace(STRIP_COMMENTS, '')
|
|
97
|
+
let result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES)
|
|
98
|
+
if (result === null)
|
|
99
|
+
result = []
|
|
100
|
+
return result
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getInputs (model) {
|
|
104
|
+
if (model.code) {
|
|
105
|
+
const params = getParamNames(model.code).filter(p => !['(', ')', '#', '{', '}'].some(c => p.includes(c)))
|
|
106
|
+
log('Trying to infer inputs from params:', params)
|
|
107
|
+
return params.map(p => ({
|
|
108
|
+
'name': p,
|
|
109
|
+
'type': 'string'
|
|
110
|
+
}))
|
|
111
|
+
}
|
|
112
|
+
return []
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getFunctionContainer (target) {
|
|
116
|
+
// Check if the number of parameters is > 1, then 'args'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default class JSEE {
|
|
120
|
+
constructor (params, alt1, alt2) {
|
|
121
|
+
// Check if JSEE was initialized with args rather than with a params object
|
|
122
|
+
// So two ways to init JSEE:
|
|
123
|
+
// 1. new JSEE({schema: ..., container: ..., verbose: ...}) <- params object
|
|
124
|
+
// 2. new JSEE(schema, container, verbose) <- args
|
|
125
|
+
// This check converts args to params object (2 -> 1)
|
|
126
|
+
if (('model' in params) || (typeof params === 'string') || (typeof params === 'function') || !(typeof alt1 === 'undefined')) {
|
|
127
|
+
params = {
|
|
128
|
+
'schema': params,
|
|
129
|
+
'container': alt1,
|
|
130
|
+
'verbose': alt2
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Set global verbose flag
|
|
135
|
+
// This check sets verbose to true in all cases except when params.verbose is explicitly set to false
|
|
136
|
+
verbose = !(params.verbose === false)
|
|
137
|
+
this.container = params.container
|
|
138
|
+
this.schema = params.schema || params.config // Previous naming
|
|
139
|
+
this.__version__ = VERSION
|
|
140
|
+
|
|
141
|
+
// Check if schema is provided
|
|
142
|
+
if (typeof this.schema === 'undefined') {
|
|
143
|
+
notyf.error('No schema provided')
|
|
144
|
+
throw new Error('No schema provided')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if container is provided
|
|
148
|
+
if (typeof this.container === 'undefined') {
|
|
149
|
+
// Check if 'jsee-container' exists
|
|
150
|
+
if (document.querySelector('#jsee-container')) {
|
|
151
|
+
this.container = '#jsee-container'
|
|
152
|
+
log(`Using default container: ${this.container}`)
|
|
153
|
+
} else {
|
|
154
|
+
notyf.error('No container provided')
|
|
155
|
+
throw new Error('No container provided')
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.init()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
log (...args) {
|
|
163
|
+
log(...args)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
notify (txt) {
|
|
167
|
+
notyf.success(txt)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
progress (i) {
|
|
171
|
+
// Check if progress div is defined
|
|
172
|
+
let progress = document.querySelector('#progress')
|
|
173
|
+
if (!progress) {
|
|
174
|
+
progress = document.createElement('div')
|
|
175
|
+
progress.setAttribute('id', 'progress')
|
|
176
|
+
progress.style = 'position: fixed; top: 0; left: 0; width: 0; height: 3px; background: #00d1b2; z-index: 1000;'
|
|
177
|
+
document.body.appendChild(progress)
|
|
178
|
+
}
|
|
179
|
+
progress.style.width = `${i}%`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async init () {
|
|
183
|
+
// At this point this.schema is defined but can be in different forms (e.g. string, object, function)
|
|
184
|
+
await this.initSchema() // -> this.schema (object)
|
|
185
|
+
await this.initModel()
|
|
186
|
+
await this.initInputs()
|
|
187
|
+
await this.initVue() // -> this.app, this.data
|
|
188
|
+
await this.initPipeline()
|
|
189
|
+
if (this.schema.autorun) {
|
|
190
|
+
log('Autorun is enabled. Running the model')
|
|
191
|
+
this.run('init')
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async initSchema () {
|
|
196
|
+
// Check if schema is a string (url to json)
|
|
197
|
+
if (typeof this.schema === 'string') {
|
|
198
|
+
this.schemaUrl = this.schema.indexOf('json') ? this.schema : this.schema + '.json'
|
|
199
|
+
this.schema = await fetch(this.schemaUrl)
|
|
200
|
+
this.schema = await this.schema.json()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check if schema is a function (model)
|
|
204
|
+
if (typeof this.schema === 'function') {
|
|
205
|
+
this.schema = {
|
|
206
|
+
model: {
|
|
207
|
+
code: this.schema,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// At this point schema should be an object
|
|
213
|
+
if (!isObject(this.schema)) {
|
|
214
|
+
notyf.error('Schema is in a wrong format')
|
|
215
|
+
throw new Error(`Schema is in a wrong format: ${this.schema}`)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async initModel () {
|
|
220
|
+
// Model is the main part of the schema that defines all computations
|
|
221
|
+
// At the end it should be an array of objects that define a sequence of tasks
|
|
222
|
+
this.model = []
|
|
223
|
+
|
|
224
|
+
// Check if model is a function (model)
|
|
225
|
+
;[this.schema.model, this.schema.render].forEach(m => {
|
|
226
|
+
// Function -> {code: Function}
|
|
227
|
+
if (typeof m === 'function') {
|
|
228
|
+
this.model.push({
|
|
229
|
+
code: m
|
|
230
|
+
})
|
|
231
|
+
} else if (Array.isArray(m)) {
|
|
232
|
+
// concatenate
|
|
233
|
+
this.model = this.model.concat(m)
|
|
234
|
+
} else if (isObject(m)) {
|
|
235
|
+
this.model.push(m)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Check if model is empty
|
|
240
|
+
if (this.model.length === 0) {
|
|
241
|
+
notyf.error('Model is in a wrong format')
|
|
242
|
+
throw new Error(`Model is in a wrong format: ${this.schema.model}`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Put worker and imports inside model blocks
|
|
246
|
+
;['worker', 'imports'].forEach(key => {
|
|
247
|
+
if (typeof this.schema[key] !== 'undefined') {
|
|
248
|
+
this.model[0][key] = this.schema[key]
|
|
249
|
+
delete this.schema[key]
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Check if autorun is defined
|
|
254
|
+
if (typeof this.model[0]['autorun'] !== 'undefined') {
|
|
255
|
+
this.schema.autorun = this.model[0]['autorun']
|
|
256
|
+
delete this.model[0]['autorun']
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Async for-loop over this.model
|
|
260
|
+
for (const [i, m] of this.model.entries()) {
|
|
261
|
+
if (typeof m.worker === 'undefined') {
|
|
262
|
+
m.worker = i === 0 // Run first model in a web worker
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Load code if url is provided
|
|
266
|
+
if (m.url && (m.url.includes('.js') || m.url.includes('.py'))) {
|
|
267
|
+
// Update model URL if needed
|
|
268
|
+
if (!m.url.includes('/') && this.schemaUrl && this.schemaUrl.includes('/')) {
|
|
269
|
+
m.url = window.location.protocol + '//' + window.location.host + this.schemaUrl.split('/').slice(0, -1).join('/') + '/' + m.url
|
|
270
|
+
log(`Changed the old model URL to ${m.url} (based on the schema URL)`)
|
|
271
|
+
}
|
|
272
|
+
log('Loaded code from:', m.url)
|
|
273
|
+
m.code = await fetch(m.url)
|
|
274
|
+
m.code = await m.code.text()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Update model name if absent
|
|
278
|
+
if (typeof m.name === 'undefined'){
|
|
279
|
+
if ((m.url) && (m.url.includes('.js'))) {
|
|
280
|
+
m.name = m.url.split('/').pop().split('.')[0]
|
|
281
|
+
log('Use model name from url:', m.name)
|
|
282
|
+
} else if (m.code) {
|
|
283
|
+
m.name = getName(m.code)
|
|
284
|
+
log('Use model name from code:', m.name)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check if imports are string -> convert to array
|
|
289
|
+
if (typeof m.imports === 'string') {
|
|
290
|
+
m.imports = [m.imports]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Infer model type
|
|
294
|
+
if (typeof m.type === 'undefined') {
|
|
295
|
+
m.type = getModelType(m)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
} // end of model-loop
|
|
300
|
+
|
|
301
|
+
log('Model initialized, size:', this.model.length)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async initInputs () {
|
|
305
|
+
// Check inputs
|
|
306
|
+
// Relies on model.code
|
|
307
|
+
// So run after possible fetching
|
|
308
|
+
if (typeof this.schema.inputs === 'undefined') {
|
|
309
|
+
this.model[0].container = 'args'
|
|
310
|
+
this.schema.inputs = getInputs(this.model[0])
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Relies on input check
|
|
314
|
+
// Set default input type
|
|
315
|
+
this.schema.inputs.forEach(input => {
|
|
316
|
+
if (typeof input.type === 'undefined') {
|
|
317
|
+
input.type = 'string'
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
log('Inputs are:', this.schema.inputs)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
initVue () {
|
|
324
|
+
return new Promise((resolve, reject) => {
|
|
325
|
+
try {
|
|
326
|
+
log('Initializing VUE')
|
|
327
|
+
this.app = createVueApp(this, (container) => {
|
|
328
|
+
// Called when the app is mounted
|
|
329
|
+
// FYI "this" here refers to port object
|
|
330
|
+
this.outputsContainer = container.querySelector('#outputs')
|
|
331
|
+
this.inputsContainer = container.querySelector('#inputs')
|
|
332
|
+
this.modelContainer = container.querySelector('#model')
|
|
333
|
+
// Init overlay
|
|
334
|
+
this.overlay = new Overlay(this.inputsContainer ? this.inputsContainer : this.outputsContainer)
|
|
335
|
+
// Add stop button to the overlay if interval is defined
|
|
336
|
+
if (this.schema.interval) {
|
|
337
|
+
this.stopElement = document.createElement('button')
|
|
338
|
+
this.stopElement.innerHTML = 'Stop'
|
|
339
|
+
this.stopElement.style = 'background: white; color: #333; border: 1px solid #DDD; padding: 10px; border-radius: 5px; cursor: pointer;'
|
|
340
|
+
this.stopElement.addEventListener('click', () => {
|
|
341
|
+
this.running = false
|
|
342
|
+
})
|
|
343
|
+
this.overlay.element.innerHTML = ''
|
|
344
|
+
this.overlay.element.appendChild(this.stopElement)
|
|
345
|
+
}
|
|
346
|
+
resolve()
|
|
347
|
+
}, log)
|
|
348
|
+
this.data = this.app.$data
|
|
349
|
+
} catch (err) {
|
|
350
|
+
reject(err)
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async initPipeline () {
|
|
356
|
+
// Initial identity operation (just pass the input to output)
|
|
357
|
+
this.pipeline = (inputs) => inputs
|
|
358
|
+
// Async for-loop over this.model (again)
|
|
359
|
+
for (const [i, m] of this.model.entries()) {
|
|
360
|
+
log('Initilizing the pipeline with model:', i)
|
|
361
|
+
let modelFunc
|
|
362
|
+
if (m.worker) {
|
|
363
|
+
// Init worker model
|
|
364
|
+
modelFunc = await this.initWorker(m)
|
|
365
|
+
} else {
|
|
366
|
+
// Init specific model types
|
|
367
|
+
switch (m.type) {
|
|
368
|
+
case 'py':
|
|
369
|
+
modelFunc = await this.initPython(m)
|
|
370
|
+
break
|
|
371
|
+
case 'tf':
|
|
372
|
+
modelFunc = await this.initTF(m)
|
|
373
|
+
break
|
|
374
|
+
case 'function':
|
|
375
|
+
case 'class':
|
|
376
|
+
case 'async-init':
|
|
377
|
+
case 'async-function':
|
|
378
|
+
modelFunc = await this.initJS(m)
|
|
379
|
+
break
|
|
380
|
+
case 'get':
|
|
381
|
+
case 'post':
|
|
382
|
+
modelFunc = await this.initAPI(m)
|
|
383
|
+
break
|
|
384
|
+
default:
|
|
385
|
+
notyf.error('No type information')
|
|
386
|
+
throw new Error(`No type information: ${m.type}`)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.pipeline = (p => {
|
|
391
|
+
return async (inputs) => {
|
|
392
|
+
const resPrev = await p(inputs)
|
|
393
|
+
const resNext = await modelFunc(resPrev)
|
|
394
|
+
if (isObject(resNext) && isObject(resPrev)) {
|
|
395
|
+
// If both results are objects, merge them
|
|
396
|
+
return Object.assign({}, resPrev, resNext)
|
|
397
|
+
} else if (typeof resNext !== 'undefined') {
|
|
398
|
+
// If next result is defined, return it
|
|
399
|
+
return resNext
|
|
400
|
+
} else {
|
|
401
|
+
// Otherwise return previous result (pass through)
|
|
402
|
+
return resPrev
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
})(this.pipeline)
|
|
406
|
+
|
|
407
|
+
notyf.success('Pipeline initialized')
|
|
408
|
+
this.overlay.hide()
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async initWorker (model) {
|
|
413
|
+
// Init worker
|
|
414
|
+
const worker = new Worker()
|
|
415
|
+
|
|
416
|
+
// Init worker with the model
|
|
417
|
+
if (typeof model.code === 'function') {
|
|
418
|
+
log('Convert code in schema to string for WebWorker')
|
|
419
|
+
model.code = model.code.toString()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Wrap anonymous functions
|
|
423
|
+
if (!model.name) {
|
|
424
|
+
model.code = `function anon () { return (${model.code})(...arguments) }`
|
|
425
|
+
model.name = 'anon'
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const modelFunc = (inputs) => new Promise((resolve, reject) => {
|
|
429
|
+
worker.onmessage = (e) => {
|
|
430
|
+
const res = e.data
|
|
431
|
+
if ((typeof res === 'object') && (res._status)) {
|
|
432
|
+
switch (res._status) {
|
|
433
|
+
case 'loaded':
|
|
434
|
+
notyf.success('Loaded model (in worker)')
|
|
435
|
+
log('Loaded model (in worker):', res)
|
|
436
|
+
resolve(res)
|
|
437
|
+
break
|
|
438
|
+
case 'log':
|
|
439
|
+
log(...res._log)
|
|
440
|
+
break
|
|
441
|
+
case 'progress':
|
|
442
|
+
this.progress(res._progress)
|
|
443
|
+
break
|
|
444
|
+
case 'error':
|
|
445
|
+
notyf.error(res._error)
|
|
446
|
+
log('Error from worker:', res._error)
|
|
447
|
+
reject(res._error)
|
|
448
|
+
break
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
log('Response from worker:', res)
|
|
452
|
+
resolve(res)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
worker.onerror = (e) => {
|
|
456
|
+
notyf.error(e.message)
|
|
457
|
+
log('Error from worker:', e)
|
|
458
|
+
reject(e)
|
|
459
|
+
}
|
|
460
|
+
worker.postMessage(inputs)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// Initial worker call with model definition
|
|
464
|
+
await modelFunc(model)
|
|
465
|
+
|
|
466
|
+
// Worker will be in the context of each modelFunc
|
|
467
|
+
return modelFunc
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async initPython (model) {
|
|
471
|
+
// Add loading indicator
|
|
472
|
+
this.overlay.show()
|
|
473
|
+
await utils.importScripts(['https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js'])
|
|
474
|
+
const pyodide = await loadPyodide()
|
|
475
|
+
if (model.imports && Array.isArray(model.imports) && model.imports.length) {
|
|
476
|
+
await pyodide.loadPackage(model.imports)
|
|
477
|
+
} else {
|
|
478
|
+
await pyodide.loadPackagesFromImports(model.code)
|
|
479
|
+
}
|
|
480
|
+
return async (data) => {
|
|
481
|
+
for (let key in data) {
|
|
482
|
+
window[key] = data[key]
|
|
483
|
+
}
|
|
484
|
+
return await pyodide.runPythonAsync(model.code);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async initJS (model) {
|
|
489
|
+
// 1. String input <- loaded from url or code(string)
|
|
490
|
+
// 2. Target object (can be function, class or a function with async init <- code(object)
|
|
491
|
+
// 3. Model function
|
|
492
|
+
|
|
493
|
+
// We always start from 1 or 2
|
|
494
|
+
// For window execution we go: [1 ->] 2 -> 3
|
|
495
|
+
// For worker: [2 ->] 1 -> Worker
|
|
496
|
+
|
|
497
|
+
// Main: Initialize model in main window
|
|
498
|
+
|
|
499
|
+
// Load imports if defined (before calling the model)
|
|
500
|
+
if (model.imports && model.imports.length) {
|
|
501
|
+
log('Loading imports from schema')
|
|
502
|
+
await utils.importScripts(...model.imports)
|
|
503
|
+
notyf.success('Loaded: JS imports')
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Target here represents raw JS object (e.g. class), not the final callable function
|
|
507
|
+
let target
|
|
508
|
+
if (typeof model.code === 'string') {
|
|
509
|
+
// 1 -> 2
|
|
510
|
+
// Danger zone
|
|
511
|
+
if (model.name) {
|
|
512
|
+
log('Evaluating code from string (has name)')
|
|
513
|
+
target = Function(
|
|
514
|
+
`${model.code} ;return ${model.name}`
|
|
515
|
+
)()
|
|
516
|
+
} else {
|
|
517
|
+
log('Evaluating code from string (no name)')
|
|
518
|
+
target = eval(`(${model.code})`) // ( ͡° ͜ʖ ͡°) YEAHVAL
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
target = model.code
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const modelFunc = await utils.getModelFuncJS(model, target, this)
|
|
525
|
+
return modelFunc
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
initAPI () {
|
|
529
|
+
this.overlay.hide()
|
|
530
|
+
if (this.schema.model.worker) {
|
|
531
|
+
// Worker:
|
|
532
|
+
this.worker.postMessage(this.schema.model)
|
|
533
|
+
} else {
|
|
534
|
+
// Main:
|
|
535
|
+
this.modelFunc = utils.getModelFuncAPI(model, log)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
initTF () {
|
|
540
|
+
let script = document.createElement('script')
|
|
541
|
+
script.src = 'dist/tf.min.js'
|
|
542
|
+
script.onload = () => {
|
|
543
|
+
log('Loaded TF.js')
|
|
544
|
+
this.overlay.hide()
|
|
545
|
+
window['tf'].loadLayersModel(this.schema.model.url).then(res => {
|
|
546
|
+
log('Loaded Tensorflow model')
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
document.head.appendChild(script)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async run (caller='run') {
|
|
553
|
+
// caller can be:
|
|
554
|
+
// 1. custom input button name
|
|
555
|
+
// 2. `run`
|
|
556
|
+
// 3. `autorun`
|
|
557
|
+
const schema = this.schema
|
|
558
|
+
const data = this.data
|
|
559
|
+
this.running = true
|
|
560
|
+
|
|
561
|
+
log('Running the pipeline...')
|
|
562
|
+
// Collect input values
|
|
563
|
+
let inputValues = {}
|
|
564
|
+
data.inputs.forEach(input => {
|
|
565
|
+
// Skip buttons
|
|
566
|
+
if (input.name && !(input.type == 'action' || input.type == 'button')) {
|
|
567
|
+
inputValues[input.name] = getValue(input)
|
|
568
|
+
}
|
|
569
|
+
})
|
|
570
|
+
// Add caller to input values so we can change model behavior based on it
|
|
571
|
+
inputValues.caller = caller
|
|
572
|
+
|
|
573
|
+
log('Input values:', inputValues)
|
|
574
|
+
// We have all input values here, pass them to worker, window.modelFunc or tf
|
|
575
|
+
if (!schema.model.autorun) {
|
|
576
|
+
this.overlay.show()
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Run pipeline
|
|
580
|
+
const results = await this.pipeline(inputValues)
|
|
581
|
+
|
|
582
|
+
// Output results
|
|
583
|
+
this.output(results)
|
|
584
|
+
|
|
585
|
+
// Check if interval is defined
|
|
586
|
+
if (schema.interval && this.running && (caller === 'run')) {
|
|
587
|
+
log('Interval is defined:', schema.interval)
|
|
588
|
+
await utils.delay(schema.interval)
|
|
589
|
+
await this.run(caller)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Hide overlay
|
|
593
|
+
this.overlay.hide()
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async outputAsync (res) {
|
|
598
|
+
this.output(res)
|
|
599
|
+
await delay(1)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
output (res) {
|
|
603
|
+
// TODO: Think about all edge cases
|
|
604
|
+
// * No output field, but reactivity
|
|
605
|
+
|
|
606
|
+
if (typeof res === 'undefined') {
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
log('[Output] Got output results of type:', typeof res)
|
|
611
|
+
|
|
612
|
+
// Process results (res)
|
|
613
|
+
const inputNames = this.schema.inputs.map(i => i.name)
|
|
614
|
+
if (isObject(res) && Object.keys(res).every(key => inputNames.includes(key))) {
|
|
615
|
+
// Update inputs from results
|
|
616
|
+
log('Updating inputs:', Object.keys(res))
|
|
617
|
+
this.data.inputs.forEach((input, i) => {
|
|
618
|
+
if (input.name && (typeof res[input.name] !== 'undefined')) {
|
|
619
|
+
log('Updating input: ', input.name, 'with data:', res[input.name])
|
|
620
|
+
const r = res[input.name]
|
|
621
|
+
if (typeof r === 'object') {
|
|
622
|
+
Object.keys(r).forEach(k => {
|
|
623
|
+
input[k] = r[k]
|
|
624
|
+
})
|
|
625
|
+
} else {
|
|
626
|
+
input.value = r
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
})
|
|
630
|
+
} else if (this.renderFunc) {
|
|
631
|
+
// Pass results to a custom render function
|
|
632
|
+
log('Calling a render function...')
|
|
633
|
+
this.renderFunc(res, this)
|
|
634
|
+
} else if (Array.isArray(res) && res.length) {
|
|
635
|
+
// Result is array
|
|
636
|
+
if (this.data.outputs && this.data.outputs.length) {
|
|
637
|
+
// We have outputs defined
|
|
638
|
+
if (this.data.outputs.length === res.length) {
|
|
639
|
+
// Same length
|
|
640
|
+
this.data.outputs.forEach((output, i) => {
|
|
641
|
+
output.value = res[i]
|
|
642
|
+
})
|
|
643
|
+
} else {
|
|
644
|
+
// Different length
|
|
645
|
+
this.data.outputs[0].value = res
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
// Outputs are not defined
|
|
649
|
+
this.data.outputs = [{
|
|
650
|
+
'type': 'array',
|
|
651
|
+
'value': res
|
|
652
|
+
}]
|
|
653
|
+
}
|
|
654
|
+
} else if (typeof res === 'object') {
|
|
655
|
+
if (this.data.outputs && this.data.outputs.length) {
|
|
656
|
+
this.data.outputs.forEach((output, i) => {
|
|
657
|
+
if (output.name && (typeof res[output.name] !== 'undefined')) {
|
|
658
|
+
log('Updating output: ', output.name)
|
|
659
|
+
output.value = res[output.name]
|
|
660
|
+
}
|
|
661
|
+
})
|
|
662
|
+
} else {
|
|
663
|
+
this.data.outputs = [{
|
|
664
|
+
'type': 'object',
|
|
665
|
+
'value': res
|
|
666
|
+
}]
|
|
667
|
+
}
|
|
668
|
+
} else if (this.schema.outputs && this.schema.outputs.length === 1) {
|
|
669
|
+
// One output value passed as raw js object
|
|
670
|
+
this.data.outputs[0].value = res
|
|
671
|
+
} else {
|
|
672
|
+
this.data.outputs = [{
|
|
673
|
+
'type': typeof res,
|
|
674
|
+
'value': res
|
|
675
|
+
}]
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|