@fugood/node-whisper-wasm 1.0.20 → 1.0.22

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(
@@ -185,6 +189,7 @@ export declare function toggleNativeLog(
185
189
  export declare function addNativeLogListener(
186
190
  listener: (level: string, text: string) => void,
187
191
  ): { remove: () => void }
192
+ export declare function isNativeLogEnabled(): boolean
188
193
 
189
194
  declare const _default: {
190
195
  WhisperContext: typeof WhisperContext
@@ -196,6 +201,8 @@ declare const _default: {
196
201
  initWhisperVad: typeof initWhisperVad
197
202
  toggleNativeLog: typeof toggleNativeLog
198
203
  addNativeLogListener: typeof addNativeLogListener
204
+ isNativeLogEnabled: typeof isNativeLogEnabled
205
+ isWasmThreadsSupported: typeof isWasmThreadsSupported
199
206
  DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES: typeof DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES
200
207
  MAX_WASM_THREADS: typeof MAX_WASM_THREADS
201
208
  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,34 +152,42 @@ 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
- )
155
+ function normalizeLogLevel(level) {
156
+ var normalized = String(level || '').toLowerCase()
157
+ if (normalized === 'warning') {
158
+ return 'warn'
159
+ }
160
+ if (
161
+ normalized === 'error' ||
162
+ normalized === 'warn' ||
163
+ normalized === 'info' ||
164
+ normalized === 'debug'
165
+ ) {
166
+ return normalized
137
167
  }
168
+ return normalized || 'info'
138
169
  }
139
170
 
140
171
  function emitLog(level, text) {
141
172
  if (!logEnabled) {
142
173
  return
143
174
  }
175
+ var normalizedLevel = normalizeLogLevel(level)
144
176
  if (typeof nativeLogCallback === 'function') {
145
- nativeLogCallback(level, text)
177
+ nativeLogCallback(normalizedLevel, text)
146
178
  }
147
179
  logListeners.slice().forEach(function (listener) {
148
- listener(level, text)
180
+ listener(normalizedLevel, text)
149
181
  })
150
182
  }
151
183
 
152
184
  function loadRuntime() {
153
185
  if (!runtimePromise) {
154
- assertThreadSupport()
155
-
156
186
  runtimePromise = (async function () {
157
187
  var indexScriptUrl = getIndexScriptUrl()
158
- var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl)
159
- var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl)
188
+ var useThreads = shouldUseThreadedRuntime()
189
+ var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl, useThreads)
190
+ var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl, useThreads)
160
191
  var moduleFactory = runtimeOptions.moduleFactory
161
192
 
162
193
  if (!moduleFactory) {
@@ -168,7 +199,7 @@ const createWhisperNodeApi = function (root) {
168
199
  }
169
200
  if (typeof moduleFactory !== 'function') {
170
201
  throw new Error(
171
- 'Failed to load whisper.node WASM runtime. Make sure wasm/whisper-node.js is built and included in the package.',
202
+ 'Failed to load whisper.node WASM runtime. Make sure the selected wasm/whisper-node*.js artifact is built and included in the package.',
172
203
  )
173
204
  }
174
205
 
@@ -193,6 +224,8 @@ const createWhisperNodeApi = function (root) {
193
224
  delete options.runtimeScriptUrl
194
225
  delete options.jsPath
195
226
  delete options.wasmPath
227
+ delete options.threads
228
+ delete options.__resolvedThreads
196
229
  delete options.locateFileBaseUrl
197
230
 
198
231
  options.noInitialRun = true
@@ -219,7 +252,9 @@ const createWhisperNodeApi = function (root) {
219
252
  }
220
253
  }
221
254
 
222
- return moduleFactory(options)
255
+ var runtime = await moduleFactory(options)
256
+ runtime.__whisperNodeWasmThreads = useThreads
257
+ return runtime
223
258
  })()
224
259
  }
225
260
 
@@ -253,8 +288,9 @@ const createWhisperNodeApi = function (root) {
253
288
 
254
289
  function createWorkerProxy() {
255
290
  var indexScriptUrl = getIndexScriptUrl()
256
- var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl)
257
- var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl)
291
+ var useThreads = shouldUseThreadedRuntime()
292
+ var runtimeScriptUrl = getRuntimeScriptUrl(indexScriptUrl, useThreads)
293
+ var wasmUrl = getWasmBinaryUrl(runtimeScriptUrl, useThreads)
258
294
  var workerScriptUrl = getWorkerScriptUrl(indexScriptUrl)
259
295
 
260
296
  if (!indexScriptUrl || !runtimeScriptUrl || !workerScriptUrl) {
@@ -367,6 +403,7 @@ const createWhisperNodeApi = function (root) {
367
403
  indexScriptUrl: indexScriptUrl,
368
404
  runtimeScriptUrl: runtimeScriptUrl,
369
405
  wasmPath: wasmUrl,
406
+ threads: useThreads,
370
407
  runtimeOptions: getWorkerRuntimeOptions(),
371
408
  locateFileBaseUrl:
372
409
  runtimeOptions.locateFileBaseUrl || resolveUrl('.', runtimeScriptUrl),
@@ -743,17 +780,21 @@ const createWhisperNodeApi = function (root) {
743
780
  return value
744
781
  }
745
782
 
746
- function normalizeMaxThreads(value) {
783
+ function normalizeMaxThreads(value, runtime) {
784
+ var maxThreads =
785
+ runtime && runtime.__whisperNodeWasmThreads !== true
786
+ ? 1
787
+ : MAX_WASM_THREADS
747
788
  var nThreads = Number(value)
748
789
  if (!Number.isFinite(nThreads) || nThreads <= 0) {
749
790
  return null
750
791
  }
751
- return Math.max(1, Math.min(MAX_WASM_THREADS, Math.floor(nThreads)))
792
+ return Math.max(1, Math.min(maxThreads, Math.floor(nThreads)))
752
793
  }
753
794
 
754
- function normalizeThreadOptions(options) {
795
+ function normalizeThreadOptions(options, runtime) {
755
796
  var normalized = Object.assign({}, options || {})
756
- var maxThreads = normalizeMaxThreads(normalized.maxThreads)
797
+ var maxThreads = normalizeMaxThreads(normalized.maxThreads, runtime)
757
798
  if (maxThreads) {
758
799
  normalized.maxThreads = maxThreads
759
800
  } else {
@@ -762,8 +803,8 @@ const createWhisperNodeApi = function (root) {
762
803
  return normalized
763
804
  }
764
805
 
765
- function normalizeTranscribeOptions(options) {
766
- var normalized = normalizeThreadOptions(options)
806
+ function normalizeTranscribeOptions(options, runtime) {
807
+ var normalized = normalizeThreadOptions(options, runtime)
767
808
  if (typeof normalized.onProgress !== 'function') {
768
809
  delete normalized.onProgress
769
810
  }
@@ -1209,7 +1250,7 @@ const createWhisperNodeApi = function (root) {
1209
1250
  await runtime.__wasm_transcribe(
1210
1251
  id,
1211
1252
  audio,
1212
- normalizeTranscribeOptions(options),
1253
+ normalizeTranscribeOptions(options, runtime),
1213
1254
  ),
1214
1255
  )
1215
1256
  if (isCancelled && isCancelled()) {
@@ -1258,7 +1299,8 @@ const createWhisperNodeApi = function (root) {
1258
1299
 
1259
1300
  WhisperContextInstance.prototype.bench = async function (nThreads) {
1260
1301
  this._assertValid()
1261
- return unwrapWasmResult(await this._runtime.__wasm_bench(this._id, nThreads || 1))
1302
+ var threads = normalizeMaxThreads(nThreads || 1, this._runtime) || 1
1303
+ return unwrapWasmResult(await this._runtime.__wasm_bench(this._id, threads))
1262
1304
  }
1263
1305
 
1264
1306
  WhisperContextInstance.prototype.release = function () {
@@ -1352,7 +1394,7 @@ const createWhisperNodeApi = function (root) {
1352
1394
  var modelSource = options.filePath || options.modelUrl
1353
1395
  var runtime = await loadRuntime()
1354
1396
  var useGpu = false
1355
- var nThreads = options.nThreads || 1
1397
+ var nThreads = normalizeMaxThreads(options.nThreads || 1, runtime) || 1
1356
1398
 
1357
1399
  if (options.useGpu === true) {
1358
1400
  emitLog(
@@ -1448,6 +1490,10 @@ const createWhisperNodeApi = function (root) {
1448
1490
  initWhisperVad: initWhisperVad,
1449
1491
  toggleNativeLog: toggleNativeLog,
1450
1492
  addNativeLogListener: addNativeLogListener,
1493
+ isNativeLogEnabled: function () {
1494
+ return logEnabled
1495
+ },
1496
+ isWasmThreadsSupported: isWasmThreadsSupported,
1451
1497
  DEFAULT_WASM_MODEL_SIZE_LIMIT_BYTES: 1500 * MIB,
1452
1498
  MAX_WASM_THREADS: MAX_WASM_THREADS,
1453
1499
  WASM_CONFIG_PATHS: WASM_CONFIG_PATHS,
@@ -1472,6 +1518,8 @@ export const initWhisper = api.initWhisper
1472
1518
  export const initWhisperVad = api.initWhisperVad
1473
1519
  export const toggleNativeLog = api.toggleNativeLog
1474
1520
  export const addNativeLogListener = api.addNativeLogListener
1521
+ export const isNativeLogEnabled = api.isNativeLogEnabled
1522
+ export const isWasmThreadsSupported = api.isWasmThreadsSupported
1475
1523
 
1476
1524
  if (root) {
1477
1525
  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.22",
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
  }