@fugood/node-whisper-wasm 1.0.20 → 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/README.md CHANGED
@@ -24,21 +24,25 @@ console.log(await promise)
24
24
  await whisper.release()
25
25
  ```
26
26
 
27
- The WASM build uses pthreads, so browsers must serve the page with COOP/COEP
28
- headers and expose `SharedArrayBuffer`. Whisper transcription defaults to up to
29
- 8 threads based on browser hardware concurrency; pass `maxThreads` to override
30
- it. Browser WASM clamps `maxThreads` to the compiled pool limit of 8. Browser
31
- pages run model loading, transcription, benchmarks, and VAD in a dedicated
32
- module worker by default so the UI thread can keep rendering. Use the main
33
- `whisper.node` package entrypoint in browser code:
27
+ The package ships both single-thread and pthread WASM artifacts. On
28
+ cross-origin isolated pages with `SharedArrayBuffer`, the loader uses the
29
+ pthread artifact; otherwise it falls back to the single-thread artifact
30
+ automatically. Whisper transcription defaults to up to 8 threads based on
31
+ browser hardware concurrency when pthreads are available; pass `maxThreads` to
32
+ override it. Browser WASM clamps `maxThreads` to the compiled pool limit of 8,
33
+ or 1 in the single-thread fallback. Browser pages run model loading,
34
+ transcription, benchmarks, and VAD in a dedicated module worker by default so
35
+ the UI thread can keep rendering. Use the main `whisper.node` package entrypoint
36
+ in browser code:
34
37
 
35
38
  ```js
36
39
  import { configureWasm, initWhisper } from '@fugood/whisper.node'
