/** * @fileoverview * @suppress {undefinedVars|checkTypes} */ let objs = []; let events = ['status', 'partialResult', 'result']; let storageWorkerURL = URL.createObjectURL(new Blob(['(', (async () => { let txtDecoder = new TextDecoder(); let txtEncoder = new TextEncoder(); let OPFSRoot = await navigator.storage.getDirectory(); onmessage = async msg => { msg = msg.data; let components = msg.storepath.split('/'); let prevDir = OPFSRoot; for(let component of components) prevDir = await prevDir.getDirectoryHandle(component, { create: true }); let idHandle = await prevDir.getFileHandle('id', { create: true }); let mdlHandle = await prevDir.getFileHandle('model.tgz', { create: true }); let idFile = await idHandle.createSyncAccessHandle(); let mdlFile = await mdlHandle.createSyncAccessHandle(); let oldIdBuf = new ArrayBuffer(idFile.getSize()); idFile.read(oldIdBuf); let tar, tgz; if(txtDecoder.decode(oldIdBuf) == msg.id) { tgz = new ArrayBuffer(mdlFile.getSize()); mdlFile.read(tgz); tar = await new Response(new Response(tgz).body.pipeThrough(new DecompressionStream('gzip'))).arrayBuffer(); } else { let res = await fetch(msg.url); if(!res.ok) throw 'Unable to download model' let teed = res.body.tee(); tgz = await new Response(teed[0].pipeThrough(new CompressionStream('gzip'))).arrayBuffer(); mdlFile.write(tgz, { at: 0 }); mdlFile.truncate(tgz.byteLength); let newId = txtEncoder.encode(msg.id); idFile.write(newId, { at: 0 }); idFile.truncate(newId.length); tar = await new Response(teed[1]).arrayBuffer(); } idFile.close(); mdlFile.close(); self.postMessage(tar, [tar]); } }).toString(), ')()'], { type: 'text/javascript' })) let storageWorker = new Worker(storageWorkerURL); let processorURL = URL.createObjectURL(new Blob(['(', (() => { registerProcessor('VoskletTransferer', class extends AudioWorkletProcessor { constructor(opts) { super(); this.count = 0; this.maxCount = opts.processorOptions.maxCount; this.buffer = new Float32Array(this.maxCount * 128); } process(inputs) { if(!inputs[0][0]) return true; this.buffer.set(inputs[0][0], this.count++ * 128); if(this.count >= this.maxCount) { this.count = 0; this.port.postMessage(this.buffer, [this.buffer.buffer]); this.buffer = new Float32Array(this.maxCount * 128); } return true; } }) }).toString(), ')()'], { type: 'text/javascript' })); class CommonModel extends EventTarget { constructor() { super(); objs.push(this); } delete() { this.obj.delete(); } static async mk(url, storepath, id, normalMdl) { let mdl = new CommonModel(); let result = new Promise((resolve, reject) => { mdl.addEventListener('status', ev => { if(!ev.detail) { if(normalMdl) mdl['findWord'] = word => mdl.obj['findWord'](word) resolve(mdl) } else reject(ev.detail) }, { once: true }) }); storageWorker.addEventListener('message', tar => { tar = tar.data; let tarStart = _malloc(tar.byteLength); HEAPU8.set(new Uint8Array(tar), tarStart); mdl.obj = new Module['CommonModel'](objs.length - 1, normalMdl, '/' + storepath, id, tarStart, tar.byteLength); }, { once: true }); storageWorker.postMessage({ url: url, storepath: storepath, id: id }); return result; } } class Recognizer extends EventTarget { constructor() { super(); // Closure workaround to prevent acceptWaveform from getting removed this['acceptWaveform'] = audioData => { let start = _malloc(audioData.length * 4); HEAPF32.set(audioData, start / 4); this.obj['acceptWaveform'](start, audioData.length); } objs.push(this); return new Proxy(this, { get(self, prop, _) { if(self[prop] == undefined && self.obj[prop] == undefined) return; let p = self[prop]; if(p) return p.bind ? p.bind(self) : p; p = self.obj[prop]; return p.bind ? p.bind(self.obj) : p; } }) } async delete(processCurrent = false) { let result = new Promise((resolve, _) => this.addEventListener('status', _ => { this.obj.delete(); resolve(); }, { once: true })); this.obj['safeDelete'](processCurrent); return result; } static async mk(model, sampleRate, mode, grammar, spkModel) { let rec = new Recognizer(); let result = new Promise((resolve, reject) => { rec.addEventListener('status', ev => { if(!ev.detail) resolve(rec); else reject(ev.detail); }, { once: true }); }) switch(mode) { case 1: rec.obj = new Module['Recognizer'](objs.length - 1, sampleRate, model); break; case 2: rec.obj = new Module['Recognizer'](objs.length -1, sampleRate, model, spkModel); break; default: rec.obj = new Module['Recognizer'](objs.length - 1, sampleRate, model, grammar, 0); } return result; } } Module = { 'cleanUp': async () => { for(let obj of objs) await obj.delete(); URL.revokeObjectURL(processorURL); URL.revokeObjectURL(storageWorkerURL); storageWorker.terminate(); }, 'createTransferer': async (ctx, bufferSize) => { await ctx.audioWorklet.addModule(processorURL); return new AudioWorkletNode(ctx, 'VoskletTransferer', { channelCountMode: 'explicit', numberOfInputs: 1, numberOfOutputs: 0, channelCount: 1, processorOptions: { maxCount: bufferSize / 128 } }); }, 'createModel': (url, storepath, id) => CommonModel.mk(url, storepath, id, true), 'createSpkModel': (url, storepath, id) => CommonModel.mk(url, storepath, id, false), 'createRecognizer': (model, sampleRate) => Recognizer.mk(model.obj, sampleRate, 1), 'createRecognizerWithGrm': (model, sampleRate, grammar) => Recognizer.mk(model.obj, sampleRate, 3, grammar, null), 'createRecognizerWithSpkModel': (model, sampleRate, spkModel) => Recognizer.mk(model.obj, sampleRate, 2, null, spkModel.obj) }