@goodandready/dsh-voice 0.8.17 → 0.8.19

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/providers.js CHANGED
@@ -1,27 +1,24 @@
1
- // Провайдеры распознавания речи: четыре встроенных плюс любые свои, объявленные
2
- // в настройках. Чистые функции: сеть приходит параметром (fetchImpl), ключи —
3
- // через resolveKey, поэтому всё проверяется без реальных запросов.
1
+ // Speech recognition providers: built-ins plus custom ones from settings.
2
+ // Pure functions: network arrives as fetchImpl, keys via resolveKey, so
3
+ // everything is testable without real requests.
4
4
 
5
- // 'browser' стоит в этом списке, но работает не здесь: речь распознаёт сам
6
- // браузер, до хоста звук не доходит. Ключ нужен, чтобы такую цепочку принимал
7
- // и валидатор настроек, и перебор ниже — иначе цепочка ['browser', 'groq'] на
8
- // хосте оборвалась бы на первом же шаге вместо перехода к groq.
5
+ // 'browser' is listed for chain validation only: recognition runs in the
6
+ // page and audio never reaches the host. Without this key a ['browser','groq']
7
+ // chain would break on the host instead of falling through to groq.
9
8
  export const PROVIDER_KEYS = ['browser', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice']
10
9
 
11
- // Свой провайдер описывается одним из двух шаблонов, потому что
12
- // OpenAI-совместимые API разошлись: у OpenRouter, например, нет
13
- // /audio/transcriptions вовсе, и распознавание там идёт через чат.
10
+ // Custom providers use one of two templates because OpenAI-compatible APIs
11
+ // diverged: OpenRouter has no /audio/transcriptions and recognizes via chat.
14
12
  export const CUSTOM_TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
15
13
 
16
- // Готовые провайдеры: то же самое, что свой провайдер, только адрес, модель и
17
- // имя ключа уже проставлены. Нужен лишь ключ.
14
+ // Presets: the same as a custom provider with address, model and key name
15
+ // already filled in. Only the key itself is required.
18
16
  //
19
- // Каждый адрес проверен запросом без ключа: все отвечают 401 «дайте ключ», то
20
- // есть путь существует. Модель — разумная отправная точка, её можно заменить
21
- // в строке цепочки, не трогая остальное.
17
+ // Each URL answers 401 without a key, so the path exists. The model is a
18
+ // reasonable starting point and can be overridden per chain row.
22
19
  //
23
- // Своим провайдером с тем же именем можно перекрыть любую заготовку целиком:
24
- // это не встроенный движок, а всего лишь заранее заполненная анкета.
20
+ // A custom provider with the same name fully replaces a preset: presets are
21
+ // prefilled forms, not special engines.
25
22
  export const PRESET_PROVIDERS = {
26
23
  openai: {
27
24
  template: 'openai-transcriptions',
@@ -53,7 +50,7 @@ export const PRESET_PROVIDERS = {
53
50
  model: 'voxtral-mini-latest',
54
51
  keyEnv: 'MISTRAL_API_KEY',
55
52
  },
56
- // У OpenRouter нет /audio/transcriptions вовсе — только через чат.
53
+ // OpenRouter has no /audio/transcriptions at all — chat only.
57
54
  openrouter: {
58
55
  template: 'openai-chat-audio',
59
56
  baseURL: 'https://openrouter.ai/api/v1',
@@ -64,15 +61,15 @@ export const PRESET_PROVIDERS = {
64
61
 
65
62
  export const PRESET_KEYS = Object.keys(PRESET_PROVIDERS)
66
63
 
67
- /** Все имена, которые можно ставить в цепочку без объявления своего провайдера. */
64
+ /** All names usable in a chain without declaring a custom provider. */
68
65
  export const KNOWN_KEYS = PROVIDER_KEYS.concat(PRESET_KEYS)
69
66
 
70
67
  const CHAT_AUDIO_PROMPT =
71
68
  'Transcribe the audio verbatim. Reply with the transcript text only, '
72
69
  + 'without comments, quotes or formatting.'
73
70
 
74
- // Форматы, которые чат-шаблон принимает в input_audio. Всё остальное —
75
- // включая webm/opus, который пишет браузер, — сначала перегоняем в WAV.
71
+ // Formats the chat template accepts in input_audio. Everything else
72
+ // (including browser webm/opus) is converted to WAV first.
76
73
  function chatAudioFormat(mime) {
77
74
  if (mime.includes('wav')) return 'wav'
78
75
  if (mime.includes('mpeg') || mime.includes('mp3')) return 'mp3'
@@ -99,8 +96,8 @@ function fileName(mime) {
99
96
  return 'audio.webm'
100
97
  }
101
98
 
102
- // Автоязык: пусто, 'auto' или список ('ru,en') — провайдер определяет сам,
103
- // поле language не отправляется. whisper.cpp при этом получает -l auto.
99
+ // Auto language: empty, 'auto', or a list ('ru,en') means the provider
100
+ // detects language; the language field is omitted. whisper.cpp gets -l auto.
104
101
  function isAutoLang(lang) {
105
102
  return !lang || lang === 'auto' || String(lang).includes(',')
106
103
  }
@@ -167,8 +164,8 @@ export function makeProviders(deps, req) {
167
164
  return { ok: text.length > 0, provider: 'hf', text, reason: text ? '' : 'empty transcript' }
168
165
  }
169
166
 
170
- // whisper.cpp принимает только WAV — на webm/opus из браузера его сервер
171
- // отвечает "Invalid request". Перегоняем, если формат не WAV.
167
+ // whisper.cpp accepts WAV only — browser webm/opus gets "Invalid request".
168
+ // Convert when the format is not WAV.
172
169
  async function localWhisper() {
173
170
  let sendBytes = bytes
174
171
  let sendMime = mime
@@ -195,11 +192,11 @@ export function makeProviders(deps, req) {
195
192
  if (vocab) form.append('prompt', vocab)
196
193
  form.append('response_format', 'json')
197
194
  const res = await fetchImpl(cfg.whisperUrl, { method: 'POST', body: form, signal })
198
- // whisper.cpp отвечает 400 с JSON-телом на внутренних сбоях (например, не
199
- // смог декодировать аудио) — читаем причину, а не бросаем исключение.
195
+ // whisper.cpp returns 400 with a JSON body on internal failures (e.g. decode
196
+ // errors) — read the reason instead of throwing.
200
197
  if (!res.ok) {
201
198
  let detail = `HTTP ${res.status}`
202
- try { const e = await res.json(); if (e?.error) detail = e.error } catch { /* тело не json */ }
199
+ try { const e = await res.json(); if (e?.error) detail = e.error } catch { /* body is not json */ }
203
200
  return { ok: false, provider: 'local-whisper', reason: `local whisper: ${detail}` }
204
201
  }
205
202
  const data = await res.json()
@@ -207,8 +204,8 @@ export function makeProviders(deps, req) {
207
204
  return { ok: text.length > 0, provider: 'local-whisper', text, reason: text ? '' : 'empty transcript' }
208
205
  }
209
206
 
210
- // SenseVoice-ONNX / Sherpa-ONNX: сверхбыстрое локальное распознавание (~50-100ms).
211
- // Поддерживает эндпоинты sherpa-onnx (/api/v1/asr) и OpenAI-совместимые (/audio/transcriptions).
207
+ // SenseVoice-ONNX / Sherpa-ONNX: ultra-fast local recognition (~50-100ms).
208
+ // Supports sherpa-onnx (/api/v1/asr) and OpenAI-compatible endpoints.
212
209
  async function sensevoice() {
213
210
  const url = String(cfg.sensevoiceUrl || 'http://127.0.0.1:6006/api/v1/asr').trim()
214
211
  let sendBytes = bytes
@@ -244,7 +241,7 @@ export function makeProviders(deps, req) {
244
241
 
245
242
  if (!res.ok) {
246
243
  let detail = `HTTP ${res.status}`
247
- try { const e = await res.json(); if (e?.error) detail = e.error } catch { /* тело не json */ }
244
+ try { const e = await res.json(); if (e?.error) detail = e.error } catch { /* body is not json */ }
248
245
  return { ok: false, provider: 'sensevoice', reason: `sensevoice: ${detail}` }
249
246
  }
250
247
 
@@ -256,7 +253,7 @@ export function makeProviders(deps, req) {
256
253
  }
257
254
 
258
255
  let text = String(data?.text || data?.transcript || '').trim()
259
- // Очищаем служебные теги эмоций и звуковых событий SenseVoice (<|NEUTRAL|>, <|HAPPY|>, <|Speech|> и др.)
256
+ // Strip SenseVoice emotion/event tags (<|NEUTRAL|>, <|HAPPY|>, <|Speech|>, ...).
260
257
  text = text.replace(/<\|[^|>]+\|>/g, '').trim()
261
258
  return {
262
259
  ok: text.length > 0,
@@ -266,8 +263,8 @@ export function makeProviders(deps, req) {
266
263
  }
267
264
  }
268
265
 
269
- // Свой провайдер. Ключ в цепочке — его имя, поэтому в остальном коде он
270
- // ничем не отличается от встроенного.
266
+ // Custom provider. The chain key is its name, so the rest of the code
267
+ // treats it like a built-in.
271
268
  function customProvider(spec) {
272
269
  const label = spec.key
273
270
  const base = String(spec.baseURL || '').replace(/\/+$/, '')
@@ -338,27 +335,27 @@ export function makeProviders(deps, req) {
338
335
  ? await viaChatAudio(headers)
339
336
  : await viaTranscriptions(headers)
340
337
  } catch (e) {
341
- // Отказ одного провайдера не должен ронять цепочку — она сама решит,
342
- // идти дальше или сдаться.
338
+ // One provider failure must not abort the chain — the chain decides
339
+ // whether to continue or give up.
343
340
  return { ok: false, provider: label, reason: `${label}: ${String(e && e.message || e)}` }
344
341
  }
345
342
  return { ok: text.length > 0, provider: label, text, reason: text ? '' : 'empty transcript' }
346
343
  }
347
344
  }
348
345
 
349
- // Если звук всё-таки доехал до хоста с 'browser' в цепочке, значит
350
- // браузерная нога не сработала: отказываем понятно и идём к следующему.
346
+ // If audio still reached the host with 'browser' in the chain, the browser
347
+ // leg failed: fail clearly and move on.
351
348
  async function browser() {
352
349
  return {
353
350
  ok: false,
354
351
  provider: 'browser',
355
- reason: 'browser: распознавание идёт в браузере, на хосте его нет',
352
+ reason: 'browser: recognition runs in the page, the host has no browser engine',
356
353
  }
357
354
  }
358
355
 
359
356
  const out = { browser, deepgram, groq, hf, 'local-whisper': localWhisper, sensevoice }
360
357
 
361
- // Заготовки: те же свои провайдеры, только анкета заполнена заранее.
358
+ // Presets: custom providers with the form pre-filled.
362
359
  for (const key of PRESET_KEYS) {
363
360
  if (out[key]) continue
364
361
  out[key] = customProvider({ ...PRESET_PROVIDERS[key], key })
@@ -366,9 +363,8 @@ export function makeProviders(deps, req) {
366
363
 
367
364
  for (const spec of Array.isArray(cfg.customProviders) ? cfg.customProviders : []) {
368
365
  const key = String(spec && spec.key || '').trim()
369
- // Встроенные движки не перекрываем: опечатка в имени тихо подменила бы
370
- // рабочего провайдера в чужой цепочке. А заготовку перекрыть можно — она
371
- // для того и заготовка, чтобы её правили.
366
+ // Do not override built-in engines: a typo would silently replace a working
367
+ // provider in a chain. Presets may be overridden — that is their purpose.
372
368
  if (!key || PROVIDER_KEYS.includes(key)) continue
373
369
  out[key] = customProvider({ ...spec, key })
374
370
  }
package/lib/stats.js CHANGED
@@ -1,5 +1,5 @@
1
- // dsh-voice — чистые утилиты метрик провайдеров и объединения контекстного словаря.
2
- // Без cordis и без сети — тестируются юнит-тестами.
1
+ // dsh-voice — pure helpers for provider metrics and context vocabulary.
2
+ // No cordis and no network — unit-tested.
3
3
 
4
4
  export function createStatsTracker() {
5
5
  const stats = new Map()
package/lib/wav.js CHANGED
@@ -1,15 +1,15 @@
1
- // Перегон произвольного аудио в WAV 16 кГц моно.
1
+ // Convert arbitrary audio to 16 kHz mono WAV.
2
2
  //
3
- // Локальный whisper.cpp принимает только WAV: на webm/opus, который пишет
4
- // браузер, его сервер отвечает "Invalid request". API-провайдеры webm едят
5
- // как есть, поэтому конвертация нужна ровно для локальной ноги цепочки.
3
+ // Local whisper.cpp accepts WAV only: browser webm/opus gets "Invalid
4
+ // request". Cloud providers accept webm as-is, so conversion is only needed
5
+ // for the local leg of the chain.
6
6
 
7
7
  import { spawn } from 'node:child_process'
8
8
 
9
9
  /**
10
- * @param bytes {Buffer|Uint8Array} исходное аудио в любом контейнере
11
- * @param ffmpegBin {string} путь к ffmpeg
12
- * @returns {Promise<Buffer>} WAV 16 кГц моно
10
+ * @param bytes {Buffer|Uint8Array} source audio in any container
11
+ * @param ffmpegBin {string} ffmpeg binary path
12
+ * @returns {Promise<Buffer>} 16 kHz mono WAV
13
13
  */
14
14
  export function toWav16k(bytes, ffmpegBin = 'ffmpeg') {
15
15
  return new Promise((resolve, reject) => {
@@ -33,7 +33,7 @@ export function toWav16k(bytes, ffmpegBin = 'ffmpeg') {
33
33
  if (wav.length < 64) { reject(new Error('ffmpeg produced empty output')); return }
34
34
  resolve(wav)
35
35
  })
36
- proc.stdin.on('error', () => { /* ffmpeg закрыл вход раньше — код возврата всё расскажет */ })
36
+ proc.stdin.on('error', () => { /* ffmpeg closed stdin early — the exit code tells the story */ })
37
37
  proc.stdin.end(Buffer.from(bytes))
38
38
  })
39
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.8.17",
3
+ "version": "0.8.19",
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",
@@ -39,7 +39,9 @@
39
39
  "url": "https://github.com/GooDAnDReaDY/dsh-voice/issues"
40
40
  },
41
41
  "scripts": {
42
- "test": "node --test test/*.test.mjs"
42
+ "test": "node --test test/*.test.mjs",
43
+ "build:client": "node scripts/build-client.mjs",
44
+ "pretest": "node scripts/build-client.mjs"
43
45
  },
44
46
  "dsh": {
45
47
  "bundle": {