@fugood/node-whisper-wasm 1.0.19
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 +40 -0
- package/index.d.ts +183 -0
- package/index.js +1250 -0
- package/package.json +46 -0
- package/whisper-node.js +2 -0
- package/whisper-node.wasm +0 -0
- package/worker.js +255 -0
package/index.js
ADDED
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) {
|
|
3
|
+
module.exports = factory(function () {
|
|
4
|
+
return require('./whisper-node.js')
|
|
5
|
+
}, root)
|
|
6
|
+
} else {
|
|
7
|
+
root.WhisperNodeWasm = factory(function () {
|
|
8
|
+
return root.createWhisperNodeModule
|
|
9
|
+
}, root)
|
|
10
|
+
}
|
|
11
|
+
})(
|
|
12
|
+
typeof globalThis !== 'undefined'
|
|
13
|
+
? globalThis
|
|
14
|
+
: typeof self !== 'undefined'
|
|
15
|
+
? self
|
|
16
|
+
: typeof window !== 'undefined'
|
|
17
|
+
? window
|
|
18
|
+
: this,
|
|
19
|
+
function (loadModuleFactory, root) {
|
|
20
|
+
'use strict'
|
|
21
|
+
|
|
22
|
+
var SAMPLE_RATE = 16000
|
|
23
|
+
var MIB = 1024 * 1024
|
|
24
|
+
var FIREFOX_MODEL_LIMIT_BYTES = 256 * MIB
|
|
25
|
+
var MODEL_MEMORY_RATIO = 0.75
|
|
26
|
+
|
|
27
|
+
var runtimePromise = null
|
|
28
|
+
var runtimeOptions = {}
|
|
29
|
+
var workerProxyPromise = null
|
|
30
|
+
var capturedScriptUrl = getCurrentScriptUrl()
|
|
31
|
+
var modelCache = Object.create(null)
|
|
32
|
+
var logEnabled = false
|
|
33
|
+
var logListeners = []
|
|
34
|
+
var nativeLogCallback = null
|
|
35
|
+
|
|
36
|
+
function configureWasm(options) {
|
|
37
|
+
if (runtimePromise || workerProxyPromise) {
|
|
38
|
+
throw new Error('configureWasm must be called before the WASM runtime is loaded')
|
|
39
|
+
}
|
|
40
|
+
runtimeOptions = Object.assign({}, runtimeOptions, options || {})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getCurrentScriptUrl() {
|
|
44
|
+
if (
|
|
45
|
+
root.document &&
|
|
46
|
+
root.document.currentScript &&
|
|
47
|
+
root.document.currentScript.src
|
|
48
|
+
) {
|
|
49
|
+
return root.document.currentScript.src
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isBrowserLike() {
|
|
55
|
+
return typeof root.window !== 'undefined' || typeof root.importScripts === 'function'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isMainBrowserThread() {
|
|
59
|
+
return (
|
|
60
|
+
typeof root.window !== 'undefined' &&
|
|
61
|
+
root.window === root &&
|
|
62
|
+
typeof root.Worker === 'function'
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveUrl(value, base) {
|
|
67
|
+
try {
|
|
68
|
+
return new URL(value, base || (root.location && root.location.href)).href
|
|
69
|
+
} catch (_) {
|
|
70
|
+
return value
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getIndexScriptUrl() {
|
|
75
|
+
var configured = runtimeOptions.indexScriptUrl || runtimeOptions.scriptUrl
|
|
76
|
+
if (configured) {
|
|
77
|
+
return resolveUrl(configured)
|
|
78
|
+
}
|
|
79
|
+
return capturedScriptUrl
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getRuntimeScriptUrl(indexScriptUrl) {
|
|
83
|
+
var configured = runtimeOptions.runtimeScriptUrl
|
|
84
|
+
if (configured) {
|
|
85
|
+
return resolveUrl(configured, indexScriptUrl)
|
|
86
|
+
}
|
|
87
|
+
return indexScriptUrl ? resolveUrl('whisper-node.js', indexScriptUrl) : null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getWorkerScriptUrl(indexScriptUrl) {
|
|
91
|
+
var configured = runtimeOptions.workerUrl
|
|
92
|
+
if (configured) {
|
|
93
|
+
return resolveUrl(configured, indexScriptUrl)
|
|
94
|
+
}
|
|
95
|
+
return indexScriptUrl ? resolveUrl('worker.js', indexScriptUrl) : null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function shouldUseWorker(options) {
|
|
99
|
+
return (
|
|
100
|
+
isMainBrowserThread() &&
|
|
101
|
+
runtimeOptions.worker !== false &&
|
|
102
|
+
(!options || options.worker !== false) &&
|
|
103
|
+
!!getIndexScriptUrl()
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getWorkerRuntimeOptions() {
|
|
108
|
+
var blocked = {
|
|
109
|
+
worker: true,
|
|
110
|
+
workerUrl: true,
|
|
111
|
+
indexScriptUrl: true,
|
|
112
|
+
scriptUrl: true,
|
|
113
|
+
runtimeScriptUrl: true,
|
|
114
|
+
locateFileBaseUrl: true,
|
|
115
|
+
locateFile: true,
|
|
116
|
+
print: true,
|
|
117
|
+
printErr: true,
|
|
118
|
+
mainScriptUrlOrBlob: true,
|
|
119
|
+
}
|
|
120
|
+
var options = {}
|
|
121
|
+
Object.keys(runtimeOptions).forEach(function (key) {
|
|
122
|
+
var value = runtimeOptions[key]
|
|
123
|
+
if (!blocked[key] && typeof value !== 'function' && value !== undefined) {
|
|
124
|
+
options[key] = value
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
return options
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertThreadSupport() {
|
|
131
|
+
if (isBrowserLike() && typeof root.SharedArrayBuffer === 'undefined') {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'whisper.node WASM is built with pthreads and requires SharedArrayBuffer. Serve the page with COOP/COEP headers so the browser is cross-origin isolated.',
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function emitLog(level, text) {
|
|
139
|
+
if (!logEnabled) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
if (typeof nativeLogCallback === 'function') {
|
|
143
|
+
nativeLogCallback(level, text)
|
|
144
|
+
}
|
|
145
|
+
logListeners.slice().forEach(function (listener) {
|
|
146
|
+
listener(level, text)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function loadRuntime() {
|
|
151
|
+
if (!runtimePromise) {
|
|
152
|
+
assertThreadSupport()
|
|
153
|
+
|
|
154
|
+
var moduleFactory = loadModuleFactory()
|
|
155
|
+
if (moduleFactory && moduleFactory.default) {
|
|
156
|
+
moduleFactory = moduleFactory.default
|
|
157
|
+
}
|
|
158
|
+
if (typeof moduleFactory !== 'function') {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'Failed to load whisper.node WASM runtime. Make sure whisper-node.js is built and loaded before index.js.',
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
var options = Object.assign({}, runtimeOptions)
|
|
165
|
+
var userPrint = options.print
|
|
166
|
+
var userPrintErr = options.printErr
|
|
167
|
+
|
|
168
|
+
options.noInitialRun = true
|
|
169
|
+
options.print = function (text) {
|
|
170
|
+
emitLog('INFO', String(text))
|
|
171
|
+
if (typeof userPrint === 'function') {
|
|
172
|
+
userPrint(text)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
options.printErr = function (text) {
|
|
176
|
+
emitLog('ERROR', String(text))
|
|
177
|
+
if (typeof userPrintErr === 'function') {
|
|
178
|
+
userPrintErr(text)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
runtimePromise = Promise.resolve(moduleFactory(options))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return runtimePromise
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toggleNativeLog(enable, callback) {
|
|
189
|
+
logEnabled = !!enable
|
|
190
|
+
nativeLogCallback = typeof callback === 'function' ? callback : null
|
|
191
|
+
return Promise.resolve()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function addNativeLogListener(listener) {
|
|
195
|
+
logListeners.push(listener)
|
|
196
|
+
return {
|
|
197
|
+
remove: function () {
|
|
198
|
+
var index = logListeners.indexOf(listener)
|
|
199
|
+
if (index >= 0) {
|
|
200
|
+
logListeners.splice(index, 1)
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function rejectAllWorkerRequests(proxy, error) {
|
|
207
|
+
Object.keys(proxy.pending).forEach(function (id) {
|
|
208
|
+
proxy.pending[id].reject(error)
|
|
209
|
+
delete proxy.pending[id]
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function createWorkerProxy() {
|
|
214
|
+
var indexScriptUrl = getIndexScriptUrl()
|
|
215
|
+
var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl)
|
|
216
|
+
var workerScriptUrl = getWorkerScriptUrl(indexScriptUrl)
|
|
217
|
+
|
|
218
|
+
if (!indexScriptUrl || !runtimeScriptUrl || !workerScriptUrl) {
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var worker = new root.Worker(workerScriptUrl, {
|
|
223
|
+
name: 'whisper.node.wasm',
|
|
224
|
+
})
|
|
225
|
+
var proxy = {
|
|
226
|
+
worker: worker,
|
|
227
|
+
nextRequestId: 1,
|
|
228
|
+
pending: Object.create(null),
|
|
229
|
+
failed: null,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
worker.onmessage = function (event) {
|
|
233
|
+
var message = event.data || {}
|
|
234
|
+
if (message.type === 'log') {
|
|
235
|
+
emitLog(message.level || 'INFO', String(message.text || ''))
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (message.type === 'callback') {
|
|
240
|
+
var pendingCallback = proxy.pending[message.id]
|
|
241
|
+
var callback =
|
|
242
|
+
pendingCallback &&
|
|
243
|
+
pendingCallback.callbacks &&
|
|
244
|
+
pendingCallback.callbacks[message.name]
|
|
245
|
+
if (typeof callback === 'function') {
|
|
246
|
+
callback(message.value)
|
|
247
|
+
}
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (message.type !== 'response') {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
var pending = proxy.pending[message.id]
|
|
256
|
+
if (!pending) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
delete proxy.pending[message.id]
|
|
260
|
+
|
|
261
|
+
if (message.error) {
|
|
262
|
+
var error = new Error(message.error.message || 'WASM worker failed')
|
|
263
|
+
if (message.error.stack) {
|
|
264
|
+
error.stack = message.error.stack
|
|
265
|
+
}
|
|
266
|
+
pending.reject(error)
|
|
267
|
+
} else {
|
|
268
|
+
pending.resolve(message.result)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
worker.onerror = function (event) {
|
|
273
|
+
var error = new Error(
|
|
274
|
+
event && event.message ? event.message : 'WASM worker failed',
|
|
275
|
+
)
|
|
276
|
+
proxy.failed = error
|
|
277
|
+
workerProxyPromise = null
|
|
278
|
+
rejectAllWorkerRequests(proxy, error)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
proxy.requestOperation = function (method, args, transfer, callbacks) {
|
|
282
|
+
var id = proxy.nextRequestId++
|
|
283
|
+
if (proxy.failed) {
|
|
284
|
+
return {
|
|
285
|
+
id: id,
|
|
286
|
+
promise: Promise.reject(proxy.failed),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
var promise = new Promise(function (resolve, reject) {
|
|
290
|
+
proxy.pending[id] = {
|
|
291
|
+
resolve: resolve,
|
|
292
|
+
reject: reject,
|
|
293
|
+
callbacks: callbacks || {},
|
|
294
|
+
}
|
|
295
|
+
worker.postMessage(
|
|
296
|
+
{
|
|
297
|
+
type: 'request',
|
|
298
|
+
id: id,
|
|
299
|
+
method: method,
|
|
300
|
+
args: args || [],
|
|
301
|
+
},
|
|
302
|
+
transfer || [],
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
return {
|
|
306
|
+
id: id,
|
|
307
|
+
promise: promise,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
proxy.request = function (method, args, transfer, callbacks) {
|
|
312
|
+
return proxy.requestOperation(method, args, transfer, callbacks).promise
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
proxy.cancel = function (id) {
|
|
316
|
+
worker.postMessage({
|
|
317
|
+
type: 'cancel',
|
|
318
|
+
id: id,
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
proxy.ready = proxy.request('__init', [
|
|
323
|
+
{
|
|
324
|
+
indexScriptUrl: indexScriptUrl,
|
|
325
|
+
runtimeScriptUrl: runtimeScriptUrl,
|
|
326
|
+
runtimeOptions: getWorkerRuntimeOptions(),
|
|
327
|
+
locateFileBaseUrl:
|
|
328
|
+
runtimeOptions.locateFileBaseUrl || resolveUrl('.', runtimeScriptUrl),
|
|
329
|
+
},
|
|
330
|
+
])
|
|
331
|
+
|
|
332
|
+
return proxy
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function getWorkerProxy() {
|
|
336
|
+
if (!shouldUseWorker()) {
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!workerProxyPromise) {
|
|
341
|
+
workerProxyPromise = (async function () {
|
|
342
|
+
var proxy = createWorkerProxy()
|
|
343
|
+
if (!proxy) {
|
|
344
|
+
return null
|
|
345
|
+
}
|
|
346
|
+
await proxy.ready
|
|
347
|
+
return proxy
|
|
348
|
+
})().catch(function (error) {
|
|
349
|
+
workerProxyPromise = null
|
|
350
|
+
emitLog(
|
|
351
|
+
'WARN',
|
|
352
|
+
'Falling back to main-thread WASM because the worker failed to start: ' +
|
|
353
|
+
(error && error.message ? error.message : String(error)),
|
|
354
|
+
)
|
|
355
|
+
return null
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return workerProxyPromise
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getFetch() {
|
|
363
|
+
if (typeof root.fetch === 'function') {
|
|
364
|
+
return root.fetch.bind(root)
|
|
365
|
+
}
|
|
366
|
+
throw new Error('fetch is required to load models or audio by URL')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function formatBytes(bytes) {
|
|
370
|
+
if (bytes >= 1024 * MIB) {
|
|
371
|
+
return (bytes / (1024 * MIB)).toFixed(2) + ' GiB'
|
|
372
|
+
}
|
|
373
|
+
return (bytes / MIB).toFixed(2) + ' MiB'
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isFirefox() {
|
|
377
|
+
return !!(
|
|
378
|
+
root.navigator &&
|
|
379
|
+
typeof root.navigator.userAgent === 'string' &&
|
|
380
|
+
root.navigator.userAgent.indexOf('Firefox/') >= 0
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function getModelSizeLimit(runtime, options) {
|
|
385
|
+
if (options && Number.isFinite(options.maxModelBytes) && options.maxModelBytes > 0) {
|
|
386
|
+
return options.maxModelBytes
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
var wasmLimit =
|
|
390
|
+
runtime && typeof runtime.__wasm_maximum_memory_bytes === 'function'
|
|
391
|
+
? runtime.__wasm_maximum_memory_bytes()
|
|
392
|
+
: 2000 * MIB
|
|
393
|
+
var limit = Math.floor(wasmLimit * MODEL_MEMORY_RATIO)
|
|
394
|
+
|
|
395
|
+
if (isFirefox()) {
|
|
396
|
+
limit = Math.min(limit, FIREFOX_MODEL_LIMIT_BYTES)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return limit
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function assertModelSize(size, limit, source) {
|
|
403
|
+
if (size > limit) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
'Whisper model ' +
|
|
406
|
+
source +
|
|
407
|
+
' is ' +
|
|
408
|
+
formatBytes(size) +
|
|
409
|
+
', which exceeds the WASM model size limit of ' +
|
|
410
|
+
formatBytes(limit) +
|
|
411
|
+
'. Use a smaller or quantized model, or pass maxModelBytes only if this browser can allocate it.',
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function hashString(value) {
|
|
417
|
+
var hash = 2166136261
|
|
418
|
+
for (var i = 0; i < value.length; i += 1) {
|
|
419
|
+
hash ^= value.charCodeAt(i)
|
|
420
|
+
hash = Math.imul(hash, 16777619)
|
|
421
|
+
}
|
|
422
|
+
return (hash >>> 0).toString(16)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function basenameFromUrl(source) {
|
|
426
|
+
var path = source
|
|
427
|
+
try {
|
|
428
|
+
path = new URL(source, root.location && root.location.href).pathname
|
|
429
|
+
} catch (_) {}
|
|
430
|
+
var name = path.split('/').filter(Boolean).pop() || 'model.bin'
|
|
431
|
+
try {
|
|
432
|
+
name = decodeURIComponent(name)
|
|
433
|
+
} catch (_) {}
|
|
434
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, '_') || 'model.bin'
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function mkdirp(FS, path) {
|
|
438
|
+
var current = ''
|
|
439
|
+
path
|
|
440
|
+
.split('/')
|
|
441
|
+
.filter(Boolean)
|
|
442
|
+
.forEach(function (part) {
|
|
443
|
+
current += '/' + part
|
|
444
|
+
try {
|
|
445
|
+
FS.mkdir(current)
|
|
446
|
+
} catch (_) {}
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function fsPathExists(FS, path) {
|
|
451
|
+
try {
|
|
452
|
+
return FS.analyzePath(path).exists
|
|
453
|
+
} catch (_) {
|
|
454
|
+
return false
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function fetchArrayBuffer(source, limit) {
|
|
459
|
+
var response = await getFetch()(source)
|
|
460
|
+
if (!response.ok) {
|
|
461
|
+
throw new Error('Failed to fetch ' + source + ': HTTP ' + response.status)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
var contentLength = Number(response.headers.get('content-length') || 0)
|
|
465
|
+
if (contentLength > 0 && limit) {
|
|
466
|
+
assertModelSize(contentLength, limit, source)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (limit && response.body && typeof response.body.getReader === 'function') {
|
|
470
|
+
var reader = response.body.getReader()
|
|
471
|
+
var chunks = []
|
|
472
|
+
var total = 0
|
|
473
|
+
|
|
474
|
+
while (true) {
|
|
475
|
+
var next = await reader.read()
|
|
476
|
+
if (next.done) {
|
|
477
|
+
break
|
|
478
|
+
}
|
|
479
|
+
total += next.value.byteLength
|
|
480
|
+
assertModelSize(total, limit, source)
|
|
481
|
+
chunks.push(next.value)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
var bytes = new Uint8Array(total)
|
|
485
|
+
var offset = 0
|
|
486
|
+
chunks.forEach(function (chunk) {
|
|
487
|
+
bytes.set(chunk, offset)
|
|
488
|
+
offset += chunk.byteLength
|
|
489
|
+
})
|
|
490
|
+
return bytes.buffer
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
var buffer = await response.arrayBuffer()
|
|
494
|
+
if (limit) {
|
|
495
|
+
assertModelSize(buffer.byteLength, limit, source)
|
|
496
|
+
}
|
|
497
|
+
return buffer
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function ensureModel(runtime, source, kind, options) {
|
|
501
|
+
if (!source) {
|
|
502
|
+
throw new Error('Model path is required')
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (source[0] === '/' && fsPathExists(runtime.FS, source)) {
|
|
506
|
+
return { virtualPath: source, bytes: 0 }
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
var cacheKey = kind + ':' + source
|
|
510
|
+
if (!modelCache[cacheKey]) {
|
|
511
|
+
modelCache[cacheKey] = (async function () {
|
|
512
|
+
var limit = getModelSizeLimit(runtime, options)
|
|
513
|
+
var buffer = await fetchArrayBuffer(source, limit)
|
|
514
|
+
var bytes = new Uint8Array(buffer)
|
|
515
|
+
var virtualPath =
|
|
516
|
+
'/models/' + kind + '-' + hashString(source) + '-' + basenameFromUrl(source)
|
|
517
|
+
|
|
518
|
+
mkdirp(runtime.FS, '/models')
|
|
519
|
+
runtime.FS.writeFile(virtualPath, bytes)
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
virtualPath: virtualPath,
|
|
523
|
+
bytes: bytes.byteLength,
|
|
524
|
+
}
|
|
525
|
+
})()
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return modelCache[cacheKey]
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function unwrapWasmResult(result) {
|
|
532
|
+
if (!result || result.ok === false) {
|
|
533
|
+
throw new Error((result && result.error) || 'WASM operation failed')
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
var unwrapped = {}
|
|
537
|
+
Object.keys(result).forEach(function (key) {
|
|
538
|
+
if (key !== 'ok') {
|
|
539
|
+
unwrapped[key] = normalizeWasmValue(result[key])
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
return unwrapped
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function normalizeWasmValue(value) {
|
|
546
|
+
if (typeof value === 'bigint') {
|
|
547
|
+
return Number(value)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (Array.isArray(value)) {
|
|
551
|
+
return value.map(normalizeWasmValue)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (value && typeof value === 'object') {
|
|
555
|
+
var normalized = {}
|
|
556
|
+
Object.keys(value).forEach(function (key) {
|
|
557
|
+
normalized[key] = normalizeWasmValue(value[key])
|
|
558
|
+
})
|
|
559
|
+
return normalized
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return value
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function normalizeTranscribeOptions(options) {
|
|
566
|
+
var normalized = Object.assign({}, options || {})
|
|
567
|
+
if (typeof normalized.onProgress !== 'function') {
|
|
568
|
+
delete normalized.onProgress
|
|
569
|
+
}
|
|
570
|
+
if (typeof normalized.onNewSegments !== 'function') {
|
|
571
|
+
delete normalized.onNewSegments
|
|
572
|
+
}
|
|
573
|
+
return normalized
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function splitTranscribeOptions(options) {
|
|
577
|
+
var normalized = Object.assign({}, options || {})
|
|
578
|
+
var callbacks = {}
|
|
579
|
+
|
|
580
|
+
if (typeof normalized.onProgress === 'function') {
|
|
581
|
+
callbacks.onProgress = normalized.onProgress
|
|
582
|
+
normalized.onProgress = true
|
|
583
|
+
} else {
|
|
584
|
+
delete normalized.onProgress
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (typeof normalized.onNewSegments === 'function') {
|
|
588
|
+
callbacks.onNewSegments = normalized.onNewSegments
|
|
589
|
+
normalized.onNewSegments = true
|
|
590
|
+
} else {
|
|
591
|
+
delete normalized.onNewSegments
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
options: normalized,
|
|
596
|
+
callbacks: callbacks,
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function abortedResult() {
|
|
601
|
+
return {
|
|
602
|
+
result: '',
|
|
603
|
+
segments: [],
|
|
604
|
+
isAborted: true,
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function defer(fn) {
|
|
609
|
+
return new Promise(function (resolve, reject) {
|
|
610
|
+
var run = function () {
|
|
611
|
+
Promise.resolve().then(fn).then(resolve, reject)
|
|
612
|
+
}
|
|
613
|
+
if (typeof root.setTimeout === 'function') {
|
|
614
|
+
root.setTimeout(run, 0)
|
|
615
|
+
} else {
|
|
616
|
+
Promise.resolve().then(run)
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function sliceViewBuffer(view) {
|
|
622
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function pcm16ToFloat32(buffer) {
|
|
626
|
+
if (buffer.byteLength % 2 !== 0) {
|
|
627
|
+
throw new Error('Audio buffer size must be even for 16-bit PCM')
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
var input = new Int16Array(buffer)
|
|
631
|
+
var output = new Float32Array(input.length)
|
|
632
|
+
for (var i = 0; i < input.length; i += 1) {
|
|
633
|
+
output[i] = input[i] / 32768
|
|
634
|
+
}
|
|
635
|
+
return output
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function toFloat32Audio(input) {
|
|
639
|
+
if (input instanceof Float32Array) {
|
|
640
|
+
return input
|
|
641
|
+
}
|
|
642
|
+
if (input instanceof Int16Array) {
|
|
643
|
+
return pcm16ToFloat32(sliceViewBuffer(input))
|
|
644
|
+
}
|
|
645
|
+
if (input instanceof ArrayBuffer) {
|
|
646
|
+
return pcm16ToFloat32(input)
|
|
647
|
+
}
|
|
648
|
+
if (ArrayBuffer.isView(input)) {
|
|
649
|
+
return pcm16ToFloat32(sliceViewBuffer(input))
|
|
650
|
+
}
|
|
651
|
+
throw new TypeError('Expected ArrayBuffer or typed audio array')
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function copyFloat32Audio(input) {
|
|
655
|
+
var audio = toFloat32Audio(input)
|
|
656
|
+
var copy = new Float32Array(audio.length)
|
|
657
|
+
copy.set(audio)
|
|
658
|
+
return copy
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function readAscii(view, offset, length) {
|
|
662
|
+
var value = ''
|
|
663
|
+
for (var i = 0; i < length; i += 1) {
|
|
664
|
+
value += String.fromCharCode(view.getUint8(offset + i))
|
|
665
|
+
}
|
|
666
|
+
return value
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function readInt24(view, offset, littleEndian) {
|
|
670
|
+
var b0 = view.getUint8(offset)
|
|
671
|
+
var b1 = view.getUint8(offset + 1)
|
|
672
|
+
var b2 = view.getUint8(offset + 2)
|
|
673
|
+
var value = littleEndian ? b0 | (b1 << 8) | (b2 << 16) : b2 | (b1 << 8) | (b0 << 16)
|
|
674
|
+
return value & 0x800000 ? value | 0xff000000 : value
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function resampleLinear(input, sourceRate, targetRate) {
|
|
678
|
+
if (sourceRate === targetRate || input.length === 0) {
|
|
679
|
+
return input
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
var outputLength = Math.max(1, Math.round((input.length * targetRate) / sourceRate))
|
|
683
|
+
var output = new Float32Array(outputLength)
|
|
684
|
+
var ratio = sourceRate / targetRate
|
|
685
|
+
|
|
686
|
+
for (var i = 0; i < outputLength; i += 1) {
|
|
687
|
+
var position = i * ratio
|
|
688
|
+
var left = Math.floor(position)
|
|
689
|
+
var right = Math.min(left + 1, input.length - 1)
|
|
690
|
+
var weight = position - left
|
|
691
|
+
output[i] = input[left] * (1 - weight) + input[right] * weight
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return output
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function decodeWav(buffer) {
|
|
698
|
+
if (buffer.byteLength < 44) {
|
|
699
|
+
return null
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
var view = new DataView(buffer)
|
|
703
|
+
if (readAscii(view, 0, 4) !== 'RIFF' || readAscii(view, 8, 4) !== 'WAVE') {
|
|
704
|
+
return null
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
var offset = 12
|
|
708
|
+
var format = null
|
|
709
|
+
var dataOffset = 0
|
|
710
|
+
var dataSize = 0
|
|
711
|
+
|
|
712
|
+
while (offset + 8 <= view.byteLength) {
|
|
713
|
+
var chunkId = readAscii(view, offset, 4)
|
|
714
|
+
var chunkSize = view.getUint32(offset + 4, true)
|
|
715
|
+
offset += 8
|
|
716
|
+
|
|
717
|
+
if (chunkId === 'fmt ') {
|
|
718
|
+
format = {
|
|
719
|
+
audioFormat: view.getUint16(offset, true),
|
|
720
|
+
channels: view.getUint16(offset + 2, true),
|
|
721
|
+
sampleRate: view.getUint32(offset + 4, true),
|
|
722
|
+
byteRate: view.getUint32(offset + 8, true),
|
|
723
|
+
blockAlign: view.getUint16(offset + 12, true),
|
|
724
|
+
bitsPerSample: view.getUint16(offset + 14, true),
|
|
725
|
+
}
|
|
726
|
+
} else if (chunkId === 'data') {
|
|
727
|
+
dataOffset = offset
|
|
728
|
+
dataSize = chunkSize
|
|
729
|
+
break
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
offset += chunkSize + (chunkSize % 2)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (!format || !dataOffset || !dataSize) {
|
|
736
|
+
return null
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
var bytesPerSample = format.bitsPerSample / 8
|
|
740
|
+
var frameCount = Math.floor(dataSize / format.blockAlign)
|
|
741
|
+
var output = new Float32Array(frameCount)
|
|
742
|
+
|
|
743
|
+
for (var frame = 0; frame < frameCount; frame += 1) {
|
|
744
|
+
var sum = 0
|
|
745
|
+
for (var channel = 0; channel < format.channels; channel += 1) {
|
|
746
|
+
var sampleOffset =
|
|
747
|
+
dataOffset + frame * format.blockAlign + channel * bytesPerSample
|
|
748
|
+
var sample = 0
|
|
749
|
+
|
|
750
|
+
if (format.audioFormat === 1) {
|
|
751
|
+
if (format.bitsPerSample === 8) {
|
|
752
|
+
sample = (view.getUint8(sampleOffset) - 128) / 128
|
|
753
|
+
} else if (format.bitsPerSample === 16) {
|
|
754
|
+
sample = view.getInt16(sampleOffset, true) / 32768
|
|
755
|
+
} else if (format.bitsPerSample === 24) {
|
|
756
|
+
sample = readInt24(view, sampleOffset, true) / 8388608
|
|
757
|
+
} else if (format.bitsPerSample === 32) {
|
|
758
|
+
sample = view.getInt32(sampleOffset, true) / 2147483648
|
|
759
|
+
} else {
|
|
760
|
+
throw new Error('Unsupported PCM WAV bit depth: ' + format.bitsPerSample)
|
|
761
|
+
}
|
|
762
|
+
} else if (format.audioFormat === 3 && format.bitsPerSample === 32) {
|
|
763
|
+
sample = view.getFloat32(sampleOffset, true)
|
|
764
|
+
} else {
|
|
765
|
+
throw new Error('Unsupported WAV format: ' + format.audioFormat)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
sum += sample
|
|
769
|
+
}
|
|
770
|
+
output[frame] = sum / format.channels
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return resampleLinear(output, format.sampleRate, SAMPLE_RATE)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function mixAudioBuffer(audioBuffer) {
|
|
777
|
+
var length = audioBuffer.length
|
|
778
|
+
var output = new Float32Array(length)
|
|
779
|
+
|
|
780
|
+
for (var channel = 0; channel < audioBuffer.numberOfChannels; channel += 1) {
|
|
781
|
+
var channelData = audioBuffer.getChannelData(channel)
|
|
782
|
+
for (var i = 0; i < length; i += 1) {
|
|
783
|
+
output[i] += channelData[i] / audioBuffer.numberOfChannels
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return resampleLinear(output, audioBuffer.sampleRate, SAMPLE_RATE)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function decodeAudioBuffer(buffer) {
|
|
791
|
+
var wav = decodeWav(buffer)
|
|
792
|
+
if (wav) {
|
|
793
|
+
return wav
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
var AudioContext = root.AudioContext || root.webkitAudioContext
|
|
797
|
+
if (!AudioContext) {
|
|
798
|
+
return pcm16ToFloat32(buffer)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
var audioContext = new AudioContext()
|
|
802
|
+
try {
|
|
803
|
+
var decoded = await audioContext.decodeAudioData(buffer.slice(0))
|
|
804
|
+
return mixAudioBuffer(decoded)
|
|
805
|
+
} finally {
|
|
806
|
+
if (typeof audioContext.close === 'function') {
|
|
807
|
+
audioContext.close()
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function loadAudioUrl(source) {
|
|
813
|
+
var buffer = await fetchArrayBuffer(source)
|
|
814
|
+
return decodeAudioBuffer(buffer)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function validateWebGpu(runtime, useGpu) {
|
|
818
|
+
if (!useGpu) {
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (
|
|
823
|
+
!runtime ||
|
|
824
|
+
typeof runtime.__wasm_webgpu_enabled !== 'function' ||
|
|
825
|
+
!runtime.__wasm_webgpu_enabled()
|
|
826
|
+
) {
|
|
827
|
+
throw new Error(
|
|
828
|
+
'This @fugood/node-whisper-wasm build was not compiled with GGML_WEBGPU=ON',
|
|
829
|
+
)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (!root.navigator || !root.navigator.gpu) {
|
|
833
|
+
throw new Error('WebGPU was requested, but navigator.gpu is not available')
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function WhisperContext(options) {
|
|
838
|
+
if (shouldUseWorker(options || {})) {
|
|
839
|
+
return createWorkerWhisperContext(options || {})
|
|
840
|
+
}
|
|
841
|
+
return createWhisperContext(options)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
WhisperContext.toggleNativeLog = toggleNativeLog
|
|
845
|
+
WhisperContext.loadModelInfo = function (path) {
|
|
846
|
+
return {
|
|
847
|
+
path: path,
|
|
848
|
+
type: 'whisper',
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function createWorkerWhisperContext(options) {
|
|
853
|
+
var proxy = await getWorkerProxy()
|
|
854
|
+
if (!proxy) {
|
|
855
|
+
return createWhisperContext(options)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
var created = await proxy.request('initWhisper', [options || {}])
|
|
859
|
+
created.meta.worker = true
|
|
860
|
+
return new WorkerWhisperContextInstance(proxy, created.id, created.meta)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function WorkerWhisperContextInstance(proxy, id, meta) {
|
|
864
|
+
this._proxy = proxy
|
|
865
|
+
this._id = id
|
|
866
|
+
this._meta = meta
|
|
867
|
+
this._released = false
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
WorkerWhisperContextInstance.prototype._assertValid = function () {
|
|
871
|
+
if (this._released) {
|
|
872
|
+
throw new Error('Invalid whisper context')
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
WorkerWhisperContextInstance.prototype.getModelInfo = function () {
|
|
877
|
+
return this._meta
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
WorkerWhisperContextInstance.prototype._transcribeWorker = function (
|
|
881
|
+
method,
|
|
882
|
+
args,
|
|
883
|
+
transfer,
|
|
884
|
+
callbacks,
|
|
885
|
+
) {
|
|
886
|
+
this._assertValid()
|
|
887
|
+
|
|
888
|
+
var cancelled = false
|
|
889
|
+
var operation = this._proxy.requestOperation(method, args, transfer, callbacks)
|
|
890
|
+
var proxy = this._proxy
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
_requestId: operation.id,
|
|
894
|
+
stop: function () {
|
|
895
|
+
cancelled = true
|
|
896
|
+
proxy.cancel(operation.id)
|
|
897
|
+
return Promise.resolve()
|
|
898
|
+
},
|
|
899
|
+
promise: operation.promise.then(function (result) {
|
|
900
|
+
if (cancelled) {
|
|
901
|
+
result.isAborted = true
|
|
902
|
+
}
|
|
903
|
+
return result
|
|
904
|
+
}),
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
WorkerWhisperContextInstance.prototype.transcribeData = function (audioData, options) {
|
|
909
|
+
var audio = copyFloat32Audio(audioData)
|
|
910
|
+
var split = splitTranscribeOptions(options)
|
|
911
|
+
var operation = this._transcribeWorker(
|
|
912
|
+
'transcribeData',
|
|
913
|
+
[this._id, audio, split.options],
|
|
914
|
+
[audio.buffer],
|
|
915
|
+
split.callbacks,
|
|
916
|
+
)
|
|
917
|
+
return operation
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
WorkerWhisperContextInstance.prototype.transcribeFile = function (filePath, options) {
|
|
921
|
+
var split = splitTranscribeOptions(options)
|
|
922
|
+
var operation = this._transcribeWorker(
|
|
923
|
+
'transcribeFile',
|
|
924
|
+
[this._id, filePath, split.options],
|
|
925
|
+
[],
|
|
926
|
+
split.callbacks,
|
|
927
|
+
)
|
|
928
|
+
return operation
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
WorkerWhisperContextInstance.prototype.transcribe =
|
|
932
|
+
WorkerWhisperContextInstance.prototype.transcribeFile
|
|
933
|
+
|
|
934
|
+
WorkerWhisperContextInstance.prototype.bench = function (nThreads) {
|
|
935
|
+
this._assertValid()
|
|
936
|
+
return this._proxy.request('benchWhisper', [this._id, nThreads || 1])
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
WorkerWhisperContextInstance.prototype.release = async function () {
|
|
940
|
+
if (!this._released) {
|
|
941
|
+
await this._proxy.request('releaseWhisper', [this._id])
|
|
942
|
+
this._released = true
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function createWhisperContext(options) {
|
|
947
|
+
options = options || {}
|
|
948
|
+
var modelSource = options.filePath || options.modelUrl
|
|
949
|
+
var runtime = await loadRuntime()
|
|
950
|
+
var useGpu = options.useGpu === true
|
|
951
|
+
|
|
952
|
+
validateWebGpu(runtime, useGpu)
|
|
953
|
+
|
|
954
|
+
var model = await ensureModel(runtime, modelSource, 'whisper', options)
|
|
955
|
+
var init = unwrapWasmResult(
|
|
956
|
+
await runtime.__wasm_init_whisper(
|
|
957
|
+
model.virtualPath,
|
|
958
|
+
useGpu,
|
|
959
|
+
options.useFlashAttn === true,
|
|
960
|
+
),
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
return new WhisperContextInstance(runtime, init.id, {
|
|
964
|
+
filePath: modelSource,
|
|
965
|
+
wasmFilePath: model.virtualPath,
|
|
966
|
+
useGpu: useGpu,
|
|
967
|
+
useFlashAttn: options.useFlashAttn === true,
|
|
968
|
+
bytes: model.bytes,
|
|
969
|
+
wasm: true,
|
|
970
|
+
})
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function WhisperContextInstance(runtime, id, meta) {
|
|
974
|
+
this._runtime = runtime
|
|
975
|
+
this._id = id
|
|
976
|
+
this._meta = meta
|
|
977
|
+
this._released = false
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
WhisperContextInstance.prototype._assertValid = function () {
|
|
981
|
+
if (this._released) {
|
|
982
|
+
throw new Error('Invalid whisper context')
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
WhisperContextInstance.prototype.getModelInfo = function () {
|
|
987
|
+
return this._meta
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
WhisperContextInstance.prototype._transcribeFloat32 = function (audio, options, isCancelled) {
|
|
991
|
+
this._assertValid()
|
|
992
|
+
|
|
993
|
+
if (isCancelled && isCancelled()) {
|
|
994
|
+
return Promise.resolve(abortedResult())
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
var runtime = this._runtime
|
|
998
|
+
var id = this._id
|
|
999
|
+
return defer(async function () {
|
|
1000
|
+
if (isCancelled && isCancelled()) {
|
|
1001
|
+
return abortedResult()
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
var result = unwrapWasmResult(
|
|
1005
|
+
await runtime.__wasm_transcribe(
|
|
1006
|
+
id,
|
|
1007
|
+
audio,
|
|
1008
|
+
normalizeTranscribeOptions(options),
|
|
1009
|
+
),
|
|
1010
|
+
)
|
|
1011
|
+
if (isCancelled && isCancelled()) {
|
|
1012
|
+
result.isAborted = true
|
|
1013
|
+
}
|
|
1014
|
+
return result
|
|
1015
|
+
})
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
WhisperContextInstance.prototype.transcribeData = function (audioData, options) {
|
|
1019
|
+
var cancelled = false
|
|
1020
|
+
var audio = toFloat32Audio(audioData)
|
|
1021
|
+
return {
|
|
1022
|
+
stop: function () {
|
|
1023
|
+
cancelled = true
|
|
1024
|
+
return Promise.resolve()
|
|
1025
|
+
},
|
|
1026
|
+
promise: this._transcribeFloat32(audio, options, function () {
|
|
1027
|
+
return cancelled
|
|
1028
|
+
}),
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
WhisperContextInstance.prototype.transcribeFile = function (filePath, options) {
|
|
1033
|
+
var cancelled = false
|
|
1034
|
+
var self = this
|
|
1035
|
+
return {
|
|
1036
|
+
stop: function () {
|
|
1037
|
+
cancelled = true
|
|
1038
|
+
return Promise.resolve()
|
|
1039
|
+
},
|
|
1040
|
+
promise: (async function () {
|
|
1041
|
+
if (cancelled) {
|
|
1042
|
+
return abortedResult()
|
|
1043
|
+
}
|
|
1044
|
+
var audio = await loadAudioUrl(filePath)
|
|
1045
|
+
return self._transcribeFloat32(audio, options, function () {
|
|
1046
|
+
return cancelled
|
|
1047
|
+
})
|
|
1048
|
+
})(),
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
WhisperContextInstance.prototype.transcribe =
|
|
1053
|
+
WhisperContextInstance.prototype.transcribeFile
|
|
1054
|
+
|
|
1055
|
+
WhisperContextInstance.prototype.bench = async function (nThreads) {
|
|
1056
|
+
this._assertValid()
|
|
1057
|
+
return unwrapWasmResult(await this._runtime.__wasm_bench(this._id, nThreads || 1))
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
WhisperContextInstance.prototype.release = function () {
|
|
1061
|
+
if (!this._released) {
|
|
1062
|
+
this._runtime.__wasm_free_whisper(this._id)
|
|
1063
|
+
this._released = true
|
|
1064
|
+
}
|
|
1065
|
+
return Promise.resolve()
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function WhisperVadContext(options) {
|
|
1069
|
+
if (shouldUseWorker(options || {})) {
|
|
1070
|
+
return createWorkerWhisperVadContext(options || {})
|
|
1071
|
+
}
|
|
1072
|
+
return createWhisperVadContext(options)
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
WhisperVadContext.toggleNativeLog = toggleNativeLog
|
|
1076
|
+
WhisperVadContext.loadModelInfo = function (path) {
|
|
1077
|
+
return {
|
|
1078
|
+
path: path,
|
|
1079
|
+
type: 'whisper_vad',
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async function createWorkerWhisperVadContext(options) {
|
|
1084
|
+
var proxy = await getWorkerProxy()
|
|
1085
|
+
if (!proxy) {
|
|
1086
|
+
return createWhisperVadContext(options)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
var created = await proxy.request('initWhisperVad', [options || {}])
|
|
1090
|
+
created.meta.worker = true
|
|
1091
|
+
return new WorkerWhisperVadContextInstance(proxy, created.id, created.meta)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function WorkerWhisperVadContextInstance(proxy, id, meta) {
|
|
1095
|
+
this._proxy = proxy
|
|
1096
|
+
this._id = id
|
|
1097
|
+
this._meta = meta
|
|
1098
|
+
this._released = false
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
WorkerWhisperVadContextInstance.prototype._assertValid = function () {
|
|
1102
|
+
if (this._released) {
|
|
1103
|
+
throw new Error('Invalid VAD context')
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
WorkerWhisperVadContextInstance.prototype.getModelInfo = function () {
|
|
1108
|
+
return this._meta
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
WorkerWhisperVadContextInstance.prototype.detectSpeechData = function (
|
|
1112
|
+
audioData,
|
|
1113
|
+
options,
|
|
1114
|
+
) {
|
|
1115
|
+
this._assertValid()
|
|
1116
|
+
var audio = copyFloat32Audio(audioData)
|
|
1117
|
+
return this._proxy.request(
|
|
1118
|
+
'detectSpeechData',
|
|
1119
|
+
[this._id, audio, options || {}],
|
|
1120
|
+
[audio.buffer],
|
|
1121
|
+
)
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
WorkerWhisperVadContextInstance.prototype.detectSpeechFile = function (
|
|
1125
|
+
filePath,
|
|
1126
|
+
options,
|
|
1127
|
+
) {
|
|
1128
|
+
this._assertValid()
|
|
1129
|
+
return this._proxy.request('detectSpeechFile', [
|
|
1130
|
+
this._id,
|
|
1131
|
+
filePath,
|
|
1132
|
+
options || {},
|
|
1133
|
+
])
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
WorkerWhisperVadContextInstance.prototype.detectSpeech =
|
|
1137
|
+
WorkerWhisperVadContextInstance.prototype.detectSpeechFile
|
|
1138
|
+
|
|
1139
|
+
WorkerWhisperVadContextInstance.prototype.release = async function () {
|
|
1140
|
+
if (!this._released) {
|
|
1141
|
+
await this._proxy.request('releaseVad', [this._id])
|
|
1142
|
+
this._released = true
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async function createWhisperVadContext(options) {
|
|
1147
|
+
options = options || {}
|
|
1148
|
+
var modelSource = options.filePath || options.modelUrl
|
|
1149
|
+
var runtime = await loadRuntime()
|
|
1150
|
+
var useGpu = false
|
|
1151
|
+
var nThreads = options.nThreads || 1
|
|
1152
|
+
|
|
1153
|
+
if (options.useGpu === true) {
|
|
1154
|
+
emitLog(
|
|
1155
|
+
'WARN',
|
|
1156
|
+
'WASM VAD currently falls back to CPU because ggml-webgpu does not support the VAD graph safely yet.',
|
|
1157
|
+
)
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
var model = await ensureModel(runtime, modelSource, 'vad', options)
|
|
1161
|
+
var init = unwrapWasmResult(
|
|
1162
|
+
await runtime.__wasm_init_vad(model.virtualPath, useGpu, nThreads),
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
return new WhisperVadContextInstance(runtime, init.id, {
|
|
1166
|
+
filePath: modelSource,
|
|
1167
|
+
wasmFilePath: model.virtualPath,
|
|
1168
|
+
useGpu: useGpu,
|
|
1169
|
+
nThreads: init.nThreads,
|
|
1170
|
+
bytes: model.bytes,
|
|
1171
|
+
wasm: true,
|
|
1172
|
+
})
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function WhisperVadContextInstance(runtime, id, meta) {
|
|
1176
|
+
this._runtime = runtime
|
|
1177
|
+
this._id = id
|
|
1178
|
+
this._meta = meta
|
|
1179
|
+
this._released = false
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
WhisperVadContextInstance.prototype._assertValid = function () {
|
|
1183
|
+
if (this._released) {
|
|
1184
|
+
throw new Error('Invalid VAD context')
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
WhisperVadContextInstance.prototype.getModelInfo = function () {
|
|
1189
|
+
return this._meta
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
WhisperVadContextInstance.prototype.detectSpeechData = async function (
|
|
1193
|
+
audioData,
|
|
1194
|
+
options,
|
|
1195
|
+
) {
|
|
1196
|
+
this._assertValid()
|
|
1197
|
+
var audio = toFloat32Audio(audioData)
|
|
1198
|
+
var result = unwrapWasmResult(
|
|
1199
|
+
await this._runtime.__wasm_detect_speech(this._id, audio, options || {}),
|
|
1200
|
+
)
|
|
1201
|
+
return result.segments || []
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
WhisperVadContextInstance.prototype.detectSpeechFile = async function (filePath, options) {
|
|
1205
|
+
this._assertValid()
|
|
1206
|
+
var audio = await loadAudioUrl(filePath)
|
|
1207
|
+
return this.detectSpeechData(audio, options)
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
WhisperVadContextInstance.prototype.detectSpeech =
|
|
1211
|
+
WhisperVadContextInstance.prototype.detectSpeechFile
|
|
1212
|
+
|
|
1213
|
+
WhisperVadContextInstance.prototype.release = function () {
|
|
1214
|
+
if (!this._released) {
|
|
1215
|
+
this._runtime.__wasm_free_vad(this._id)
|
|
1216
|
+
this._released = true
|
|
1217
|
+
}
|
|
1218
|
+
return Promise.resolve()
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function loadWhisperModule() {
|
|
1222
|
+
return Promise.resolve(api)
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function initWhisper(options) {
|
|
1226
|
+
return Promise.resolve(new WhisperContext(options))
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function initWhisperVad(options) {
|
|
1230
|
+
return Promise.resolve(new WhisperVadContext(options))
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
var api = {
|
|
1234
|
+
WhisperContext: WhisperContext,
|
|
1235
|
+
WhisperVadContext: WhisperVadContext,
|
|
1236
|
+
configureWasm: configureWasm,
|
|
1237
|
+
loadWasmModule: loadRuntime,
|
|
1238
|
+
loadWhisperModule: loadWhisperModule,
|
|
1239
|
+
initWhisper: initWhisper,
|
|
1240
|
+
initWhisperVad: initWhisperVad,
|
|
1241
|
+
toggleNativeLog: toggleNativeLog,
|
|
1242
|
+
addNativeLogListener: addNativeLogListener,
|
|
1243
|
+
DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES: 1500 * MIB,
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
api.default = api
|
|
1247
|
+
|
|
1248
|
+
return api
|
|
1249
|
+
},
|
|
1250
|
+
)
|