@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 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 = ['browser', 'deepgram', 'groq', 'hf', 'local-whisper']
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: one of ${PROVIDER_KEYS.join(', ')}, `
30
- + 'or the name of an entry from customProviders. '
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 (!PROVIDER_KEYS.includes(entry.provider) && !customKeys.includes(entry.provider)) continue
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: PROVIDER_KEYS.concat(
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
- if (!key || out[key]) continue
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.5.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",