@goodandready/dsh-voice 0.5.0 → 0.6.0

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
@@ -636,10 +636,20 @@ window.__ModuleLoader__.load({
636
636
  }
637
637
 
638
638
  // ------------------------------------------------------- settings page
639
- const BUILTIN = ['browser', 'deepgram', 'groq', 'hf', 'local-whisper']
639
+ const BUILTIN = [
640
+ 'browser', 'deepgram', 'groq', 'hf', 'local-whisper',
641
+ // Заготовки: адрес и модель уже прописаны на хосте, нужен только ключ.
642
+ 'openai', 'siliconflow', 'deepinfra', 'fireworks', 'mistral', 'openrouter',
643
+ ]
640
644
  const TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
641
645
  const MODEL_HINT = {
642
646
  browser: 'распознаёт сам браузер, ключ не нужен',
647
+ openai: 'whisper-1 · ключ OPENAI_API_KEY',
648
+ siliconflow: 'FunAudioLLM/SenseVoiceSmall · ключ SILICONFLOW_API_KEY',
649
+ deepinfra: 'openai/whisper-large-v3-turbo · ключ DEEPINFRA_API_KEY',
650
+ fireworks: 'whisper-v3-turbo · ключ FIREWORKS_API_KEY',
651
+ mistral: 'voxtral-mini-latest · ключ MISTRAL_API_KEY',
652
+ openrouter: 'google/gemini-2.5-flash · ключ OPENROUTER_API_KEY',
643
653
  deepgram: 'nova-2', groq: 'whisper-large-v3-turbo',
644
654
  hf: 'openai/whisper-large-v3', 'local-whisper': 'задаётся при запуске сервера',
645
655
  }
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.0",
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",