37
40
  ```
38
41
 
39
42
  Use `configureWasm({ worker: false })` only when you explicitly need the
40
- in-thread runtime, or pass `workerPath`, `jsPath`, and `wasmPath` when serving
41
- the package files from custom URLs. The older `workerUrl` and
43
+ in-thread runtime, `configureWasm({ threads: false })` to force the
44
+ single-thread artifact, or pass `workerPath`, `jsPath`, and `wasmPath` when
45
+ serving the package files from custom URLs. The older `workerUrl` and
42
46
  `runtimeScriptUrl` option names still work. Model downloads are cached in
43
47
  browser Cache Storage by default. Pass `cacheModel: false` to disable persistent
44
48
  caching, `modelCacheName` to isolate the cache namespace, or `modelCacheKey`
@@ -47,9 +51,11 @@ model. Set `useGpu: true` only with a package built using `GGML_WEBGPU=ON` and a
47
51
  browser that exposes `navigator.gpu`. VAD currently falls back to CPU in the
48
52
  browser package because the Silero VAD graph hits unsupported WebGPU ops.
49
53
 
50
- The default build emits `wasm/whisper-node.js` and `wasm/whisper-node.wasm`.
51
- Use `bash scripts/build-wasm.sh --single-file` only when you want the WASM
52
- binary embedded into `wasm/whisper-node.js`. Modern Emscripten embeds the
53
- pthread worker bootstrap in the main JS file, so a separate
54
- `whisper-node.worker.js` is not expected. The package module worker that keeps
55
- UI work off the main thread is `worker.js`.
54
+ The default build emits `wasm/whisper-node.js`, `wasm/whisper-node.wasm`,
55
+ `wasm/whisper-node.threads.js`, and `wasm/whisper-node.threads.wasm`. Use
56
+ `bash scripts/build-wasm.sh --single-file` only when you want the WASM binary
57
+ embedded into each generated JS file. Modern Emscripten embeds the pthread
58
+ worker bootstrap in the main JS file, so a separate `whisper-node.worker.js` is
59
+ not expected. The package module worker that keeps UI work off the main thread
60
+ is `worker.js`. Use `npm run build-wasm-docker` to build through Docker; it
61
+ selects a native arm64 Emscripten image on Apple Silicon hosts.
package/index.d.ts CHANGED
@@ -148,6 +148,7 @@ export interface WasmRuntimeOptions {
148
148
  runtimeScriptUrl?: string
149
149
  jsPath?: string
150
150
  wasmPath?: string
151
+ threads?: boolean
151
152
  locateFileBaseUrl?: string
152
153
  locateFile?: (path: string, prefix: string) => string
153
154
  mainScriptUrlOrBlob?: string | Blob
@@ -166,10 +167,13 @@ export declare const WASM_CONFIG_PATHS: {
166
167
  index: string
167
168
  js: string
168
169
  wasm: string
170
+ threadsJs: string
171
+ threadsWasm: string
169
172
  worker: string
170
173
  }
171
174
 
172
175
  export declare function configureWasm(options: WasmRuntimeOptions): void
176
+ export declare function isWasmThreadsSupported(): boolean
173
177
  export declare function loadWasmModule(): Promise<unknown>
174
178
  export declare function loadWhisperModule(): Promise<Module>
175
179
  export declare function initWhisper(
@@ -196,6 +200,7 @@ declare const _default: {
196
200
  initWhisperVad: typeof initWhisperVad
197
201
  toggleNativeLog: typeof toggleNativeLog
198
202
  addNativeLogListener: typeof addNativeLogListener
203
+ isWasmThreadsSupported: typeof isWasmThreadsSupported
199
204
  DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES: typeof DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES
200
205
  MAX_WASM_THREADS: typeof MAX_WASM_THREADS
201
206
  WASM_CONFIG_PATHS: typeof WASM_CONFIG_PATHS
package/index.js CHANGED
@@ -11,6 +11,8 @@ export const WASM_CONFIG_PATHS = {
11
11
  index: new URL('./index.js', import.meta.url).href,
12
12
  js: new URL('./wasm/whisper-node.js', import.meta.url).href,
13
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,
14
16
  worker: new URL('./worker.js', import.meta.url).href,
15
17
  }
16
18
 
@@ -68,20 +70,39 @@ const createWhisperNodeApi = function (root) {
68
70
  return capturedScriptUrl
69
71
  }
70
72
 
71
- function getRuntimeScriptUrl(indexScriptUrl) {
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) {
72
93
  var configured = runtimeOptions.jsPath || runtimeOptions.runtimeScriptUrl
73
94
  if (configured) {
74
95
  return resolveUrl(configured, indexScriptUrl)
75
96
  }
76
- return WASM_CONFIG_PATHS.js
97
+ return useThreads ? WASM_CONFIG_PATHS.threadsJs : WASM_CONFIG_PATHS.js
77
98
  }
78
99
 
79
- function getWasmBinaryUrl(runtimeScriptUrl) {
100
+ function getWasmBinaryUrl(runtimeScriptUrl, useThreads) {
80
101
  var configured = runtimeOptions.wasmPath
81
102
  if (configured) {
82
103
  return resolveUrl(configured, runtimeScriptUrl)
83
104
  }
84
- return WASM_CONFIG_PATHS.wasm
105
+ return useThreads ? WASM_CONFIG_PATHS.threadsWasm : WASM_CONFIG_PATHS.wasm
85
106
  }
86
107
 
87
108
  function getWorkerScriptUrl(indexScriptUrl) {
@@ -111,6 +132,8 @@ const createWhisperNodeApi = function (root) {
111
132
  runtimeScriptUrl: true,
112
133
  jsPath: true,
113
134
  wasmPath: true,
135
+ threads: true,
136
+ __resolvedThreads: true,
114
137
  locateFileBaseUrl: true,
115
138
  locateFile: true,
116
139
  moduleFactory: true,
@@ -129,14 +152,6 @@ const createWhisperNodeApi = function (root) {
129
152
  return options
130
153
  }
131
154
 
132
- function assertThreadSupport() {
133
- if (isBrowserLike() && typeof root.SharedArrayBuffer === 'undefined') {
134
- throw new Error(
135
- 'whisper.node WASM is built with pthreads and requires SharedArrayBuffer. Serve the page with COOP/COEP headers so the browser is cross-origin isolated.',
136
- )
137
- }
138
- }
139
-
140
155
  function emitLog(level, text) {
141
156
  if (!logEnabled) {
142
157
  return
@@ -151,12 +166,11 @@ const createWhisperNodeApi = function (root) {
151
166
 
152
167
  function loadRuntime() {
153
168
  if (!runtimePromise) {
154
- assertThreadSupport()
155
-
156
169
  runtimePromise = (async function () {
157
170
  var indexScriptUrl = getIndexScriptUrl()
158
- var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl)
159
- var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl)
171
+ var useThreads = shouldUseThreadedRuntime()
172
+ var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl, useThreads)
173
+ var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl, useThreads)
160
174
  var moduleFactory = runtimeOptions.moduleFactory
161
175
 
162
176
  if (!moduleFactory) {
@@ -168,7 +182,7 @@ const createWhisperNodeApi = function (root) {
168
182
  }
169
183
  if (typeof moduleFactory !== 'function') {
170
184
  throw new Error(
171
- 'Failed to load whisper.node WASM runtime. Make sure wasm/whisper-node.js is built and included in the package.',
185
+ 'Failed to load whisper.node WASM runtime. Make sure the selected wasm/whisper-node*.js artifact is built and included in the package.',
172
186
  )
173
187
  }
174
188
 
@@ -193,6 +207,8 @@ const createWhisperNodeApi = function (root) {
193
207
  delete options.runtimeScriptUrl
194
208
  delete options.jsPath
195
209
  delete options.wasmPath
210
+ delete options.threads
211
+ delete options.__resolvedThreads
196
212
  delete options.locateFileBaseUrl
197
213
 
198
214
  options.noInitialRun = true
@@ -219,7 +235,9 @@ const createWhisperNodeApi = function (root) {
219
235
  }
220
236
  }
221
237
 
222
- return moduleFactory(options)
238
+ var runtime = await moduleFactory(options)
239
+ runtime.__whisperNodeWasmThreads = useThreads
240
+ return runtime
223
241
  })()
224
242
  }
225
243
 
@@ -253,8 +271,9 @@ const createWhisperNodeApi = function (root) {
253
271
 
254
272
  function createWorkerProxy() {
255
273
  var indexScriptUrl = getIndexScriptUrl()
256
- var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl)
257
- var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl)
274
+ var useThreads = shouldUseThreadedRuntime()
275
+ var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl, useThreads)
276
+ var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl, useThreads)
258
277
  var workerScriptUrl = getWorkerScriptUrl(indexScriptUrl)
259
278
 
260
279
  if (!indexScriptUrl || !runtimeScriptUrl || !workerScriptUrl) {
@@ -367,6 +386,7 @@ const createWhisperNodeApi = function (root) {
367
386
  indexScriptUrl: indexScriptUrl,
368
387
  runtimeScriptUrl: runtimeScriptUrl,
369
388
  wasmPath: wasmUrl,
389
+ threads: useThreads,
370
390
  runtimeOptions: getWorkerRuntimeOptions(),
371
391
  locateFileBaseUrl:
372
392
  runtimeOptions.locateFileBaseUrl || resolveUrl('.', runtimeScriptUrl),
@@ -743,17 +763,21 @@ const createWhisperNodeApi = function (root) {
743
763
  return value
744
764
  }
745
765
 
746
- function normalizeMaxThreads(value) {
766
+ function normalizeMaxThreads(value, runtime) {
767
+ var maxThreads =
768
+ runtime && runtime.__whisperNodeWasmThreads !== true
769
+ ? 1
770
+ : MAX_WASM_THREADS
747
771
  var nThreads = Number(value)
748
772
  if (!Number.isFinite(nThreads) || nThreads <= 0) {
749
773
  return null
750
774
  }
751
- return Math.max(1, Math.min(MAX_WASM_THREADS, Math.floor(nThreads)))
775
+ return Math.max(1, Math.min(maxThreads, Math.floor(nThreads)))
752
776
  }
753
777
 
754
- function normalizeThreadOptions(options) {
778
+ function normalizeThreadOptions(options, runtime) {
755
779
  var normalized = Object.assign({}, options || {})
756
- var maxThreads = normalizeMaxThreads(normalized.maxThreads)
780
+ var maxThreads = normalizeMaxThreads(normalized.maxThreads, runtime)
757
781
  if (maxThreads) {
758
782
  normalized.maxThreads = maxThreads
759
783
  } else {
@@ -762,8 +786,8 @@ const createWhisperNodeApi = function (root) {
762
786
  return normalized
763
787
  }
764
788
 
765
- function normalizeTranscribeOptions(options) {
766
- var normalized = normalizeThreadOptions(options)
789
+ function normalizeTranscribeOptions(options, runtime) {
790
+ var normalized = normalizeThreadOptions(options, runtime)
767
791
  if (typeof normalized.onProgress !== 'function') {
768
792
  delete normalized.onProgress
769
793
  }
@@ -1209,7 +1233,7 @@ const createWhisperNodeApi = function (root) {
1209
1233
  await runtime.__wasm_transcribe(
1210
1234
  id,
1211
1235
  audio,
1212
- normalizeTranscribeOptions(options),
1236
+ normalizeTranscribeOptions(options, runtime),
1213
1237
  ),
1214
1238
  )
1215
1239
  if (isCancelled && isCancelled()) {
@@ -1258,7 +1282,8 @@ const createWhisperNodeApi = function (root) {
1258
1282
 
1259
1283
  WhisperContextInstance.prototype.bench = async function (nThreads) {
1260
1284
  this._assertValid()
1261
- 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))
1262
1287
  }
1263
1288
 
1264
1289
  WhisperContextInstance.prototype.release = function () {
@@ -1352,7 +1377,7 @@ const createWhisperNodeApi = function (root) {
1352
1377
  var modelSource = options.filePath || options.modelUrl
1353
1378
  var runtime = await loadRuntime()
1354
1379
  var useGpu = false
1355
- var nThreads = options.nThreads || 1
1380
+ var nThreads = normalizeMaxThreads(options.nThreads || 1, runtime) || 1
1356
1381
 
1357
1382
  if (options.useGpu === true) {
1358
1383
  emitLog(
@@ -1448,6 +1473,7 @@ const createWhisperNodeApi = function (root) {
1448
1473
  initWhisperVad: initWhisperVad,
1449
1474
  toggleNativeLog: toggleNativeLog,
1450
1475
  addNativeLogListener: addNativeLogListener,
1476
+ isWasmThreadsSupported: isWasmThreadsSupported,
1451
1477
  DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES: 1500 * MIB,
1452
1478
  MAX_WASM_THREADS: MAX_WASM_THREADS,
1453
1479
  WASM_CONFIG_PATHS: WASM_CONFIG_PATHS,
@@ -1472,6 +1498,7 @@ export const initWhisper = api.initWhisper
1472
1498
  export const initWhisperVad = api.initWhisperVad
1473
1499
  export const toggleNativeLog = api.toggleNativeLog
1474
1500
  export const addNativeLogListener = api.addNativeLogListener
1501
+ export const isWasmThreadsSupported = api.isWasmThreadsSupported
1475
1502
 
1476
1503
  if (root) {
1477
1504
  root.WhisperNodeWasm = api
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fugood/node-whisper-wasm",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "Browser WASM module for whisper.node",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -42,6 +42,8 @@
42
42
  "README.md",
43
43
  "worker.js",
44
44
  "wasm/whisper-node.js",
45
- "wasm/whisper-node.wasm"
45
+ "wasm/whisper-node.wasm",
46
+ "wasm/whisper-node.threads.js",
47
+ "wasm/whisper-node.threads.wasm"
46
48
  ]
47
49
  }