@fugood/node-whisper-wasm 1.0.19 → 1.0.21

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 CHANGED
@@ -1,33 +1,35 @@
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
- })(
1
+ const root =
12
2
  typeof globalThis !== 'undefined'
13
3
  ? globalThis
14
4
  : typeof self !== 'undefined'
15
5
  ? self
16
6
  : typeof window !== 'undefined'
17
7
  ? window
18
- : this,
19
- function (loadModuleFactory, root) {
8
+ : undefined
9
+
10
+ export const WASM_CONFIG_PATHS = {
11
+ index: new URL('./index.js', import.meta.url).href,
12
+ js: new URL('./wasm/whisper-node.js', import.meta.url).href,
13
+ wasm: new URL('./wasm/whisper-node.wasm', import.meta.url).href,
14
+ threadsJs: new URL('./wasm/whisper-node.threads.js', import.meta.url).href,
15
+ threadsWasm: new URL('./wasm/whisper-node.threads.wasm', import.meta.url).href,
16
+ worker: new URL('./worker.js', import.meta.url).href,
17
+ }
18
+
19
+ const createWhisperNodeApi = function (root) {
20
20
  'use strict'
21
21
 
22
22
  var SAMPLE_RATE = 16000
23
23
  var MIB = 1024 * 1024
24
24
  var FIREFOX_MODEL_LIMIT_BYTES = 256 * MIB
25
25
  var MODEL_MEMORY_RATIO = 0.75
26
+ var MAX_WASM_THREADS = 8
27
+ var DEFAULT_MODEL_CACHE_NAME = 'whisper.node.wasm.models'
26
28
 
27
29
  var runtimePromise = null
28
30
  var runtimeOptions = {}
29
31
  var workerProxyPromise = null
30
- var capturedScriptUrl = getCurrentScriptUrl()
32
+ var capturedScriptUrl = WASM_CONFIG_PATHS.index
31
33
  var modelCache = Object.create(null)
32
34
  var logEnabled = false
33
35
  var logListeners = []
@@ -40,19 +42,8 @@
40
42
  runtimeOptions = Object.assign({}, runtimeOptions, options || {})
41
43
  }
42
44
 
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
45
  function isBrowserLike() {
55
- return typeof root.window !== 'undefined' || typeof root.importScripts === 'function'
46
+ return typeof root.window !== 'undefined' || typeof root.WorkerGlobalScope !== 'undefined'
56
47
  }
57
48
 
58
49
  function isMainBrowserThread() {
@@ -79,20 +70,47 @@
79
70
  return capturedScriptUrl
80
71
  }
81
72
 
82
- function getRuntimeScriptUrl(indexScriptUrl) {
83
- var configured = runtimeOptions.runtimeScriptUrl
73
+ function isWasmThreadsSupported() {
74
+ if (!isBrowserLike()) {
75
+ return true
76
+ }
77
+ return (
78
+ typeof root.SharedArrayBuffer !== 'undefined' &&
79
+ typeof root.Atomics !== 'undefined' &&
80
+ typeof root.crossOriginIsolated !== 'undefined' &&
81
+ root.crossOriginIsolated === true
82
+ )
83
+ }
84
+
85
+ function shouldUseThreadedRuntime() {
86
+ if (typeof runtimeOptions.__resolvedThreads === 'boolean') {
87
+ return runtimeOptions.__resolvedThreads
88
+ }
89
+ return runtimeOptions.threads !== false && isWasmThreadsSupported()
90
+ }
91
+
92
+ function getRuntimeScriptUrl(indexScriptUrl, useThreads) {
93
+ var configured = runtimeOptions.jsPath || runtimeOptions.runtimeScriptUrl
84
94
  if (configured) {
85
95
  return resolveUrl(configured, indexScriptUrl)
86
96
  }
87
- return indexScriptUrl ? resolveUrl('whisper-node.js', indexScriptUrl) : null
97
+ return useThreads ? WASM_CONFIG_PATHS.threadsJs : WASM_CONFIG_PATHS.js
98
+ }
99
+
100
+ function getWasmBinaryUrl(runtimeScriptUrl, useThreads) {
101
+ var configured = runtimeOptions.wasmPath
102
+ if (configured) {
103
+ return resolveUrl(configured, runtimeScriptUrl)
104
+ }
105
+ return useThreads ? WASM_CONFIG_PATHS.threadsWasm : WASM_CONFIG_PATHS.wasm
88
106
  }
89
107
 
90
108
  function getWorkerScriptUrl(indexScriptUrl) {
91
- var configured = runtimeOptions.workerUrl
109
+ var configured = runtimeOptions.workerPath || runtimeOptions.workerUrl
92
110
  if (configured) {
93
111
  return resolveUrl(configured, indexScriptUrl)
94
112
  }
95
- return indexScriptUrl ? resolveUrl('worker.js', indexScriptUrl) : null
113
+ return WASM_CONFIG_PATHS.worker
96
114
  }
97
115
 
98
116
  function shouldUseWorker(options) {
@@ -108,11 +126,18 @@
108
126
  var blocked = {
109
127
  worker: true,
110
128
  workerUrl: true,
129
+ workerPath: true,
111
130
  indexScriptUrl: true,
112
131
  scriptUrl: true,
113
132
  runtimeScriptUrl: true,
133
+ jsPath: true,
134
+ wasmPath: true,
135
+ threads: true,
136
+ __resolvedThreads: true,
114
137
  locateFileBaseUrl: true,
115
138
  locateFile: true,
139
+ moduleFactory: true,
140
+ moduleOptions: true,
116
141
  print: true,
117
142
  printErr: true,
118
143
  mainScriptUrlOrBlob: true,
@@ -127,14 +152,6 @@
127
152
  return options
128
153
  }
129
154
 
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
155
  function emitLog(level, text) {
139
156
  if (!logEnabled) {
140
157
  return
@@ -149,37 +166,79 @@
149
166
 
150
167
  function loadRuntime() {
151
168
  if (!runtimePromise) {
152
- assertThreadSupport()
169
+ runtimePromise = (async function () {
170
+ var indexScriptUrl = getIndexScriptUrl()
171
+ var useThreads = shouldUseThreadedRuntime()
172
+ var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl, useThreads)
173
+ var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl, useThreads)
174
+ var moduleFactory = runtimeOptions.moduleFactory
175
+
176
+ if (!moduleFactory) {
177
+ var runtimeModule = await import(runtimeScriptUrl)
178
+ moduleFactory =
179
+ runtimeModule.default ||
180
+ runtimeModule.createWhisperNodeModule ||
181
+ runtimeModule
182
+ }
183
+ if (typeof moduleFactory !== 'function') {
184
+ throw new Error(
185
+ 'Failed to load whisper.node WASM runtime. Make sure the selected wasm/whisper-node*.js artifact is built and included in the package.',
186
+ )
187
+ }
153
188
 
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.',
189
+ var options = Object.assign(
190
+ {},
191
+ runtimeOptions.moduleOptions || {},
192
+ runtimeOptions,
161
193
  )
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)
194
+ var userPrint = options.print
195
+ var userPrintErr = options.printErr
196
+ var userLocateFile = options.locateFile
197
+ var locateFileBaseUrl =
198
+ options.locateFileBaseUrl || resolveUrl('.', runtimeScriptUrl)
199
+
200
+ delete options.moduleFactory
201
+ delete options.moduleOptions
202
+ delete options.worker
203
+ delete options.workerUrl
204
+ delete options.workerPath
205
+ delete options.indexScriptUrl
206
+ delete options.scriptUrl
207
+ delete options.runtimeScriptUrl
208
+ delete options.jsPath
209
+ delete options.wasmPath
210
+ delete options.threads
211
+ delete options.__resolvedThreads
212
+ delete options.locateFileBaseUrl
213
+
214
+ options.noInitialRun = true
215
+ options.mainScriptUrlOrBlob = options.mainScriptUrlOrBlob || runtimeScriptUrl
216
+ options.locateFile = function (path, prefix) {
217
+ if (path === 'whisper-node.wasm' || path.slice(-5) === '.wasm') {
218
+ return wasmUrl
219
+ }
220
+ if (typeof userLocateFile === 'function') {
221
+ return userLocateFile(path, prefix)
222
+ }
223
+ return resolveUrl(path, locateFileBaseUrl || prefix)
173
224
  }
174
- }
175
- options.printErr = function (text) {
176
- emitLog('ERROR', String(text))
177
- if (typeof userPrintErr === 'function') {
178
- userPrintErr(text)
225
+ options.print = function (text) {
226
+ emitLog('INFO', String(text))
227
+ if (typeof userPrint === 'function') {
228
+ userPrint(text)
229
+ }
230
+ }
231
+ options.printErr = function (text) {
232
+ emitLog('ERROR', String(text))
233
+ if (typeof userPrintErr === 'function') {
234
+ userPrintErr(text)
235
+ }
179
236
  }
180
- }
181
237
 
182
- runtimePromise = Promise.resolve(moduleFactory(options))
238
+ var runtime = await moduleFactory(options)
239
+ runtime.__whisperNodeWasmThreads = useThreads
240
+ return runtime
241
+ })()
183
242
  }
184
243
 
185
244
  return runtimePromise
@@ -212,7 +271,9 @@
212
271
 
213
272
  function createWorkerProxy() {
214
273
  var indexScriptUrl = getIndexScriptUrl()
215
- var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl)
274
+ var useThreads = shouldUseThreadedRuntime()
275
+ var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl, useThreads)
276
+ var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl, useThreads)
216
277
  var workerScriptUrl = getWorkerScriptUrl(indexScriptUrl)
217
278
 
218
279
  if (!indexScriptUrl || !runtimeScriptUrl || !workerScriptUrl) {
@@ -220,6 +281,7 @@
220
281
  }
221
282
 
222
283
  var worker = new root.Worker(workerScriptUrl, {
284
+ type: 'module',
223
285
  name: 'whisper.node.wasm',
224
286
  })
225
287
  var proxy = {
@@ -323,6 +385,8 @@
323
385
  {
324
386
  indexScriptUrl: indexScriptUrl,
325
387
  runtimeScriptUrl: runtimeScriptUrl,
388
+ wasmPath: wasmUrl,
389
+ threads: useThreads,
326
390
  runtimeOptions: getWorkerRuntimeOptions(),
327
391
  locateFileBaseUrl:
328
392
  runtimeOptions.locateFileBaseUrl || resolveUrl('.', runtimeScriptUrl),
@@ -366,6 +430,39 @@
366
430
  throw new Error('fetch is required to load models or audio by URL')
367
431
  }
368
432
 
433
+ function getCacheStorage() {
434
+ return root.caches || null
435
+ }
436
+
437
+ function isModelCacheEnabled(options) {
438
+ return !options || options.cacheModel !== false
439
+ }
440
+
441
+ function getModelCacheName(options) {
442
+ return (
443
+ (options && options.modelCacheName) ||
444
+ runtimeOptions.modelCacheName ||
445
+ DEFAULT_MODEL_CACHE_NAME
446
+ )
447
+ }
448
+
449
+ function getModelCacheKey(source, options) {
450
+ var key = (options && options.modelCacheKey) || source
451
+ try {
452
+ return new URL(key, root.location && root.location.href).href
453
+ } catch (_) {
454
+ return key
455
+ }
456
+ }
457
+
458
+ function canUseModelCache(options) {
459
+ return (
460
+ isModelCacheEnabled(options) &&
461
+ !!getCacheStorage() &&
462
+ typeof root.Response === 'function'
463
+ )
464
+ }
465
+
369
466
  function formatBytes(bytes) {
370
467
  if (bytes >= 1024 * MIB) {
371
468
  return (bytes / (1024 * MIB)).toFixed(2) + ' GiB'
@@ -497,6 +594,106 @@
497
594
  return buffer
498
595
  }
499
596
 
597
+ async function readCachedModel(source, limit, options) {
598
+ if (!canUseModelCache(options)) {
599
+ return null
600
+ }
601
+
602
+ var cacheName = getModelCacheName(options)
603
+ var cacheKey = getModelCacheKey(source, options)
604
+
605
+ try {
606
+ var cache = await getCacheStorage().open(cacheName)
607
+ var response = await cache.match(cacheKey)
608
+ if (!response) {
609
+ return null
610
+ }
611
+
612
+ var buffer = await response.arrayBuffer()
613
+ if (limit) {
614
+ assertModelSize(buffer.byteLength, limit, source)
615
+ }
616
+ emitLog('INFO', 'Loaded cached WASM model: ' + source)
617
+ return {
618
+ buffer: buffer,
619
+ cacheHit: true,
620
+ cacheStored: false,
621
+ cacheName: cacheName,
622
+ cacheKey: cacheKey,
623
+ }
624
+ } catch (error) {
625
+ emitLog(
626
+ 'WARN',
627
+ 'Failed to read cached WASM model ' +
628
+ source +
629
+ ': ' +
630
+ (error && error.message ? error.message : String(error)),
631
+ )
632
+ return null
633
+ }
634
+ }
635
+
636
+ async function writeCachedModel(source, buffer, options) {
637
+ if (!canUseModelCache(options)) {
638
+ return {
639
+ cacheStored: false,
640
+ cacheName: null,
641
+ cacheKey: null,
642
+ }
643
+ }
644
+
645
+ var cacheName = getModelCacheName(options)
646
+ var cacheKey = getModelCacheKey(source, options)
647
+
648
+ try {
649
+ var cache = await getCacheStorage().open(cacheName)
650
+ await cache.put(
651
+ cacheKey,
652
+ new root.Response(buffer.slice(0), {
653
+ headers: {
654
+ 'content-type': 'application/octet-stream',
655
+ },
656
+ }),
657
+ )
658
+ emitLog('INFO', 'Cached WASM model download: ' + source)
659
+ return {
660
+ cacheStored: true,
661
+ cacheName: cacheName,
662
+ cacheKey: cacheKey,
663
+ }
664
+ } catch (error) {
665
+ emitLog(
666
+ 'WARN',
667
+ 'Failed to cache WASM model ' +
668
+ source +
669
+ ': ' +
670
+ (error && error.message ? error.message : String(error)),
671
+ )
672
+ return {
673
+ cacheStored: false,
674
+ cacheName: cacheName,
675
+ cacheKey: cacheKey,
676
+ }
677
+ }
678
+ }
679
+
680
+ async function fetchModelArrayBuffer(source, limit, options) {
681
+ var cached = await readCachedModel(source, limit, options)
682
+ if (cached) {
683
+ return cached
684
+ }
685
+
686
+ var buffer = await fetchArrayBuffer(source, limit)
687
+ var cache = await writeCachedModel(source, buffer, options)
688
+ return {
689
+ buffer: buffer,
690
+ cacheHit: false,
691
+ cacheStored: cache.cacheStored,
692
+ cacheName: cache.cacheName,
693
+ cacheKey: cache.cacheKey,
694
+ }
695
+ }
696
+
500
697
  async function ensureModel(runtime, source, kind, options) {
501
698
  if (!source) {
502
699
  throw new Error('Model path is required')
@@ -510,8 +707,8 @@
510
707
  if (!modelCache[cacheKey]) {
511
708
  modelCache[cacheKey] = (async function () {
512
709
  var limit = getModelSizeLimit(runtime, options)
513
- var buffer = await fetchArrayBuffer(source, limit)
514
- var bytes = new Uint8Array(buffer)
710
+ var loaded = await fetchModelArrayBuffer(source, limit, options)
711
+ var bytes = new Uint8Array(loaded.buffer)
515
712
  var virtualPath =
516
713
  '/models/' + kind + '-' + hashString(source) + '-' + basenameFromUrl(source)
517
714
 
@@ -521,6 +718,10 @@
521
718
  return {
522
719
  virtualPath: virtualPath,
523
720
  bytes: bytes.byteLength,
721
+ cacheHit: loaded.cacheHit,
722
+ cacheStored: loaded.cacheStored,
723
+ cacheName: loaded.cacheName,
724
+ cacheKey: loaded.cacheKey,
524
725
  }
525
726
  })()
526
727
  }
@@ -562,8 +763,31 @@
562
763
  return value
563
764
  }
564
765
 
565
- function normalizeTranscribeOptions(options) {
766
+ function normalizeMaxThreads(value, runtime) {
767
+ var maxThreads =
768
+ runtime && runtime.__whisperNodeWasmThreads !== true
769
+ ? 1
770
+ : MAX_WASM_THREADS
771
+ var nThreads = Number(value)
772
+ if (!Number.isFinite(nThreads) || nThreads <= 0) {
773
+ return null
774
+ }
775
+ return Math.max(1, Math.min(maxThreads, Math.floor(nThreads)))
776
+ }
777
+
778
+ function normalizeThreadOptions(options, runtime) {
566
779
  var normalized = Object.assign({}, options || {})
780
+ var maxThreads = normalizeMaxThreads(normalized.maxThreads, runtime)
781
+ if (maxThreads) {
782
+ normalized.maxThreads = maxThreads
783
+ } else {
784
+ delete normalized.maxThreads
785
+ }
786
+ return normalized
787
+ }
788
+
789
+ function normalizeTranscribeOptions(options, runtime) {
790
+ var normalized = normalizeThreadOptions(options, runtime)
567
791
  if (typeof normalized.onProgress !== 'function') {
568
792
  delete normalized.onProgress
569
793
  }
@@ -574,7 +798,7 @@
574
798
  }
575
799
 
576
800
  function splitTranscribeOptions(options) {
577
- var normalized = Object.assign({}, options || {})
801
+ var normalized = normalizeThreadOptions(options)
578
802
  var callbacks = {}
579
803
 
580
804
  if (typeof normalized.onProgress === 'function') {
@@ -966,6 +1190,10 @@
966
1190
  useGpu: useGpu,
967
1191
  useFlashAttn: options.useFlashAttn === true,
968
1192
  bytes: model.bytes,
1193
+ cacheHit: model.cacheHit,
1194
+ cacheStored: model.cacheStored,
1195
+ cacheName: model.cacheName,
1196
+ cacheKey: model.cacheKey,
969
1197
  wasm: true,
970
1198
  })
971
1199
  }
@@ -1005,7 +1233,7 @@
1005
1233
  await runtime.__wasm_transcribe(
1006
1234
  id,
1007
1235
  audio,
1008
- normalizeTranscribeOptions(options),
1236
+ normalizeTranscribeOptions(options, runtime),
1009
1237
  ),
1010
1238
  )
1011
1239
  if (isCancelled && isCancelled()) {
@@ -1054,7 +1282,8 @@
1054
1282
 
1055
1283
  WhisperContextInstance.prototype.bench = async function (nThreads) {
1056
1284
  this._assertValid()
1057
- return unwrapWasmResult(await this._runtime.__wasm_bench(this._id, nThreads || 1))
1285
+ var threads = normalizeMaxThreads(nThreads || 1, this._runtime) || 1
1286
+ return unwrapWasmResult(await this._runtime.__wasm_bench(this._id, threads))
1058
1287
  }
1059
1288
 
1060
1289
  WhisperContextInstance.prototype.release = function () {
@@ -1148,7 +1377,7 @@
1148
1377
  var modelSource = options.filePath || options.modelUrl
1149
1378
  var runtime = await loadRuntime()
1150
1379
  var useGpu = false
1151
- var nThreads = options.nThreads || 1
1380
+ var nThreads = normalizeMaxThreads(options.nThreads || 1, runtime) || 1
1152
1381
 
1153
1382
  if (options.useGpu === true) {
1154
1383
  emitLog(
@@ -1168,6 +1397,10 @@
1168
1397
  useGpu: useGpu,
1169
1398
  nThreads: init.nThreads,
1170
1399
  bytes: model.bytes,
1400
+ cacheHit: model.cacheHit,
1401
+ cacheStored: model.cacheStored,
1402
+ cacheName: model.cacheName,
1403
+ cacheKey: model.cacheKey,
1171
1404
  wasm: true,
1172
1405
  })
1173
1406
  }
@@ -1240,11 +1473,35 @@
1240
1473
  initWhisperVad: initWhisperVad,
1241
1474
  toggleNativeLog: toggleNativeLog,
1242
1475
  addNativeLogListener: addNativeLogListener,
1476
+ isWasmThreadsSupported: isWasmThreadsSupported,
1243
1477
  DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES: 1500 * MIB,
1478
+ MAX_WASM_THREADS: MAX_WASM_THREADS,
1479
+ WASM_CONFIG_PATHS: WASM_CONFIG_PATHS,
1244
1480
  }
1245
1481
 
1246
1482
  api.default = api
1247
1483
 
1248
1484
  return api
1249
- },
1250
- )
1485
+ }
1486
+
1487
+ const api = createWhisperNodeApi(root)
1488
+
1489
+ export const WhisperContext = api.WhisperContext
1490
+ export const WhisperVadContext = api.WhisperVadContext
1491
+ export const DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES =
1492
+ api.DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES
1493
+ export const MAX_WASM_THREADS = api.MAX_WASM_THREADS
1494
+ export const configureWasm = api.configureWasm
1495
+ export const loadWasmModule = api.loadWasmModule
1496
+ export const loadWhisperModule = api.loadWhisperModule
1497
+ export const initWhisper = api.initWhisper
1498
+ export const initWhisperVad = api.initWhisperVad
1499
+ export const toggleNativeLog = api.toggleNativeLog
1500
+ export const addNativeLogListener = api.addNativeLogListener
1501
+ export const isWasmThreadsSupported = api.isWasmThreadsSupported
1502
+
1503
+ if (root) {
1504
+ root.WhisperNodeWasm = api
1505
+ }
1506
+
1507
+ export default api
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@fugood/node-whisper-wasm",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Browser WASM module for whisper.node",
5
+ "type": "module",
5
6
  "main": "index.js",
6
7
  "browser": "index.js",
7
8
  "types": "index.d.ts",
@@ -39,8 +40,10 @@
39
40
  "index.js",
40
41
  "index.d.ts",
41
42
  "README.md",
42
- "whisper-node.js",
43
43
  "worker.js",
44
- "whisper-node.wasm"
44
+ "wasm/whisper-node.js",
45
+ "wasm/whisper-node.wasm",
46
+ "wasm/whisper-node.threads.js",
47
+ "wasm/whisper-node.threads.wasm"
45
48
  ]
46
- }
49
+ }