@goodandready/dsh-voice 0.9.14 → 0.9.15
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 +2 -1
- package/README.ru.md +2 -1
- package/lib/client.js +155 -1
- package/lib/index.js +8 -20
- package/lib/providers.js +12 -4
- package/lib/schema.js +9 -2
- package/lib/sensevoice-installer.js +14 -0
- package/lib/transcribe-core.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,12 +151,13 @@ Quality batch after the v0.8.18 review (Gitea #79–#87, PR #88):
|
|
|
151
151
|
|
|
152
152
|
| Provider Key | Service Backend | Default Model | Credential Ref | Features & Notes |
|
|
153
153
|
|---|---|---|---|---|
|
|
154
|
+
| `browser-webgpu` | In-Browser WebGPU/WASM Whisper | `onnx-community/whisper-tiny` | *None* | 100% private offline client-side GPU transcription; falls back to server chain |
|
|
154
155
|
| `browser` | Web Speech API | Native Browser | *None* | Zero latency, floating live captions in Chrome (vendor server-based; not offline) |
|
|
155
156
|
| `deepgram` | Deepgram API | `nova-2` | `DEEPGRAM_API_KEY` | Ultra-fast cloud transcription |
|
|
156
157
|
| `groq` | Groq Whisper | `whisper-large-v3-turbo` | `GROQ_API_KEY` | Near-instant inference speed |
|
|
157
158
|
| `hf` | HuggingFace Inference | `openai/whisper-large-v3` | `HF_TOKEN` | High-accuracy open Whisper |
|
|
158
159
|
| `local-whisper` | Local whisper.cpp | Server defined | *None* | 100% private, offline, no internet needed |
|
|
159
|
-
| `sensevoice` | SenseVoice-ONNX / Sherpa-ONNX | `SenseVoiceSmall` | *None* | Ultra-fast (~50ms) local
|
|
160
|
+
| `sensevoice` | SenseVoice-ONNX / Sherpa-ONNX | `SenseVoiceSmall` | *None* | Ultra-fast (~50ms) local STT; supports Rockchip RK3588 NPU (rknpu), OpenVINO, CUDA |
|
|
160
161
|
|
|
161
162
|
### 🚀 Ready-Made Presets (Plug & Play)
|
|
162
163
|
|
package/README.ru.md
CHANGED
|
@@ -151,12 +151,13 @@ Quality-батч после ревью v0.8.18 (Gitea #79–#87, PR #88):
|
|
|
151
151
|
|
|
152
152
|
| Ключ | Сервис | Модель по умолчанию | Переменная секрета | Особенности |
|
|
153
153
|
|---|---|---|---|---|
|
|
154
|
+
| `browser-webgpu` | WebGPU/WASM Whisper в браузере | `onnx-community/whisper-tiny` | *Не требуется* | 100% приватное офлайн распознавание на GPU клиента; авто-фоллбек на серверную цепочку |
|
|
154
155
|
| `browser` | Web Speech API | Нативная в браузере | *Не требуется* | Нулевая задержка, живые субтитры в Chrome (обработка на серверах вендора; не является офлайн) |
|
|
155
156
|
| `deepgram` | Deepgram API | `nova-2` | `DEEPGRAM_API_KEY` | Сверхбыстрая облачная транскрипция |
|
|
156
157
|
| `groq` | Groq Whisper | `whisper-large-v3-turbo` | `GROQ_API_KEY` | Мгновенная скорость генерации |
|
|
157
158
|
| `hf` | HuggingFace Inference | `openai/whisper-large-v3` | `HF_TOKEN` | Высокоточный облачный Whisper |
|
|
158
159
|
| `local-whisper` | Локальный whisper.cpp | из параметров сервера | *Не требуется* | 100% приватность, оффлайн, без интернета |
|
|
159
|
-
| `sensevoice` | SenseVoice-ONNX / Sherpa-ONNX | `SenseVoiceSmall` | *Не требуется* | Сверхбыстрый (~50мс) локальный
|
|
160
|
+
| `sensevoice` | SenseVoice-ONNX / Sherpa-ONNX | `SenseVoiceSmall` | *Не требуется* | Сверхбыстрый (~50мс) локальный STT; ускорение на Rockchip RK3588 NPU (rknpu), OpenVINO, CUDA |
|
|
160
161
|
|
|
161
162
|
### 🚀 Готовые пресеты (Plug & Play)
|
|
162
163
|
|
package/lib/client.js
CHANGED
|
@@ -63,6 +63,14 @@ window.__ModuleLoader__.load({
|
|
|
63
63
|
'title': 'Voice',
|
|
64
64
|
'recordSlot': 'Voice recording',
|
|
65
65
|
'browserHint': 'Web Speech recognition (uses page/browser language when auto, Chrome sends audio to Google)',
|
|
66
|
+
'browserWebGpu': 'Browser WebGPU Whisper (Offline)',
|
|
67
|
+
'browserWebGpuHint': 'Runs Whisper model directly in browser GPU via WebGPU/WASM: offline, no host or cloud upload.',
|
|
68
|
+
'npuProvider': 'Hardware Acceleration (SBC NPU / Host)',
|
|
69
|
+
'npuProviderHint': 'Execution provider for SenseVoice / sherpa-onnx: CPU, Rockchip RK3588 NPU (rknpu), OpenVINO, or CUDA.',
|
|
70
|
+
'npuThreads': 'Threads (CPU/SBC)',
|
|
71
|
+
'webgpuStatus': 'Browser WebGPU Status',
|
|
72
|
+
'webgpuAvailable': 'WebGPU Available (hardware acceleration ready)',
|
|
73
|
+
'webgpuNotAvailable': 'WebGPU Not Detected (will fallback to server)',
|
|
66
74
|
'openaiHint': 'whisper-1 · key OPENAI_API_KEY',
|
|
67
75
|
'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
|
|
68
76
|
'deepinfraHint': 'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY',
|
|
@@ -270,6 +278,14 @@ window.__ModuleLoader__.load({
|
|
|
270
278
|
'title': '语音',
|
|
271
279
|
'recordSlot': '语音录制',
|
|
272
280
|
'browserHint': 'Web Speech 语音识别 (设为 auto 时使用页面或浏览器语言,Chrome 会上传音频至 Google)',
|
|
281
|
+
'browserWebGpu': '浏览器 WebGPU Whisper (离线)',
|
|
282
|
+
'browserWebGpuHint': '通过 WebGPU/WASM 在浏览器本地运行 Whisper 模型:完全离线,音频无需上传到主机。',
|
|
283
|
+
'npuProvider': '硬件加速 (SBC NPU / 主机)',
|
|
284
|
+
'npuProviderHint': 'SenseVoice / sherpa-onnx 执行加速引擎:CPU、Rockchip RK3588 NPU (rknpu)、OpenVINO 或 CUDA。',
|
|
285
|
+
'npuThreads': '计算线程 (CPU/SBC)',
|
|
286
|
+
'webgpuStatus': '浏览器 WebGPU 状态',
|
|
287
|
+
'webgpuAvailable': 'WebGPU 可用 (硬件加速就绪)',
|
|
288
|
+
'webgpuNotAvailable': '未检测到 WebGPU (将自动回退到服务端)',
|
|
273
289
|
'openaiHint': 'whisper-1 · 密钥 OPENAI_API_KEY',
|
|
274
290
|
'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · 密钥 SILICONFLOW_API_KEY',
|
|
275
291
|
'deepinfraHint': 'openai/whisper-large-v3-turbo · 密钥 DEEPINFRA_API_KEY',
|
|
@@ -717,6 +733,23 @@ window.__ModuleLoader__.load({
|
|
|
717
733
|
}
|
|
718
734
|
|
|
719
735
|
async function sendAudio(blob, mime, mode) {
|
|
736
|
+
const activeChain = (voice._activeChain || [])
|
|
737
|
+
if (activeChain.includes('browser-webgpu')) {
|
|
738
|
+
if (typeof isWebGpuSupported === 'function' && isWebGpuSupported()) {
|
|
739
|
+
try {
|
|
740
|
+
const out = await transcribeWithWebGpu(blob, mime, {
|
|
741
|
+
language: voice._activeLang || (voice.settings && voice.settings.dictation && voice.settings.dictation.language) || '',
|
|
742
|
+
model: voice.settings && voice.settings.webgpuModel,
|
|
743
|
+
})
|
|
744
|
+
if (out && typeof out.text === 'string') {
|
|
745
|
+
return { text: tidyPhrase(out.text), command: null, action: null, provider: 'browser-webgpu' }
|
|
746
|
+
}
|
|
747
|
+
} catch (webGpuErr) {
|
|
748
|
+
// Graceful fallback to server chain
|
|
749
|
+
console.warn('[dsh-voice] WebGPU Whisper failed, falling back to server chain:', webGpuErr)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
720
753
|
const dataBase64 = await blobToBase64(blob)
|
|
721
754
|
const payload = { dataBase64, mimeType: mime, mode }
|
|
722
755
|
const contextWords = extractContextKeywords()
|
|
@@ -1046,6 +1079,78 @@ window.__ModuleLoader__.load({
|
|
|
1046
1079
|
// Fetch the mode chain from the host once. Only needed to decide whether
|
|
1047
1080
|
|
|
1048
1081
|
voice._core = { tidyPhrase, applyVoiceCommands, VOICE_COMMANDS, undoLastInsert, insertHistory, insertDraftText, appendDraft, resolveBrowserRecognitionLang }
|
|
1082
|
+
// lib/client-src/35-webgpu.js — Client-side WebGPU Whisper offline recognition
|
|
1083
|
+
// Detects WebGPU availability in browser and provides offline transcription with graceful fallback.
|
|
1084
|
+
|
|
1085
|
+
function isWebGpuSupported() {
|
|
1086
|
+
if (typeof navigator === 'undefined' || !navigator.gpu) return false
|
|
1087
|
+
return true
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
let _webGpuPipeline = null
|
|
1091
|
+
let _webGpuLoading = false
|
|
1092
|
+
|
|
1093
|
+
async function loadWebGpuWhisper(modelName) {
|
|
1094
|
+
if (_webGpuPipeline) return _webGpuPipeline
|
|
1095
|
+
if (_webGpuLoading) {
|
|
1096
|
+
while (_webGpuLoading) {
|
|
1097
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
1098
|
+
}
|
|
1099
|
+
if (_webGpuPipeline) return _webGpuPipeline
|
|
1100
|
+
}
|
|
1101
|
+
_webGpuLoading = true
|
|
1102
|
+
try {
|
|
1103
|
+
if (!isWebGpuSupported()) {
|
|
1104
|
+
throw new Error('WebGPU is not supported in this browser environment')
|
|
1105
|
+
}
|
|
1106
|
+
// Dynamic import of transformers.js from ESM CDN
|
|
1107
|
+
const mod = await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.3.3')
|
|
1108
|
+
const pipeline = mod.pipeline || (mod.default && mod.default.pipeline)
|
|
1109
|
+
if (!pipeline) throw new Error('Transformers pipeline not found in loaded bundle')
|
|
1110
|
+
const targetModel = modelName || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'
|
|
1111
|
+
_webGpuPipeline = await pipeline('automatic-speech-recognition', targetModel, {
|
|
1112
|
+
device: 'webgpu',
|
|
1113
|
+
dtype: 'fp32',
|
|
1114
|
+
})
|
|
1115
|
+
return _webGpuPipeline
|
|
1116
|
+
} finally {
|
|
1117
|
+
_webGpuLoading = false
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async function audioBlobToFloat32Array(blob) {
|
|
1122
|
+
const arrayBuffer = await blob.arrayBuffer()
|
|
1123
|
+
const AC = typeof AudioContext !== 'undefined' ? AudioContext
|
|
1124
|
+
: (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
|
|
1125
|
+
if (!AC) throw new Error('AudioContext unavailable for audio decoding')
|
|
1126
|
+
const ctx = new AC({ sampleRate: 16000 })
|
|
1127
|
+
try {
|
|
1128
|
+
const audioBuffer = await ctx.decodeAudioData(arrayBuffer)
|
|
1129
|
+
return audioBuffer.getChannelData(0)
|
|
1130
|
+
} finally {
|
|
1131
|
+
try { await ctx.close() } catch (_) {}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function transcribeWithWebGpu(blob, mime, options) {
|
|
1136
|
+
if (!isWebGpuSupported()) {
|
|
1137
|
+
throw new Error('WebGPU unavailable in browser')
|
|
1138
|
+
}
|
|
1139
|
+
const transcriber = await loadWebGpuWhisper(options && options.model)
|
|
1140
|
+
const audioData = await audioBlobToFloat32Array(blob)
|
|
1141
|
+
const generateArgs = {}
|
|
1142
|
+
if (options && options.language && options.language !== 'auto') {
|
|
1143
|
+
generateArgs.language = options.language
|
|
1144
|
+
}
|
|
1145
|
+
const result = await transcriber(audioData, generateArgs)
|
|
1146
|
+
const text = (result && typeof result.text === 'string') ? result.text.trim() : ''
|
|
1147
|
+
return { text, provider: 'browser-webgpu' }
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
voice.webGpu = {
|
|
1151
|
+
isSupported: isWebGpuSupported,
|
|
1152
|
+
transcribe: transcribeWithWebGpu,
|
|
1153
|
+
}
|
|
1049
1154
|
voice._chainsPromise = null
|
|
1050
1155
|
function modeChain(mode) {
|
|
1051
1156
|
if (!voice._chainsPromise) {
|
|
@@ -1276,6 +1381,8 @@ window.__ModuleLoader__.load({
|
|
|
1276
1381
|
dictationQueue = Promise.resolve()
|
|
1277
1382
|
voice.set({ phase: 'recording', mode, error: '', levels: [], caption: '', interimTranscript: '' })
|
|
1278
1383
|
modeChain(mode).then((info) => {
|
|
1384
|
+
voice._activeChain = info.chain || []
|
|
1385
|
+
voice._activeLang = info.language || ''
|
|
1279
1386
|
if (info.chain[0] === 'browser' && startBrowserLeg(mode, info.language)) return
|
|
1280
1387
|
openMic(mode)
|
|
1281
1388
|
.then((rec) => {
|
|
@@ -2133,7 +2240,7 @@ window.__ModuleLoader__.load({
|
|
|
2133
2240
|
|
|
2134
2241
|
// ------------------------------------------------------- settings page
|
|
2135
2242
|
const BUILTIN = [
|
|
2136
|
-
'browser', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice',
|
|
2243
|
+
'browser', 'browser-webgpu', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice',
|
|
2137
2244
|
// Presets: address and model are filled on the host; only a key is needed.
|
|
2138
2245
|
'openai', 'siliconflow', 'deepinfra', 'fireworks', 'mistral', 'openrouter',
|
|
2139
2246
|
]
|
|
@@ -2141,6 +2248,7 @@ window.__ModuleLoader__.load({
|
|
|
2141
2248
|
// Resolve at render time: t() must not be captured before locale bind.
|
|
2142
2249
|
const MODEL_HINT_KEYS = {
|
|
2143
2250
|
browser: 'browserHint',
|
|
2251
|
+
'browser-webgpu': 'browserWebGpuHint',
|
|
2144
2252
|
openai: 'openaiHint',
|
|
2145
2253
|
siliconflow: 'siliconflowHint',
|
|
2146
2254
|
deepinfra: 'deepinfraHint',
|
|
@@ -2151,6 +2259,7 @@ window.__ModuleLoader__.load({
|
|
|
2151
2259
|
sensevoice: 'sensevoiceHint',
|
|
2152
2260
|
}
|
|
2153
2261
|
const MODEL_HINT_STATIC = {
|
|
2262
|
+
'browser-webgpu': 'onnx-community/whisper-tiny',
|
|
2154
2263
|
deepgram: 'nova-2',
|
|
2155
2264
|
groq: 'whisper-large-v3-turbo',
|
|
2156
2265
|
hf: 'openai/whisper-large-v3',
|
|
@@ -2589,6 +2698,51 @@ window.__ModuleLoader__.load({
|
|
|
2589
2698
|
React.createElement('option', { value: 'off' }, t('visOff')),
|
|
2590
2699
|
),
|
|
2591
2700
|
),
|
|
2701
|
+
// Hardware Acceleration (SBC NPU / Host)
|
|
2702
|
+
React.createElement('div', { className: 'cb-field' },
|
|
2703
|
+
React.createElement('label', null, t('npuProvider')),
|
|
2704
|
+
React.createElement('div', { className: 'cb-row' },
|
|
2705
|
+
React.createElement('select', {
|
|
2706
|
+
value: String((draft && draft.sensevoiceProvider) || 'cpu'),
|
|
2707
|
+
disabled: !writable,
|
|
2708
|
+
onChange: (e) => setTop('sensevoiceProvider', e.target.value),
|
|
2709
|
+
style: { flex: 1 },
|
|
2710
|
+
},
|
|
2711
|
+
React.createElement('option', { value: 'cpu' }, 'CPU (Standard)'),
|
|
2712
|
+
React.createElement('option', { value: 'rknpu' }, 'Rockchip RK3588 NPU (rknpu)'),
|
|
2713
|
+
React.createElement('option', { value: 'openvino' }, 'Intel OpenVINO'),
|
|
2714
|
+
React.createElement('option', { value: 'cuda' }, 'NVIDIA CUDA'),
|
|
2715
|
+
),
|
|
2716
|
+
React.createElement('input', {
|
|
2717
|
+
type: 'number',
|
|
2718
|
+
min: 1,
|
|
2719
|
+
max: 32,
|
|
2720
|
+
value: Number((draft && draft.sensevoiceThreads) || 4),
|
|
2721
|
+
disabled: !writable,
|
|
2722
|
+
title: t('npuThreads'),
|
|
2723
|
+
style: { width: '80px' },
|
|
2724
|
+
onChange: (e) => setTop('sensevoiceThreads', Math.max(1, Math.min(32, Number(e.target.value) || 4))),
|
|
2725
|
+
}),
|
|
2726
|
+
),
|
|
2727
|
+
React.createElement('span', { className: 'dvs-sub' }, t('npuProviderHint')),
|
|
2728
|
+
),
|
|
2729
|
+
// WebGPU Status Indicator
|
|
2730
|
+
React.createElement('div', { className: 'cb-field' },
|
|
2731
|
+
React.createElement('label', null, t('webgpuStatus')),
|
|
2732
|
+
React.createElement('div', {
|
|
2733
|
+
style: {
|
|
2734
|
+
padding: '6px 10px',
|
|
2735
|
+
borderRadius: '6px',
|
|
2736
|
+
fontSize: '12px',
|
|
2737
|
+
backgroundColor: 'var(--dsw-alias-fill-quaternary)',
|
|
2738
|
+
color: (typeof isWebGpuSupported === 'function' && isWebGpuSupported())
|
|
2739
|
+
? 'var(--dsw-alias-state-success)'
|
|
2740
|
+
: 'var(--dsw-alias-label-secondary)'
|
|
2741
|
+
}
|
|
2742
|
+
}, (typeof isWebGpuSupported === 'function' && isWebGpuSupported())
|
|
2743
|
+
? ('✓ ' + t('webgpuAvailable'))
|
|
2744
|
+
: ('ℹ ' + t('webgpuNotAvailable'))),
|
|
2745
|
+
),
|
|
2592
2746
|
// Vocabulary textarea
|
|
2593
2747
|
React.createElement('div', { className: 'cb-field' },
|
|
2594
2748
|
React.createElement('label', null, t('vocabulary')),
|
package/lib/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import { createPolishText } from "./polish.js"
|
|
|
25
25
|
import { createUserMessage } from "@deepseek-ai/dsh-llm"
|
|
26
26
|
import { buildProviderOrder, sessionCommand } from "./transcribe-core.js"
|
|
27
27
|
import { registerPluginUpdater } from "./updater.js"
|
|
28
|
-
import { registerSensevoiceInstaller,
|
|
28
|
+
import { registerSensevoiceInstaller, buildSensevoiceArgs } from "./sensevoice-installer.js"
|
|
29
29
|
import { createConfigReader } from "./config-compat.js"
|
|
30
30
|
import { registerConfigRoutes } from "./config-routes.js"
|
|
31
31
|
import { isAutoLang } from "./provider-http.js"
|
|
@@ -202,25 +202,10 @@ export function apply(ctx, baseConfig) {
|
|
|
202
202
|
}
|
|
203
203
|
let port = '6006'
|
|
204
204
|
try { port = new URL(cfg.sensevoiceUrl).port || '6006' } catch { /* invalid URL */ }
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
command: `${JSON.stringify(cfg.sensevoiceBin)}`
|
|
210
|
-
+ ` --sense-voice-model=${JSON.stringify(cfg.sensevoiceModel)}`
|
|
211
|
-
+ tokensArg
|
|
212
|
-
+ ` --port=${port} --num-threads=4`,
|
|
213
|
-
timeoutMs: 0,
|
|
214
|
-
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
215
|
-
})
|
|
216
|
-
: {
|
|
217
|
-
command: `${JSON.stringify(cfg.sensevoiceBin)}`
|
|
218
|
-
+ ` --sense-voice-model=${JSON.stringify(cfg.sensevoiceModel)}`
|
|
219
|
-
+ tokensArg
|
|
220
|
-
+ ` --port=${port} --num-threads=4`,
|
|
221
|
-
timeoutMs: 0,
|
|
222
|
-
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
223
|
-
}
|
|
205
|
+
const cmdArgs = buildSensevoiceArgs(cfg, port)
|
|
206
|
+
const commandStr = `${JSON.stringify(cfg.sensevoiceBin)}${cmdArgs}`
|
|
207
|
+
const specOpts = { command: commandStr, timeoutMs: 0, stdoutMaxBytes: 4 * 1024 * 1024 }
|
|
208
|
+
const spec = typeof ctx.shell?.resolve === 'function' ? ctx.shell.resolve(specOpts) : specOpts
|
|
224
209
|
sensevoiceChild = typeof ctx.shell?.execute === 'function'
|
|
225
210
|
? ctx.shell.execute(spec)
|
|
226
211
|
: typeof ctx.shell?.start === 'function'
|
|
@@ -399,6 +384,9 @@ export function apply(ctx, baseConfig) {
|
|
|
399
384
|
contextGlossary: cfg.contextGlossary !== false,
|
|
400
385
|
sensevoiceUrl: cfg.sensevoiceUrl,
|
|
401
386
|
sensevoiceAutostart: !!cfg.sensevoiceAutostart,
|
|
387
|
+
sensevoiceProvider: cfg.sensevoiceProvider || 'cpu',
|
|
388
|
+
sensevoiceThreads: cfg.sensevoiceThreads || 4,
|
|
389
|
+
webgpuModel: cfg.webgpuModel || 'onnx-community/whisper-tiny',
|
|
402
390
|
deepgramBaseUrl: cfg.deepgramBaseUrl || 'https://api.deepgram.com',
|
|
403
391
|
visualizerStyle: cfg.visualizerStyle || 'liquid-wave',
|
|
404
392
|
gatedTurnTaking: cfg.gatedTurnTaking !== false,
|
package/lib/providers.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 'browser' is listed for chain validation only: recognition runs in the
|
|
6
6
|
// page and audio never reaches the host. Without this key a ['browser','groq']
|
|
7
7
|
// chain would break on the host instead of falling through to groq.
|
|
8
|
-
export const PROVIDER_KEYS = ['browser', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice']
|
|
8
|
+
export const PROVIDER_KEYS = ['browser', 'browser-webgpu', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice']
|
|
9
9
|
|
|
10
10
|
// Custom providers use one of two templates because OpenAI-compatible APIs
|
|
11
11
|
// diverged: OpenRouter has no /audio/transcriptions and recognizes via chat.
|
|
@@ -342,8 +342,8 @@ export function makeProviders(deps, req) {
|
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
// If audio still reached the host with 'browser' in the chain,
|
|
346
|
-
// leg failed: fail clearly and move on.
|
|
345
|
+
// If audio still reached the host with 'browser' or 'browser-webgpu' in the chain,
|
|
346
|
+
// the client-side leg failed: fail clearly and move on.
|
|
347
347
|
async function browser() {
|
|
348
348
|
return {
|
|
349
349
|
ok: false,
|
|
@@ -352,7 +352,15 @@ export function makeProviders(deps, req) {
|
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
|
|
355
|
+
async function browserWebGpu() {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
provider: 'browser-webgpu',
|
|
359
|
+
reason: 'browser-webgpu: recognition runs in the page via WebGPU/WASM, the host has no browser engine',
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const out = { browser, 'browser-webgpu': browserWebGpu, deepgram, groq, hf, 'local-whisper': localWhisper, sensevoice }
|
|
356
364
|
|
|
357
365
|
// Presets: custom providers with the form pre-filled.
|
|
358
366
|
for (const key of PRESET_KEYS) {
|
package/lib/schema.js
CHANGED
|
@@ -7,8 +7,9 @@ export const ChainEntry = z.object({
|
|
|
7
7
|
.description(`Provider key. Built in: ${PROVIDER_KEYS.join(', ')}. `
|
|
8
8
|
+ `Ready-made, just add the key: ${PRESET_KEYS.join(', ')}. `
|
|
9
9
|
+ 'Or the name of an entry from customProviders. '
|
|
10
|
-
+ '"browser" recognises speech
|
|
11
|
-
+ '
|
|
10
|
+
+ '"browser" recognises speech via Web Speech API; '
|
|
11
|
+
+ '"browser-webgpu" runs offline Whisper directly in the page via WebGPU/WASM; '
|
|
12
|
+
+ 'put a normal host provider after them as a fallback.'),
|
|
12
13
|
model: z.string().default('')
|
|
13
14
|
.description('Model override. Empty means the provider default.'),
|
|
14
15
|
})
|
|
@@ -132,6 +133,12 @@ export const BaseConfig = z.object({
|
|
|
132
133
|
.description('SenseVoice / sherpa-onnx model path or model identifier.'),
|
|
133
134
|
sensevoiceAutostart: z.boolean().default(false)
|
|
134
135
|
.description('Start the local SenseVoice / sherpa-onnx server process automatically on startup.'),
|
|
136
|
+
sensevoiceProvider: z.string().default('cpu')
|
|
137
|
+
.description('Execution provider for sherpa-onnx: "cpu", "rknpu" (Rockchip RK3588/RK3568 NPU on ARM SBC), "openvino", "cuda".'),
|
|
138
|
+
sensevoiceThreads: z.number().default(4)
|
|
139
|
+
.description('Execution thread count for local SenseVoice / sherpa-onnx on CPU/SBC.'),
|
|
140
|
+
webgpuModel: z.string().default('onnx-community/whisper-tiny')
|
|
141
|
+
.description('In-browser offline WebGPU Whisper model identifier (e.g. "onnx-community/whisper-tiny").'),
|
|
135
142
|
visualizerStyle: z.union(['liquid-wave', 'dynamic-orb', 'bars', 'off']).default('liquid-wave')
|
|
136
143
|
.description('Audio visualizer animation style in the recording pill: "liquid-wave", "dynamic-orb", "bars", or "off".'),
|
|
137
144
|
gatedTurnTaking: z.boolean().default(true)
|
|
@@ -230,3 +230,17 @@ export function registerSensevoiceInstaller(ctx, { liveConfig, isAlive, updateCo
|
|
|
230
230
|
},
|
|
231
231
|
})
|
|
232
232
|
}
|
|
233
|
+
|
|
234
|
+
export function buildSensevoiceArgs(cfg, port) {
|
|
235
|
+
const modelDir = cfg?.sensevoiceModel ? path.dirname(cfg.sensevoiceModel) : getDefaultModelDir()
|
|
236
|
+
const tokensPath = findTokensFile(modelDir)
|
|
237
|
+
const tokensArg = tokensPath ? ` --tokens=${JSON.stringify(tokensPath)}` : ''
|
|
238
|
+
const provider = (cfg?.sensevoiceProvider && cfg.sensevoiceProvider !== 'cpu')
|
|
239
|
+
? ` --provider=${cfg.sensevoiceProvider}`
|
|
240
|
+
: ''
|
|
241
|
+
const threads = Number(cfg?.sensevoiceThreads) || 4
|
|
242
|
+
return ` --sense-voice-model=${JSON.stringify(cfg?.sensevoiceModel || '')}`
|
|
243
|
+
+ tokensArg
|
|
244
|
+
+ ` --port=${port} --num-threads=${threads}`
|
|
245
|
+
+ provider
|
|
246
|
+
}
|
package/lib/transcribe-core.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @returns {{order: string[], models: Record<string, string>}}
|
|
12
12
|
*/
|
|
13
13
|
export function buildProviderOrder({ localOnly, chain, customKeys, knownKeys, defaultModels }) {
|
|
14
|
-
const localEngines = new Set(['local-whisper', 'sensevoice'])
|
|
14
|
+
const localEngines = new Set(['local-whisper', 'sensevoice', 'browser-webgpu'])
|
|
15
15
|
const source = localOnly
|
|
16
16
|
? (chain || []).filter((e) => e && localEngines.has(e.provider))
|
|
17
17
|
: (chain || [])
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.15",
|
|
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",
|