@goodandready/dsh-voice 0.9.33 → 0.9.34
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 +105 -25
- package/lib/host-routes.js +66 -3
- package/lib/stats.js +51 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -71,16 +71,10 @@ window.__ModuleLoader__.load({
|
|
|
71
71
|
'webgpuStatus': 'Browser WebGPU Status',
|
|
72
72
|
'webgpuAvailable': 'WebGPU Available (hardware acceleration ready)',
|
|
73
73
|
'webgpuNotAvailable': 'WebGPU Not Detected (will fallback to server)',
|
|
74
|
-
'openaiHint': 'whisper-1 · key OPENAI_API_KEY',
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'mistralHint': 'voxtral-mini-latest · key MISTRAL_API_KEY',
|
|
79
|
-
'openrouterHint': 'google/gemini-2.5-flash · key OPENROUTER_API_KEY',
|
|
80
|
-
'localHint': 'set when the server is started',
|
|
81
|
-
'up': 'Up',
|
|
82
|
-
'down': 'Down',
|
|
83
|
-
'remove': 'Remove',
|
|
74
|
+
'openaiHint': 'whisper-1 · key OPENAI_API_KEY', 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
|
|
75
|
+
'deepinfraHint': 'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY', 'fireworksHint': 'whisper-v3-turbo · key FIREWORKS_API_KEY',
|
|
76
|
+
'mistralHint': 'voxtral-mini-latest · key MISTRAL_API_KEY', 'openrouterHint': 'google/gemini-2.5-flash · key OPENROUTER_API_KEY',
|
|
77
|
+
'localHint': 'set when the server is started', 'up': 'Up', 'down': 'Down', 'remove': 'Remove',
|
|
84
78
|
'addProvider': 'Add provider',
|
|
85
79
|
'chainHint': 'Top to bottom is the order they are tried in',
|
|
86
80
|
'customName': 'name used in the chain',
|
|
@@ -322,6 +316,12 @@ window.__ModuleLoader__.load({
|
|
|
322
316
|
'wizardFinishSuccess': 'Setup & Verification Complete',
|
|
323
317
|
'wizardSkip': 'Skip Wizard',
|
|
324
318
|
'wizardStatusCompleted': 'Microphone and STT verified',
|
|
319
|
+
'exportDiagnosticsBtn': 'Export Diagnostics Report', 'exportingDiagnostics': 'Exporting report…',
|
|
320
|
+
'diagnosticsExportSuccess': 'Report downloaded', 'recentRecognitionsTitle': 'Recent Recognition Telemetry',
|
|
321
|
+
'noRecognitionsYet': 'No recent recognition records yet.', 'diagLatencyBreakdown': 'Latency breakdown:',
|
|
322
|
+
'diagVad': 'VAD: {ms}ms', 'diagStt': 'STT: {ms}ms', 'diagPolish': 'Polish: {ms}ms',
|
|
323
|
+
'diagTotal': 'Total: {ms}ms', 'diagBytes': 'Audio: {bytes} B', 'diagWords': 'Words: {count}',
|
|
324
|
+
'diagExportNotice': 'Exported JSON contains anonymized timings, provider chain, MIME format, and error codes without raw audio or speech transcripts.',
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
const zh = {
|
|
@@ -361,16 +361,10 @@ window.__ModuleLoader__.load({
|
|
|
361
361
|
'webgpuStatus': '浏览器 WebGPU 状态',
|
|
362
362
|
'webgpuAvailable': 'WebGPU 可用 (硬件加速就绪)',
|
|
363
363
|
'webgpuNotAvailable': '未检测到 WebGPU (将自动回退到服务端)',
|
|
364
|
-
'openaiHint': 'whisper-1 · 密钥 OPENAI_API_KEY',
|
|
365
|
-
'
|
|
366
|
-
'
|
|
367
|
-
'
|
|
368
|
-
'mistralHint': 'voxtral-mini-latest · 密钥 MISTRAL_API_KEY',
|
|
369
|
-
'openrouterHint': 'google/gemini-2.5-flash · 密钥 OPENROUTER_API_KEY',
|
|
370
|
-
'localHint': '服务器启动时配置',
|
|
371
|
-
'up': '上移',
|
|
372
|
-
'down': '下移',
|
|
373
|
-
'remove': '移除',
|
|
364
|
+
'openaiHint': 'whisper-1 · 密钥 OPENAI_API_KEY', 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · 密钥 SILICONFLOW_API_KEY',
|
|
365
|
+
'deepinfraHint': 'openai/whisper-large-v3-turbo · 密钥 DEEPINFRA_API_KEY', 'fireworksHint': 'whisper-v3-turbo · 密钥 FIREWORKS_API_KEY',
|
|
366
|
+
'mistralHint': 'voxtral-mini-latest · 密钥 MISTRAL_API_KEY', 'openrouterHint': 'google/gemini-2.5-flash · 密钥 OPENROUTER_API_KEY',
|
|
367
|
+
'localHint': '服务器启动时配置', 'up': '上移', 'down': '下移', 'remove': '移除',
|
|
374
368
|
'addProvider': '添加服务商',
|
|
375
369
|
'chainHint': '自上而下为故障转移尝试顺序',
|
|
376
370
|
'customName': '备用链中使用的名称',
|
|
@@ -612,6 +606,12 @@ window.__ModuleLoader__.load({
|
|
|
612
606
|
'wizardFinishSuccess': '设置与验证已完成',
|
|
613
607
|
'wizardSkip': '跳过向导',
|
|
614
608
|
'wizardStatusCompleted': '麦克风与 STT 诊断已就绪',
|
|
609
|
+
'exportDiagnosticsBtn': '导出诊断报告', 'exportingDiagnostics': '正在导出报告…',
|
|
610
|
+
'diagnosticsExportSuccess': '诊断报告已下载', 'recentRecognitionsTitle': '近期识别遥测详情',
|
|
611
|
+
'noRecognitionsYet': '暂无近期识别记录。', 'diagLatencyBreakdown': '耗时分布:',
|
|
612
|
+
'diagVad': 'VAD:{ms}毫秒', 'diagStt': 'STT:{ms}毫秒', 'diagPolish': '后处理:{ms}毫秒',
|
|
613
|
+
'diagTotal': '总耗时:{ms}毫秒', 'diagBytes': '音频大小:{bytes} 字节', 'diagWords': '词数:{count}',
|
|
614
|
+
'diagExportNotice': '导出的 JSON 报告包含脱敏的耗时统计、备用链尝试、MIME 格式与错误码,不含任何原始音频或语音文本。',
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
// Strings are also needed outside components — in recording handlers and
|
|
@@ -3160,18 +3160,61 @@ function cycleDictationMode() {
|
|
|
3160
3160
|
),
|
|
3161
3161
|
)
|
|
3162
3162
|
}
|
|
3163
|
-
// Provider Telemetry & Latency Dashboard
|
|
3163
|
+
// Provider Telemetry & Latency Dashboard (#266)
|
|
3164
3164
|
function ProviderDashboard(props) {
|
|
3165
3165
|
const t = props.t || moduleT
|
|
3166
3166
|
const statsData = props.statsData || {}
|
|
3167
|
+
const recentDiagnostics = props.recentDiagnostics || []
|
|
3168
|
+
const [exporting, setExporting] = React.useState(false)
|
|
3169
|
+
const [exportMsg, setExportMsg] = React.useState('')
|
|
3170
|
+
|
|
3171
|
+
const exportReport = async () => {
|
|
3172
|
+
setExporting(true)
|
|
3173
|
+
setExportMsg('')
|
|
3174
|
+
try {
|
|
3175
|
+
const res = await fetch('/dsh-voice/diagnostics/export')
|
|
3176
|
+
const data = await res.json()
|
|
3177
|
+
if (!res.ok || !data || !data.ok) {
|
|
3178
|
+
throw new Error((data && data.error && data.error.message) || `HTTP ${res.status}`)
|
|
3179
|
+
}
|
|
3180
|
+
const blob = new Blob([JSON.stringify(data.report || {}, null, 2)], { type: 'application/json' })
|
|
3181
|
+
const url = URL.createObjectURL(blob)
|
|
3182
|
+
const a = document.createElement('a')
|
|
3183
|
+
a.href = url
|
|
3184
|
+
a.download = `dsh-voice-diagnostics-${new Date().toISOString().replace(/[:.]/g, '-')}.json`
|
|
3185
|
+
document.body.appendChild(a)
|
|
3186
|
+
a.click()
|
|
3187
|
+
document.body.removeChild(a)
|
|
3188
|
+
URL.revokeObjectURL(url)
|
|
3189
|
+
setExportMsg('✓ ' + t('diagnosticsExportSuccess'))
|
|
3190
|
+
} catch (err) {
|
|
3191
|
+
setExportMsg(String(err && err.message ? err.message : err))
|
|
3192
|
+
} finally {
|
|
3193
|
+
setExporting(false)
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3167
3197
|
return React.createElement('div', { className: 'cb-section-card' },
|
|
3168
3198
|
React.createElement('div', { className: 'cb-section-title' },
|
|
3169
3199
|
React.createElement('span', null, '📊 ' + t('providerDashboard')),
|
|
3170
|
-
React.createElement('
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3200
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
3201
|
+
React.createElement('button', {
|
|
3202
|
+
type: 'button',
|
|
3203
|
+
className: 'cb-btn',
|
|
3204
|
+
style: { fontSize: '12px', padding: '4px 10px' },
|
|
3205
|
+
disabled: exporting,
|
|
3206
|
+
onClick: exportReport,
|
|
3207
|
+
}, exporting ? t('exportingDiagnostics') : ('📥 ' + t('exportDiagnosticsBtn'))),
|
|
3208
|
+
React.createElement('button', {
|
|
3209
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
3210
|
+
onClick: props.onRefresh,
|
|
3211
|
+
}, '🔄 ' + t('refreshStats')),
|
|
3212
|
+
),
|
|
3174
3213
|
),
|
|
3214
|
+
exportMsg ? React.createElement('div', {
|
|
3215
|
+
className: exportMsg.startsWith('✓') ? 'dvs-ok' : 'cb-alert-err',
|
|
3216
|
+
style: { fontSize: '12px' },
|
|
3217
|
+
}, exportMsg) : null,
|
|
3175
3218
|
React.createElement('div', { className: 'dvo-dash' },
|
|
3176
3219
|
React.createElement('div', { className: 'dvo-dash-grid' },
|
|
3177
3220
|
Object.keys(statsData).length === 0
|
|
@@ -3211,6 +3254,41 @@ function cycleDictationMode() {
|
|
|
3211
3254
|
}),
|
|
3212
3255
|
),
|
|
3213
3256
|
),
|
|
3257
|
+
// Per-recognition telemetry breakdown list (#266)
|
|
3258
|
+
React.createElement('div', { style: { marginTop: '12px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '10px' } },
|
|
3259
|
+
React.createElement('div', { className: 'dvs-h', style: { marginBottom: '8px' } }, t('recentRecognitionsTitle')),
|
|
3260
|
+
recentDiagnostics.length === 0
|
|
3261
|
+
? React.createElement('div', { className: 'dvs-sub' }, t('noRecognitionsYet'))
|
|
3262
|
+
: React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
|
|
3263
|
+
recentDiagnostics.slice(-5).reverse().map((d) => React.createElement('div', {
|
|
3264
|
+
key: d.id,
|
|
3265
|
+
className: 'dvs-card',
|
|
3266
|
+
style: { padding: '8px 12px', fontSize: '12px' },
|
|
3267
|
+
},
|
|
3268
|
+
React.createElement('div', { className: 'cb-row', style: { justifyContent: 'space-between', alignItems: 'center' } },
|
|
3269
|
+
React.createElement('div', { style: { display: 'flex', gap: '6px', alignItems: 'center' } },
|
|
3270
|
+
React.createElement('span', { className: 'cb-badge ' + (d.success ? 'cb-badge-ok' : 'cb-badge-err') },
|
|
3271
|
+
d.success ? '✓ ' + (d.provider || 'STT') : '✗ ' + (d.error ? d.error.slice(0, 30) : 'fail')
|
|
3272
|
+
),
|
|
3273
|
+
React.createElement('span', { className: 'dvs-sub' }, `[${d.mode}/${d.subMode}]`),
|
|
3274
|
+
d.audioBytes ? React.createElement('span', { className: 'dvs-sub' }, t('diagBytes').replace('{bytes}', d.audioBytes)) : null,
|
|
3275
|
+
),
|
|
3276
|
+
React.createElement('span', { style: { fontWeight: '600' } },
|
|
3277
|
+
t('diagTotal').replace('{ms}', d.totalTookMs)
|
|
3278
|
+
),
|
|
3279
|
+
),
|
|
3280
|
+
React.createElement('div', { className: 'cb-row', style: { gap: '12px', marginTop: '4px', fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
3281
|
+
React.createElement('span', null, t('diagVad').replace('{ms}', (d.breakdown && d.breakdown.vadMs) || 0)),
|
|
3282
|
+
React.createElement('span', null, t('diagStt').replace('{ms}', (d.breakdown && d.breakdown.sttMs) || 0)),
|
|
3283
|
+
d.breakdown && d.breakdown.polishMs ? React.createElement('span', null, t('diagPolish').replace('{ms}', d.breakdown.polishMs)) : null,
|
|
3284
|
+
typeof d.wordCount === 'number' ? React.createElement('span', null, t('diagWords').replace('{count}', d.wordCount)) : null,
|
|
3285
|
+
),
|
|
3286
|
+
))
|
|
3287
|
+
),
|
|
3288
|
+
React.createElement('div', { className: 'dvs-sub', style: { marginTop: '8px', fontStyle: 'italic' } },
|
|
3289
|
+
t('diagExportNotice')
|
|
3290
|
+
),
|
|
3291
|
+
),
|
|
3214
3292
|
)
|
|
3215
3293
|
}
|
|
3216
3294
|
// ------------------------------------------------------------------ 72-dictation-modes
|
|
@@ -4303,6 +4381,7 @@ function MacrosSection(props) {
|
|
|
4303
4381
|
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
4304
4382
|
)
|
|
4305
4383
|
const statsData = (statusData && statusData.providerStats) || {}
|
|
4384
|
+
const recentDiagnostics = (statusData && statusData.recentDiagnostics) || []
|
|
4306
4385
|
const hostOnline = statusLatency !== null
|
|
4307
4386
|
const whisperRunning = !!(statusData && statusData.whisperRunning)
|
|
4308
4387
|
const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
|
|
@@ -4535,6 +4614,7 @@ function MacrosSection(props) {
|
|
|
4535
4614
|
React.createElement(ProviderDashboard, {
|
|
4536
4615
|
t: t,
|
|
4537
4616
|
statsData: statsData,
|
|
4617
|
+
recentDiagnostics: recentDiagnostics,
|
|
4538
4618
|
onRefresh: fetchStatus,
|
|
4539
4619
|
}),
|
|
4540
4620
|
React.createElement(UpdaterCard, {
|
package/lib/host-routes.js
CHANGED
|
@@ -96,6 +96,7 @@ export function registerHostRoutes(ctx, deps) {
|
|
|
96
96
|
dictationPrompts: cfg.dictationPrompts || {},
|
|
97
97
|
voiceMacros: Array.isArray(cfg.voiceMacros) ? cfg.voiceMacros : [],
|
|
98
98
|
providerStats: statsTracker.get(),
|
|
99
|
+
recentDiagnostics: typeof statsTracker.getDiagnostics === 'function' ? statsTracker.getDiagnostics() : [],
|
|
99
100
|
})
|
|
100
101
|
},
|
|
101
102
|
}), 'dsh-voice: /status route')
|
|
@@ -133,6 +134,19 @@ export function registerHostRoutes(ctx, deps) {
|
|
|
133
134
|
},
|
|
134
135
|
}), 'dsh-voice: /polish route')
|
|
135
136
|
|
|
137
|
+
ctx.effect(() => ctx.webServer.register({
|
|
138
|
+
kind: 'exact',
|
|
139
|
+
path: '/dsh-voice/diagnostics/export',
|
|
140
|
+
handler: async (req, res) => {
|
|
141
|
+
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
142
|
+
if (!isTrustedCaller(req)) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
|
|
143
|
+
const report = typeof statsTracker.exportAnonymizedReport === 'function'
|
|
144
|
+
? statsTracker.exportAnonymizedReport({ version: '0.9.34', environment: 'production' })
|
|
145
|
+
: { summary: statsTracker.get(), recentDiagnostics: [] }
|
|
146
|
+
writeJson(res, 200, { ok: true, report })
|
|
147
|
+
},
|
|
148
|
+
}), 'dsh-voice: /diagnostics/export route')
|
|
149
|
+
|
|
136
150
|
ctx.effect(() => ctx.webServer.register({
|
|
137
151
|
kind: 'exact',
|
|
138
152
|
path: '/dsh-voice/transcribe',
|
|
@@ -178,13 +192,31 @@ export function registerHostRoutes(ctx, deps) {
|
|
|
178
192
|
await startSensevoice()
|
|
179
193
|
}
|
|
180
194
|
|
|
195
|
+
const reqStart = Date.now()
|
|
181
196
|
const controller = new AbortController()
|
|
182
197
|
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs)
|
|
198
|
+
let out = null
|
|
199
|
+
let polishTookMs = 0
|
|
183
200
|
try {
|
|
184
|
-
|
|
201
|
+
out = await transcribe(modeCfg, bytes, mime, controller.signal, payload.contextWords)
|
|
185
202
|
if (payload.mode === 'message' && modeCfg.sessionCommands === true) {
|
|
186
203
|
const cmd = sessionCommand(out.text)
|
|
187
|
-
if (cmd) {
|
|
204
|
+
if (cmd) {
|
|
205
|
+
if (typeof statsTracker.recordDiagnostic === 'function') {
|
|
206
|
+
statsTracker.recordDiagnostic({
|
|
207
|
+
mode: payload.mode || 'dictation',
|
|
208
|
+
subMode: 'command',
|
|
209
|
+
mimeType: mime,
|
|
210
|
+
audioBytes: bytes.length,
|
|
211
|
+
provider: out.provider,
|
|
212
|
+
success: true,
|
|
213
|
+
totalTookMs: Date.now() - reqStart,
|
|
214
|
+
sttMs: out.tookMs || 0,
|
|
215
|
+
vadMs: Number(payload.vadMs) || 0,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
writeJson(res, 200, { ok: true, command: cmd, provider: out.provider, tookMs: out.tookMs }); return
|
|
219
|
+
}
|
|
188
220
|
}
|
|
189
221
|
const subMode = payload.subMode || payload.dictationMode || cfg.dictationMode || 'clean'
|
|
190
222
|
const rawText = out.text
|
|
@@ -192,7 +224,9 @@ export function registerHostRoutes(ctx, deps) {
|
|
|
192
224
|
|
|
193
225
|
// Dictation modes & polish (#264): verbatim skips LLM entirely (0 LLM calls)
|
|
194
226
|
if (subMode !== 'verbatim' && (modeCfg.polish === true || payload.polish === true)) {
|
|
227
|
+
const polishStart = Date.now()
|
|
195
228
|
text = await polishText(rawText, { ...modeCfg, subMode, prompt: payload.prompt }, controller.signal)
|
|
229
|
+
polishTookMs = Date.now() - polishStart
|
|
196
230
|
}
|
|
197
231
|
|
|
198
232
|
// Voice action macros (#167)
|
|
@@ -211,8 +245,37 @@ export function registerHostRoutes(ctx, deps) {
|
|
|
211
245
|
action = actionRes.action
|
|
212
246
|
}
|
|
213
247
|
}
|
|
214
|
-
|
|
248
|
+
const totalTookMs = Date.now() - reqStart
|
|
249
|
+
if (typeof statsTracker.recordDiagnostic === 'function') {
|
|
250
|
+
statsTracker.recordDiagnostic({
|
|
251
|
+
mode: payload.mode || 'dictation',
|
|
252
|
+
subMode,
|
|
253
|
+
mimeType: mime,
|
|
254
|
+
audioBytes: bytes.length,
|
|
255
|
+
provider: out.provider,
|
|
256
|
+
success: true,
|
|
257
|
+
totalTookMs,
|
|
258
|
+
sttMs: out.tookMs || 0,
|
|
259
|
+
polishMs: polishTookMs,
|
|
260
|
+
vadMs: Number(payload.vadMs) || 0,
|
|
261
|
+
wordCount: text ? text.split(/\s+/).filter(Boolean).length : 0,
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
writeJson(res, 200, { ok: true, text, rawText, subMode, action, provider: out.provider, tookMs: out.tookMs, polishTookMs, totalTookMs })
|
|
215
265
|
} catch (e) {
|
|
266
|
+
if (typeof statsTracker.recordDiagnostic === 'function') {
|
|
267
|
+
statsTracker.recordDiagnostic({
|
|
268
|
+
mode: payload.mode || 'dictation',
|
|
269
|
+
subMode: payload.subMode || 'clean',
|
|
270
|
+
mimeType: mime,
|
|
271
|
+
audioBytes: bytes.length,
|
|
272
|
+
provider: (out && out.provider) || 'unknown',
|
|
273
|
+
success: false,
|
|
274
|
+
totalTookMs: Date.now() - reqStart,
|
|
275
|
+
vadMs: Number(payload.vadMs) || 0,
|
|
276
|
+
error: String(e && e.message || e),
|
|
277
|
+
})
|
|
278
|
+
}
|
|
216
279
|
writeJson(res, 502, { ok: false, error: { code: 'chain', message: String(e && e.message || e) } })
|
|
217
280
|
} finally {
|
|
218
281
|
clearTimeout(timer)
|
package/lib/stats.js
CHANGED
|
@@ -3,6 +3,55 @@
|
|
|
3
3
|
|
|
4
4
|
export function createStatsTracker() {
|
|
5
5
|
const stats = new Map()
|
|
6
|
+
const diagnostics = []
|
|
7
|
+
const MAX_DIAGNOSTICS = 50
|
|
8
|
+
|
|
9
|
+
function recordDiagnostic(entry = {}) {
|
|
10
|
+
diagnostics.push({
|
|
11
|
+
id: Math.random().toString(36).slice(2, 10),
|
|
12
|
+
timestamp: Date.now(),
|
|
13
|
+
mode: entry.mode || 'dictation',
|
|
14
|
+
subMode: entry.subMode || 'clean',
|
|
15
|
+
mimeType: entry.mimeType || 'audio/webm',
|
|
16
|
+
audioBytes: Number(entry.audioBytes) || 0,
|
|
17
|
+
provider: entry.provider || 'unknown',
|
|
18
|
+
success: !!entry.success,
|
|
19
|
+
totalTookMs: Number(entry.totalTookMs) || 0,
|
|
20
|
+
breakdown: {
|
|
21
|
+
vadMs: Number(entry.vadMs) || 0,
|
|
22
|
+
sttMs: Number(entry.sttMs) || 0,
|
|
23
|
+
polishMs: Number(entry.polishMs) || 0,
|
|
24
|
+
},
|
|
25
|
+
chainAttempts: Array.isArray(entry.chainAttempts) ? entry.chainAttempts.map((a) => ({
|
|
26
|
+
provider: a.provider || 'unknown',
|
|
27
|
+
ok: !!a.ok,
|
|
28
|
+
tookMs: Number(a.tookMs) || 0,
|
|
29
|
+
reason: a.reason ? String(a.reason).slice(0, 150) : undefined,
|
|
30
|
+
})) : [],
|
|
31
|
+
error: entry.error ? String(entry.error).slice(0, 200) : undefined,
|
|
32
|
+
wordCount: typeof entry.wordCount === 'number' ? entry.wordCount : undefined,
|
|
33
|
+
})
|
|
34
|
+
if (diagnostics.length > MAX_DIAGNOSTICS) {
|
|
35
|
+
diagnostics.shift()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getDiagnostics() {
|
|
40
|
+
return diagnostics.slice()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exportAnonymizedReport(appInfo = {}) {
|
|
44
|
+
return {
|
|
45
|
+
exportedAt: new Date().toISOString(),
|
|
46
|
+
app: {
|
|
47
|
+
name: '@goodandready/dsh-voice',
|
|
48
|
+
version: appInfo.version || '0.9.34',
|
|
49
|
+
environment: appInfo.environment || 'production',
|
|
50
|
+
},
|
|
51
|
+
summary: get(),
|
|
52
|
+
recentDiagnostics: getDiagnostics(),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
6
55
|
|
|
7
56
|
function record(key, res = {}) {
|
|
8
57
|
if (!key) return
|
|
@@ -51,9 +100,10 @@ export function createStatsTracker() {
|
|
|
51
100
|
|
|
52
101
|
function clear() {
|
|
53
102
|
stats.clear()
|
|
103
|
+
diagnostics.length = 0
|
|
54
104
|
}
|
|
55
105
|
|
|
56
|
-
return { record, get, clear }
|
|
106
|
+
return { record, get, clear, recordDiagnostic, getDiagnostics, exportAnonymizedReport }
|
|
57
107
|
}
|
|
58
108
|
|
|
59
109
|
export function mergeContextVocabulary(baseVocab = [], contextWords = [], maxWords = 50) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.34",
|
|
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",
|