@goodandready/dsh-voice 0.9.28 → 0.9.29

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/lib/client.js CHANGED
@@ -1080,39 +1080,73 @@ window.__ModuleLoader__.load({
1080
1080
  // Fetch the mode chain from the host once. Only needed to decide whether
1081
1081
 
1082
1082
  voice._core = { tidyPhrase, applyVoiceCommands, VOICE_COMMANDS, undoLastInsert, insertHistory, insertDraftText, appendDraft, resolveBrowserRecognitionLang }
1083
- // lib/client-src/35-webgpu.js — Client-side WebGPU Whisper offline recognition
1084
- // Detects WebGPU availability in browser and provides offline transcription with graceful fallback.
1083
+ // lib/client-src/35-webgpu.js — Client-side WebGPU/WASM Whisper offline recognition
1084
+ // Detects WebGPU and WebAssembly availability in browser and provides offline transcription with graceful WASM fallback.
1085
1085
 
1086
1086
  function isWebGpuSupported() {
1087
1087
  if (typeof navigator === 'undefined' || !navigator.gpu) return false
1088
1088
  return true
1089
1089
  }
1090
1090
 
1091
+ function isWasmSupported() {
1092
+ return typeof WebAssembly !== 'undefined'
1093
+ }
1094
+
1095
+ function isOfflineSupported() {
1096
+ return isWebGpuSupported() || isWasmSupported()
1097
+ }
1098
+
1091
1099
  const _webGpuPipelines = new Map()
1092
1100
 
1093
- function loadWebGpuWhisper(modelName) {
1101
+ function loadWebGpuWhisper(modelName, options) {
1094
1102
  const targetModel = modelName || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'
1095
- if (_webGpuPipelines.has(targetModel)) {
1096
- return _webGpuPipelines.get(targetModel)
1103
+ const forceDevice = options?.device
1104
+ const key = `${targetModel}:${forceDevice || 'auto'}`
1105
+ if (_webGpuPipelines.has(key)) {
1106
+ return _webGpuPipelines.get(key)
1097
1107
  }
1098
1108
  const promise = (async () => {
1099
- if (!isWebGpuSupported()) {
1100
- throw new Error('WebGPU is not supported in this browser environment')
1109
+ if (!isOfflineSupported()) {
1110
+ throw new Error('Neither WebGPU nor WebAssembly is supported in this browser environment')
1101
1111
  }
1102
1112
  const mod = typeof globalThis !== 'undefined' && globalThis.__mockTransformers
1103
1113
  ? globalThis.__mockTransformers
1104
1114
  : (await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.3.3'))
1105
1115
  const pipeline = mod.pipeline || (mod.default && mod.default.pipeline)
1106
1116
  if (!pipeline) throw new Error('Transformers pipeline not found in loaded bundle')
1107
- return pipeline('automatic-speech-recognition', targetModel, {
1108
- device: 'webgpu',
1109
- dtype: 'fp32',
1110
- })
1117
+
1118
+ // Prefer WebGPU if available and not forced to wasm; fall back to WASM on error or when WebGPU is absent
1119
+ const canGpu = forceDevice === 'webgpu' || (!forceDevice && isWebGpuSupported())
1120
+ if (canGpu) {
1121
+ try {
1122
+ return await pipeline('automatic-speech-recognition', targetModel, {
1123
+ device: 'webgpu',
1124
+ dtype: 'fp32',
1125
+ })
1126
+ } catch (gpuErr) {
1127
+ if (isWasmSupported()) {
1128
+ return await pipeline('automatic-speech-recognition', targetModel, {
1129
+ device: 'wasm',
1130
+ dtype: 'q8',
1131
+ })
1132
+ }
1133
+ throw gpuErr
1134
+ }
1135
+ }
1136
+
1137
+ if (isWasmSupported()) {
1138
+ return await pipeline('automatic-speech-recognition', targetModel, {
1139
+ device: 'wasm',
1140
+ dtype: 'q8',
1141
+ })
1142
+ }
1143
+
1144
+ throw new Error('Offline speech recognition failed: no WebGPU or WebAssembly available')
1111
1145
  })().catch((err) => {
1112
- _webGpuPipelines.delete(targetModel)
1146
+ _webGpuPipelines.delete(key)
1113
1147
  throw err
1114
1148
  })
1115
- _webGpuPipelines.set(targetModel, promise)
1149
+ _webGpuPipelines.set(key, promise)
1116
1150
  return promise
1117
1151
  }
1118
1152
 
@@ -1131,10 +1165,10 @@ async function audioBlobToFloat32Array(blob) {
1131
1165
  }
1132
1166
 
1133
1167
  async function transcribeWithWebGpu(blob, mime, options) {
1134
- if (!isWebGpuSupported()) {
1135
- throw new Error('WebGPU unavailable in browser')
1168
+ if (!isOfflineSupported()) {
1169
+ throw new Error('Offline recognition (WebGPU/WASM) unavailable in browser')
1136
1170
  }
1137
- const transcriber = await loadWebGpuWhisper(options && options.model)
1171
+ const transcriber = await loadWebGpuWhisper(options && options.model, options)
1138
1172
  const audioData = await audioBlobToFloat32Array(blob)
1139
1173
  const generateArgs = {}
1140
1174
  if (options && options.language && options.language !== 'auto') {
@@ -1146,7 +1180,9 @@ async function transcribeWithWebGpu(blob, mime, options) {
1146
1180
  }
1147
1181
 
1148
1182
  voice.webGpu = {
1149
- isSupported: isWebGpuSupported,
1183
+ isSupported: isOfflineSupported,
1184
+ isWebGpuSupported,
1185
+ isWasmSupported,
1150
1186
  transcribe: transcribeWithWebGpu,
1151
1187
  load: loadWebGpuWhisper,
1152
1188
  clearCache: () => { _webGpuPipelines.clear() },
@@ -3997,6 +4033,9 @@ voice._chainsPromise = null
3997
4033
  stopCurrent,
3998
4034
  submitPending,
3999
4035
  loadWebGpuWhisper,
4036
+ isWebGpuSupported,
4037
+ isWasmSupported,
4038
+ isOfflineSupported,
4000
4039
  waitStop,
4001
4040
  currentLevel,
4002
4041
  accentColor,
@@ -249,8 +249,13 @@ export function resolveSensevoiceProvider(cfg) {
249
249
  return 'cpu'
250
250
  }
251
251
  if (ext === '.rknn') {
252
- return 'rknn'
252
+ if (modelPath && existsSync(modelPath)) {
253
+ return 'rknn'
254
+ }
255
+ // Guard: missing .rknn model file cannot be claimed as running accelerator (#168)
256
+ return 'cpu'
253
257
  }
258
+ return 'cpu'
254
259
  }
255
260
 
256
261
  return normProv || 'cpu'
@@ -263,10 +268,9 @@ export function buildSensevoiceArgs(cfg, port) {
263
268
  ? cfg.sensevoiceTokens
264
269
  : findTokensFile(modelDir)
265
270
  const tokensArg = tokensPath ? ` --tokens=${JSON.stringify(tokensPath)}` : ''
266
- const rawProv = String(cfg?.sensevoiceProvider || '').trim().toLowerCase()
267
- let normProv = rawProv === 'rknpu' ? 'rknn' : rawProv
271
+ const effectiveProv = resolveSensevoiceProvider(cfg)
268
272
 
269
- if (normProv === 'rknn') {
273
+ if (effectiveProv === 'rknn') {
270
274
  const ext = path.extname(modelPath).toLowerCase()
271
275
  if (ext === '.onnx') {
272
276
  const rknnCandidate = path.join(modelDir, path.basename(modelPath, ext) + '.rknn')
@@ -275,15 +279,12 @@ export function buildSensevoiceArgs(cfg, port) {
275
279
  modelPath = rknnCandidate
276
280
  } else if (existsSync(defaultRknn)) {
277
281
  modelPath = defaultRknn
278
- } else {
279
- // Guard: ordinary ONNX model cannot be loaded by sherpa-onnx RKNN runtime (#221)
280
- normProv = 'cpu'
281
282
  }
282
283
  }
283
284
  }
284
285
 
285
- const provider = (normProv && normProv !== 'cpu')
286
- ? ` --provider=${normProv}`
286
+ const provider = (effectiveProv && effectiveProv !== 'cpu')
287
+ ? ` --provider=${effectiveProv}`
287
288
  : ''
288
289
  const threads = Number(cfg?.sensevoiceThreads) || 4
289
290
  return ` --sense-voice-model=${JSON.stringify(modelPath)}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.9.28",
3
+ "version": "0.9.29",
4
4
  "description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
5
5
  "license": "MIT",
6
6
  "type": "module",