@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/client.js +1222 -1188
- package/lib/index.js +9 -4
- package/lib/providers.js +9 -7
- package/lib/schema.js +4 -0
- package/lib/transcribe-core.js +44 -1
- package/package.json +1 -1
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 =
|
|
161
|
-
|
|
162
|
-
:
|
|
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
|
-
+ (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
+ (
|
|
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)
|
package/lib/transcribe-core.js
CHANGED
|
@@ -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.
|
|
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",
|