@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 +56 -17
- package/lib/sensevoice-installer.js +10 -9
- package/package.json +1 -1
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
|
-
|
|
1096
|
-
|
|
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 (!
|
|
1100
|
-
throw new Error('WebGPU is
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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(
|
|
1146
|
+
_webGpuPipelines.delete(key)
|
|
1113
1147
|
throw err
|
|
1114
1148
|
})
|
|
1115
|
-
_webGpuPipelines.set(
|
|
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 (!
|
|
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:
|
|
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
|
-
|
|
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
|
|
267
|
-
let normProv = rawProv === 'rknpu' ? 'rknn' : rawProv
|
|
271
|
+
const effectiveProv = resolveSensevoiceProvider(cfg)
|
|
268
272
|
|
|
269
|
-
if (
|
|
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 = (
|
|
286
|
-
? ` --provider=${
|
|
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.
|
|
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",
|