@goodandready/dsh-voice 0.9.36 → 0.9.37

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/index.js CHANGED
@@ -17,6 +17,7 @@ import { registerConfigRoutes } from './config-routes.js'
17
17
  import { registerHostRoutes } from './host-routes.js'
18
18
  import { createConfigReader } from './config-compat.js'
19
19
  import { createStatsTracker, mergeContextVocabulary } from './stats.js'
20
+ import { buildTranscriptionPrompt } from './transcribe-core.js'
20
21
  import { registerTranscribeAudioTool } from './tool.js'
21
22
  import { createPolishText } from './polish.js'
22
23
  import { buildProviderOrder, sessionCommand } from './transcribe-core.js'
@@ -157,13 +158,17 @@ export function apply(ctx, baseConfig) {
157
158
  defaultModels: DEFAULT_MODELS,
158
159
  })
159
160
 
160
- const vocab = cfg.contextGlossary
161
- ? mergeContextVocabulary(cfg.vocabulary, contextWords)
162
- : (Array.isArray(cfg.vocabulary) ? cfg.vocabulary : [])
161
+ const { promptText, keywords, vocabulary: vocab } = buildTranscriptionPrompt({
162
+ vocabulary: cfg.vocabulary,
163
+ contextWords: cfg.contextGlossary ? contextWords : [],
164
+ bilingualDeveloperPrompt: cfg.bilingualDeveloperPrompt !== false,
165
+ bilingualCustomPrompt: cfg.bilingualCustomPrompt || '',
166
+ language: modeCfg.language,
167
+ })
163
168
 
164
169
  const providers = makeProviders(
165
170
  { resolveKey, fetchImpl: fetch, cfg, toWav: (b, sig) => toWav16k(b, cfg.ffmpegBin, sig || signal) },
166
- { bytes, mime, lang: modeCfg.language, signal, models, vocabulary: vocab },
171
+ { bytes, mime, lang: modeCfg.language, signal, models, vocabulary: vocab, promptText, keywords },
167
172
  )
168
173
  return runChain(order, providers, statsTracker.record)
169
174
  }
