@goodandready/dsh-voice 0.5.0 → 0.6.1
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 +26 -0
- package/lib/client.js +28 -1
- package/lib/index.js +6 -5
- package/lib/providers.js +65 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,32 @@ Keys are read through the DSH credentials service (Settings → Credentials, or
|
|
|
40
40
|
`$DSH_HOME/.credentials.yaml`), falling back to the process environment. A
|
|
41
41
|
provider without a key is skipped, not fatal.
|
|
42
42
|
|
|
43
|
+
### Ready-made providers
|
|
44
|
+
|
|
45
|
+
Six providers are filled in already — put the name in a chain and add the key:
|
|
46
|
+
|
|
47
|
+
| Name | Model | Credential |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `openai` | `whisper-1` | `OPENAI_API_KEY` |
|
|
50
|
+
| `siliconflow` | `FunAudioLLM/SenseVoiceSmall` | `SILICONFLOW_API_KEY` |
|
|
51
|
+
| `deepinfra` | `openai/whisper-large-v3-turbo` | `DEEPINFRA_API_KEY` |
|
|
52
|
+
| `fireworks` | `whisper-v3-turbo` | `FIREWORKS_API_KEY` |
|
|
53
|
+
| `mistral` | `voxtral-mini-latest` | `MISTRAL_API_KEY` |
|
|
54
|
+
| `openrouter` | `google/gemini-2.5-flash` | `OPENROUTER_API_KEY` |
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
- id: dsh-voice
|
|
58
|
+
config:
|
|
59
|
+
message:
|
|
60
|
+
chain:
|
|
61
|
+
- provider: openai
|
|
62
|
+
- provider: local-whisper
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every endpoint was probed without a key before being written down: all six answered `401`, the answer of a path that exists and wants credentials. The model ids are starting points — override `model` in a chain row to change one.
|
|
66
|
+
|
|
67
|
+
A preset is the same form as a custom provider with the fields filled in, so a `customProviders` entry under the same name replaces it outright.
|
|
68
|
+
|
|
43
69
|
### Your own providers
|
|
44
70
|
|
|
45
71
|
Any OpenAI-compatible API can be added as a provider and used in the chains
|
package/lib/client.js
CHANGED
|
@@ -125,6 +125,18 @@ window.__ModuleLoader__.load({
|
|
|
125
125
|
actions.setDraft(draft ? draft + ' ' + text : text)
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Объявление о том, что человек заговорил.
|
|
129
|
+
//
|
|
130
|
+
// Нужно, чтобы озвучка немедленно замолчала: слушать ответ и говорить
|
|
131
|
+
// одновременно невозможно, а перекрикивать собственный плагин — глупо.
|
|
132
|
+
// Связи между плагинами нет: голос кричит в окно, кто хочет — слышит.
|
|
133
|
+
// Поэтому оба плагина работают и поодиночке.
|
|
134
|
+
function announceVoice(phase) {
|
|
135
|
+
try {
|
|
136
|
+
window.dispatchEvent(new CustomEvent('dsh-voice:speaking', { detail: { phase } }))
|
|
137
|
+
} catch (noEvents) { /* окна нет — значит и слушать некому */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
// ------------------------------------------------- распознавание в браузере
|
|
129
141
|
//
|
|
130
142
|
// Отдельная нога, не похожая на все остальные: речь распознаёт сам браузер,
|
|
@@ -318,6 +330,9 @@ window.__ModuleLoader__.load({
|
|
|
318
330
|
|
|
319
331
|
function startRecording(mode) {
|
|
320
332
|
if (voice.phase !== 'idle' && voice.phase !== 'error') return
|
|
333
|
+
// Кричим до открытия микрофона, а не после: чем раньше замолчит
|
|
334
|
+
// озвучка, тем меньше её попадёт в запись.
|
|
335
|
+
announceVoice('start')
|
|
321
336
|
voice.set({ phase: 'recording', mode, error: '', levels: [], caption: '' })
|
|
322
337
|
modeChain(mode).then((info) => {
|
|
323
338
|
// Браузерная нога — только если её прямо поставили первой в цепочке.
|
|
@@ -399,6 +414,7 @@ window.__ModuleLoader__.load({
|
|
|
399
414
|
}
|
|
400
415
|
|
|
401
416
|
function cancelCurrent() {
|
|
417
|
+
announceVoice('end')
|
|
402
418
|
if (voice.browser) {
|
|
403
419
|
voice.browser.abort()
|
|
404
420
|
voice.browser = null
|
|
@@ -418,6 +434,7 @@ window.__ModuleLoader__.load({
|
|
|
418
434
|
// Останов по второму нажатию: диктовка досылает хвост, голосовое —
|
|
419
435
|
// отправляет всю запись и открывает окно отмены.
|
|
420
436
|
function stopCurrent() {
|
|
437
|
+
announceVoice('end')
|
|
421
438
|
if (voice.browser) {
|
|
422
439
|
const mode = voice.mode
|
|
423
440
|
const said = (voice.browserFinals || []).join(' ').trim()
|
|
@@ -636,10 +653,20 @@ window.__ModuleLoader__.load({
|
|
|
636
653
|
}
|
|
637
654
|
|
|
638
655
|
// ------------------------------------------------------- settings page
|
|
639
|
-
const BUILTIN = [
|
|
656
|
+
const BUILTIN = [
|
|
657
|
+
'browser', 'deepgram', 'groq', 'hf', 'local-whisper',
|
|
658
|
+
// Заготовки: адрес и модель уже прописаны на хосте, нужен только ключ.
|
|
659
|
+
'openai', 'siliconflow', 'deepinfra', 'fireworks', 'mistral', 'openrouter',
|
|
660
|
+
]
|
|
640
661
|
const TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
|
|
641
662
|
const MODEL_HINT = {
|
|
642
663
|
browser: 'распознаёт сам браузер, ключ не нужен',
|
|
664
|
+
openai: 'whisper-1 · ключ OPENAI_API_KEY',
|
|
665
|
+
siliconflow: 'FunAudioLLM/SenseVoiceSmall · ключ SILICONFLOW_API_KEY',
|
|
666
|
+
deepinfra: 'openai/whisper-large-v3-turbo · ключ DEEPINFRA_API_KEY',
|
|
667
|
+
fireworks: 'whisper-v3-turbo · ключ FIREWORKS_API_KEY',
|
|
668
|
+
mistral: 'voxtral-mini-latest · ключ MISTRAL_API_KEY',
|
|
669
|
+
openrouter: 'google/gemini-2.5-flash · ключ OPENROUTER_API_KEY',
|
|
643
670
|
deepgram: 'nova-2', groq: 'whisper-large-v3-turbo',
|
|
644
671
|
hf: 'openai/whisper-large-v3', 'local-whisper': 'задаётся при запуске сервера',
|
|
645
672
|
}
|
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
|
18
18
|
import { readFile, stat } from 'node:fs/promises'
|
|
19
19
|
import path from 'node:path'
|
|
20
20
|
import { runChain } from './chain.js'
|
|
21
|
-
import { makeProviders, PROVIDER_KEYS, DEFAULT_MODELS, CUSTOM_TEMPLATES } from './providers.js'
|
|
21
|
+
import { makeProviders, PROVIDER_KEYS, PRESET_KEYS, KNOWN_KEYS, DEFAULT_MODELS, CUSTOM_TEMPLATES } from './providers.js'
|
|
22
22
|
import { toWav16k } from './wav.js'
|
|
23
23
|
|
|
24
24
|
export const name = 'dsh-voice'
|
|
@@ -26,8 +26,9 @@ export const inject = ['tools', 'credentials', 'webServer', 'shell', 'settings']
|
|
|
26
26
|
|
|
27
27
|
const ChainEntry = z.object({
|
|
28
28
|
provider: z.string().default('local-whisper')
|
|
29
|
-
.description(`Provider key
|
|
30
|
-
+
|
|
29
|
+
.description(`Provider key. Built in: ${PROVIDER_KEYS.join(', ')}. `
|
|
30
|
+
+ `Ready-made, just add the key: ${PRESET_KEYS.join(', ')}. `
|
|
31
|
+
+ 'Or the name of an entry from customProviders. '
|
|
31
32
|
+ '"browser" recognises speech in the page itself — no key, no upload to this host, '
|
|
32
33
|
+ 'text appears while you speak; put a normal provider after it as a fallback.'),
|
|
33
34
|
model: z.string().default('')
|
|
@@ -187,7 +188,7 @@ export function apply(ctx, baseConfig) {
|
|
|
187
188
|
const models = {}
|
|
188
189
|
const order = []
|
|
189
190
|
for (const entry of Array.isArray(modeCfg.chain) ? modeCfg.chain : []) {
|
|
190
|
-
if (!
|
|
191
|
+
if (!KNOWN_KEYS.includes(entry.provider) && !customKeys.includes(entry.provider)) continue
|
|
191
192
|
order.push(entry.provider)
|
|
192
193
|
// Для своего провайдера модель по умолчанию живёт в его описании,
|
|
193
194
|
// подставит makeProviders — здесь пусто означает «бери оттуда».
|
|
@@ -211,7 +212,7 @@ export function apply(ctx, baseConfig) {
|
|
|
211
212
|
whisperRunning: await whisperAlive(),
|
|
212
213
|
// Клавиша нужна браузерной половине: она вешает обработчик удержания.
|
|
213
214
|
hotkey: cfg.hotkey,
|
|
214
|
-
providers:
|
|
215
|
+
providers: KNOWN_KEYS.concat(
|
|
215
216
|
(Array.isArray(cfg.customProviders) ? cfg.customProviders : [])
|
|
216
217
|
.map((c) => String(c && c.key || '').trim()).filter(Boolean),
|
|
217
218
|
),
|
package/lib/providers.js
CHANGED
|
@@ -13,6 +13,60 @@ export const PROVIDER_KEYS = ['browser', 'deepgram', 'groq', 'hf', 'local-whispe
|
|
|
13
13
|
// /audio/transcriptions вовсе, и распознавание там идёт через чат.
|
|
14
14
|
export const CUSTOM_TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
|
|
15
15
|
|
|
16
|
+
// Готовые провайдеры: то же самое, что свой провайдер, только адрес, модель и
|
|
17
|
+
// имя ключа уже проставлены. Нужен лишь ключ.
|
|
18
|
+
//
|
|
19
|
+
// Каждый адрес проверен запросом без ключа: все отвечают 401 «дайте ключ», то
|
|
20
|
+
// есть путь существует. Модель — разумная отправная точка, её можно заменить
|
|
21
|
+
// в строке цепочки, не трогая остальное.
|
|
22
|
+
//
|
|
23
|
+
// Своим провайдером с тем же именем можно перекрыть любую заготовку целиком:
|
|
24
|
+
// это не встроенный движок, а всего лишь заранее заполненная анкета.
|
|
25
|
+
export const PRESET_PROVIDERS = {
|
|
26
|
+
openai: {
|
|
27
|
+
template: 'openai-transcriptions',
|
|
28
|
+
baseURL: 'https://api.openai.com/v1',
|
|
29
|
+
model: 'whisper-1',
|
|
30
|
+
keyEnv: 'OPENAI_API_KEY',
|
|
31
|
+
},
|
|
32
|
+
siliconflow: {
|
|
33
|
+
template: 'openai-transcriptions',
|
|
34
|
+
baseURL: 'https://api.siliconflow.cn/v1',
|
|
35
|
+
model: 'FunAudioLLM/SenseVoiceSmall',
|
|
36
|
+
keyEnv: 'SILICONFLOW_API_KEY',
|
|
37
|
+
},
|
|
38
|
+
deepinfra: {
|
|
39
|
+
template: 'openai-transcriptions',
|
|
40
|
+
baseURL: 'https://api.deepinfra.com/v1/openai',
|
|
41
|
+
model: 'openai/whisper-large-v3-turbo',
|
|
42
|
+
keyEnv: 'DEEPINFRA_API_KEY',
|
|
43
|
+
},
|
|
44
|
+
fireworks: {
|
|
45
|
+
template: 'openai-transcriptions',
|
|
46
|
+
baseURL: 'https://api.fireworks.ai/inference/v1',
|
|
47
|
+
model: 'whisper-v3-turbo',
|
|
48
|
+
keyEnv: 'FIREWORKS_API_KEY',
|
|
49
|
+
},
|
|
50
|
+
mistral: {
|
|
51
|
+
template: 'openai-transcriptions',
|
|
52
|
+
baseURL: 'https://api.mistral.ai/v1',
|
|
53
|
+
model: 'voxtral-mini-latest',
|
|
54
|
+
keyEnv: 'MISTRAL_API_KEY',
|
|
55
|
+
},
|
|
56
|
+
// У OpenRouter нет /audio/transcriptions вовсе — только через чат.
|
|
57
|
+
openrouter: {
|
|
58
|
+
template: 'openai-chat-audio',
|
|
59
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
60
|
+
model: 'google/gemini-2.5-flash',
|
|
61
|
+
keyEnv: 'OPENROUTER_API_KEY',
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const PRESET_KEYS = Object.keys(PRESET_PROVIDERS)
|
|
66
|
+
|
|
67
|
+
/** Все имена, которые можно ставить в цепочку без объявления своего провайдера. */
|
|
68
|
+
export const KNOWN_KEYS = PROVIDER_KEYS.concat(PRESET_KEYS)
|
|
69
|
+
|
|
16
70
|
const CHAT_AUDIO_PROMPT =
|
|
17
71
|
'Transcribe the audio verbatim. Reply with the transcript text only, '
|
|
18
72
|
+ 'without comments, quotes or formatting.'
|
|
@@ -225,11 +279,19 @@ export function makeProviders(deps, req) {
|
|
|
225
279
|
}
|
|
226
280
|
|
|
227
281
|
const out = { browser, deepgram, groq, hf, 'local-whisper': localWhisper }
|
|
282
|
+
|
|
283
|
+
// Заготовки: те же свои провайдеры, только анкета заполнена заранее.
|
|
284
|
+
for (const key of PRESET_KEYS) {
|
|
285
|
+
if (out[key]) continue
|
|
286
|
+
out[key] = customProvider({ ...PRESET_PROVIDERS[key], key })
|
|
287
|
+
}
|
|
288
|
+
|
|
228
289
|
for (const spec of Array.isArray(cfg.customProviders) ? cfg.customProviders : []) {
|
|
229
290
|
const key = String(spec && spec.key || '').trim()
|
|
230
|
-
// Встроенные не перекрываем:
|
|
231
|
-
// провайдера в чужой цепочке.
|
|
232
|
-
|
|
291
|
+
// Встроенные движки не перекрываем: опечатка в имени тихо подменила бы
|
|
292
|
+
// рабочего провайдера в чужой цепочке. А заготовку перекрыть можно — она
|
|
293
|
+
// для того и заготовка, чтобы её правили.
|
|
294
|
+
if (!key || PROVIDER_KEYS.includes(key)) continue
|
|
233
295
|
out[key] = customProvider({ ...spec, key })
|
|
234
296
|
}
|
|
235
297
|
return out
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|