@goodandready/dsh-voice 0.9.31 → 0.9.32
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/README.md +255 -252
- package/README.ru.md +254 -251
- package/README.zh.md +182 -179
- package/lib/client.js +295 -56
- package/lib/dictation-modes.js +28 -0
- package/lib/host-routes.js +222 -188
- package/lib/macros.js +53 -0
- package/lib/polish.js +14 -6
- package/lib/providers.js +3 -1
- package/lib/schema.js +173 -161
- package/package.json +72 -72
package/lib/client.js
CHANGED
|
@@ -248,6 +248,27 @@ window.__ModuleLoader__.load({
|
|
|
248
248
|
'voiceActionsLabel': 'Voice action commands ("send", "cancel", "clear", "new line")',
|
|
249
249
|
'liveInterimPreview': 'Live speech preview (ghost text preview)',
|
|
250
250
|
'structuredPromptVoice': 'Spoken answers for interactive choice prompts',
|
|
251
|
+
'modeVerbatim': 'Verbatim',
|
|
252
|
+
'modeClean': 'Clean Text',
|
|
253
|
+
'modeTask': 'Agent Task',
|
|
254
|
+
'modeNote': 'Note',
|
|
255
|
+
'dictationModeTitle': 'Dictation Post-Processing Mode',
|
|
256
|
+
'dictationModeDesc': 'Select how dictated speech is processed before insertion: Verbatim (0 LLM calls), Clean text (formatting & filler removal), Agent task (structured prompt), or Note (key points & actions).',
|
|
257
|
+
'dictationPromptsTitle': 'Dictation Prompt Templates',
|
|
258
|
+
'dictationPromptsDesc': 'Custom prompt templates for dictation post-processing. Use {text} as placeholder for recognized speech.',
|
|
259
|
+
'promptClean': 'Clean text prompt',
|
|
260
|
+
'promptTask': 'Agent task prompt',
|
|
261
|
+
'promptNote': 'Note prompt',
|
|
262
|
+
'resetPrompts': 'Reset to defaults',
|
|
263
|
+
'voiceMacrosTitle': 'Voice Action Macros',
|
|
264
|
+
'voiceMacrosDesc': 'Spoken trigger phrases automatically expanded into structured prompt templates.',
|
|
265
|
+
'addMacro': 'Add macro',
|
|
266
|
+
'macroTrigger': 'Spoken trigger phrase',
|
|
267
|
+
'macroTemplate': 'Expanded prompt template',
|
|
268
|
+
'macroDelete': 'Delete',
|
|
269
|
+
'rawTab': 'Raw STT',
|
|
270
|
+
'resultTab': 'Result',
|
|
271
|
+
'llmUnavailableFallback': 'LLM unavailable, showing raw transcript',
|
|
251
272
|
'jargonTitle': 'Developer Lexicon & IT Jargon',
|
|
252
273
|
'techJargonCorrection': 'Auto-correct technical jargon (GitHub, Docker, pnpm, Kubernetes, etc.)',
|
|
253
274
|
'jargonAdd': 'Add replacement',
|
|
@@ -474,6 +495,27 @@ window.__ModuleLoader__.load({
|
|
|
474
495
|
'voiceActionsLabel': '语音动作指令 ("发送", "取消", "清空", "换行")',
|
|
475
496
|
'liveInterimPreview': '实时语音文本预览 (幽灵文本)',
|
|
476
497
|
'structuredPromptVoice': '语音应答交互式选择提示',
|
|
498
|
+
'modeVerbatim': '逐字直录',
|
|
499
|
+
'modeClean': '清爽文本',
|
|
500
|
+
'modeTask': '智能体任务',
|
|
501
|
+
'modeNote': '便签备忘',
|
|
502
|
+
'dictationModeTitle': '听写后处理模式',
|
|
503
|
+
'dictationModeDesc': '选择听写文本后处理方式:逐字直录(零LLM调用)、清爽文本(整理标点去除语气词)、智能体任务(结构化提示词)或便签(要点与行动项)。',
|
|
504
|
+
'dictationPromptsTitle': '听写提示词模板',
|
|
505
|
+
'dictationPromptsDesc': '自定义听写后处理提示词模板。使用 {text} 作为识别语音的占位符。',
|
|
506
|
+
'promptClean': '清爽文本提示词',
|
|
507
|
+
'promptTask': '智能体任务提示词',
|
|
508
|
+
'promptNote': '便签提示词',
|
|
509
|
+
'resetPrompts': '恢复默认模板',
|
|
510
|
+
'voiceMacrosTitle': '语音动作宏',
|
|
511
|
+
'voiceMacrosDesc': '语音触发词自动展开为结构化提示词模板。',
|
|
512
|
+
'addMacro': '添加动作宏',
|
|
513
|
+
'macroTrigger': '语音触发短语',
|
|
514
|
+
'macroTemplate': '展开提示词模板',
|
|
515
|
+
'macroDelete': '删除',
|
|
516
|
+
'rawTab': '原始识别',
|
|
517
|
+
'resultTab': '处理结果',
|
|
518
|
+
'llmUnavailableFallback': 'LLM不可用,已保留原始识别文本',
|
|
477
519
|
'jargonTitle': '开发者词汇与 IT 术语',
|
|
478
520
|
'techJargonCorrection': '自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)',
|
|
479
521
|
'jargonAdd': '添加替换项',
|
|
@@ -547,7 +589,7 @@ window.__ModuleLoader__.load({
|
|
|
547
589
|
'.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}' +
|
|
548
590
|
'.dvo-btn-send{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:none;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);transition:opacity 150ms ease}' +
|
|
549
591
|
'.dvo-btn-send:hover{opacity:0.9}' +
|
|
550
|
-
'.dvo-warn-banner{font-size:12px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);padding:4px 8px;border-radius:6px;margin-top:4px}'
|
|
592
|
+
'.dvo-warn-banner{font-size:12px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);padding:4px 8px;border-radius:6px;margin-top:4px}' + '.dvo-mode-pill-btn{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 8px;font-size:11px;font-weight:600;border-radius:14px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);user-select:none;touch-action:manipulation;transition:all 150ms ease}' + '.dvo-mode-pill-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}' + '.dvo-mode-badge{font-size:11px;font-weight:600;padding:2px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);margin-right:4px}' + '.dvo-macro-item{display:flex;flex-direction:column;gap:6px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:6px}' + '.dvo-macro-actions{display:flex;justify-content:flex-end;gap:6px}' + '.dvo-fallback-badge{font-size:11px;padding:2px 6px;border-radius:4px;color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-warning-primary)}'
|
|
551
593
|
const cssId = 'dsh-voice/client.module.css'
|
|
552
594
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {
|
|
553
595
|
const tag = document.createElement('style')
|
|
@@ -743,7 +785,26 @@ window.__ModuleLoader__.load({
|
|
|
743
785
|
|
|
744
786
|
function extractContextKeywords() {
|
|
745
787
|
if (!voice.settings || voice.settings.contextGlossary === false) return []
|
|
746
|
-
const
|
|
788
|
+
const chunks = []
|
|
789
|
+
const draft = (voice.input && typeof voice.input.draft === 'string') ? voice.input.draft : ''
|
|
790
|
+
if (draft && draft.length >= 3) chunks.push(draft)
|
|
791
|
+
if (voice.session && Array.isArray(voice.session.messages)) {
|
|
792
|
+
const msgs = voice.session.messages.slice(-5)
|
|
793
|
+
for (const m of msgs) {
|
|
794
|
+
const t = (m && typeof m.content === 'string') ? m.content : ((m && typeof m.text === 'string') ? m.text : '')
|
|
795
|
+
if (t && t.length >= 3) chunks.push(t)
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (typeof document !== 'undefined') {
|
|
799
|
+
try {
|
|
800
|
+
const nodes = document.querySelectorAll('.message-content code, .dsh-message-body code, pre code')
|
|
801
|
+
for (let i = 0; i < Math.min(nodes.length, 10); i++) {
|
|
802
|
+
const ct = nodes[i].textContent
|
|
803
|
+
if (ct && ct.length < 100) chunks.push(ct)
|
|
804
|
+
}
|
|
805
|
+
} catch (_) {}
|
|
806
|
+
}
|
|
807
|
+
const text = chunks.join(' ')
|
|
747
808
|
if (!text || text.length < 3) return []
|
|
748
809
|
const matches = text.match(/[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,29}/g) || []
|
|
749
810
|
// Linguistic filter: functional stop-word dictionary for vocabulary keyword extraction.
|
|
@@ -775,7 +836,11 @@ window.__ModuleLoader__.load({
|
|
|
775
836
|
|
|
776
837
|
const tryHost = async (chainOverride) => {
|
|
777
838
|
const dataBase64 = await blobToBase64(blob)
|
|
778
|
-
const
|
|
839
|
+
const subMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
840
|
+
|| voice.selectedDictationMode
|
|
841
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
842
|
+
|| 'clean'
|
|
843
|
+
const payload = { dataBase64, mimeType: mime, mode, subMode }
|
|
779
844
|
if (Array.isArray(chainOverride)) {
|
|
780
845
|
payload.chain = chainOverride.map((p) => (typeof p === 'string' ? { provider: p, model: modelMap[p] } : p))
|
|
781
846
|
}
|
|
@@ -1491,7 +1556,10 @@ voice._chainsPromise = null
|
|
|
1491
1556
|
const targetSession = voice.session || (targetInput && targetInput.session)
|
|
1492
1557
|
const sessionId = (targetSession && (targetSession.sessionId || targetSession.id))
|
|
1493
1558
|
|| voice.currentSessionId || ''
|
|
1494
|
-
const
|
|
1559
|
+
const snapshotSubMode = voice.selectedDictationMode
|
|
1560
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
1561
|
+
|| 'clean'
|
|
1562
|
+
const target = { actions: targetActions, input: targetInput, session: targetSession, span, initialDraft, sessionId, dictationMode: snapshotSubMode }
|
|
1495
1563
|
voice._activeTarget = target
|
|
1496
1564
|
|
|
1497
1565
|
if (voice.isTtsSpeaking) {
|
|
@@ -1738,7 +1806,15 @@ voice._chainsPromise = null
|
|
|
1738
1806
|
if (v.phase !== 'idle' && v.phase !== 'error' && !hold.armed) return null
|
|
1739
1807
|
const err = v.phase === 'error'
|
|
1740
1808
|
const hk = voice.hotkey ? ` (${voice.hotkey})` : ''
|
|
1809
|
+
const curMode = voice.selectedDictationMode || (voice.settings && voice.settings.dictationMode) || 'clean'
|
|
1741
1810
|
return React.createElement(React.Fragment, null,
|
|
1811
|
+
React.createElement('button', {
|
|
1812
|
+
type: 'button',
|
|
1813
|
+
className: 'dvo-mode-pill-btn',
|
|
1814
|
+
title: t('dictationModeTitle') + ': ' + getDictationModeLabel(curMode, t),
|
|
1815
|
+
'aria-label': t('dictationModeTitle') + ': ' + getDictationModeLabel(curMode, t),
|
|
1816
|
+
onClick: cycleDictationMode,
|
|
1817
|
+
}, getDictationModeLabel(curMode, t)),
|
|
1742
1818
|
React.createElement('button', {
|
|
1743
1819
|
type: 'button', className: 'dvo-btn', 'data-err': err ? '1' : '0',
|
|
1744
1820
|
title: err ? v.error : t('dictationBtn'),
|
|
@@ -2208,6 +2284,25 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2208
2284
|
? React.createElement('div', { className: 'dvo-warn-banner' }, t('sessionMismatch'))
|
|
2209
2285
|
: null,
|
|
2210
2286
|
)
|
|
2287
|
+
}
|
|
2288
|
+
// ------------------------------------------------------------------ 45-dictation
|
|
2289
|
+
// Dictation modes UI helper, mode cycler and labels (#264)
|
|
2290
|
+
|
|
2291
|
+
const DICTATION_MODES = ['clean', 'task', 'note', 'verbatim']
|
|
2292
|
+
|
|
2293
|
+
function getDictationModeLabel(m, t) {
|
|
2294
|
+
const trans = t || moduleT
|
|
2295
|
+
if (m === 'verbatim') return trans('modeVerbatim')
|
|
2296
|
+
if (m === 'task') return trans('modeTask')
|
|
2297
|
+
if (m === 'note') return trans('modeNote')
|
|
2298
|
+
return trans('modeClean')
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function cycleDictationMode() {
|
|
2302
|
+
const cur = voice.selectedDictationMode || (voice.settings && voice.settings.dictationMode) || 'clean'
|
|
2303
|
+
const nextIdx = (DICTATION_MODES.indexOf(cur) + 1) % DICTATION_MODES.length
|
|
2304
|
+
voice.selectedDictationMode = DICTATION_MODES[nextIdx]
|
|
2305
|
+
voice.notify()
|
|
2211
2306
|
}
|
|
2212
2307
|
// ----------------------------------------------------------- visualizers
|
|
2213
2308
|
let _accentColorCache = { color: '', expires: 0 }
|
|
@@ -2530,7 +2625,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2530
2625
|
// Live caption of what is heard right now. Until the browser emits a
|
|
2531
2626
|
// final chunk the text is interim and changes on screen.
|
|
2532
2627
|
const caption = voice.caption || (inBrowser ? '' : null)
|
|
2628
|
+
const activeSubMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
2629
|
+
|| voice.selectedDictationMode
|
|
2630
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
2631
|
+
|| 'clean'
|
|
2533
2632
|
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
2633
|
+
React.createElement('span', { className: 'dvo-mode-badge' }, getDictationModeLabel(activeSubMode, t)),
|
|
2534
2634
|
React.createElement('button', {
|
|
2535
2635
|
type: 'button', className: 'dvo-pbtn', title: t('cancel'),
|
|
2536
2636
|
'aria-label': t('cancel'),
|
|
@@ -2559,8 +2659,16 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2559
2659
|
}
|
|
2560
2660
|
|
|
2561
2661
|
if (v.phase === 'processing') {
|
|
2662
|
+
const activeSubMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
2663
|
+
|| voice.selectedDictationMode
|
|
2664
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
2665
|
+
|| 'clean'
|
|
2562
2666
|
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
2563
|
-
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
2667
|
+
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
2668
|
+
spinIcon(),
|
|
2669
|
+
React.createElement('span', { className: 'dvo-mode-badge' }, getDictationModeLabel(activeSubMode, t)),
|
|
2670
|
+
t('transcribing'),
|
|
2671
|
+
))
|
|
2564
2672
|
}
|
|
2565
2673
|
|
|
2566
2674
|
if (v.phase === 'pending') {
|
|
@@ -2677,6 +2785,9 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2677
2785
|
voiceCommands: !!(data && data.voiceCommands),
|
|
2678
2786
|
vadSilenceMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.vadSilenceMs) || 700,
|
|
2679
2787
|
autoSendMs: Number(data && data.modes && data.modes.message && data.modes.message.autoSendMs) || 4000,
|
|
2788
|
+
dictationMode: String((data && data.dictationMode) || 'clean'),
|
|
2789
|
+
dictationPrompts: (data && data.dictationPrompts) || {},
|
|
2790
|
+
voiceMacros: Array.isArray(data && data.voiceMacros) ? data.voiceMacros : [],
|
|
2680
2791
|
sendDelayMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.sendDelayMs) || 0,
|
|
2681
2792
|
stream: !!(data && data.modes && data.modes.dictation && data.modes.dictation.stream),
|
|
2682
2793
|
streamChunkMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.streamChunkMs) || 1200,
|
|
@@ -3016,6 +3127,93 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3016
3127
|
),
|
|
3017
3128
|
)
|
|
3018
3129
|
}
|
|
3130
|
+
// ------------------------------------------------------------------ 72-dictation-modes
|
|
3131
|
+
// Dictation modes configuration card and prompt template editors (#264)
|
|
3132
|
+
|
|
3133
|
+
function DictationModesSection(props) {
|
|
3134
|
+
const { t, draft, setTop, writable } = props
|
|
3135
|
+
const prompts = (draft && draft.dictationPrompts) || {}
|
|
3136
|
+
const currentMode = (draft && draft.dictationMode) || 'clean'
|
|
3137
|
+
|
|
3138
|
+
const setPrompt = (key, val) => {
|
|
3139
|
+
const updated = { ...prompts, [key]: val }
|
|
3140
|
+
setTop('dictationPrompts', updated)
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
const resetPrompts = () => {
|
|
3144
|
+
setTop('dictationPrompts', { clean: '', task: '', note: '' })
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
3148
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
3149
|
+
React.createElement('span', null, '🎙️ ' + t('dictationModeTitle')),
|
|
3150
|
+
),
|
|
3151
|
+
React.createElement('div', { className: 'cb-field-desc' }, t('dictationModeDesc')),
|
|
3152
|
+
React.createElement('div', { className: 'cb-grid-2', style: { marginTop: '8px' } },
|
|
3153
|
+
['clean', 'task', 'note', 'verbatim'].map((m) =>
|
|
3154
|
+
React.createElement('label', { key: m, className: 'cb-check-label' },
|
|
3155
|
+
React.createElement('input', {
|
|
3156
|
+
type: 'radio',
|
|
3157
|
+
name: 'dictationMode',
|
|
3158
|
+
value: m,
|
|
3159
|
+
checked: currentMode === m,
|
|
3160
|
+
disabled: !writable,
|
|
3161
|
+
onChange: () => setTop('dictationMode', m),
|
|
3162
|
+
}),
|
|
3163
|
+
getDictationModeLabel(m, t),
|
|
3164
|
+
),
|
|
3165
|
+
),
|
|
3166
|
+
),
|
|
3167
|
+
React.createElement('div', { style: { marginTop: '12px', borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: '10px' } },
|
|
3168
|
+
React.createElement('div', { className: 'cb-section-title', style: { fontSize: '13px' } },
|
|
3169
|
+
React.createElement('span', null, t('dictationPromptsTitle')),
|
|
3170
|
+
React.createElement('button', {
|
|
3171
|
+
type: 'button',
|
|
3172
|
+
className: 'cb-btn cb-btn-secondary',
|
|
3173
|
+
style: { fontSize: '11px', padding: '2px 8px' },
|
|
3174
|
+
disabled: !writable,
|
|
3175
|
+
onClick: resetPrompts,
|
|
3176
|
+
}, t('resetPrompts')),
|
|
3177
|
+
),
|
|
3178
|
+
React.createElement('div', { className: 'cb-field-desc' }, t('dictationPromptsDesc')),
|
|
3179
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' } },
|
|
3180
|
+
React.createElement('div', null,
|
|
3181
|
+
React.createElement('label', { className: 'cb-label' }, t('promptClean')),
|
|
3182
|
+
React.createElement('textarea', {
|
|
3183
|
+
className: 'cb-input',
|
|
3184
|
+
style: { minHeight: '48px', resize: 'vertical' },
|
|
3185
|
+
value: prompts.clean || '',
|
|
3186
|
+
placeholder: 'Default: punctuation & filler-word cleanup',
|
|
3187
|
+
disabled: !writable,
|
|
3188
|
+
onChange: (e) => setPrompt('clean', e.target.value),
|
|
3189
|
+
}),
|
|
3190
|
+
),
|
|
3191
|
+
React.createElement('div', null,
|
|
3192
|
+
React.createElement('label', { className: 'cb-label' }, t('promptTask')),
|
|
3193
|
+
React.createElement('textarea', {
|
|
3194
|
+
className: 'cb-input',
|
|
3195
|
+
style: { minHeight: '48px', resize: 'vertical' },
|
|
3196
|
+
value: prompts.task || '',
|
|
3197
|
+
placeholder: 'Default: structured problem/outcome/constraints/criteria',
|
|
3198
|
+
disabled: !writable,
|
|
3199
|
+
onChange: (e) => setPrompt('task', e.target.value),
|
|
3200
|
+
}),
|
|
3201
|
+
),
|
|
3202
|
+
React.createElement('div', null,
|
|
3203
|
+
React.createElement('label', { className: 'cb-label' }, t('promptNote')),
|
|
3204
|
+
React.createElement('textarea', {
|
|
3205
|
+
className: 'cb-input',
|
|
3206
|
+
style: { minHeight: '48px', resize: 'vertical' },
|
|
3207
|
+
value: prompts.note || '',
|
|
3208
|
+
placeholder: 'Default: key points & action items summary',
|
|
3209
|
+
disabled: !writable,
|
|
3210
|
+
onChange: (e) => setPrompt('note', e.target.value),
|
|
3211
|
+
}),
|
|
3212
|
+
),
|
|
3213
|
+
),
|
|
3214
|
+
),
|
|
3215
|
+
)
|
|
3216
|
+
}
|
|
3019
3217
|
// 5. Hardware & Audio Engines Card
|
|
3020
3218
|
function HardwareOptionsCard(props) {
|
|
3021
3219
|
const t = props.t || moduleT
|
|
@@ -3499,6 +3697,78 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3499
3697
|
),
|
|
3500
3698
|
)
|
|
3501
3699
|
}
|
|
3700
|
+
// ------------------------------------------------------------------ 72-macros
|
|
3701
|
+
// Voice action macros configuration card (#167)
|
|
3702
|
+
|
|
3703
|
+
function MacrosSection(props) {
|
|
3704
|
+
const { t, draft, setTop, writable } = props
|
|
3705
|
+
const macros = Array.isArray(draft && draft.voiceMacros) ? draft.voiceMacros : []
|
|
3706
|
+
|
|
3707
|
+
const addMacro = () => {
|
|
3708
|
+
const next = [...macros, { trigger: '', template: '' }]
|
|
3709
|
+
setTop('voiceMacros', next)
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
const updateMacro = (idx, field, val) => {
|
|
3713
|
+
const next = macros.map((item, i) => (i === idx ? { ...item, [field]: val } : item))
|
|
3714
|
+
setTop('voiceMacros', next)
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
const removeMacro = (idx) => {
|
|
3718
|
+
const next = macros.filter((_, i) => i !== idx)
|
|
3719
|
+
setTop('voiceMacros', next)
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
3723
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
3724
|
+
React.createElement('span', null, '⚡ ' + t('voiceMacrosTitle')),
|
|
3725
|
+
React.createElement('button', {
|
|
3726
|
+
type: 'button',
|
|
3727
|
+
className: 'cb-btn cb-btn-secondary',
|
|
3728
|
+
style: { fontSize: '11px', padding: '2px 8px' },
|
|
3729
|
+
disabled: !writable,
|
|
3730
|
+
onClick: addMacro,
|
|
3731
|
+
}, '+ ' + t('addMacro')),
|
|
3732
|
+
),
|
|
3733
|
+
React.createElement('div', { className: 'cb-field-desc' }, t('voiceMacrosDesc')),
|
|
3734
|
+
macros.length === 0
|
|
3735
|
+
? React.createElement('div', { style: { fontStyle: 'italic', color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px', marginTop: '6px' } },
|
|
3736
|
+
'No macros configured.',
|
|
3737
|
+
)
|
|
3738
|
+
: React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '6px' } },
|
|
3739
|
+
macros.map((m, idx) =>
|
|
3740
|
+
React.createElement('div', { key: idx, className: 'dvo-macro-item' },
|
|
3741
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
3742
|
+
React.createElement('input', {
|
|
3743
|
+
type: 'text',
|
|
3744
|
+
className: 'cb-input',
|
|
3745
|
+
style: { flex: '1', height: '28px', fontSize: '12px' },
|
|
3746
|
+
placeholder: t('macroTrigger'),
|
|
3747
|
+
value: m.trigger || '',
|
|
3748
|
+
disabled: !writable,
|
|
3749
|
+
onChange: (e) => updateMacro(idx, 'trigger', e.target.value),
|
|
3750
|
+
}),
|
|
3751
|
+
React.createElement('button', {
|
|
3752
|
+
type: 'button',
|
|
3753
|
+
className: 'cb-btn cb-btn-secondary',
|
|
3754
|
+
style: { padding: '2px 6px', fontSize: '11px', color: 'var(--dsw-alias-state-error-primary)' },
|
|
3755
|
+
disabled: !writable,
|
|
3756
|
+
onClick: () => removeMacro(idx),
|
|
3757
|
+
}, t('macroDelete')),
|
|
3758
|
+
),
|
|
3759
|
+
React.createElement('textarea', {
|
|
3760
|
+
className: 'cb-input',
|
|
3761
|
+
style: { minHeight: '38px', resize: 'vertical', fontSize: '12px' },
|
|
3762
|
+
placeholder: t('macroTemplate') + ' (use {text} for argument)',
|
|
3763
|
+
value: m.template || '',
|
|
3764
|
+
disabled: !writable,
|
|
3765
|
+
onChange: (e) => updateMacro(idx, 'template', e.target.value),
|
|
3766
|
+
}),
|
|
3767
|
+
),
|
|
3768
|
+
),
|
|
3769
|
+
),
|
|
3770
|
+
)
|
|
3771
|
+
}
|
|
3502
3772
|
// SenseVoice Local ASR (1-Click Setup)
|
|
3503
3773
|
function SensevoiceSection(props) {
|
|
3504
3774
|
const t = props.t || moduleT
|
|
@@ -3652,7 +3922,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3652
3922
|
const [serverConfig, setServerConfig] = React.useState(null)
|
|
3653
3923
|
const [configLoading, setConfigLoading] = React.useState(true)
|
|
3654
3924
|
const [revision, setRevision] = React.useState(null)
|
|
3655
|
-
|
|
3656
3925
|
React.useEffect(() => {
|
|
3657
3926
|
let alive = true
|
|
3658
3927
|
fetch('/dsh-voice/config', { cache: 'no-store' })
|
|
@@ -3670,7 +3939,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3670
3939
|
})
|
|
3671
3940
|
return () => { alive = false }
|
|
3672
3941
|
}, [])
|
|
3673
|
-
|
|
3674
3942
|
// Hotkey picker: not a name field — press the key you want.
|
|
3675
3943
|
const [catching, setCatching] = React.useState(false)
|
|
3676
3944
|
const [jargonInput, setJargonInput] = React.useState(null)
|
|
@@ -3693,7 +3961,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3693
3961
|
document.addEventListener('keydown', onKey, true)
|
|
3694
3962
|
return () => document.removeEventListener('keydown', onKey, true)
|
|
3695
3963
|
}, [catching, ctx])
|
|
3696
|
-
|
|
3697
3964
|
React.useEffect(() => {
|
|
3698
3965
|
let alive = true
|
|
3699
3966
|
const render = () => { if (alive && scope && typeof scope.getSnapshot === 'function') setSnap(scope.getSnapshot()) }
|
|
@@ -3701,7 +3968,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3701
3968
|
const off = (scope && typeof scope.subscribe === 'function') ? scope.subscribe(render) : () => {}
|
|
3702
3969
|
return () => { alive = false; off() }
|
|
3703
3970
|
}, [scope])
|
|
3704
|
-
|
|
3705
3971
|
const [devices, setDevices] = React.useState([])
|
|
3706
3972
|
React.useEffect(() => {
|
|
3707
3973
|
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return
|
|
@@ -3709,11 +3975,9 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3709
3975
|
.then((list) => setDevices(list.filter((d) => d.kind === 'audioinput')))
|
|
3710
3976
|
.catch(() => {})
|
|
3711
3977
|
}, [])
|
|
3712
|
-
|
|
3713
3978
|
const ready = !!serverConfig || (!!snap && snap.status === 'ready')
|
|
3714
3979
|
const value = (snap && snap.status === 'ready' && snap.value) ? snap.value : (serverConfig || {})
|
|
3715
3980
|
const writable = ready
|
|
3716
|
-
|
|
3717
3981
|
React.useEffect(() => {
|
|
3718
3982
|
if (ready) return undefined
|
|
3719
3983
|
let tries = 0
|
|
@@ -3730,14 +3994,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3730
3994
|
}, 1000)
|
|
3731
3995
|
return () => clearInterval(timer)
|
|
3732
3996
|
}, [ready])
|
|
3733
|
-
|
|
3734
3997
|
const deepClone = (obj) => {
|
|
3735
3998
|
if (typeof structuredClone === 'function') {
|
|
3736
3999
|
try { return structuredClone(obj) } catch (e) { /* fallback */ }
|
|
3737
4000
|
}
|
|
3738
4001
|
try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
|
|
3739
4002
|
}
|
|
3740
|
-
|
|
3741
4003
|
const setTop = (key, val) => {
|
|
3742
4004
|
setDraft((d) => Object.assign({}, d || {}, { [key]: val }))
|
|
3743
4005
|
}
|
|
@@ -3758,9 +4020,7 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3758
4020
|
}),
|
|
3759
4021
|
hint ? React.createElement('span', { className: 'dvs-sub' }, hint) : null,
|
|
3760
4022
|
)
|
|
3761
|
-
|
|
3762
4023
|
React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
|
|
3763
|
-
|
|
3764
4024
|
React.useEffect(() => {
|
|
3765
4025
|
if (!ready) return
|
|
3766
4026
|
const val = value || {}
|
|
@@ -3792,10 +4052,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3792
4052
|
techJargonCorrection: val.techJargonCorrection !== false,
|
|
3793
4053
|
liveInterimPreview: val.liveInterimPreview !== false,
|
|
3794
4054
|
structuredPromptVoice: val.structuredPromptVoice !== false,
|
|
4055
|
+
dictationMode: val.dictationMode || 'clean',
|
|
4056
|
+
dictationPrompts: val.dictationPrompts || {},
|
|
4057
|
+
voiceMacros: Array.isArray(val.voiceMacros) ? val.voiceMacros : [],
|
|
3795
4058
|
webgpuModel: String(val.webgpuModel || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'),
|
|
3796
4059
|
})
|
|
3797
4060
|
}, [ready, value])
|
|
3798
|
-
|
|
3799
4061
|
const [statusData, setStatusData] = React.useState(null)
|
|
3800
4062
|
const [statusLatency, setStatusLatency] = React.useState(null)
|
|
3801
4063
|
const fetchStatus = React.useCallback(() => {
|
|
@@ -3819,17 +4081,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3819
4081
|
setStatusLatency(null)
|
|
3820
4082
|
})
|
|
3821
4083
|
}, [])
|
|
3822
|
-
|
|
3823
4084
|
React.useEffect(() => {
|
|
3824
4085
|
fetchStatus()
|
|
3825
4086
|
}, [fetchStatus])
|
|
3826
|
-
|
|
3827
4087
|
const { sensevoiceState, installingSensevoice, triggerInstallSensevoice } = useSensevoiceInstaller(fetchStatus)
|
|
3828
|
-
|
|
3829
4088
|
const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
|
|
3830
|
-
|
|
3831
4089
|
const { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
|
|
3832
|
-
|
|
3833
4090
|
const save = async () => {
|
|
3834
4091
|
setErr(''); setSaved(false)
|
|
3835
4092
|
if (!draft) return
|
|
@@ -3848,7 +4105,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3848
4105
|
.filter(Boolean)
|
|
3849
4106
|
draft.jargonDictionary = parsed
|
|
3850
4107
|
}
|
|
3851
|
-
|
|
3852
4108
|
let httpOk = false
|
|
3853
4109
|
let httpError = null
|
|
3854
4110
|
try {
|
|
@@ -3870,7 +4126,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3870
4126
|
} catch (e) {
|
|
3871
4127
|
httpError = e?.message || String(e)
|
|
3872
4128
|
}
|
|
3873
|
-
|
|
3874
4129
|
if (httpOk && scope && typeof scope.set === 'function') {
|
|
3875
4130
|
for (const k of Object.keys(draft)) {
|
|
3876
4131
|
try {
|
|
@@ -3880,12 +4135,10 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3880
4135
|
}
|
|
3881
4136
|
}
|
|
3882
4137
|
}
|
|
3883
|
-
|
|
3884
4138
|
if (!httpOk) {
|
|
3885
4139
|
setErr(t('saveFailed') + ': ' + httpError)
|
|
3886
4140
|
return
|
|
3887
4141
|
}
|
|
3888
|
-
|
|
3889
4142
|
const d = draft || {}
|
|
3890
4143
|
voice.hotkey = d.hotkey || ''
|
|
3891
4144
|
if (voice.hotkey) {
|
|
@@ -3917,23 +4170,19 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3917
4170
|
structuredPromptVoice: d.structuredPromptVoice !== false,
|
|
3918
4171
|
})
|
|
3919
4172
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
3920
|
-
|
|
3921
4173
|
setSaved(true); setJargonInput(null)
|
|
3922
4174
|
setTimeout(() => setSaved(false), 2000)
|
|
3923
4175
|
fetchStatus()
|
|
3924
4176
|
}
|
|
3925
|
-
|
|
3926
4177
|
const modeVal = (mode, key, fallback) => {
|
|
3927
4178
|
const m = draft && draft[mode]
|
|
3928
4179
|
return m && m[key] !== undefined ? m[key] : fallback
|
|
3929
4180
|
}
|
|
3930
|
-
|
|
3931
4181
|
const chainOptions = BUILTIN.concat(
|
|
3932
4182
|
(draft && Array.isArray(draft.customProviders) ? draft.customProviders : [])
|
|
3933
4183
|
.map((c) => String(c && c.key || '').trim())
|
|
3934
4184
|
.filter((k) => k && BUILTIN.indexOf(k) < 0),
|
|
3935
4185
|
)
|
|
3936
|
-
|
|
3937
4186
|
const langField = (mode) => {
|
|
3938
4187
|
const val = modeVal(mode, 'language', '')
|
|
3939
4188
|
const opts = SUPPORTED_LANGS.slice()
|
|
@@ -3950,7 +4199,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3950
4199
|
))),
|
|
3951
4200
|
)
|
|
3952
4201
|
}
|
|
3953
|
-
|
|
3954
4202
|
const numField = (mode, key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
3955
4203
|
React.createElement('label', null, label),
|
|
3956
4204
|
React.createElement('input', {
|
|
@@ -3959,15 +4207,11 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3959
4207
|
}),
|
|
3960
4208
|
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
3961
4209
|
)
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
4210
|
const statsData = (statusData && statusData.providerStats) || {}
|
|
3965
4211
|
const hostOnline = statusLatency !== null
|
|
3966
4212
|
const whisperRunning = !!(statusData && statusData.whisperRunning)
|
|
3967
4213
|
const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
|
|
3968
|
-
|
|
3969
4214
|
return React.createElement('div', { className: 'cb-page dvo-settings-root' },
|
|
3970
|
-
// 1. Connection & Engine Status Card
|
|
3971
4215
|
React.createElement(ConnectionStatusCard, {
|
|
3972
4216
|
t: t,
|
|
3973
4217
|
hostOnline: hostOnline,
|
|
@@ -3976,8 +4220,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3976
4220
|
providerCount: providerCount,
|
|
3977
4221
|
onRefresh: fetchStatus,
|
|
3978
4222
|
}),
|
|
3979
|
-
|
|
3980
|
-
// 2. Dictation Mode Card
|
|
3981
4223
|
React.createElement('div', { className: 'cb-section-card' },
|
|
3982
4224
|
React.createElement('div', { className: 'cb-section-title' },
|
|
3983
4225
|
React.createElement('span', null, '🎙️ ' + t('dictationBtn')),
|
|
@@ -4021,8 +4263,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4021
4263
|
),
|
|
4022
4264
|
textField('wakeWord', t('wakeWord'), t('wakeWordHint')),
|
|
4023
4265
|
),
|
|
4024
|
-
|
|
4025
|
-
// 3. Voice Message Card
|
|
4026
4266
|
React.createElement('div', { className: 'cb-section-card' },
|
|
4027
4267
|
React.createElement('div', { className: 'cb-section-title' },
|
|
4028
4268
|
React.createElement('span', null, '💬 ' + t('messageTitle')),
|
|
@@ -4061,8 +4301,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4061
4301
|
),
|
|
4062
4302
|
),
|
|
4063
4303
|
),
|
|
4064
|
-
|
|
4065
|
-
// 4. Custom Providers Card
|
|
4066
4304
|
React.createElement('div', { className: 'cb-section-card' },
|
|
4067
4305
|
React.createElement('div', { className: 'cb-section-title' },
|
|
4068
4306
|
React.createElement('span', null, '⚙️ ' + t('customTitle')),
|
|
@@ -4074,8 +4312,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4074
4312
|
onChange: (v) => setTop('customProviders', v),
|
|
4075
4313
|
}),
|
|
4076
4314
|
),
|
|
4077
|
-
|
|
4078
|
-
// 5. Hardware & Audio Engines Card
|
|
4079
4315
|
React.createElement(HardwareOptionsCard, {
|
|
4080
4316
|
t: t,
|
|
4081
4317
|
draft: draft,
|
|
@@ -4095,8 +4331,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4095
4331
|
clearGlobalHotkey()
|
|
4096
4332
|
},
|
|
4097
4333
|
}),
|
|
4098
|
-
|
|
4099
|
-
// 6. SenseVoice Local ASR (1-Click Setup)
|
|
4100
4334
|
React.createElement(SensevoiceSection, {
|
|
4101
4335
|
t: t,
|
|
4102
4336
|
sensevoiceState: sensevoiceState,
|
|
@@ -4104,8 +4338,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4104
4338
|
writable: writable,
|
|
4105
4339
|
onInstall: triggerInstallSensevoice,
|
|
4106
4340
|
}),
|
|
4107
|
-
|
|
4108
|
-
// 7. Turn-Taking & Audio Coordination
|
|
4109
4341
|
React.createElement('div', { className: 'cb-section-card' },
|
|
4110
4342
|
React.createElement('div', { className: 'cb-section-title' },
|
|
4111
4343
|
React.createElement('span', null, '🎙️ ' + t('turnTakingTitle')),
|
|
@@ -4132,8 +4364,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4132
4364
|
),
|
|
4133
4365
|
),
|
|
4134
4366
|
),
|
|
4135
|
-
|
|
4136
|
-
// 8. Hands-free & Smart Actions
|
|
4137
4367
|
React.createElement('div', { className: 'cb-section-card' },
|
|
4138
4368
|
React.createElement('div', { className: 'cb-section-title' },
|
|
4139
4369
|
React.createElement('span', null, '✨ ' + t('smartActionsTitle')),
|
|
@@ -4177,8 +4407,20 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4177
4407
|
),
|
|
4178
4408
|
),
|
|
4179
4409
|
),
|
|
4180
|
-
|
|
4181
|
-
|
|
4410
|
+
// Dictation Modes & Prompts (#264)
|
|
4411
|
+
React.createElement(DictationModesSection, {
|
|
4412
|
+
t: t,
|
|
4413
|
+
draft: draft,
|
|
4414
|
+
setTop: setTop,
|
|
4415
|
+
writable: writable,
|
|
4416
|
+
}),
|
|
4417
|
+
// Voice Action Macros (#167)
|
|
4418
|
+
React.createElement(MacrosSection, {
|
|
4419
|
+
t: t,
|
|
4420
|
+
draft: draft,
|
|
4421
|
+
setTop: setTop,
|
|
4422
|
+
writable: writable,
|
|
4423
|
+
}),
|
|
4182
4424
|
React.createElement(JargonEditor, {
|
|
4183
4425
|
t: t,
|
|
4184
4426
|
draft: draft,
|
|
@@ -4187,15 +4429,11 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4187
4429
|
jargonInput: jargonInput,
|
|
4188
4430
|
setJargonInput: setJargonInput,
|
|
4189
4431
|
}),
|
|
4190
|
-
|
|
4191
|
-
// 10. Provider Telemetry & Latency Dashboard
|
|
4192
4432
|
React.createElement(ProviderDashboard, {
|
|
4193
4433
|
t: t,
|
|
4194
4434
|
statsData: statsData,
|
|
4195
4435
|
onRefresh: fetchStatus,
|
|
4196
4436
|
}),
|
|
4197
|
-
|
|
4198
|
-
// 7. Plugin Version & Updates Card
|
|
4199
4437
|
React.createElement(UpdaterCard, {
|
|
4200
4438
|
t: t,
|
|
4201
4439
|
updaterStatus: updaterStatus,
|
|
@@ -4206,8 +4444,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4206
4444
|
onCheck: checkUpdater,
|
|
4207
4445
|
onUpdateNow: onUpdateNow,
|
|
4208
4446
|
}),
|
|
4209
|
-
|
|
4210
|
-
// Action bar (Save)
|
|
4211
4447
|
React.createElement('div', { className: 'cb-row' },
|
|
4212
4448
|
React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable, onClick: save }, t('save')),
|
|
4213
4449
|
saved ? React.createElement('span', { className: 'dvs-ok' }, t('saved')) : null,
|
|
@@ -4320,6 +4556,9 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4320
4556
|
isAllowedInEditable,
|
|
4321
4557
|
isHotkeyRelease,
|
|
4322
4558
|
extractContextKeywords,
|
|
4559
|
+
DICTATION_MODES,
|
|
4560
|
+
getDictationModeLabel,
|
|
4561
|
+
cycleDictationMode,
|
|
4323
4562
|
resolveBrowserRecognitionLang,
|
|
4324
4563
|
VoiceSection,
|
|
4325
4564
|
cutPhrase,
|