package/lib/providers.js CHANGED
@@ -87,10 +87,12 @@ function pickModel(models, key) {
87
87
 
88
88
  export function makeProviders(deps, req) {
89
89
  const { resolveKey, fetchImpl, cfg } = deps
90
- const { bytes, mime, lang, signal, models } = req
90
+ const { bytes, mime, lang, signal, models, promptText, keywords } = req
91
91
  const vocab = Array.isArray(req.vocabulary)
92
92
  ? req.vocabulary.map((w) => String(w || '').trim()).filter(Boolean).join(', ')
93
93
  : ''
94
+ const effectivePrompt = (typeof promptText === 'string' && promptText.trim()) ? promptText.trim() : vocab
95
+ const effectiveKeywords = (typeof keywords === 'string' && keywords.trim()) ? keywords.trim() : vocab
94
96
 
95
97
  async function deepgram() {
96
98
  const key = await resolveKey(cfg.deepgramKeyEnv)
@@ -99,7 +101,7 @@ export function makeProviders(deps, req) {
99
101
  const base = String(cfg.deepgramBaseUrl || 'https://api.deepgram.com').replace(/\/+$/, '')
100
102
  const url = `${base}/v1/listen?model=${encodeURIComponent(model)}`
101
103
  + (!isAutoLang(lang) ? `&language=${encodeURIComponent(lang)}` : '')
102
- + (vocab ? `&keywords=${encodeURIComponent(vocab)}` : '')
104
+ + (effectiveKeywords ? `&keywords=${encodeURIComponent(effectiveKeywords)}` : '')
103
105
  + '&smart_format=true'
104
106
  const res = await fetchImpl(url, {
105
107
  method: 'POST',
@@ -124,7 +126,7 @@ export function makeProviders(deps, req) {
124
126
  form.append('file', new Blob([bytes], { type: mime }), fileName(mime))
125
127
  form.append('model', pickModel(models, 'groq'))
126
128
  if (!isAutoLang(lang)) form.append('language', lang)
127
- if (vocab) form.append('prompt', vocab)
129
+ if (effectivePrompt) form.append('prompt', effectivePrompt)
128
130
  form.append('response_format', 'json')
129
131
  const res = await fetchImpl('https://api.groq.com/openai/v1/audio/transcriptions', {
130
132
  method: 'POST',
@@ -187,7 +189,7 @@ export function makeProviders(deps, req) {
187
189
  // Also override a server started with --translate: this plugin always needs
188
190
  // a transcript in the spoken language, never Whisper's English translation.
189
191
  form.append('translate', 'false')
190
- if (vocab) form.append('prompt', vocab)
192
+ if (effectivePrompt) form.append('prompt', effectivePrompt)
191
193
  form.append('response_format', 'json')
192
194
  const res = await fetchImpl(cfg.whisperUrl, { method: 'POST', body: form, signal })
193
195
  // whisper.cpp returns 400 with a JSON body on internal failures (e.g. decode
@@ -228,7 +230,7 @@ export function makeProviders(deps, req) {
228
230
  if (isOpenAI) {
229
231
  form.append('model', pickModel(models, 'sensevoice'))
230
232
  if (!isAutoLang(lang)) form.append('language', lang)
231
- if (vocab) form.append('prompt', vocab)
233
+ if (effectivePrompt) form.append('prompt', effectivePrompt)
232
234
  form.append('response_format', 'json')
233
235
  }
234
236
 
@@ -282,7 +284,7 @@ export function makeProviders(deps, req) {
282
284
  form.append('file', new Blob([bytes], { type: mime }), fileName(mime))
283
285
  form.append('model', model)
284
286
  if (lang && lang !== 'auto') form.append('language', lang)
285
- if (vocab) form.append('prompt', vocab)
287
+ if (effectivePrompt) form.append('prompt', effectivePrompt)
286
288
  form.append('response_format', 'json')
287
289
  const res = await fetchImpl(`${base}/audio/transcriptions`, {
288
290
  method: 'POST', headers, body: form, signal,
@@ -303,7 +305,7 @@ export function makeProviders(deps, req) {
303
305
  format = 'wav'
304
306
  }
305
307
  const ask = (spec.prompt || CHAT_AUDIO_PROMPT)
306
- + (vocab ? ` Vocabulary hints (spell these correctly): ${vocab}.` : '')
308
+ + (effectivePrompt ? ` Vocabulary hints (spell these correctly): ${effectivePrompt}.` : '')
307
309
  + (lang && lang !== 'auto' ? ` The audio language is ${lang}.` : '')
308
310
  const res = await fetchImpl(`${base}/chat/completions`, {
309
311
  method: 'POST',
package/lib/schema.js CHANGED
@@ -112,6 +112,10 @@ export const BaseConfig = z.object({
112
112
  .description('Custom words (names, terms) hinted to providers so they recognize them correctly.'),
113
113
  contextGlossary: z.boolean().default(true)
114
114
  .description('Automatically extract code words and terms from context and hint them to STT providers.'),
115
+ bilingualDeveloperPrompt: z.boolean().default(true)
116
+ .description('Inject bilingual developer primer (git, docker, npm, PR, etc.) into the STT prompt so mixed RU/EN technical speech is transcribed accurately.'),
117
+ bilingualCustomPrompt: z.string().default('')
118
+ .description('Optional custom terms or prompt primer for the bilingual developer prompt.'),
115
119
  noiseSuppression: z.boolean().default(true)
116
120
  .description('Enable hardware noise suppression, echo cancellation, and auto gain control.'),
117
121
  voiceCommands: z.boolean().default(false)
@@ -1,3 +1,4 @@
1
+ import { mergeContextVocabulary } from './stats.js'
1
2
  // Pure helpers for the transcribe path: chain selection and session commands.
2
3
 
3
4
  /**
@@ -47,4 +48,46 @@ export function sessionCommand(text) {
47
48
  if (re.test(t)) return cmd
48
49
  }
49
50
  return null
50
- }
51
+ }
52
+
53
+ export const DEFAULT_BILINGUAL_PROMPT = 'TypeScript, Python, JavaScript, Docker, Git commit, PR, push, merge, branch, npm, pnpm, API, JSON, async, await, bug, deploy, Linux, bash. Смешанная речь разработчика: русский язык с английскими терминами, командами и кодом.'
54
+
55
+ export function buildTranscriptionPrompt({
56
+ vocabulary = [],
57
+ contextWords = [],
58
+ bilingualDeveloperPrompt = true,
59
+ bilingualCustomPrompt = '',
60
+ language = 'ru',
61
+ maxWords = 50,
62
+ } = {}) {
63
+ const mergedVocab = mergeContextVocabulary(vocabulary, contextWords, maxWords)
64
+ const isRuOrAuto = !language || language === 'ru' || language === 'auto'
65
+ let primer = ''
66
+ if (bilingualDeveloperPrompt && isRuOrAuto) {
67
+ primer = (typeof bilingualCustomPrompt === 'string' && bilingualCustomPrompt.trim())
68
+ ? bilingualCustomPrompt.trim()
69
+ : DEFAULT_BILINGUAL_PROMPT
70
+ }
71
+ const vocabStr = mergedVocab.join(', ')
72
+ let promptText = ''
73
+ if (primer && vocabStr) {
74
+ promptText = `${primer} Термины: ${vocabStr}.`
75
+ } else if (primer) {
76
+ promptText = primer
77
+ } else {
78
+ promptText = vocabStr
79
+ }
80
+
81
+ const keywords = [...mergedVocab]
82
+ if (bilingualDeveloperPrompt && isRuOrAuto) {
83
+ for (const w of ['Docker', 'Git', 'commit', 'PR', 'merge', 'branch', 'pnpm', 'npm', 'API', 'JSON', 'TypeScript']) {
84
+ if (!keywords.includes(w)) keywords.push(w)
85
+ }
86
+ }
87
+
88
+ return {
89
+ vocabulary: mergedVocab,
90
+ promptText: promptText.trim(),
91
+ keywords: keywords.slice(0, maxWords).join(', '),
92
+ }
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.9.36",
3
+ "version": "0.9.37",
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",