@goodandready/dsh-voice 0.9.6 → 0.9.8
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 +747 -630
- package/lib/index.js +29 -26
- package/lib/sensevoice-installer.js +1 -1
- package/package.json +5 -5
package/lib/client.js
CHANGED
|
@@ -2020,14 +2020,20 @@ window.__ModuleLoader__.load({
|
|
|
2020
2020
|
// locale: NS is required — the core resolves `label` through the inject face,
|
|
2021
2021
|
// and without it the label throws "cannot get property t without inject" while
|
|
2022
2022
|
// the slot snapshot is built, breaking the whole client batch.
|
|
2023
|
-
ctx.
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2023
|
+
ctx.effect(() => {
|
|
2024
|
+
const offRight = ctx.slots.inject('conversation.input.right', () => ctx.slots.register(
|
|
2025
|
+
{ name: 'conversation.input.right', id: '@goodandready/dsh-voice', order: 6, locale: NS, label: () => t('title') },
|
|
2026
|
+
(props) => React.createElement(VoiceButtons, { input: props.input, inputActions: props.inputActions }),
|
|
2027
|
+
))
|
|
2028
|
+
const offDock = ctx.slots.inject('conversation.input.dock', () => ctx.slots.register(
|
|
2029
|
+
{ name: 'conversation.input.dock', id: 'dsh-voice-rec', order: 0, locale: NS, label: () => t('recordSlot') },
|
|
2030
|
+
(props) => React.createElement(RecordPill, { input: props.input, inputActions: props.inputActions, ctx: ctx }),
|
|
2031
|
+
))
|
|
2032
|
+
return () => {
|
|
2033
|
+
if (typeof offRight === 'function') offRight()
|
|
2034
|
+
if (typeof offDock === 'function') offDock()
|
|
2035
|
+
}
|
|
2036
|
+
}, 'dsh-voice: composer slots')
|
|
2031
2037
|
}
|
|
2032
2038
|
|
|
2033
2039
|
// ------------------------------------------------------- settings page
|
|
@@ -2158,6 +2164,7 @@ window.__ModuleLoader__.load({
|
|
|
2158
2164
|
|
|
2159
2165
|
// One chain editor: provider+model rows with reordering.
|
|
2160
2166
|
function ChainEditor(props) {
|
|
2167
|
+
const t = (props && props.t) || moduleT
|
|
2161
2168
|
const rows = Array.isArray(props.value) ? props.value : []
|
|
2162
2169
|
const change = (i, patch) => {
|
|
2163
2170
|
const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
|
|
@@ -2198,6 +2205,7 @@ window.__ModuleLoader__.load({
|
|
|
2198
2205
|
|
|
2199
2206
|
// Custom providers: name, API template, where to call, how to authorize.
|
|
2200
2207
|
function CustomEditor(props) {
|
|
2208
|
+
const t = (props && props.t) || moduleT
|
|
2201
2209
|
const rows = Array.isArray(props.value) ? props.value : []
|
|
2202
2210
|
const change = (i, patch) => props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
|
|
2203
2211
|
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
@@ -2216,7 +2224,7 @@ window.__ModuleLoader__.load({
|
|
|
2216
2224
|
React.createElement('select', {
|
|
2217
2225
|
value: row.template || 'openai-transcriptions', disabled: !props.writable,
|
|
2218
2226
|
onChange: (e) => change(i, { template: e.target.value }),
|
|
2219
|
-
}, TEMPLATES.map((
|
|
2227
|
+
}, TEMPLATES.map((tmpl) => React.createElement('option', { key: tmpl, value: tmpl }, tmpl))),
|
|
2220
2228
|
React.createElement('button', {
|
|
2221
2229
|
type: 'button', className: 'dvs-mini', title: t('remove'),
|
|
2222
2230
|
disabled: !props.writable, onClick: () => remove(i),
|
|
@@ -2244,6 +2252,598 @@ window.__ModuleLoader__.load({
|
|
|
2244
2252
|
),
|
|
2245
2253
|
)
|
|
2246
2254
|
}
|
|
2255
|
+
// Provider Telemetry & Latency Dashboard
|
|
2256
|
+
function ProviderDashboard(props) {
|
|
2257
|
+
const t = props.t || moduleT
|
|
2258
|
+
const statsData = props.statsData || {}
|
|
2259
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
2260
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
2261
|
+
React.createElement('span', null, '📊 ' + t('providerDashboard')),
|
|
2262
|
+
React.createElement('button', {
|
|
2263
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
2264
|
+
onClick: props.onRefresh,
|
|
2265
|
+
}, '🔄 ' + t('refreshStats')),
|
|
2266
|
+
),
|
|
2267
|
+
React.createElement('div', { className: 'dvo-dash' },
|
|
2268
|
+
React.createElement('div', { className: 'dvo-dash-grid' },
|
|
2269
|
+
Object.keys(statsData).length === 0
|
|
2270
|
+
? React.createElement('div', { className: 'dvs-sub' }, t('idle'))
|
|
2271
|
+
: Object.entries(statsData).map(([key, item]) => {
|
|
2272
|
+
const avg = item.avgTookMs || 0
|
|
2273
|
+
const hasErrors = item.failures > 0
|
|
2274
|
+
let badgeClass = 'dvo-badge-idle'
|
|
2275
|
+
let badgeText = t('idle')
|
|
2276
|
+
if (item.attempts > 0) {
|
|
2277
|
+
if (hasErrors && item.successes === 0) {
|
|
2278
|
+
badgeClass = 'dvo-badge-err'
|
|
2279
|
+
badgeText = t('error')
|
|
2280
|
+
} else if (avg > 0 && avg < 400) {
|
|
2281
|
+
badgeClass = 'dvo-badge-fast'
|
|
2282
|
+
badgeText = avg + 'ms · ' + t('fast')
|
|
2283
|
+
} else if (avg >= 400 && avg <= 1500) {
|
|
2284
|
+
badgeClass = 'dvo-badge-norm'
|
|
2285
|
+
badgeText = avg + 'ms · ' + t('normal')
|
|
2286
|
+
} else {
|
|
2287
|
+
badgeClass = 'dvo-badge-slow'
|
|
2288
|
+
badgeText = avg + 'ms · ' + t('slow')
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const rate = item.attempts > 0 ? Math.round((item.successes / item.attempts) * 100) : 100
|
|
2292
|
+
return React.createElement('div', { key, className: 'dvo-dash-item', title: item.lastError ? `Error: ${item.lastError}` : '' },
|
|
2293
|
+
React.createElement('div', { className: 'dvo-dash-name' }, key),
|
|
2294
|
+
React.createElement('div', { className: 'dvo-dash-row' },
|
|
2295
|
+
React.createElement('span', { className: 'dvo-badge ' + badgeClass }, badgeText),
|
|
2296
|
+
React.createElement('span', null, rate + '%'),
|
|
2297
|
+
),
|
|
2298
|
+
React.createElement('div', { className: 'dvo-dash-row' },
|
|
2299
|
+
React.createElement('span', null, `${item.successes}/${item.attempts}`),
|
|
2300
|
+
React.createElement('span', null, t('successRate')),
|
|
2301
|
+
),
|
|
2302
|
+
)
|
|
2303
|
+
}),
|
|
2304
|
+
),
|
|
2305
|
+
),
|
|
2306
|
+
)
|
|
2307
|
+
}
|
|
2308
|
+
// 5. Hardware & Audio Engines Card
|
|
2309
|
+
function HardwareOptionsCard(props) {
|
|
2310
|
+
const t = props.t || moduleT
|
|
2311
|
+
const draft = props.draft
|
|
2312
|
+
const setTop = props.setTop
|
|
2313
|
+
const writable = props.writable
|
|
2314
|
+
const catching = props.catching
|
|
2315
|
+
const setCatching = props.setCatching
|
|
2316
|
+
const devices = props.devices || []
|
|
2317
|
+
const testingMic = props.testingMic
|
|
2318
|
+
const toggleTestMic = props.toggleTestMic
|
|
2319
|
+
const testLevel = props.testLevel || 0
|
|
2320
|
+
const gateDbVal = props.gateDbVal
|
|
2321
|
+
const gateThresholdPercent = props.gateThresholdPercent
|
|
2322
|
+
const onClearKey = props.onClearKey
|
|
2323
|
+
|
|
2324
|
+
const textField = (key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
2325
|
+
React.createElement('label', null, label),
|
|
2326
|
+
React.createElement('input', {
|
|
2327
|
+
type: 'text', value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
2328
|
+
onChange: (e) => setTop(key, e.target.value),
|
|
2329
|
+
}),
|
|
2330
|
+
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
2331
|
+
)
|
|
2332
|
+
|
|
2333
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
2334
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
2335
|
+
React.createElement('span', null, '⚙️ ' + t('hardwareTitle')),
|
|
2336
|
+
),
|
|
2337
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('hardwareDesc')),
|
|
2338
|
+
// Hotkey picker
|
|
2339
|
+
React.createElement('div', { className: 'cb-field' },
|
|
2340
|
+
React.createElement('label', null, t('hotkey')),
|
|
2341
|
+
React.createElement('div', { className: 'cb-row' },
|
|
2342
|
+
React.createElement('button', {
|
|
2343
|
+
type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable,
|
|
2344
|
+
onClick: () => setCatching(true),
|
|
2345
|
+
}, catching ? t('pressKey') : keyLabel(draft && draft.hotkey)),
|
|
2346
|
+
React.createElement('button', {
|
|
2347
|
+
type: 'button', className: 'cb-btn cb-btn-mini', title: t('clearKey'),
|
|
2348
|
+
disabled: !writable, onClick: onClearKey,
|
|
2349
|
+
}, '×'),
|
|
2350
|
+
),
|
|
2351
|
+
React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
|
|
2352
|
+
),
|
|
2353
|
+
// Mic & noise gate
|
|
2354
|
+
React.createElement('div', { className: 'cb-field' },
|
|
2355
|
+
React.createElement('label', null, t('mic')),
|
|
2356
|
+
React.createElement('div', { className: 'cb-row' },
|
|
2357
|
+
React.createElement('select', {
|
|
2358
|
+
value: String((draft && draft.micDeviceId) ?? ''), disabled: !writable || testingMic,
|
|
2359
|
+
onChange: (e) => setTop('micDeviceId', e.target.value),
|
|
2360
|
+
style: { flex: 1 },
|
|
2361
|
+
},
|
|
2362
|
+
React.createElement('option', { value: '' }, t('micDefault')),
|
|
2363
|
+
devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId },
|
|
2364
|
+
d.label || d.deviceId.slice(0, 12))),
|
|
2365
|
+
),
|
|
2366
|
+
React.createElement('button', {
|
|
2367
|
+
type: 'button',
|
|
2368
|
+
className: 'cb-btn ' + (testingMic ? 'cb-btn-primary' : 'cb-btn-secondary'),
|
|
2369
|
+
onClick: toggleTestMic,
|
|
2370
|
+
style: { flex: 'none' },
|
|
2371
|
+
}, testingMic ? t('stopTest') : t('testMic')),
|
|
2372
|
+
),
|
|
2373
|
+
testingMic ? React.createElement('div', { className: 'dvo-mic-test' },
|
|
2374
|
+
React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('micLevel') + ':'),
|
|
2375
|
+
React.createElement('div', { className: 'dvo-meter-bar' },
|
|
2376
|
+
React.createElement('div', { className: 'dvo-meter-fill', style: { width: `${testLevel}%` } }),
|
|
2377
|
+
gateThresholdPercent > 0 ? React.createElement('div', { className: 'dvo-meter-gate', style: { left: `${gateThresholdPercent}%` }, title: `${gateDbVal} dB` }) : null,
|
|
2378
|
+
),
|
|
2379
|
+
React.createElement('span', { style: { fontSize: '11px', fontVariantNumeric: 'tabular-nums', width: '32px', textAlign: 'right' } }, `${testLevel}%`),
|
|
2380
|
+
) : null,
|
|
2381
|
+
React.createElement('div', { className: 'cb-row', style: { marginTop: '6px' } },
|
|
2382
|
+
React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)', minWidth: '120px' } }, t('noiseGate') + ':'),
|
|
2383
|
+
React.createElement('select', {
|
|
2384
|
+
value: String(gateDbVal), disabled: !writable,
|
|
2385
|
+
onChange: (e) => setTop('noiseGateDb', Number(e.target.value)),
|
|
2386
|
+
style: { flex: 1 },
|
|
2387
|
+
},
|
|
2388
|
+
React.createElement('option', { value: '-999' }, t('noiseGateOff')),
|
|
2389
|
+
React.createElement('option', { value: '-50' }, '-50 dB (Low / Gentle)'),
|
|
2390
|
+
React.createElement('option', { value: '-45' }, '-45 dB (Standard Default)'),
|
|
2391
|
+
React.createElement('option', { value: '-35' }, '-35 dB (Medium Room)'),
|
|
2392
|
+
React.createElement('option', { value: '-25' }, '-25 dB (High / Mechanical Clicks)'),
|
|
2393
|
+
),
|
|
2394
|
+
),
|
|
2395
|
+
React.createElement('span', { className: 'dvs-sub' }, t('noiseGateHint')),
|
|
2396
|
+
),
|
|
2397
|
+
// Endpoint text fields
|
|
2398
|
+
React.createElement('div', { className: 'cb-grid-2' },
|
|
2399
|
+
textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
|
|
2400
|
+
textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
|
|
2401
|
+
textField('whisperModel', t('whisperModel'), t('whisperModelHint')),
|
|
2402
|
+
textField('deepgramBaseUrl', t('deepgramEndpoint'), t('deepgramEndpointHint')),
|
|
2403
|
+
textField('sensevoiceUrl', t('sensevoiceEndpoint'), t('sensevoiceEndpointHint')),
|
|
2404
|
+
textField('sensevoiceBin', t('sensevoiceBin'), t('sensevoiceBinHint')),
|
|
2405
|
+
textField('sensevoiceModel', t('sensevoiceModel'), t('sensevoiceModelHint')),
|
|
2406
|
+
textField('polishBaseUrl', t('polishBaseUrl'), t('polishBaseUrlHint')),
|
|
2407
|
+
textField('polishModel', t('polishModel'), ''),
|
|
2408
|
+
textField('polishKeyEnv', t('polishKeyEnv'), ''),
|
|
2409
|
+
),
|
|
2410
|
+
// Engine checkboxes
|
|
2411
|
+
React.createElement('div', { className: 'cb-row' },
|
|
2412
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
2413
|
+
React.createElement('input', {
|
|
2414
|
+
type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
|
|
2415
|
+
onChange: (e) => setTop('autoStart', e.target.checked),
|
|
2416
|
+
}),
|
|
2417
|
+
t('whisperAutostart'),
|
|
2418
|
+
),
|
|
2419
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
2420
|
+
React.createElement('input', {
|
|
2421
|
+
type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
|
|
2422
|
+
onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
|
|
2423
|
+
}),
|
|
2424
|
+
t('sensevoiceAutostart'),
|
|
2425
|
+
),
|
|
2426
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
2427
|
+
React.createElement('input', {
|
|
2428
|
+
type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
|
|
2429
|
+
onChange: (e) => setTop('localOnly', e.target.checked),
|
|
2430
|
+
}),
|
|
2431
|
+
t('localOnly'),
|
|
2432
|
+
),
|
|
2433
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
2434
|
+
React.createElement('input', {
|
|
2435
|
+
type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
|
|
2436
|
+
onChange: (e) => setTop('beep', e.target.checked),
|
|
2437
|
+
}),
|
|
2438
|
+
t('beep'),
|
|
2439
|
+
),
|
|
2440
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
2441
|
+
React.createElement('input', {
|
|
2442
|
+
type: 'checkbox', checked: draft ? draft.noiseSuppression !== false : true, disabled: !writable,
|
|
2443
|
+
onChange: (e) => setTop('noiseSuppression', e.target.checked),
|
|
2444
|
+
}),
|
|
2445
|
+
t('noiseSuppression'),
|
|
2446
|
+
),
|
|
2447
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
2448
|
+
React.createElement('input', {
|
|
2449
|
+
type: 'checkbox', checked: draft ? draft.contextGlossary !== false : true, disabled: !writable,
|
|
2450
|
+
onChange: (e) => setTop('contextGlossary', e.target.checked),
|
|
2451
|
+
}),
|
|
2452
|
+
t('contextGlossary'),
|
|
2453
|
+
),
|
|
2454
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
2455
|
+
React.createElement('input', {
|
|
2456
|
+
type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
|
|
2457
|
+
onChange: (e) => setTop('voiceCommands', e.target.checked),
|
|
2458
|
+
}),
|
|
2459
|
+
t('voiceCommandsLabel'),
|
|
2460
|
+
),
|
|
2461
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
2462
|
+
React.createElement('input', {
|
|
2463
|
+
type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
|
|
2464
|
+
onChange: (e) => setTop('normalizeTranscript', e.target.checked),
|
|
2465
|
+
}),
|
|
2466
|
+
t('normalizeTranscript'),
|
|
2467
|
+
),
|
|
2468
|
+
),
|
|
2469
|
+
// Visualizer select
|
|
2470
|
+
React.createElement('div', { className: 'cb-field' },
|
|
2471
|
+
React.createElement('label', null, t('visualizerStyle')),
|
|
2472
|
+
React.createElement('select', {
|
|
2473
|
+
value: String((draft && draft.visualizerStyle) || 'liquid-wave'), disabled: !writable,
|
|
2474
|
+
onChange: (e) => setTop('visualizerStyle', e.target.value),
|
|
2475
|
+
},
|
|
2476
|
+
React.createElement('option', { value: 'liquid-wave' }, t('visLiquidWave')),
|
|
2477
|
+
React.createElement('option', { value: 'dynamic-orb' }, t('visDynamicOrb')),
|
|
2478
|
+
React.createElement('option', { value: 'bars' }, t('visBars')),
|
|
2479
|
+
React.createElement('option', { value: 'off' }, t('visOff')),
|
|
2480
|
+
),
|
|
2481
|
+
),
|
|
2482
|
+
// Vocabulary textarea
|
|
2483
|
+
React.createElement('div', { className: 'cb-field' },
|
|
2484
|
+
React.createElement('label', null, t('vocabulary')),
|
|
2485
|
+
React.createElement('textarea', {
|
|
2486
|
+
rows: 3, disabled: !writable,
|
|
2487
|
+
value: Array.isArray(draft && draft.vocabulary) ? draft.vocabulary.join('\n') : '',
|
|
2488
|
+
onChange: (e) => setTop('vocabulary', e.target.value.split('\n').map((x) => x.trim()).filter(Boolean)),
|
|
2489
|
+
}),
|
|
2490
|
+
),
|
|
2491
|
+
)
|
|
2492
|
+
}
|
|
2493
|
+
// Custom hooks for VoiceSection modularity
|
|
2494
|
+
function useSensevoiceInstaller(fetchStatus) {
|
|
2495
|
+
const [sensevoiceState, setSensevoiceState] = React.useState(null)
|
|
2496
|
+
const [installingSensevoice, setInstallingSensevoice] = React.useState(false)
|
|
2497
|
+
|
|
2498
|
+
const fetchSensevoice = () => {
|
|
2499
|
+
fetch('/dsh-voice/sensevoice-installer')
|
|
2500
|
+
.then((r) => r.json())
|
|
2501
|
+
.then((d) => {
|
|
2502
|
+
setSensevoiceState(d)
|
|
2503
|
+
setInstallingSensevoice(!!(d && d.installing))
|
|
2504
|
+
})
|
|
2505
|
+
.catch(() => {})
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
React.useEffect(() => {
|
|
2509
|
+
fetchSensevoice()
|
|
2510
|
+
}, [])
|
|
2511
|
+
|
|
2512
|
+
const triggerInstallSensevoice = () => {
|
|
2513
|
+
setInstallingSensevoice(true)
|
|
2514
|
+
fetch('/dsh-voice/sensevoice-installer', {
|
|
2515
|
+
method: 'POST',
|
|
2516
|
+
headers: {
|
|
2517
|
+
'x-dsh-plugin-update': '1',
|
|
2518
|
+
'Content-Type': 'application/json',
|
|
2519
|
+
},
|
|
2520
|
+
})
|
|
2521
|
+
.then((r) => r.json())
|
|
2522
|
+
.then(() => {
|
|
2523
|
+
const timer = setInterval(() => {
|
|
2524
|
+
fetch('/dsh-voice/sensevoice-installer')
|
|
2525
|
+
.then((r) => r.json())
|
|
2526
|
+
.then((d) => {
|
|
2527
|
+
setSensevoiceState(d)
|
|
2528
|
+
if (!d.installing) {
|
|
2529
|
+
clearInterval(timer)
|
|
2530
|
+
setInstallingSensevoice(false)
|
|
2531
|
+
if (typeof fetchStatus === 'function') fetchStatus()
|
|
2532
|
+
}
|
|
2533
|
+
})
|
|
2534
|
+
.catch(() => clearInterval(timer))
|
|
2535
|
+
}, 2000)
|
|
2536
|
+
})
|
|
2537
|
+
.catch(() => setInstallingSensevoice(false))
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
return { sensevoiceState, installingSensevoice, triggerInstallSensevoice }
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
function useMicTester(draft, value, setErr) {
|
|
2544
|
+
const [testingMic, setTestingMic] = React.useState(false)
|
|
2545
|
+
const [testLevel, setTestLevel] = React.useState(0)
|
|
2546
|
+
const testRef = React.useRef(null)
|
|
2547
|
+
|
|
2548
|
+
const toggleTestMic = async () => {
|
|
2549
|
+
if (testingMic) {
|
|
2550
|
+
if (testRef.current) {
|
|
2551
|
+
testRef.current.active = false
|
|
2552
|
+
try { testRef.current.stream.getTracks().forEach((t) => t.stop()) } catch (e) { /* already stopped */ }
|
|
2553
|
+
if (testRef.current.audioCtx) { try { testRef.current.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
2554
|
+
testRef.current = null
|
|
2555
|
+
}
|
|
2556
|
+
setTestingMic(false)
|
|
2557
|
+
setTestLevel(0)
|
|
2558
|
+
return
|
|
2559
|
+
}
|
|
2560
|
+
try {
|
|
2561
|
+
const devId = (draft && draft.micDeviceId) || (value && value.micDeviceId) || ''
|
|
2562
|
+
const audio = { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
|
|
2563
|
+
if (devId) audio.deviceId = { exact: devId }
|
|
2564
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio })
|
|
2565
|
+
const AC = typeof AudioContext !== 'undefined' ? AudioContext : (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
|
|
2566
|
+
if (!AC) return
|
|
2567
|
+
const audioCtx = new AC()
|
|
2568
|
+
if (audioCtx.state === 'suspended') await audioCtx.resume().catch(() => {})
|
|
2569
|
+
const src = audioCtx.createMediaStreamSource(stream)
|
|
2570
|
+
let lastNode = src
|
|
2571
|
+
try {
|
|
2572
|
+
const filter = audioCtx.createBiquadFilter()
|
|
2573
|
+
filter.type = 'highpass'
|
|
2574
|
+
filter.frequency.value = 80
|
|
2575
|
+
filter.Q.value = 0.707
|
|
2576
|
+
src.connect(filter)
|
|
2577
|
+
lastNode = filter
|
|
2578
|
+
} catch (e) { /* filter fallback */ }
|
|
2579
|
+
const analyser = audioCtx.createAnalyser()
|
|
2580
|
+
analyser.fftSize = 128
|
|
2581
|
+
lastNode.connect(analyser)
|
|
2582
|
+
testRef.current = { stream, audioCtx, analyser, active: true }
|
|
2583
|
+
setTestingMic(true)
|
|
2584
|
+
|
|
2585
|
+
const check = () => {
|
|
2586
|
+
if (!testRef.current || !testRef.current.active) return
|
|
2587
|
+
const arr = new Uint8Array(analyser.frequencyBinCount)
|
|
2588
|
+
analyser.getByteFrequencyData(arr)
|
|
2589
|
+
let sum = 0
|
|
2590
|
+
for (let i = 0; i < arr.length; i++) sum += arr[i]
|
|
2591
|
+
const avg = sum / (arr.length || 1)
|
|
2592
|
+
setTestLevel(Math.min(100, Math.round((avg / 128) * 100)))
|
|
2593
|
+
requestAnimationFrame(check)
|
|
2594
|
+
}
|
|
2595
|
+
requestAnimationFrame(check)
|
|
2596
|
+
} catch (e) {
|
|
2597
|
+
if (typeof setErr === 'function') setErr(String(e && e.message ? e.message : e))
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
React.useEffect(() => {
|
|
2602
|
+
return () => {
|
|
2603
|
+
if (testRef.current) {
|
|
2604
|
+
testRef.current.active = false
|
|
2605
|
+
try { testRef.current.stream.getTracks().forEach((t) => t.stop()) } catch (e) { /* already stopped */ }
|
|
2606
|
+
if (testRef.current.audioCtx) { try { testRef.current.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
2607
|
+
testRef.current = null
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}, [])
|
|
2611
|
+
|
|
2612
|
+
const gateDbVal = Number((draft && draft.noiseGateDb !== undefined) ? draft.noiseGateDb : (value && value.noiseGateDb !== undefined) ? value.noiseGateDb : -45)
|
|
2613
|
+
const gateThresholdPercent = gateDbVal <= -90 ? 0 : Math.min(100, Math.max(0, Math.round(((gateDbVal + 60) / 40) * 100)))
|
|
2614
|
+
|
|
2615
|
+
return { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent }
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
function usePluginUpdater() {
|
|
2619
|
+
const [updaterStatus, setUpdaterStatus] = React.useState(null)
|
|
2620
|
+
const [updating, setUpdating] = React.useState(false)
|
|
2621
|
+
const [updaterMsg, setUpdaterMsg] = React.useState('')
|
|
2622
|
+
const [updaterLoading, setUpdaterLoading] = React.useState(false)
|
|
2623
|
+
|
|
2624
|
+
const checkUpdater = React.useCallback(async () => {
|
|
2625
|
+
setUpdaterLoading(true)
|
|
2626
|
+
setUpdaterMsg('')
|
|
2627
|
+
try {
|
|
2628
|
+
const res = await fetch('/api/dsh-voice/update', {
|
|
2629
|
+
headers: { accept: 'application/json' },
|
|
2630
|
+
cache: 'no-store',
|
|
2631
|
+
})
|
|
2632
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
2633
|
+
const data = await res.json()
|
|
2634
|
+
setUpdaterStatus(data)
|
|
2635
|
+
} catch (e) {
|
|
2636
|
+
/* best effort update check */
|
|
2637
|
+
} finally {
|
|
2638
|
+
setUpdaterLoading(false)
|
|
2639
|
+
}
|
|
2640
|
+
}, [])
|
|
2641
|
+
|
|
2642
|
+
const onUpdateNow = async () => {
|
|
2643
|
+
if (updating) return
|
|
2644
|
+
setUpdating(true)
|
|
2645
|
+
setUpdaterMsg('')
|
|
2646
|
+
try {
|
|
2647
|
+
const res = await fetch('/api/dsh-voice/update', {
|
|
2648
|
+
method: 'POST',
|
|
2649
|
+
headers: {
|
|
2650
|
+
'content-type': 'application/json',
|
|
2651
|
+
'x-dsh-plugin-update': '1',
|
|
2652
|
+
},
|
|
2653
|
+
cache: 'no-store',
|
|
2654
|
+
})
|
|
2655
|
+
const data = await res.json().catch(() => ({}))
|
|
2656
|
+
if (!res.ok || data.ok === false) {
|
|
2657
|
+
throw new Error(data.error || `HTTP ${res.status}`)
|
|
2658
|
+
}
|
|
2659
|
+
setUpdaterMsg(data.message || 'Update completed successfully.')
|
|
2660
|
+
checkUpdater()
|
|
2661
|
+
} catch (err) {
|
|
2662
|
+
setUpdaterMsg(err && err.message ? err.message : String(err))
|
|
2663
|
+
} finally {
|
|
2664
|
+
setUpdating(false)
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
return { updaterStatus, updating, updaterMsg, updaterLoading, checkUpdater, onUpdateNow }
|
|
2669
|
+
}
|
|
2670
|
+
// Developer Lexicon & IT Jargon
|
|
2671
|
+
function JargonEditor(props) {
|
|
2672
|
+
const t = props.t || moduleT
|
|
2673
|
+
const draft = props.draft
|
|
2674
|
+
const setTop = props.setTop
|
|
2675
|
+
const writable = props.writable
|
|
2676
|
+
const [jargonInput, setJargonInput] = React.useState(null)
|
|
2677
|
+
|
|
2678
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
2679
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
2680
|
+
React.createElement('span', null, '📖 ' + t('jargonTitle')),
|
|
2681
|
+
),
|
|
2682
|
+
React.createElement('label', { className: 'cb-check-label', style: { marginBottom: '8px' } },
|
|
2683
|
+
React.createElement('input', {
|
|
2684
|
+
type: 'checkbox',
|
|
2685
|
+
checked: draft ? draft.techJargonCorrection !== false : true,
|
|
2686
|
+
disabled: !writable,
|
|
2687
|
+
onChange: (e) => setTop('techJargonCorrection', e.target.checked),
|
|
2688
|
+
}),
|
|
2689
|
+
t('techJargonCorrection'),
|
|
2690
|
+
),
|
|
2691
|
+
React.createElement('div', { className: 'cb-field' },
|
|
2692
|
+
React.createElement('label', null, t('jargonAdd') + ' (spoken -> replacement)'),
|
|
2693
|
+
React.createElement('textarea', {
|
|
2694
|
+
rows: 3,
|
|
2695
|
+
disabled: !writable,
|
|
2696
|
+
placeholder: t('jargonPlaceholder'),
|
|
2697
|
+
value: jargonInput !== null
|
|
2698
|
+
? jargonInput
|
|
2699
|
+
: (Array.isArray(draft && draft.jargonDictionary)
|
|
2700
|
+
? draft.jargonDictionary.map((item) => item && item.from ? `${item.from} -> ${item.to}` : '').filter(Boolean).join('\n')
|
|
2701
|
+
: ''),
|
|
2702
|
+
onChange: (e) => setJargonInput(e.target.value),
|
|
2703
|
+
onBlur: () => {
|
|
2704
|
+
if (jargonInput === null) return
|
|
2705
|
+
const lines = jargonInput.split('\n')
|
|
2706
|
+
const parsed = lines
|
|
2707
|
+
.map((line) => {
|
|
2708
|
+
const parts = line.split(/->|=|:/)
|
|
2709
|
+
if (parts.length >= 2) {
|
|
2710
|
+
const from = parts[0].trim()
|
|
2711
|
+
const to = parts.slice(1).join(':').trim()
|
|
2712
|
+
if (from && to) return { from, to }
|
|
2713
|
+
}
|
|
2714
|
+
return null
|
|
2715
|
+
})
|
|
2716
|
+
.filter(Boolean)
|
|
2717
|
+
setTop('jargonDictionary', parsed)
|
|
2718
|
+
},
|
|
2719
|
+
}),
|
|
2720
|
+
),
|
|
2721
|
+
)
|
|
2722
|
+
}
|
|
2723
|
+
// SenseVoice Local ASR (1-Click Setup)
|
|
2724
|
+
function SensevoiceSection(props) {
|
|
2725
|
+
const t = props.t || moduleT
|
|
2726
|
+
const sensevoiceState = props.sensevoiceState
|
|
2727
|
+
const installingSensevoice = props.installingSensevoice
|
|
2728
|
+
const writable = props.writable
|
|
2729
|
+
const onInstall = props.onInstall
|
|
2730
|
+
|
|
2731
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
2732
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
2733
|
+
React.createElement('span', null, '⚡ ' + t('sensevoiceTitle')),
|
|
2734
|
+
sensevoiceState && sensevoiceState.installed
|
|
2735
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, t('sensevoiceReady') + ' (small-int8)')
|
|
2736
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
2737
|
+
installingSensevoice ? t('sensevoiceDownloading') : t('sensevoiceNotInstalled')
|
|
2738
|
+
),
|
|
2739
|
+
),
|
|
2740
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('sensevoiceDesc')),
|
|
2741
|
+
React.createElement('div', { className: 'dvo-installer-box' },
|
|
2742
|
+
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', justifyContent: 'space-between', gap: '12px' } },
|
|
2743
|
+
React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-content-secondary)' } },
|
|
2744
|
+
sensevoiceState && sensevoiceState.path
|
|
2745
|
+
? ('~/' + sensevoiceState.path.replace(/^.*[\\/](\.dsh[\\/]models[\\/].*)$/, '$1'))
|
|
2746
|
+
: '~/.dsh/models/sensevoice'
|
|
2747
|
+
),
|
|
2748
|
+
React.createElement('button', {
|
|
2749
|
+
type: 'button',
|
|
2750
|
+
className: 'cb-btn cb-btn-primary',
|
|
2751
|
+
style: { fontSize: '12px', padding: '6px 12px' },
|
|
2752
|
+
disabled: installingSensevoice || !writable,
|
|
2753
|
+
onClick: onInstall,
|
|
2754
|
+
}, installingSensevoice ? ('⏳ ' + t('installingSensevoice')) : ('📥 ' + t('installSensevoice'))),
|
|
2755
|
+
),
|
|
2756
|
+
installingSensevoice
|
|
2757
|
+
? React.createElement('div', { style: { marginTop: '8px' } },
|
|
2758
|
+
React.createElement('div', { className: 'dvo-progress-bar' },
|
|
2759
|
+
React.createElement('div', { className: 'dvo-progress-bar-fill' })
|
|
2760
|
+
),
|
|
2761
|
+
React.createElement('div', { style: { fontSize: '11px', marginTop: '4px', color: 'var(--dsw-alias-content-secondary)' } },
|
|
2762
|
+
(sensevoiceState && sensevoiceState.progress) || t('sensevoiceDownloading')
|
|
2763
|
+
)
|
|
2764
|
+
)
|
|
2765
|
+
: null
|
|
2766
|
+
)
|
|
2767
|
+
)
|
|
2768
|
+
}
|
|
2769
|
+
// 1. Connection & Engine Status Card
|
|
2770
|
+
function ConnectionStatusCard(props) {
|
|
2771
|
+
const t = props.t || moduleT
|
|
2772
|
+
const hostOnline = props.hostOnline
|
|
2773
|
+
const statusLatency = props.statusLatency
|
|
2774
|
+
const whisperRunning = props.whisperRunning
|
|
2775
|
+
const providerCount = props.providerCount
|
|
2776
|
+
const onRefresh = props.onRefresh
|
|
2777
|
+
|
|
2778
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
2779
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
2780
|
+
React.createElement('span', null, t('statusTitle')),
|
|
2781
|
+
React.createElement('button', {
|
|
2782
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
2783
|
+
onClick: onRefresh,
|
|
2784
|
+
}, '🔄 ' + t('refreshStats')),
|
|
2785
|
+
),
|
|
2786
|
+
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', gap: '12px', flexWrap: 'wrap' } },
|
|
2787
|
+
React.createElement('span', { className: 'cb-badge ' + (hostOnline ? 'cb-badge-ok' : 'cb-badge-err') },
|
|
2788
|
+
hostOnline ? (`✓ ${t('hostOnline')} (${statusLatency}ms)`) : (`✗ ${t('hostOffline')}`)
|
|
2789
|
+
),
|
|
2790
|
+
React.createElement('span', { className: 'cb-badge ' + (whisperRunning ? 'cb-badge-ok' : 'cb-badge-warn') },
|
|
2791
|
+
whisperRunning ? (`✓ ${t('whisperReady')}`) : (`· ${t('whisperStopped')}`)
|
|
2792
|
+
),
|
|
2793
|
+
React.createElement('span', { className: 'cb-badge' }, `${providerCount} ${t('providersAvailable')}`),
|
|
2794
|
+
),
|
|
2795
|
+
)
|
|
2796
|
+
}
|
|
2797
|
+
// Plugin Version & Updates Card
|
|
2798
|
+
function UpdaterCard(props) {
|
|
2799
|
+
const t = props.t || moduleT
|
|
2800
|
+
const updaterStatus = props.updaterStatus
|
|
2801
|
+
const updaterLoading = props.updaterLoading
|
|
2802
|
+
const updating = props.updating
|
|
2803
|
+
const updaterMsg = props.updaterMsg
|
|
2804
|
+
const onCheck = props.onCheck
|
|
2805
|
+
const onUpdateNow = props.onUpdateNow
|
|
2806
|
+
|
|
2807
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
2808
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
2809
|
+
React.createElement('span', null, '📦 ' + t('versionTitle')),
|
|
2810
|
+
React.createElement('button', {
|
|
2811
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
2812
|
+
onClick: onCheck, disabled: updaterLoading || updating,
|
|
2813
|
+
}, updaterLoading ? '⏳…' : '🔄 ' + t('refreshStats')),
|
|
2814
|
+
),
|
|
2815
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('versionHint')),
|
|
2816
|
+
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', gap: '12px' } },
|
|
2817
|
+
React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
2818
|
+
'v' + ((updaterStatus && updaterStatus.currentVersion) || '0.9.6')
|
|
2819
|
+
),
|
|
2820
|
+
updaterStatus && updaterStatus.updateAvailable
|
|
2821
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
2822
|
+
t('updateAvailable') + ': v' + updaterStatus.latestVersion
|
|
2823
|
+
)
|
|
2824
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
2825
|
+
t('upToDate')
|
|
2826
|
+
),
|
|
2827
|
+
updaterStatus && updaterStatus.updateAvailable
|
|
2828
|
+
? React.createElement('button', {
|
|
2829
|
+
type: 'button', className: 'cb-btn cb-btn-primary', style: { fontSize: '12px', padding: '4px 10px' },
|
|
2830
|
+
onClick: onUpdateNow, disabled: updating,
|
|
2831
|
+
}, updating ? t('updating') : t('updateNow'))
|
|
2832
|
+
: null,
|
|
2833
|
+
),
|
|
2834
|
+
updaterMsg
|
|
2835
|
+
? React.createElement('div', {
|
|
2836
|
+
style: {
|
|
2837
|
+
marginTop: '8px',
|
|
2838
|
+
fontSize: '12px',
|
|
2839
|
+
color: updaterMsg.includes('fail') || updaterMsg.includes('失败')
|
|
2840
|
+
? 'var(--dsw-alias-state-warning-primary)'
|
|
2841
|
+
: 'var(--dsw-alias-state-success-primary)',
|
|
2842
|
+
},
|
|
2843
|
+
}, updaterMsg)
|
|
2844
|
+
: null,
|
|
2845
|
+
)
|
|
2846
|
+
}
|
|
2247
2847
|
// ------------------------------------------------------- settings voice section
|
|
2248
2848
|
function VoiceSection(props) {
|
|
2249
2849
|
const t = (props && props.t) || moduleT
|
|
@@ -2263,7 +2863,6 @@ window.__ModuleLoader__.load({
|
|
|
2263
2863
|
const [err, setErr] = React.useState('')
|
|
2264
2864
|
const [serverConfig, setServerConfig] = React.useState(null)
|
|
2265
2865
|
const [configLoading, setConfigLoading] = React.useState(true)
|
|
2266
|
-
const [jargonInput, setJargonInput] = React.useState(null)
|
|
2267
2866
|
|
|
2268
2867
|
React.useEffect(() => {
|
|
2269
2868
|
let alive = true
|
|
@@ -2336,165 +2935,55 @@ window.__ModuleLoader__.load({
|
|
|
2336
2935
|
} else if (ctx.settingsScope && typeof ctx.settingsScope.describe === 'function') {
|
|
2337
2936
|
ctx.settingsScope.describe().load()
|
|
2338
2937
|
}
|
|
2339
|
-
} catch (e) { /* service not up yet */ }
|
|
2340
|
-
}, 1000)
|
|
2341
|
-
return () => clearInterval(timer)
|
|
2342
|
-
}, [ready])
|
|
2343
|
-
|
|
2344
|
-
const deepClone = (obj) => {
|
|
2345
|
-
if (typeof structuredClone === 'function') {
|
|
2346
|
-
try { return structuredClone(obj) } catch (e) { /* fallback */ }
|
|
2347
|
-
}
|
|
2348
|
-
try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
|
|
2352
|
-
|
|
2353
|
-
React.useEffect(() => {
|
|
2354
|
-
if (!ready) return
|
|
2355
|
-
const val = value || {}
|
|
2356
|
-
voice.settings = Object.assign({}, voice.settings, {
|
|
2357
|
-
vadSilenceMs: Number(val.dictation && val.dictation.vadSilenceMs) || 700,
|
|
2358
|
-
autoSendMs: Number(val.message && val.message.autoSendMs) || 4000,
|
|
2359
|
-
beep: !!val.beep,
|
|
2360
|
-
micDeviceId: String(val.micDeviceId || ''),
|
|
2361
|
-
historyLimit: Number(val.historyLimit) || 20,
|
|
2362
|
-
voiceCommands: !!val.voiceCommands,
|
|
2363
|
-
sendDelayMs: Number(val.dictation && val.dictation.sendDelayMs) || 0,
|
|
2364
|
-
stream: !!(val.dictation && val.dictation.stream),
|
|
2365
|
-
streamChunkMs: Number(val.dictation && val.dictation.streamChunkMs) || 1200,
|
|
2366
|
-
vadAdapt: Number(val.dictation && val.dictation.vadAdapt) || 0,
|
|
2367
|
-
wakeWord: String(val.wakeWord || ''),
|
|
2368
|
-
bargeIn: !!val.bargeIn,
|
|
2369
|
-
polishSend: !!(val.message && val.message.polishSend),
|
|
2370
|
-
sessionCommands: !!(val.message && val.message.sessionCommands),
|
|
2371
|
-
polishBaseUrl: String(val.polishBaseUrl || ''),
|
|
2372
|
-
noiseSuppression: val.noiseSuppression !== false,
|
|
2373
|
-
noiseGateDb: Number(val.noiseGateDb !== undefined ? val.noiseGateDb : -45),
|
|
2374
|
-
contextGlossary: val.contextGlossary !== false,
|
|
2375
|
-
visualizerStyle: val.visualizerStyle || 'liquid-wave',
|
|
2376
|
-
gatedTurnTaking: val.gatedTurnTaking !== false,
|
|
2377
|
-
autoSendVisualRing: val.autoSendVisualRing !== false,
|
|
2378
|
-
voiceActions: val.voiceActions !== false,
|
|
2379
|
-
techJargonCorrection: val.techJargonCorrection !== false,
|
|
2380
|
-
liveInterimPreview: val.liveInterimPreview !== false,
|
|
2381
|
-
structuredPromptVoice: val.structuredPromptVoice !== false,
|
|
2382
|
-
})
|
|
2383
|
-
}, [ready, value])
|
|
2384
|
-
|
|
2385
|
-
const [sensevoiceState, setSensevoiceState] = React.useState(null)
|
|
2386
|
-
const [installingSensevoice, setInstallingSensevoice] = React.useState(false)
|
|
2387
|
-
|
|
2388
|
-
const fetchSensevoice = () => {
|
|
2389
|
-
fetch('/dsh-voice/sensevoice-installer')
|
|
2390
|
-
.then((r) => r.json())
|
|
2391
|
-
.then((d) => {
|
|
2392
|
-
setSensevoiceState(d)
|
|
2393
|
-
setInstallingSensevoice(!!(d && d.installing))
|
|
2394
|
-
})
|
|
2395
|
-
.catch(() => {})
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
React.useEffect(() => {
|
|
2399
|
-
fetchSensevoice()
|
|
2400
|
-
}, [])
|
|
2401
|
-
|
|
2402
|
-
const triggerInstallSensevoice = () => {
|
|
2403
|
-
setInstallingSensevoice(true)
|
|
2404
|
-
fetch('/dsh-voice/sensevoice-installer', {
|
|
2405
|
-
method: 'POST',
|
|
2406
|
-
headers: {
|
|
2407
|
-
'x-dsh-plugin-update': '1',
|
|
2408
|
-
'Content-Type': 'application/json',
|
|
2409
|
-
},
|
|
2410
|
-
})
|
|
2411
|
-
.then((r) => r.json())
|
|
2412
|
-
.then(() => {
|
|
2413
|
-
const timer = setInterval(() => {
|
|
2414
|
-
fetch('/dsh-voice/sensevoice-installer')
|
|
2415
|
-
.then((r) => r.json())
|
|
2416
|
-
.then((d) => {
|
|
2417
|
-
setSensevoiceState(d)
|
|
2418
|
-
if (!d.installing) {
|
|
2419
|
-
clearInterval(timer)
|
|
2420
|
-
setInstallingSensevoice(false)
|
|
2421
|
-
fetchStatus()
|
|
2422
|
-
}
|
|
2423
|
-
})
|
|
2424
|
-
.catch(() => clearInterval(timer))
|
|
2425
|
-
}, 2000)
|
|
2426
|
-
})
|
|
2427
|
-
.catch(() => setInstallingSensevoice(false))
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
// Interactive microphone tester
|
|
2431
|
-
const [testingMic, setTestingMic] = React.useState(false)
|
|
2432
|
-
const [testLevel, setTestLevel] = React.useState(0)
|
|
2433
|
-
const testRef = React.useRef(null)
|
|
2434
|
-
|
|
2435
|
-
const toggleTestMic = async () => {
|
|
2436
|
-
if (testingMic) {
|
|
2437
|
-
if (testRef.current) {
|
|
2438
|
-
testRef.current.active = false
|
|
2439
|
-
try { testRef.current.stream.getTracks().forEach((t) => t.stop()) } catch (e) { /* already stopped */ }
|
|
2440
|
-
if (testRef.current.audioCtx) { try { testRef.current.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
2441
|
-
testRef.current = null
|
|
2442
|
-
}
|
|
2443
|
-
setTestingMic(false)
|
|
2444
|
-
setTestLevel(0)
|
|
2445
|
-
return
|
|
2446
|
-
}
|
|
2447
|
-
try {
|
|
2448
|
-
const devId = (draft && draft.micDeviceId) || (value && value.micDeviceId) || ''
|
|
2449
|
-
const audio = { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
|
|
2450
|
-
if (devId) audio.deviceId = { exact: devId }
|
|
2451
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio })
|
|
2452
|
-
const AC = typeof AudioContext !== 'undefined' ? AudioContext : (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
|
|
2453
|
-
if (!AC) return
|
|
2454
|
-
const audioCtx = new AC()
|
|
2455
|
-
if (audioCtx.state === 'suspended') await audioCtx.resume().catch(() => {})
|
|
2456
|
-
const src = audioCtx.createMediaStreamSource(stream)
|
|
2457
|
-
let lastNode = src
|
|
2458
|
-
try {
|
|
2459
|
-
const filter = audioCtx.createBiquadFilter()
|
|
2460
|
-
filter.type = 'highpass'
|
|
2461
|
-
filter.frequency.value = 80
|
|
2462
|
-
filter.Q.value = 0.707
|
|
2463
|
-
src.connect(filter)
|
|
2464
|
-
lastNode = filter
|
|
2465
|
-
} catch (e) { /* filter fallback */ }
|
|
2466
|
-
const analyser = audioCtx.createAnalyser()
|
|
2467
|
-
analyser.fftSize = 128
|
|
2468
|
-
lastNode.connect(analyser)
|
|
2469
|
-
testRef.current = { stream, audioCtx, analyser, active: true }
|
|
2470
|
-
setTestingMic(true)
|
|
2938
|
+
} catch (e) { /* service not up yet */ }
|
|
2939
|
+
}, 1000)
|
|
2940
|
+
return () => clearInterval(timer)
|
|
2941
|
+
}, [ready])
|
|
2471
2942
|
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
analyser.getByteFrequencyData(arr)
|
|
2476
|
-
let sum = 0
|
|
2477
|
-
for (let i = 0; i < arr.length; i++) sum += arr[i]
|
|
2478
|
-
const avg = sum / (arr.length || 1)
|
|
2479
|
-
setTestLevel(Math.min(100, Math.round((avg / 128) * 100)))
|
|
2480
|
-
requestAnimationFrame(check)
|
|
2481
|
-
}
|
|
2482
|
-
requestAnimationFrame(check)
|
|
2483
|
-
} catch (e) {
|
|
2484
|
-
setErr(String(e && e.message ? e.message : e))
|
|
2943
|
+
const deepClone = (obj) => {
|
|
2944
|
+
if (typeof structuredClone === 'function') {
|
|
2945
|
+
try { return structuredClone(obj) } catch (e) { /* fallback */ }
|
|
2485
2946
|
}
|
|
2947
|
+
try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
|
|
2486
2948
|
}
|
|
2487
2949
|
|
|
2950
|
+
React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
|
|
2951
|
+
|
|
2488
2952
|
React.useEffect(() => {
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2953
|
+
if (!ready) return
|
|
2954
|
+
const val = value || {}
|
|
2955
|
+
voice.settings = Object.assign({}, voice.settings, {
|
|
2956
|
+
vadSilenceMs: Number(val.dictation && val.dictation.vadSilenceMs) || 700,
|
|
2957
|
+
autoSendMs: Number(val.message && val.message.autoSendMs) || 4000,
|
|
2958
|
+
beep: !!val.beep,
|
|
2959
|
+
micDeviceId: String(val.micDeviceId || ''),
|
|
2960
|
+
historyLimit: Number(val.historyLimit) || 20,
|
|
2961
|
+
voiceCommands: !!val.voiceCommands,
|
|
2962
|
+
sendDelayMs: Number(val.dictation && val.dictation.sendDelayMs) || 0,
|
|
2963
|
+
stream: !!(val.dictation && val.dictation.stream),
|
|
2964
|
+
streamChunkMs: Number(val.dictation && val.dictation.streamChunkMs) || 1200,
|
|
2965
|
+
vadAdapt: Number(val.dictation && val.dictation.vadAdapt) || 0,
|
|
2966
|
+
wakeWord: String(val.wakeWord || ''),
|
|
2967
|
+
bargeIn: !!val.bargeIn,
|
|
2968
|
+
polishSend: !!(val.message && val.message.polishSend),
|
|
2969
|
+
sessionCommands: !!(val.message && val.message.sessionCommands),
|
|
2970
|
+
polishBaseUrl: String(val.polishBaseUrl || ''),
|
|
2971
|
+
noiseSuppression: val.noiseSuppression !== false,
|
|
2972
|
+
noiseGateDb: Number(val.noiseGateDb !== undefined ? val.noiseGateDb : -45),
|
|
2973
|
+
contextGlossary: val.contextGlossary !== false,
|
|
2974
|
+
visualizerStyle: val.visualizerStyle || 'liquid-wave',
|
|
2975
|
+
gatedTurnTaking: val.gatedTurnTaking !== false,
|
|
2976
|
+
autoSendVisualRing: val.autoSendVisualRing !== false,
|
|
2977
|
+
voiceActions: val.voiceActions !== false,
|
|
2978
|
+
techJargonCorrection: val.techJargonCorrection !== false,
|
|
2979
|
+
liveInterimPreview: val.liveInterimPreview !== false,
|
|
2980
|
+
structuredPromptVoice: val.structuredPromptVoice !== false,
|
|
2981
|
+
})
|
|
2982
|
+
}, [ready, value])
|
|
2983
|
+
|
|
2984
|
+
const { sensevoiceState, installingSensevoice, triggerInstallSensevoice } = useSensevoiceInstaller(fetchStatus)
|
|
2985
|
+
|
|
2986
|
+
const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
|
|
2498
2987
|
|
|
2499
2988
|
const [statusData, setStatusData] = React.useState(null)
|
|
2500
2989
|
const [statusLatency, setStatusLatency] = React.useState(null)
|
|
@@ -2521,72 +3010,7 @@ window.__ModuleLoader__.load({
|
|
|
2521
3010
|
fetchStatus()
|
|
2522
3011
|
}, [fetchStatus])
|
|
2523
3012
|
|
|
2524
|
-
const
|
|
2525
|
-
const [updating, setUpdating] = React.useState(false)
|
|
2526
|
-
const [updaterMsg, setUpdaterMsg] = React.useState('')
|
|
2527
|
-
const [updaterLoading, setUpdaterLoading] = React.useState(false)
|
|
2528
|
-
|
|
2529
|
-
const checkUpdater = React.useCallback(async () => {
|
|
2530
|
-
setUpdaterLoading(true)
|
|
2531
|
-
setUpdaterMsg('')
|
|
2532
|
-
try {
|
|
2533
|
-
const res = await fetch('/api/dsh-voice/update', {
|
|
2534
|
-
headers: { accept: 'application/json' },
|
|
2535
|
-
cache: 'no-store',
|
|
2536
|
-
})
|
|
2537
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
2538
|
-
const data = await res.json()
|
|
2539
|
-
setUpdaterStatus(data)
|
|
2540
|
-
} catch (e) {
|
|
2541
|
-
/* best effort update check */
|
|
2542
|
-
} finally {
|
|
2543
|
-
setUpdaterLoading(false)
|
|
2544
|
-
}
|
|
2545
|
-
}, [])
|
|
2546
|
-
|
|
2547
|
-
const onUpdateNow = async () => {
|
|
2548
|
-
setUpdating(true)
|
|
2549
|
-
setUpdaterMsg('')
|
|
2550
|
-
try {
|
|
2551
|
-
const res = await fetch('/api/dsh-voice/update', {
|
|
2552
|
-
method: 'POST',
|
|
2553
|
-
headers: {
|
|
2554
|
-
'x-dsh-plugin-update': '1',
|
|
2555
|
-
'content-type': 'application/json',
|
|
2556
|
-
},
|
|
2557
|
-
})
|
|
2558
|
-
const data = await res.json()
|
|
2559
|
-
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`)
|
|
2560
|
-
setUpdaterStatus(data)
|
|
2561
|
-
setUpdaterMsg(t('updateSuccess'))
|
|
2562
|
-
} catch (e) {
|
|
2563
|
-
setUpdaterMsg(t('updateFailed') + ' ' + (e?.message || String(e)))
|
|
2564
|
-
} finally {
|
|
2565
|
-
setUpdating(false)
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
React.useEffect(() => {
|
|
2570
|
-
checkUpdater()
|
|
2571
|
-
}, [checkUpdater])
|
|
2572
|
-
|
|
2573
|
-
if (!ready) {
|
|
2574
|
-
const waiting = configLoading || (!snap || snap.status === 'loading')
|
|
2575
|
-
return React.createElement('div', { className: 'cb-page' },
|
|
2576
|
-
React.createElement('div', { className: 'dvs-wait' }, waiting
|
|
2577
|
-
? t('loadingSettings')
|
|
2578
|
-
: t('notReady1')
|
|
2579
|
-
+ t('notReady2')),
|
|
2580
|
-
)
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
const setIn = (mode, key, v) => setDraft((d) => {
|
|
2584
|
-
const next = deepClone(d || {})
|
|
2585
|
-
next[mode] = next[mode] || {}
|
|
2586
|
-
next[mode][key] = v
|
|
2587
|
-
return next
|
|
2588
|
-
})
|
|
2589
|
-
const setTop = (key, v) => setDraft((d) => Object.assign({}, d || {}, { [key]: v }))
|
|
3013
|
+
const { updaterStatus, updating, updaterMsg, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
|
|
2590
3014
|
|
|
2591
3015
|
const save = async () => {
|
|
2592
3016
|
setErr(''); setSaved(false)
|
|
@@ -2691,72 +3115,6 @@ window.__ModuleLoader__.load({
|
|
|
2691
3115
|
.filter((k) => k && BUILTIN.indexOf(k) < 0),
|
|
2692
3116
|
)
|
|
2693
3117
|
|
|
2694
|
-
const hotkeyField = () => React.createElement('div', { className: 'cb-field' },
|
|
2695
|
-
React.createElement('label', null, t('hotkey')),
|
|
2696
|
-
React.createElement('div', { className: 'cb-row' },
|
|
2697
|
-
React.createElement('button', {
|
|
2698
|
-
type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable,
|
|
2699
|
-
onClick: () => setCatching(true),
|
|
2700
|
-
}, catching ? t('pressKey') : keyLabel(draft && draft.hotkey)),
|
|
2701
|
-
React.createElement('button', {
|
|
2702
|
-
type: 'button', className: 'cb-btn cb-btn-mini', title: t('clearKey'),
|
|
2703
|
-
disabled: !writable, onClick: () => {
|
|
2704
|
-
setDraft((d) => Object.assign({}, d || {}, { hotkey: '' }))
|
|
2705
|
-
voice.hotkey = ''
|
|
2706
|
-
clearGlobalHotkey()
|
|
2707
|
-
},
|
|
2708
|
-
}, '×'),
|
|
2709
|
-
),
|
|
2710
|
-
React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
|
|
2711
|
-
)
|
|
2712
|
-
|
|
2713
|
-
const gateDbVal = Number((draft && draft.noiseGateDb !== undefined) ? draft.noiseGateDb : (value && value.noiseGateDb !== undefined) ? value.noiseGateDb : -45)
|
|
2714
|
-
const gateThresholdPercent = gateDbVal <= -90 ? 0 : Math.min(100, Math.max(0, Math.round(((gateDbVal + 60) / 40) * 100)))
|
|
2715
|
-
|
|
2716
|
-
const micField = () => React.createElement('div', { className: 'cb-field' },
|
|
2717
|
-
React.createElement('label', null, t('mic')),
|
|
2718
|
-
React.createElement('div', { className: 'cb-row' },
|
|
2719
|
-
React.createElement('select', {
|
|
2720
|
-
value: String((draft && draft.micDeviceId) ?? (value && value.micDeviceId) ?? ''), disabled: !writable || testingMic,
|
|
2721
|
-
onChange: (e) => setTop('micDeviceId', e.target.value),
|
|
2722
|
-
style: { flex: 1 },
|
|
2723
|
-
},
|
|
2724
|
-
React.createElement('option', { value: '' }, t('micDefault')),
|
|
2725
|
-
devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId },
|
|
2726
|
-
d.label || d.deviceId.slice(0, 12))),
|
|
2727
|
-
),
|
|
2728
|
-
React.createElement('button', {
|
|
2729
|
-
type: 'button',
|
|
2730
|
-
className: 'cb-btn ' + (testingMic ? 'cb-btn-primary' : 'cb-btn-secondary'),
|
|
2731
|
-
onClick: toggleTestMic,
|
|
2732
|
-
style: { flex: 'none' },
|
|
2733
|
-
}, testingMic ? t('stopTest') : t('testMic')),
|
|
2734
|
-
),
|
|
2735
|
-
testingMic ? React.createElement('div', { className: 'dvo-mic-test' },
|
|
2736
|
-
React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('micLevel') + ':'),
|
|
2737
|
-
React.createElement('div', { className: 'dvo-meter-bar' },
|
|
2738
|
-
React.createElement('div', { className: 'dvo-meter-fill', style: { width: `${testLevel}%` } }),
|
|
2739
|
-
gateThresholdPercent > 0 ? React.createElement('div', { className: 'dvo-meter-gate', style: { left: `${gateThresholdPercent}%` }, title: `${gateDbVal} dB` }) : null,
|
|
2740
|
-
),
|
|
2741
|
-
React.createElement('span', { style: { fontSize: '11px', fontVariantNumeric: 'tabular-nums', width: '32px', textAlign: 'right' } }, `${testLevel}%`),
|
|
2742
|
-
) : null,
|
|
2743
|
-
React.createElement('div', { className: 'cb-row', style: { marginTop: '6px' } },
|
|
2744
|
-
React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)', minWidth: '120px' } }, t('noiseGate') + ':'),
|
|
2745
|
-
React.createElement('select', {
|
|
2746
|
-
value: String(gateDbVal), disabled: !writable,
|
|
2747
|
-
onChange: (e) => setTop('noiseGateDb', Number(e.target.value)),
|
|
2748
|
-
style: { flex: 1 },
|
|
2749
|
-
},
|
|
2750
|
-
React.createElement('option', { value: '-999' }, t('noiseGateOff')),
|
|
2751
|
-
React.createElement('option', { value: '-50' }, '-50 dB (Low / Gentle)'),
|
|
2752
|
-
React.createElement('option', { value: '-45' }, '-45 dB (Standard Default)'),
|
|
2753
|
-
React.createElement('option', { value: '-35' }, '-35 dB (Medium Room)'),
|
|
2754
|
-
React.createElement('option', { value: '-25' }, '-25 dB (High / Mechanical Clicks)'),
|
|
2755
|
-
),
|
|
2756
|
-
),
|
|
2757
|
-
React.createElement('span', { className: 'dvs-sub' }, t('noiseGateHint')),
|
|
2758
|
-
)
|
|
2759
|
-
|
|
2760
3118
|
const langField = (mode) => React.createElement('div', { className: 'cb-field' },
|
|
2761
3119
|
React.createElement('label', null, t('language')),
|
|
2762
3120
|
React.createElement('select', {
|
|
@@ -2774,14 +3132,6 @@ window.__ModuleLoader__.load({
|
|
|
2774
3132
|
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
2775
3133
|
)
|
|
2776
3134
|
|
|
2777
|
-
const textField = (key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
2778
|
-
React.createElement('label', null, label),
|
|
2779
|
-
React.createElement('input', {
|
|
2780
|
-
type: 'text', value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
2781
|
-
onChange: (e) => setTop(key, e.target.value),
|
|
2782
|
-
}),
|
|
2783
|
-
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
2784
|
-
)
|
|
2785
3135
|
|
|
2786
3136
|
const statsData = (statusData && statusData.providerStats) || {}
|
|
2787
3137
|
const hostOnline = statusLatency !== null
|
|
@@ -2790,25 +3140,14 @@ window.__ModuleLoader__.load({
|
|
|
2790
3140
|
|
|
2791
3141
|
return React.createElement('div', { className: 'cb-page' },
|
|
2792
3142
|
// 1. Connection & Engine Status Card
|
|
2793
|
-
React.createElement(
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
React.createElement('div', { className: 'cb-section-desc' }, t('statusDesc')),
|
|
2802
|
-
React.createElement('div', { className: 'cb-row' },
|
|
2803
|
-
hostOnline
|
|
2804
|
-
? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '● ' + t('badgeHostOnline').replace('{ms}', statusLatency))
|
|
2805
|
-
: React.createElement('span', { className: 'cb-badge cb-badge-bad' }, '● ' + t('badgeHostOffline')),
|
|
2806
|
-
whisperRunning
|
|
2807
|
-
? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '● ' + t('badgeWhisperActive'))
|
|
2808
|
-
: React.createElement('span', { className: 'cb-badge cb-badge-warn' }, '○ ' + t('badgeWhisperInactive')),
|
|
2809
|
-
React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '✓ ' + t('badgeProviders').replace('{count}', providerCount)),
|
|
2810
|
-
),
|
|
2811
|
-
),
|
|
3143
|
+
React.createElement(ConnectionStatusCard, {
|
|
3144
|
+
t: t,
|
|
3145
|
+
hostOnline: hostOnline,
|
|
3146
|
+
statusLatency: statusLatency,
|
|
3147
|
+
whisperRunning: whisperRunning,
|
|
3148
|
+
providerCount: providerCount,
|
|
3149
|
+
onRefresh: fetchStatus,
|
|
3150
|
+
}),
|
|
2812
3151
|
|
|
2813
3152
|
// 2. Dictation Mode Card
|
|
2814
3153
|
React.createElement('div', { className: 'cb-section-card' },
|
|
@@ -2817,7 +3156,7 @@ window.__ModuleLoader__.load({
|
|
|
2817
3156
|
),
|
|
2818
3157
|
React.createElement('div', { className: 'cb-section-desc' }, t('dictationHint')),
|
|
2819
3158
|
React.createElement(ChainEditor, {
|
|
2820
|
-
value: draft && draft.dictation ? draft.dictation.chain : [], writable: writable,
|
|
3159
|
+
t: t, value: draft && draft.dictation ? draft.dictation.chain : [], writable: writable,
|
|
2821
3160
|
options: chainOptions,
|
|
2822
3161
|
onChange: (v) => setIn('dictation', 'chain', v),
|
|
2823
3162
|
}),
|
|
@@ -2862,7 +3201,7 @@ window.__ModuleLoader__.load({
|
|
|
2862
3201
|
),
|
|
2863
3202
|
React.createElement('div', { className: 'cb-section-desc' }, t('messageHint')),
|
|
2864
3203
|
React.createElement(ChainEditor, {
|
|
2865
|
-
value: draft && draft.message ? draft.message.chain : [], writable: writable,
|
|
3204
|
+
t: t, value: draft && draft.message ? draft.message.chain : [], writable: writable,
|
|
2866
3205
|
options: chainOptions,
|
|
2867
3206
|
onChange: (v) => setIn('message', 'chain', v),
|
|
2868
3207
|
}),
|
|
@@ -2908,144 +3247,35 @@ window.__ModuleLoader__.load({
|
|
|
2908
3247
|
}),
|
|
2909
3248
|
),
|
|
2910
3249
|
|
|
2911
|
-
// 5. Hardware &
|
|
2912
|
-
React.createElement(
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
React.createElement('div', { className: 'cb-row' },
|
|
2932
|
-
React.createElement('label', { className: 'cb-check-label' },
|
|
2933
|
-
React.createElement('input', {
|
|
2934
|
-
type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
|
|
2935
|
-
onChange: (e) => setTop('autoStart', e.target.checked),
|
|
2936
|
-
}),
|
|
2937
|
-
t('whisperAutostart'),
|
|
2938
|
-
),
|
|
2939
|
-
React.createElement('label', { className: 'cb-check-label' },
|
|
2940
|
-
React.createElement('input', {
|
|
2941
|
-
type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
|
|
2942
|
-
onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
|
|
2943
|
-
}),
|
|
2944
|
-
t('sensevoiceAutostart'),
|
|
2945
|
-
),
|
|
2946
|
-
React.createElement('label', { className: 'cb-check-label' },
|
|
2947
|
-
React.createElement('input', {
|
|
2948
|
-
type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
|
|
2949
|
-
onChange: (e) => setTop('localOnly', e.target.checked),
|
|
2950
|
-
}),
|
|
2951
|
-
t('localOnly'),
|
|
2952
|
-
),
|
|
2953
|
-
React.createElement('label', { className: 'cb-check-label' },
|
|
2954
|
-
React.createElement('input', {
|
|
2955
|
-
type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
|
|
2956
|
-
onChange: (e) => setTop('beep', e.target.checked),
|
|
2957
|
-
}),
|
|
2958
|
-
t('beep'),
|
|
2959
|
-
),
|
|
2960
|
-
React.createElement('label', { className: 'cb-check-label' },
|
|
2961
|
-
React.createElement('input', {
|
|
2962
|
-
type: 'checkbox', checked: draft ? draft.noiseSuppression !== false : true, disabled: !writable,
|
|
2963
|
-
onChange: (e) => setTop('noiseSuppression', e.target.checked),
|
|
2964
|
-
}),
|
|
2965
|
-
t('noiseSuppression'),
|
|
2966
|
-
),
|
|
2967
|
-
React.createElement('label', { className: 'cb-check-label' },
|
|
2968
|
-
React.createElement('input', {
|
|
2969
|
-
type: 'checkbox', checked: draft ? draft.contextGlossary !== false : true, disabled: !writable,
|
|
2970
|
-
onChange: (e) => setTop('contextGlossary', e.target.checked),
|
|
2971
|
-
}),
|
|
2972
|
-
t('contextGlossary'),
|
|
2973
|
-
),
|
|
2974
|
-
React.createElement('label', { className: 'cb-check-label' },
|
|
2975
|
-
React.createElement('input', {
|
|
2976
|
-
type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
|
|
2977
|
-
onChange: (e) => setTop('voiceCommands', e.target.checked),
|
|
2978
|
-
}),
|
|
2979
|
-
t('voiceCommandsLabel'),
|
|
2980
|
-
),
|
|
2981
|
-
React.createElement('label', { className: 'cb-check-label' },
|
|
2982
|
-
React.createElement('input', {
|
|
2983
|
-
type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
|
|
2984
|
-
onChange: (e) => setTop('normalizeTranscript', e.target.checked),
|
|
2985
|
-
}),
|
|
2986
|
-
t('normalizeTranscript'),
|
|
2987
|
-
),
|
|
2988
|
-
),
|
|
2989
|
-
React.createElement('div', { className: 'cb-field' },
|
|
2990
|
-
React.createElement('label', null, t('visualizerStyle')),
|
|
2991
|
-
React.createElement('select', {
|
|
2992
|
-
value: String((draft && draft.visualizerStyle) || 'liquid-wave'), disabled: !writable,
|
|
2993
|
-
onChange: (e) => setTop('visualizerStyle', e.target.value),
|
|
2994
|
-
},
|
|
2995
|
-
React.createElement('option', { value: 'liquid-wave' }, t('visLiquidWave')),
|
|
2996
|
-
React.createElement('option', { value: 'dynamic-orb' }, t('visDynamicOrb')),
|
|
2997
|
-
React.createElement('option', { value: 'bars' }, t('visBars')),
|
|
2998
|
-
React.createElement('option', { value: 'off' }, t('visOff')),
|
|
2999
|
-
),
|
|
3000
|
-
),
|
|
3001
|
-
React.createElement('div', { className: 'cb-field' },
|
|
3002
|
-
React.createElement('label', null, t('vocabulary')),
|
|
3003
|
-
React.createElement('textarea', {
|
|
3004
|
-
rows: 3, disabled: !writable,
|
|
3005
|
-
value: Array.isArray(draft && draft.vocabulary) ? draft.vocabulary.join('\n') : '',
|
|
3006
|
-
onChange: (e) => setTop('vocabulary', e.target.value.split('\n').map((x) => x.trim()).filter(Boolean)),
|
|
3007
|
-
}),
|
|
3008
|
-
),
|
|
3009
|
-
),
|
|
3250
|
+
// 5. Hardware & Audio Engines Card
|
|
3251
|
+
React.createElement(HardwareOptionsCard, {
|
|
3252
|
+
t: t,
|
|
3253
|
+
draft: draft,
|
|
3254
|
+
setTop: setTop,
|
|
3255
|
+
writable: writable,
|
|
3256
|
+
catching: catching,
|
|
3257
|
+
setCatching: setCatching,
|
|
3258
|
+
devices: devices,
|
|
3259
|
+
testingMic: testingMic,
|
|
3260
|
+
toggleTestMic: toggleTestMic,
|
|
3261
|
+
testLevel: testLevel,
|
|
3262
|
+
gateDbVal: gateDbVal,
|
|
3263
|
+
gateThresholdPercent: gateThresholdPercent,
|
|
3264
|
+
onClearKey: () => {
|
|
3265
|
+
setDraft((d) => Object.assign({}, d || {}, { hotkey: '' }))
|
|
3266
|
+
voice.hotkey = ''
|
|
3267
|
+
clearGlobalHotkey()
|
|
3268
|
+
},
|
|
3269
|
+
}),
|
|
3010
3270
|
|
|
3011
3271
|
// 6. SenseVoice Local ASR (1-Click Setup)
|
|
3012
|
-
React.createElement(
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
),
|
|
3020
|
-
),
|
|
3021
|
-
React.createElement('div', { className: 'cb-section-desc' }, t('sensevoiceDesc')),
|
|
3022
|
-
React.createElement('div', { className: 'dvo-installer-box' },
|
|
3023
|
-
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', justifyContent: 'space-between', gap: '12px' } },
|
|
3024
|
-
React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-content-secondary)' } },
|
|
3025
|
-
sensevoiceState && sensevoiceState.path
|
|
3026
|
-
? ('~/' + sensevoiceState.path.replace(/^.*[\\/](\.dsh[\\/]models[\\/].*)$/, '$1'))
|
|
3027
|
-
: '~/.dsh/models/sensevoice'
|
|
3028
|
-
),
|
|
3029
|
-
React.createElement('button', {
|
|
3030
|
-
type: 'button',
|
|
3031
|
-
className: 'cb-btn cb-btn-primary',
|
|
3032
|
-
style: { fontSize: '12px', padding: '6px 12px' },
|
|
3033
|
-
disabled: installingSensevoice || !writable,
|
|
3034
|
-
onClick: triggerInstallSensevoice,
|
|
3035
|
-
}, installingSensevoice ? ('⏳ ' + t('installingSensevoice')) : ('📥 ' + t('installSensevoice'))),
|
|
3036
|
-
),
|
|
3037
|
-
installingSensevoice
|
|
3038
|
-
? React.createElement('div', { style: { marginTop: '8px' } },
|
|
3039
|
-
React.createElement('div', { className: 'dvo-progress-bar' },
|
|
3040
|
-
React.createElement('div', { className: 'dvo-progress-bar-fill' })
|
|
3041
|
-
),
|
|
3042
|
-
React.createElement('div', { style: { fontSize: '11px', marginTop: '4px', color: 'var(--dsw-alias-content-secondary)' } },
|
|
3043
|
-
(sensevoiceState && sensevoiceState.progress) || t('sensevoiceDownloading')
|
|
3044
|
-
)
|
|
3045
|
-
)
|
|
3046
|
-
: null
|
|
3047
|
-
)
|
|
3048
|
-
),
|
|
3272
|
+
React.createElement(SensevoiceSection, {
|
|
3273
|
+
t: t,
|
|
3274
|
+
sensevoiceState: sensevoiceState,
|
|
3275
|
+
installingSensevoice: installingSensevoice,
|
|
3276
|
+
writable: writable,
|
|
3277
|
+
onInstall: triggerInstallSensevoice,
|
|
3278
|
+
}),
|
|
3049
3279
|
|
|
3050
3280
|
// 7. Turn-Taking & Audio Coordination
|
|
3051
3281
|
React.createElement('div', { className: 'cb-section-card' },
|
|
@@ -3121,143 +3351,30 @@ window.__ModuleLoader__.load({
|
|
|
3121
3351
|
),
|
|
3122
3352
|
|
|
3123
3353
|
// 9. Developer Lexicon & IT Jargon
|
|
3124
|
-
React.createElement(
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
type: 'checkbox',
|
|
3131
|
-
checked: draft ? draft.techJargonCorrection !== false : true,
|
|
3132
|
-
disabled: !writable,
|
|
3133
|
-
onChange: (e) => setTop('techJargonCorrection', e.target.checked),
|
|
3134
|
-
}),
|
|
3135
|
-
t('techJargonCorrection'),
|
|
3136
|
-
),
|
|
3137
|
-
React.createElement('div', { className: 'cb-field' },
|
|
3138
|
-
React.createElement('label', null, t('jargonAdd') + ' (spoken -> replacement)'),
|
|
3139
|
-
React.createElement('textarea', {
|
|
3140
|
-
rows: 3,
|
|
3141
|
-
disabled: !writable,
|
|
3142
|
-
placeholder: t('jargonPlaceholder'),
|
|
3143
|
-
value: jargonInput !== null
|
|
3144
|
-
? jargonInput
|
|
3145
|
-
: (Array.isArray(draft && draft.jargonDictionary)
|
|
3146
|
-
? draft.jargonDictionary.map((item) => item && item.from ? `${item.from} -> ${item.to}` : '').filter(Boolean).join('\n')
|
|
3147
|
-
: ''),
|
|
3148
|
-
onChange: (e) => {
|
|
3149
|
-
setJargonInput(e.target.value)
|
|
3150
|
-
},
|
|
3151
|
-
onBlur: () => {
|
|
3152
|
-
if (jargonInput === null) return
|
|
3153
|
-
const lines = jargonInput.split('\n')
|
|
3154
|
-
const parsed = lines
|
|
3155
|
-
.map((line) => {
|
|
3156
|
-
const parts = line.split(/->|=|:/)
|
|
3157
|
-
if (parts.length >= 2) {
|
|
3158
|
-
const from = parts[0].trim()
|
|
3159
|
-
const to = parts.slice(1).join(':').trim()
|
|
3160
|
-
if (from && to) return { from, to }
|
|
3161
|
-
}
|
|
3162
|
-
return null
|
|
3163
|
-
})
|
|
3164
|
-
.filter(Boolean)
|
|
3165
|
-
setTop('jargonDictionary', parsed)
|
|
3166
|
-
},
|
|
3167
|
-
}),
|
|
3168
|
-
),
|
|
3169
|
-
),
|
|
3354
|
+
React.createElement(JargonEditor, {
|
|
3355
|
+
t: t,
|
|
3356
|
+
draft: draft,
|
|
3357
|
+
setTop: setTop,
|
|
3358
|
+
writable: writable,
|
|
3359
|
+
}),
|
|
3170
3360
|
|
|
3171
3361
|
// 10. Provider Telemetry & Latency Dashboard
|
|
3172
|
-
React.createElement(
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
onClick: fetchStatus,
|
|
3178
|
-
}, '🔄 ' + t('refreshStats')),
|
|
3179
|
-
),
|
|
3180
|
-
React.createElement('div', { className: 'dvo-dash' },
|
|
3181
|
-
React.createElement('div', { className: 'dvo-dash-grid' },
|
|
3182
|
-
Object.keys(statsData).length === 0
|
|
3183
|
-
? React.createElement('div', { className: 'dvs-sub' }, t('idle'))
|
|
3184
|
-
: Object.entries(statsData).map(([key, item]) => {
|
|
3185
|
-
const avg = item.avgTookMs || 0
|
|
3186
|
-
const hasErrors = item.failures > 0
|
|
3187
|
-
let badgeClass = 'dvo-badge-idle'
|
|
3188
|
-
let badgeText = t('idle')
|
|
3189
|
-
if (item.attempts > 0) {
|
|
3190
|
-
if (hasErrors && item.successes === 0) {
|
|
3191
|
-
badgeClass = 'dvo-badge-err'
|
|
3192
|
-
badgeText = t('error')
|
|
3193
|
-
} else if (avg > 0 && avg < 400) {
|
|
3194
|
-
badgeClass = 'dvo-badge-fast'
|
|
3195
|
-
badgeText = avg + 'ms · ' + t('fast')
|
|
3196
|
-
} else if (avg >= 400 && avg <= 1500) {
|
|
3197
|
-
badgeClass = 'dvo-badge-norm'
|
|
3198
|
-
badgeText = avg + 'ms · ' + t('normal')
|
|
3199
|
-
} else {
|
|
3200
|
-
badgeClass = 'dvo-badge-slow'
|
|
3201
|
-
badgeText = avg + 'ms · ' + t('slow')
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
const rate = item.attempts > 0 ? Math.round((item.successes / item.attempts) * 100) : 100
|
|
3205
|
-
return React.createElement('div', { key, className: 'dvo-dash-item', title: item.lastError ? `Error: ${item.lastError}` : '' },
|
|
3206
|
-
React.createElement('div', { className: 'dvo-dash-name' }, key),
|
|
3207
|
-
React.createElement('div', { className: 'dvo-dash-row' },
|
|
3208
|
-
React.createElement('span', { className: 'dvo-badge ' + badgeClass }, badgeText),
|
|
3209
|
-
React.createElement('span', null, rate + '%'),
|
|
3210
|
-
),
|
|
3211
|
-
React.createElement('div', { className: 'dvo-dash-row' },
|
|
3212
|
-
React.createElement('span', null, `${item.successes}/${item.attempts}`),
|
|
3213
|
-
React.createElement('span', null, t('successRate')),
|
|
3214
|
-
),
|
|
3215
|
-
)
|
|
3216
|
-
}),
|
|
3217
|
-
),
|
|
3218
|
-
),
|
|
3219
|
-
),
|
|
3362
|
+
React.createElement(ProviderDashboard, {
|
|
3363
|
+
t: t,
|
|
3364
|
+
statsData: statsData,
|
|
3365
|
+
onRefresh: fetchStatus,
|
|
3366
|
+
}),
|
|
3220
3367
|
|
|
3221
3368
|
// 7. Plugin Version & Updates Card
|
|
3222
|
-
React.createElement(
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', gap: '12px' } },
|
|
3232
|
-
React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
3233
|
-
'v' + ((updaterStatus && updaterStatus.currentVersion) || '0.9.4')
|
|
3234
|
-
),
|
|
3235
|
-
updaterStatus && updaterStatus.updateAvailable
|
|
3236
|
-
? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
3237
|
-
t('updateAvailable') + ': v' + updaterStatus.latestVersion
|
|
3238
|
-
)
|
|
3239
|
-
: React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
3240
|
-
t('upToDate')
|
|
3241
|
-
),
|
|
3242
|
-
updaterStatus && updaterStatus.updateAvailable
|
|
3243
|
-
? React.createElement('button', {
|
|
3244
|
-
type: 'button', className: 'cb-btn cb-btn-primary', style: { fontSize: '12px', padding: '4px 10px' },
|
|
3245
|
-
onClick: onUpdateNow, disabled: updating,
|
|
3246
|
-
}, updating ? t('updating') : t('updateNow'))
|
|
3247
|
-
: null,
|
|
3248
|
-
),
|
|
3249
|
-
updaterMsg
|
|
3250
|
-
? React.createElement('div', {
|
|
3251
|
-
style: {
|
|
3252
|
-
marginTop: '8px',
|
|
3253
|
-
fontSize: '12px',
|
|
3254
|
-
color: updaterMsg.includes('fail') || updaterMsg.includes('失败')
|
|
3255
|
-
? 'var(--dsw-alias-state-warning-primary)'
|
|
3256
|
-
: 'var(--dsw-alias-state-success-primary)',
|
|
3257
|
-
},
|
|
3258
|
-
}, updaterMsg)
|
|
3259
|
-
: null,
|
|
3260
|
-
),
|
|
3369
|
+
React.createElement(UpdaterCard, {
|
|
3370
|
+
t: t,
|
|
3371
|
+
updaterStatus: updaterStatus,
|
|
3372
|
+
updaterLoading: updaterLoading,
|
|
3373
|
+
updating: updating,
|
|
3374
|
+
updaterMsg: updaterMsg,
|
|
3375
|
+
onCheck: checkUpdater,
|
|
3376
|
+
onUpdateNow: onUpdateNow,
|
|
3377
|
+
}),
|
|
3261
3378
|
|
|
3262
3379
|
// Action bar (Save)
|
|
3263
3380
|
React.createElement('div', { className: 'cb-row' },
|
|
@@ -3301,42 +3418,42 @@ window.__ModuleLoader__.load({
|
|
|
3301
3418
|
}
|
|
3302
3419
|
|
|
3303
3420
|
function registerSettings(ctx) {
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
)
|
|
3421
|
+
ctx.effect(() => {
|
|
3422
|
+
const offItem = ctx.slots.inject('plugins.item', () => ctx.slots.register(
|
|
3423
|
+
{
|
|
3424
|
+
name: 'plugins.item',
|
|
3425
|
+
id: ROW_ID,
|
|
3426
|
+
order: 60,
|
|
3427
|
+
label: () => 'Voice input',
|
|
3428
|
+
locale: NS,
|
|
3429
|
+
inject: () => ({ ctx: ctx }),
|
|
3430
|
+
},
|
|
3431
|
+
PluginCard,
|
|
3432
|
+
))
|
|
3433
|
+
const offRow = ctx.slots.inject('plugins.row.config', () => ctx.slots.register(
|
|
3434
|
+
{
|
|
3435
|
+
name: 'plugins.row.config',
|
|
3436
|
+
key: ROW_CONFIG_KEY,
|
|
3437
|
+
locale: NS,
|
|
3438
|
+
inject: () => ({ ctx: ctx }),
|
|
3439
|
+
},
|
|
3440
|
+
PluginCard,
|
|
3441
|
+
))
|
|
3442
|
+
const offLegacy = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
3443
|
+
{
|
|
3444
|
+
name: 'settings.plugin.item',
|
|
3445
|
+
key: NS,
|
|
3446
|
+
locale: NS,
|
|
3447
|
+
inject: () => ({ ctx: ctx }),
|
|
3448
|
+
},
|
|
3449
|
+
PluginCard,
|
|
3450
|
+
))
|
|
3451
|
+
return () => {
|
|
3452
|
+
if (typeof offItem === 'function') offItem()
|
|
3453
|
+
if (typeof offRow === 'function') offRow()
|
|
3454
|
+
if (typeof offLegacy === 'function') offLegacy()
|
|
3455
|
+
}
|
|
3456
|
+
}, 'dsh-voice: settings card slots')
|
|
3340
3457
|
}
|
|
3341
3458
|
|
|
3342
3459
|
exports.inject = ['timer', 'slots', 'configForms', 'locale']
|