@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/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
+ )