@goodandready/dsh-voice 0.8.21 → 0.8.23
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-src/70-settings-base.js +126 -0
- package/lib/client-src/71-chains.js +88 -0
- package/lib/client-src/{70-settings.js → 72-voice-section.js} +10 -255
- package/lib/client-src/73-plugin-card.js +38 -0
- package/lib/client.js +50 -43
- package/lib/http-util.js +28 -0
- package/lib/index.js +21 -104
- package/lib/polish.js +74 -0
- package/lib/providers.js +26 -6
- package/lib/transcribe-core.js +49 -0
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ------------------------------------------------------- settings page
|
|
2
|
+
const BUILTIN = [
|
|
3
|
+
'browser', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice',
|
|
4
|
+
// Presets: address and model are filled on the host; only a key is needed.
|
|
5
|
+
'openai', 'siliconflow', 'deepinfra', 'fireworks', 'mistral', 'openrouter',
|
|
6
|
+
]
|
|
7
|
+
const TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
|
|
8
|
+
// Resolve at render time: t() must not be captured before locale bind.
|
|
9
|
+
const MODEL_HINT_KEYS = {
|
|
10
|
+
browser: 'browserHint',
|
|
11
|
+
openai: 'openaiHint',
|
|
12
|
+
siliconflow: 'siliconflowHint',
|
|
13
|
+
deepinfra: 'deepinfraHint',
|
|
14
|
+
fireworks: 'fireworksHint',
|
|
15
|
+
mistral: 'mistralHint',
|
|
16
|
+
openrouter: 'openrouterHint',
|
|
17
|
+
'local-whisper': 'localHint',
|
|
18
|
+
sensevoice: 'sensevoiceHint',
|
|
19
|
+
}
|
|
20
|
+
const MODEL_HINT_STATIC = {
|
|
21
|
+
deepgram: 'nova-2',
|
|
22
|
+
groq: 'whisper-large-v3-turbo',
|
|
23
|
+
hf: 'openai/whisper-large-v3',
|
|
24
|
+
}
|
|
25
|
+
function modelHint(provider) {
|
|
26
|
+
const key = MODEL_HINT_KEYS[provider]
|
|
27
|
+
if (key) return t(key)
|
|
28
|
+
return MODEL_HINT_STATIC[provider] || ''
|
|
29
|
+
}
|
|
30
|
+
const LANGS = ['auto', 'ru', 'en', 'uk', 'de']
|
|
31
|
+
|
|
32
|
+
const SET_CSS =
|
|
33
|
+
'.cb-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}' +
|
|
34
|
+
'.cb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}' +
|
|
35
|
+
'.cb-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
36
|
+
'.cb-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}' +
|
|
37
|
+
'.cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}' +
|
|
38
|
+
'.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:12px}' +
|
|
39
|
+
'.cb-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
40
|
+
'.cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' +
|
|
41
|
+
'.cb-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' +
|
|
42
|
+
'.cb-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' +
|
|
43
|
+
'.cb-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
44
|
+
'.cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
45
|
+
'.cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}' +
|
|
46
|
+
'.cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
47
|
+
'.cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}' +
|
|
48
|
+
'.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88}' +
|
|
49
|
+
'.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}' +
|
|
50
|
+
'.cb-btn-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:13px}' +
|
|
51
|
+
'.cb-btn-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}' +
|
|
52
|
+
'.cb-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
53
|
+
'.cb-field select,.cb-field input[type="text"],.cb-field input[type="number"],.cb-field textarea{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
54
|
+
'.cb-field textarea{height:auto;padding:8px 10px;font-family:inherit}' +
|
|
55
|
+
'.cb-check-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}' +
|
|
56
|
+
'.cb-check-label input[type="checkbox"]{width:16px;height:16px;cursor:pointer}' +
|
|
57
|
+
'.cb-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}' +
|
|
58
|
+
'.dvs-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0;max-width:960px}' +
|
|
59
|
+
'.dvs-block{display:flex;flex-direction:column;gap:10px}' +
|
|
60
|
+
'.dvs-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
61
|
+
'.dvs-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
62
|
+
'.dvs-row{display:flex;gap:8px;align-items:center}' +
|
|
63
|
+
'.dvs-row select,.dvs-row input{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px}' +
|
|
64
|
+
'.dvs-row .dvs-model{flex:1}' +
|
|
65
|
+
'.dvs-card{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:12px}' +
|
|
66
|
+
'.dvs-card input,.dvs-card select{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}' +
|
|
67
|
+
'.dvs-wait{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5;max-width:520px}' +
|
|
68
|
+
'.dvs-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
|
|
69
|
+
'.dvs-bad{font-size:12px;color:var(--dsw-alias-state-error-primary)}' +
|
|
70
|
+
'.dvo-pcard{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px}' +
|
|
71
|
+
'.dvo-phead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
72
|
+
'.dvo-pheadtext{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}' +
|
|
73
|
+
'.dvo-ptitle{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
74
|
+
'.dvo-pdesc{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
75
|
+
'.dvo-pchev{flex:none;display:flex;color:var(--dsw-alias-label-secondary);transition:transform .15s ease}' +
|
|
76
|
+
'.dvo-pcardOpen .dvo-pchev{transform:rotate(180deg)}' +
|
|
77
|
+
'.dvo-pbody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}'
|
|
78
|
+
const setCssId = 'dsh-voice/settings.module.css'
|
|
79
|
+
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + setCssId + '"]')) {
|
|
80
|
+
const tag = document.createElement('style')
|
|
81
|
+
tag.textContent = SET_CSS
|
|
82
|
+
tag.setAttribute('data-dsh-plugin', 'dsh-voice')
|
|
83
|
+
tag.dataset.pluginCss = setCssId
|
|
84
|
+
document.head.appendChild(tag)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createErrorBoundary() {
|
|
88
|
+
if (!React || typeof React.Component !== 'function') {
|
|
89
|
+
return function NoopBoundary(props) { return props?.children || null }
|
|
90
|
+
}
|
|
91
|
+
return class ErrorBoundary extends React.Component {
|
|
92
|
+
constructor(props) {
|
|
93
|
+
super(props)
|
|
94
|
+
this.state = { hasError: false, error: null }
|
|
95
|
+
}
|
|
96
|
+
static getDerivedStateFromError(error) {
|
|
97
|
+
return { hasError: true, error }
|
|
98
|
+
}
|
|
99
|
+
componentDidCatch(error, errorInfo) {
|
|
100
|
+
console.error('[dsh-voice] React Error:', error, errorInfo)
|
|
101
|
+
}
|
|
102
|
+
render() {
|
|
103
|
+
if (this.state.hasError) {
|
|
104
|
+
return React.createElement(
|
|
105
|
+
'div',
|
|
106
|
+
{ className: 'cb-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
|
|
107
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ ' + t('uiError')),
|
|
108
|
+
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
109
|
+
React.createElement(
|
|
110
|
+
'button',
|
|
111
|
+
{
|
|
112
|
+
type: 'button',
|
|
113
|
+
className: 'cb-btn',
|
|
114
|
+
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
115
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
116
|
+
},
|
|
117
|
+
t('retry'),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
return this.props?.children || null
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const ErrorBoundary = createErrorBoundary()
|
|
126
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// One chain editor: provider+model rows with reordering.
|
|
2
|
+
function ChainEditor(props) {
|
|
3
|
+
const rows = Array.isArray(props.value) ? props.value : []
|
|
4
|
+
const change = (i, patch) => {
|
|
5
|
+
const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
|
|
6
|
+
props.onChange(next)
|
|
7
|
+
}
|
|
8
|
+
const move = (i, delta) => {
|
|
9
|
+
const j = i + delta
|
|
10
|
+
if (j < 0 || j >= rows.length) return
|
|
11
|
+
const next = rows.slice()
|
|
12
|
+
const tmp = next[i]; next[i] = next[j]; next[j] = tmp
|
|
13
|
+
props.onChange(next)
|
|
14
|
+
}
|
|
15
|
+
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
16
|
+
const add = () => props.onChange(rows.concat([{ provider: 'local-whisper', model: '' }]))
|
|
17
|
+
const options = Array.isArray(props.options) && props.options.length ? props.options : BUILTIN
|
|
18
|
+
|
|
19
|
+
return React.createElement('div', { className: 'dvs-block' },
|
|
20
|
+
rows.map((row, i) => React.createElement('div', { className: 'dvs-row', key: i },
|
|
21
|
+
React.createElement('select', {
|
|
22
|
+
value: row.provider, disabled: !props.writable,
|
|
23
|
+
onChange: (e) => change(i, { provider: e.target.value }),
|
|
24
|
+
}, (options.indexOf(row.provider) < 0 ? options.concat([row.provider]) : options)
|
|
25
|
+
.map((p) => React.createElement('option', { key: p, value: p }, p))),
|
|
26
|
+
React.createElement('input', {
|
|
27
|
+
className: 'dvs-model', value: row.model || '', disabled: !props.writable,
|
|
28
|
+
placeholder: modelHint(row.provider), onChange: (e) => change(i, { model: e.target.value }),
|
|
29
|
+
}),
|
|
30
|
+
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '↑'),
|
|
31
|
+
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '↓'),
|
|
32
|
+
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '×'),
|
|
33
|
+
)),
|
|
34
|
+
React.createElement('div', { className: 'dvs-row' },
|
|
35
|
+
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
|
|
36
|
+
React.createElement('span', { className: 'dvs-sub' }, t('chainHint')),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Custom providers: name, API template, where to call, how to authorize.
|
|
42
|
+
function CustomEditor(props) {
|
|
43
|
+
const rows = Array.isArray(props.value) ? props.value : []
|
|
44
|
+
const change = (i, patch) => props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
|
|
45
|
+
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
46
|
+
const add = () => props.onChange(rows.concat([
|
|
47
|
+
{ key: '', template: 'openai-transcriptions', baseURL: '', model: '', keyEnv: '', prompt: '' },
|
|
48
|
+
]))
|
|
49
|
+
const field = (i, row, name, placeholder, wide) => React.createElement('input', {
|
|
50
|
+
className: wide ? 'dvs-model' : '', value: row[name] || '', placeholder: placeholder,
|
|
51
|
+
disabled: !props.writable, onChange: (e) => change(i, { [name]: e.target.value }),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return React.createElement('div', { className: 'dvs-block' },
|
|
55
|
+
rows.map((row, i) => React.createElement('div', { className: 'dvs-card', key: i },
|
|
56
|
+
React.createElement('div', { className: 'dvs-row' },
|
|
57
|
+
field(i, row, 'key', t('customName')),
|
|
58
|
+
React.createElement('select', {
|
|
59
|
+
value: row.template || 'openai-transcriptions', disabled: !props.writable,
|
|
60
|
+
onChange: (e) => change(i, { template: e.target.value }),
|
|
61
|
+
}, TEMPLATES.map((t) => React.createElement('option', { key: t, value: t }, t))),
|
|
62
|
+
React.createElement('button', {
|
|
63
|
+
type: 'button', className: 'dvs-mini', title: t('remove'),
|
|
64
|
+
disabled: !props.writable, onClick: () => remove(i),
|
|
65
|
+
}, '\u00d7'),
|
|
66
|
+
),
|
|
67
|
+
React.createElement('div', { className: 'dvs-row' },
|
|
68
|
+
field(i, row, 'baseURL', 'https://openrouter.ai/api/v1', true),
|
|
69
|
+
),
|
|
70
|
+
React.createElement('div', { className: 'dvs-row' },
|
|
71
|
+
field(i, row, 'model', t('customModel'), true),
|
|
72
|
+
field(i, row, 'keyEnv', t('customKeyName')),
|
|
73
|
+
),
|
|
74
|
+
row.template === 'openai-chat-audio'
|
|
75
|
+
? React.createElement('div', { className: 'dvs-row' },
|
|
76
|
+
field(i, row, 'prompt', t('customModelHint'), true))
|
|
77
|
+
: null,
|
|
78
|
+
)),
|
|
79
|
+
React.createElement('div', { className: 'dvs-row' },
|
|
80
|
+
React.createElement('button', {
|
|
81
|
+
type: 'button', className: 'dvs-mini', title: t('addCustom'),
|
|
82
|
+
disabled: !props.writable, onClick: add,
|
|
83
|
+
}, '+'),
|
|
84
|
+
React.createElement('span', { className: 'dvs-sub' },
|
|
85
|
+
t('openrouterWarning')),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -1,218 +1,4 @@
|
|
|
1
|
-
// ------------------------------------------------------- settings
|
|
2
|
-
const BUILTIN = [
|
|
3
|
-
'browser', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice',
|
|
4
|
-
// Presets: address and model are filled on the host; only a key is needed.
|
|
5
|
-
'openai', 'siliconflow', 'deepinfra', 'fireworks', 'mistral', 'openrouter',
|
|
6
|
-
]
|
|
7
|
-
const TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
|
|
8
|
-
// Resolve at render time: t() must not be captured before locale bind.
|
|
9
|
-
const MODEL_HINT_KEYS = {
|
|
10
|
-
browser: 'browserHint',
|
|
11
|
-
openai: 'openaiHint',
|
|
12
|
-
siliconflow: 'siliconflowHint',
|
|
13
|
-
deepinfra: 'deepinfraHint',
|
|
14
|
-
fireworks: 'fireworksHint',
|
|
15
|
-
mistral: 'mistralHint',
|
|
16
|
-
openrouter: 'openrouterHint',
|
|
17
|
-
'local-whisper': 'localHint',
|
|
18
|
-
sensevoice: 'sensevoiceHint',
|
|
19
|
-
}
|
|
20
|
-
const MODEL_HINT_STATIC = {
|
|
21
|
-
deepgram: 'nova-2',
|
|
22
|
-
groq: 'whisper-large-v3-turbo',
|
|
23
|
-
hf: 'openai/whisper-large-v3',
|
|
24
|
-
}
|
|
25
|
-
function modelHint(provider) {
|
|
26
|
-
const key = MODEL_HINT_KEYS[provider]
|
|
27
|
-
if (key) return t(key)
|
|
28
|
-
return MODEL_HINT_STATIC[provider] || ''
|
|
29
|
-
}
|
|
30
|
-
const LANGS = ['auto', 'ru', 'en', 'uk', 'de']
|
|
31
|
-
|
|
32
|
-
const SET_CSS =
|
|
33
|
-
'.cb-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}' +
|
|
34
|
-
'.cb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}' +
|
|
35
|
-
'.cb-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
36
|
-
'.cb-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}' +
|
|
37
|
-
'.cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}' +
|
|
38
|
-
'.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:12px}' +
|
|
39
|
-
'.cb-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
40
|
-
'.cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' +
|
|
41
|
-
'.cb-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' +
|
|
42
|
-
'.cb-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' +
|
|
43
|
-
'.cb-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
44
|
-
'.cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
45
|
-
'.cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}' +
|
|
46
|
-
'.cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
47
|
-
'.cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}' +
|
|
48
|
-
'.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88}' +
|
|
49
|
-
'.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}' +
|
|
50
|
-
'.cb-btn-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:13px}' +
|
|
51
|
-
'.cb-btn-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}' +
|
|
52
|
-
'.cb-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
53
|
-
'.cb-field select,.cb-field input[type="text"],.cb-field input[type="number"],.cb-field textarea{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
54
|
-
'.cb-field textarea{height:auto;padding:8px 10px;font-family:inherit}' +
|
|
55
|
-
'.cb-check-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}' +
|
|
56
|
-
'.cb-check-label input[type="checkbox"]{width:16px;height:16px;cursor:pointer}' +
|
|
57
|
-
'.cb-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}' +
|
|
58
|
-
'.dvs-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0;max-width:960px}' +
|
|
59
|
-
'.dvs-block{display:flex;flex-direction:column;gap:10px}' +
|
|
60
|
-
'.dvs-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
61
|
-
'.dvs-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
62
|
-
'.dvs-row{display:flex;gap:8px;align-items:center}' +
|
|
63
|
-
'.dvs-row select,.dvs-row input{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px}' +
|
|
64
|
-
'.dvs-row .dvs-model{flex:1}' +
|
|
65
|
-
'.dvs-card{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:12px}' +
|
|
66
|
-
'.dvs-card input,.dvs-card select{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}' +
|
|
67
|
-
'.dvs-wait{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5;max-width:520px}' +
|
|
68
|
-
'.dvs-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
|
|
69
|
-
'.dvs-bad{font-size:12px;color:var(--dsw-alias-state-error-primary)}' +
|
|
70
|
-
'.dvo-pcard{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px}' +
|
|
71
|
-
'.dvo-phead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
72
|
-
'.dvo-pheadtext{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}' +
|
|
73
|
-
'.dvo-ptitle{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
74
|
-
'.dvo-pdesc{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
75
|
-
'.dvo-pchev{flex:none;display:flex;color:var(--dsw-alias-label-secondary);transition:transform .15s ease}' +
|
|
76
|
-
'.dvo-pcardOpen .dvo-pchev{transform:rotate(180deg)}' +
|
|
77
|
-
'.dvo-pbody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}'
|
|
78
|
-
const setCssId = 'dsh-voice/settings.module.css'
|
|
79
|
-
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + setCssId + '"]')) {
|
|
80
|
-
const tag = document.createElement('style')
|
|
81
|
-
tag.textContent = SET_CSS
|
|
82
|
-
tag.setAttribute('data-dsh-plugin', 'dsh-voice')
|
|
83
|
-
tag.dataset.pluginCss = setCssId
|
|
84
|
-
document.head.appendChild(tag)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// One chain editor: provider+model rows with reordering.
|
|
88
|
-
function ChainEditor(props) {
|
|
89
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
90
|
-
const change = (i, patch) => {
|
|
91
|
-
const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
|
|
92
|
-
props.onChange(next)
|
|
93
|
-
}
|
|
94
|
-
const move = (i, delta) => {
|
|
95
|
-
const j = i + delta
|
|
96
|
-
if (j < 0 || j >= rows.length) return
|
|
97
|
-
const next = rows.slice()
|
|
98
|
-
const tmp = next[i]; next[i] = next[j]; next[j] = tmp
|
|
99
|
-
props.onChange(next)
|
|
100
|
-
}
|
|
101
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
102
|
-
const add = () => props.onChange(rows.concat([{ provider: 'local-whisper', model: '' }]))
|
|
103
|
-
const options = Array.isArray(props.options) && props.options.length ? props.options : BUILTIN
|
|
104
|
-
|
|
105
|
-
return React.createElement('div', { className: 'dvs-block' },
|
|
106
|
-
rows.map((row, i) => React.createElement('div', { className: 'dvs-row', key: i },
|
|
107
|
-
React.createElement('select', {
|
|
108
|
-
value: row.provider, disabled: !props.writable,
|
|
109
|
-
onChange: (e) => change(i, { provider: e.target.value }),
|
|
110
|
-
}, (options.indexOf(row.provider) < 0 ? options.concat([row.provider]) : options)
|
|
111
|
-
.map((p) => React.createElement('option', { key: p, value: p }, p))),
|
|
112
|
-
React.createElement('input', {
|
|
113
|
-
className: 'dvs-model', value: row.model || '', disabled: !props.writable,
|
|
114
|
-
placeholder: modelHint(row.provider), onChange: (e) => change(i, { model: e.target.value }),
|
|
115
|
-
}),
|
|
116
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '↑'),
|
|
117
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '↓'),
|
|
118
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '×'),
|
|
119
|
-
)),
|
|
120
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
121
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
|
|
122
|
-
React.createElement('span', { className: 'dvs-sub' }, t('chainHint')),
|
|
123
|
-
),
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Custom providers: name, API template, where to call, how to authorize.
|
|
128
|
-
function CustomEditor(props) {
|
|
129
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
130
|
-
const change = (i, patch) => props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
|
|
131
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
132
|
-
const add = () => props.onChange(rows.concat([
|
|
133
|
-
{ key: '', template: 'openai-transcriptions', baseURL: '', model: '', keyEnv: '', prompt: '' },
|
|
134
|
-
]))
|
|
135
|
-
const field = (i, row, name, placeholder, wide) => React.createElement('input', {
|
|
136
|
-
className: wide ? 'dvs-model' : '', value: row[name] || '', placeholder: placeholder,
|
|
137
|
-
disabled: !props.writable, onChange: (e) => change(i, { [name]: e.target.value }),
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
return React.createElement('div', { className: 'dvs-block' },
|
|
141
|
-
rows.map((row, i) => React.createElement('div', { className: 'dvs-card', key: i },
|
|
142
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
143
|
-
field(i, row, 'key', t('customName')),
|
|
144
|
-
React.createElement('select', {
|
|
145
|
-
value: row.template || 'openai-transcriptions', disabled: !props.writable,
|
|
146
|
-
onChange: (e) => change(i, { template: e.target.value }),
|
|
147
|
-
}, TEMPLATES.map((t) => React.createElement('option', { key: t, value: t }, t))),
|
|
148
|
-
React.createElement('button', {
|
|
149
|
-
type: 'button', className: 'dvs-mini', title: t('remove'),
|
|
150
|
-
disabled: !props.writable, onClick: () => remove(i),
|
|
151
|
-
}, '\u00d7'),
|
|
152
|
-
),
|
|
153
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
154
|
-
field(i, row, 'baseURL', 'https://openrouter.ai/api/v1', true),
|
|
155
|
-
),
|
|
156
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
157
|
-
field(i, row, 'model', t('customModel'), true),
|
|
158
|
-
field(i, row, 'keyEnv', t('customKeyName')),
|
|
159
|
-
),
|
|
160
|
-
row.template === 'openai-chat-audio'
|
|
161
|
-
? React.createElement('div', { className: 'dvs-row' },
|
|
162
|
-
field(i, row, 'prompt', t('customModelHint'), true))
|
|
163
|
-
: null,
|
|
164
|
-
)),
|
|
165
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
166
|
-
React.createElement('button', {
|
|
167
|
-
type: 'button', className: 'dvs-mini', title: t('addCustom'),
|
|
168
|
-
disabled: !props.writable, onClick: add,
|
|
169
|
-
}, '+'),
|
|
170
|
-
React.createElement('span', { className: 'dvs-sub' },
|
|
171
|
-
t('openrouterWarning')),
|
|
172
|
-
),
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function createErrorBoundary() {
|
|
177
|
-
if (!React || typeof React.Component !== 'function') {
|
|
178
|
-
return function NoopBoundary(props) { return props?.children || null }
|
|
179
|
-
}
|
|
180
|
-
return class ErrorBoundary extends React.Component {
|
|
181
|
-
constructor(props) {
|
|
182
|
-
super(props)
|
|
183
|
-
this.state = { hasError: false, error: null }
|
|
184
|
-
}
|
|
185
|
-
static getDerivedStateFromError(error) {
|
|
186
|
-
return { hasError: true, error }
|
|
187
|
-
}
|
|
188
|
-
componentDidCatch(error, errorInfo) {
|
|
189
|
-
console.error('[dsh-voice] React Error:', error, errorInfo)
|
|
190
|
-
}
|
|
191
|
-
render() {
|
|
192
|
-
if (this.state.hasError) {
|
|
193
|
-
return React.createElement(
|
|
194
|
-
'div',
|
|
195
|
-
{ className: 'cb-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
|
|
196
|
-
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ ' + t('uiError')),
|
|
197
|
-
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
198
|
-
React.createElement(
|
|
199
|
-
'button',
|
|
200
|
-
{
|
|
201
|
-
type: 'button',
|
|
202
|
-
className: 'cb-btn',
|
|
203
|
-
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
204
|
-
onClick: () => this.setState({ hasError: false, error: null }),
|
|
205
|
-
},
|
|
206
|
-
t('retry'),
|
|
207
|
-
),
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
return this.props?.children || null
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const ErrorBoundary = createErrorBoundary()
|
|
215
|
-
|
|
1
|
+
// ------------------------------------------------------- settings voice section
|
|
216
2
|
function VoiceSection(props) {
|
|
217
3
|
const t = (props && props.t) || moduleT
|
|
218
4
|
const ctx = props.ctx
|
|
@@ -274,7 +60,14 @@
|
|
|
274
60
|
return () => clearInterval(timer)
|
|
275
61
|
}, [ready])
|
|
276
62
|
|
|
277
|
-
|
|
63
|
+
const deepClone = (obj) => {
|
|
64
|
+
if (typeof structuredClone === 'function') {
|
|
65
|
+
try { return structuredClone(obj) } catch (e) { /* fallback */ }
|
|
66
|
+
}
|
|
67
|
+
try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
|
|
278
71
|
|
|
279
72
|
React.useEffect(() => {
|
|
280
73
|
if (!ready) return
|
|
@@ -335,7 +128,7 @@
|
|
|
335
128
|
}
|
|
336
129
|
|
|
337
130
|
const setIn = (mode, key, v) => setDraft((d) => {
|
|
338
|
-
const next =
|
|
131
|
+
const next = deepClone(d || {})
|
|
339
132
|
next[mode] = next[mode] || {}
|
|
340
133
|
next[mode][key] = v
|
|
341
134
|
return next
|
|
@@ -725,41 +518,3 @@
|
|
|
725
518
|
)
|
|
726
519
|
}
|
|
727
520
|
|
|
728
|
-
// Card in Settings → Plugins → Plugin settings (#18)
|
|
729
|
-
function PluginCard(props) {
|
|
730
|
-
const [open, setOpen] = React.useState(false)
|
|
731
|
-
const tt = (props && props.t) || t
|
|
732
|
-
const title = tt('title')
|
|
733
|
-
return React.createElement(ErrorBoundary, null,
|
|
734
|
-
React.createElement('li',
|
|
735
|
-
{ className: open ? 'dvo-pcard dvo-pcardOpen' : 'dvo-pcard' },
|
|
736
|
-
React.createElement('button',
|
|
737
|
-
{
|
|
738
|
-
type: 'button', className: 'dvo-phead',
|
|
739
|
-
'aria-expanded': open ? 'true' : 'false',
|
|
740
|
-
'aria-label': (open ? tt('collapse') : tt('expand')) + ': ' + title,
|
|
741
|
-
onClick: () => setOpen((v) => !v),
|
|
742
|
-
},
|
|
743
|
-
React.createElement('span', { className: 'dvo-pheadtext' },
|
|
744
|
-
React.createElement('span', { className: 'dvo-ptitle' }, title),
|
|
745
|
-
React.createElement('span', { className: 'dvo-pdesc' }, tt('cardHint')),
|
|
746
|
-
),
|
|
747
|
-
React.createElement('span', { className: 'dvo-pchev' }, chevronIcon()),
|
|
748
|
-
),
|
|
749
|
-
open ? React.createElement('div', { className: 'dvo-pbody' }, React.createElement(VoiceSection, props)) : null,
|
|
750
|
-
),
|
|
751
|
-
)
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function registerSettings(ctx) {
|
|
755
|
-
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
756
|
-
{
|
|
757
|
-
name: 'settings.plugin.item',
|
|
758
|
-
key: NS,
|
|
759
|
-
locale: NS,
|
|
760
|
-
inject: () => ({ ctx: ctx }),
|
|
761
|
-
},
|
|
762
|
-
PluginCard,
|
|
763
|
-
))
|
|
764
|
-
}
|
|
765
|
-
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Card in Settings → Plugins → Plugin settings (#18)
|
|
2
|
+
function PluginCard(props) {
|
|
3
|
+
const [open, setOpen] = React.useState(false)
|
|
4
|
+
const tt = (props && props.t) || t
|
|
5
|
+
const title = tt('title')
|
|
6
|
+
return React.createElement(ErrorBoundary, null,
|
|
7
|
+
React.createElement('li',
|
|
8
|
+
{ className: open ? 'dvo-pcard dvo-pcardOpen' : 'dvo-pcard' },
|
|
9
|
+
React.createElement('button',
|
|
10
|
+
{
|
|
11
|
+
type: 'button', className: 'dvo-phead',
|
|
12
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
13
|
+
'aria-label': (open ? tt('collapse') : tt('expand')) + ': ' + title,
|
|
14
|
+
onClick: () => setOpen((v) => !v),
|
|
15
|
+
},
|
|
16
|
+
React.createElement('span', { className: 'dvo-pheadtext' },
|
|
17
|
+
React.createElement('span', { className: 'dvo-ptitle' }, title),
|
|
18
|
+
React.createElement('span', { className: 'dvo-pdesc' }, tt('cardHint')),
|
|
19
|
+
),
|
|
20
|
+
React.createElement('span', { className: 'dvo-pchev' }, chevronIcon()),
|
|
21
|
+
),
|
|
22
|
+
open ? React.createElement('div', { className: 'dvo-pbody' }, React.createElement(VoiceSection, props)) : null,
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function registerSettings(ctx) {
|
|
28
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
29
|
+
{
|
|
30
|
+
name: 'settings.plugin.item',
|
|
31
|
+
key: NS,
|
|
32
|
+
locale: NS,
|
|
33
|
+
inject: () => ({ ctx: ctx }),
|
|
34
|
+
},
|
|
35
|
+
PluginCard,
|
|
36
|
+
))
|
|
37
|
+
}
|
|
38
|
+
|
package/lib/client.js
CHANGED
|
@@ -1416,6 +1416,46 @@ window.__ModuleLoader__.load({
|
|
|
1416
1416
|
document.head.appendChild(tag)
|
|
1417
1417
|
}
|
|
1418
1418
|
|
|
1419
|
+
function createErrorBoundary() {
|
|
1420
|
+
if (!React || typeof React.Component !== 'function') {
|
|
1421
|
+
return function NoopBoundary(props) { return props?.children || null }
|
|
1422
|
+
}
|
|
1423
|
+
return class ErrorBoundary extends React.Component {
|
|
1424
|
+
constructor(props) {
|
|
1425
|
+
super(props)
|
|
1426
|
+
this.state = { hasError: false, error: null }
|
|
1427
|
+
}
|
|
1428
|
+
static getDerivedStateFromError(error) {
|
|
1429
|
+
return { hasError: true, error }
|
|
1430
|
+
}
|
|
1431
|
+
componentDidCatch(error, errorInfo) {
|
|
1432
|
+
console.error('[dsh-voice] React Error:', error, errorInfo)
|
|
1433
|
+
}
|
|
1434
|
+
render() {
|
|
1435
|
+
if (this.state.hasError) {
|
|
1436
|
+
return React.createElement(
|
|
1437
|
+
'div',
|
|
1438
|
+
{ className: 'cb-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
|
|
1439
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ ' + t('uiError')),
|
|
1440
|
+
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
1441
|
+
React.createElement(
|
|
1442
|
+
'button',
|
|
1443
|
+
{
|
|
1444
|
+
type: 'button',
|
|
1445
|
+
className: 'cb-btn',
|
|
1446
|
+
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
1447
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
1448
|
+
},
|
|
1449
|
+
t('retry'),
|
|
1450
|
+
),
|
|
1451
|
+
)
|
|
1452
|
+
}
|
|
1453
|
+
return this.props?.children || null
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const ErrorBoundary = createErrorBoundary()
|
|
1458
|
+
|
|
1419
1459
|
// One chain editor: provider+model rows with reordering.
|
|
1420
1460
|
function ChainEditor(props) {
|
|
1421
1461
|
const rows = Array.isArray(props.value) ? props.value : []
|
|
@@ -1504,47 +1544,7 @@ window.__ModuleLoader__.load({
|
|
|
1504
1544
|
),
|
|
1505
1545
|
)
|
|
1506
1546
|
}
|
|
1507
|
-
|
|
1508
|
-
function createErrorBoundary() {
|
|
1509
|
-
if (!React || typeof React.Component !== 'function') {
|
|
1510
|
-
return function NoopBoundary(props) { return props?.children || null }
|
|
1511
|
-
}
|
|
1512
|
-
return class ErrorBoundary extends React.Component {
|
|
1513
|
-
constructor(props) {
|
|
1514
|
-
super(props)
|
|
1515
|
-
this.state = { hasError: false, error: null }
|
|
1516
|
-
}
|
|
1517
|
-
static getDerivedStateFromError(error) {
|
|
1518
|
-
return { hasError: true, error }
|
|
1519
|
-
}
|
|
1520
|
-
componentDidCatch(error, errorInfo) {
|
|
1521
|
-
console.error('[dsh-voice] React Error:', error, errorInfo)
|
|
1522
|
-
}
|
|
1523
|
-
render() {
|
|
1524
|
-
if (this.state.hasError) {
|
|
1525
|
-
return React.createElement(
|
|
1526
|
-
'div',
|
|
1527
|
-
{ className: 'cb-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
|
|
1528
|
-
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ ' + t('uiError')),
|
|
1529
|
-
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
1530
|
-
React.createElement(
|
|
1531
|
-
'button',
|
|
1532
|
-
{
|
|
1533
|
-
type: 'button',
|
|
1534
|
-
className: 'cb-btn',
|
|
1535
|
-
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
1536
|
-
onClick: () => this.setState({ hasError: false, error: null }),
|
|
1537
|
-
},
|
|
1538
|
-
t('retry'),
|
|
1539
|
-
),
|
|
1540
|
-
)
|
|
1541
|
-
}
|
|
1542
|
-
return this.props?.children || null
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
const ErrorBoundary = createErrorBoundary()
|
|
1547
|
-
|
|
1547
|
+
// ------------------------------------------------------- settings voice section
|
|
1548
1548
|
function VoiceSection(props) {
|
|
1549
1549
|
const t = (props && props.t) || moduleT
|
|
1550
1550
|
const ctx = props.ctx
|
|
@@ -1606,7 +1606,14 @@ window.__ModuleLoader__.load({
|
|
|
1606
1606
|
return () => clearInterval(timer)
|
|
1607
1607
|
}, [ready])
|
|
1608
1608
|
|
|
1609
|
-
|
|
1609
|
+
const deepClone = (obj) => {
|
|
1610
|
+
if (typeof structuredClone === 'function') {
|
|
1611
|
+
try { return structuredClone(obj) } catch (e) { /* fallback */ }
|
|
1612
|
+
}
|
|
1613
|
+
try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
|
|
1610
1617
|
|
|
1611
1618
|
React.useEffect(() => {
|
|
1612
1619
|
if (!ready) return
|
|
@@ -1667,7 +1674,7 @@ window.__ModuleLoader__.load({
|
|
|
1667
1674
|
}
|
|
1668
1675
|
|
|
1669
1676
|
const setIn = (mode, key, v) => setDraft((d) => {
|
|
1670
|
-
const next =
|
|
1677
|
+
const next = deepClone(d || {})
|
|
1671
1678
|
next[mode] = next[mode] || {}
|
|
1672
1679
|
next[mode][key] = v
|
|
1673
1680
|
return next
|
package/lib/http-util.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Pure HTTP helpers shared by host routes. No cordis, no network.
|
|
2
|
+
|
|
3
|
+
export function writeJson(res, code, body) {
|
|
4
|
+
try {
|
|
5
|
+
res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
|
|
6
|
+
res.end(JSON.stringify(body))
|
|
7
|
+
} catch { /* socket may already be closed */ }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Collect request body with a hard size cap.
|
|
12
|
+
* @param {import('node:stream').Readable} req
|
|
13
|
+
* @param {number} maxBytes
|
|
14
|
+
* @returns {Promise<Buffer>}
|
|
15
|
+
*/
|
|
16
|
+
export function readBody(req, maxBytes) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const chunks = []
|
|
19
|
+
let size = 0
|
|
20
|
+
req.on('data', (c) => {
|
|
21
|
+
size += c.length
|
|
22
|
+
if (size > maxBytes) { reject(new Error('body too large')); req.destroy(); return }
|
|
23
|
+
chunks.push(c)
|
|
24
|
+
})
|
|
25
|
+
req.on('end', () => resolve(Buffer.concat(chunks)))
|
|
26
|
+
req.on('error', reject)
|
|
27
|
+
})
|
|
28
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -22,7 +22,10 @@ import { makeProviders, PROVIDER_KEYS, PRESET_KEYS, KNOWN_KEYS, DEFAULT_MODELS,
|
|
|
22
22
|
import { toWav16k } from './wav.js'
|
|
23
23
|
import { normalizePhrase } from './normalize.js'
|
|
24
24
|
import { createStatsTracker, mergeContextVocabulary } from './stats.js'
|
|
25
|
+
import { writeJson, readBody } from './http-util.js'
|
|
26
|
+
import { createPolishText } from './polish.js'
|
|
25
27
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
28
|
+
import { buildProviderOrder, sessionCommand } from './transcribe-core.js'
|
|
26
29
|
|
|
27
30
|
function isAutoLang(lang) {
|
|
28
31
|
return !lang || lang === 'auto' || String(lang).includes(',')
|
|
@@ -164,26 +167,6 @@ const MIME_BY_EXT = {
|
|
|
164
167
|
'.ogg': 'audio/ogg', '.oga': 'audio/ogg', '.flac': 'audio/flac', '.webm': 'audio/webm', '.aac': 'audio/aac',
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
function writeJson(res, code, body) {
|
|
168
|
-
try {
|
|
169
|
-
res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
|
|
170
|
-
res.end(JSON.stringify(body))
|
|
171
|
-
} catch { /* socket may already be closed */ }
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function readBody(req, maxBytes) {
|
|
175
|
-
return new Promise((resolve, reject) => {
|
|
176
|
-
const chunks = []
|
|
177
|
-
let size = 0
|
|
178
|
-
req.on('data', (c) => {
|
|
179
|
-
size += c.length
|
|
180
|
-
if (size > maxBytes) { reject(new Error('body too large')); req.destroy(); return }
|
|
181
|
-
chunks.push(c)
|
|
182
|
-
})
|
|
183
|
-
req.on('end', () => resolve(Buffer.concat(chunks)))
|
|
184
|
-
req.on('error', reject)
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
170
|
|
|
188
171
|
export function apply(ctx, baseConfig) {
|
|
189
172
|
let child = null
|
|
@@ -297,22 +280,13 @@ export function apply(ctx, baseConfig) {
|
|
|
297
280
|
const cfg = live()
|
|
298
281
|
const customKeys = (Array.isArray(cfg.customProviders) ? cfg.customProviders : [])
|
|
299
282
|
.map((c) => String(c && c.key || '').trim()).filter(Boolean)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (!KNOWN_KEYS.includes(entry.provider) && !customKeys.includes(entry.provider)) continue
|
|
308
|
-
order.push(entry.provider)
|
|
309
|
-
// Custom provider default models live in their spec; makeProviders fills
|
|
310
|
-
// them in — empty here means "use the provider default".
|
|
311
|
-
models[entry.provider] = entry.model || DEFAULT_MODELS[entry.provider] || ''
|
|
312
|
-
}
|
|
313
|
-
if (cfg.localOnly && order.length === 0) {
|
|
314
|
-
throw new Error('localOnly mode is on, but local-whisper is not in the chain')
|
|
315
|
-
}
|
|
283
|
+
const { order, models } = buildProviderOrder({
|
|
284
|
+
localOnly: !!cfg.localOnly,
|
|
285
|
+
chain: modeCfg.chain,
|
|
286
|
+
customKeys,
|
|
287
|
+
knownKeys: KNOWN_KEYS,
|
|
288
|
+
defaultModels: DEFAULT_MODELS,
|
|
289
|
+
})
|
|
316
290
|
|
|
317
291
|
const vocab = cfg.contextGlossary
|
|
318
292
|
? mergeContextVocabulary(cfg.vocabulary, contextWords)
|
|
@@ -327,75 +301,18 @@ export function apply(ctx, baseConfig) {
|
|
|
327
301
|
|
|
328
302
|
// Transcript polish (#35) with local LLM support (#47).
|
|
329
303
|
// Errors/timeouts do not block: return the raw text.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Local OpenAI-compatible endpoint (#47) when configured.
|
|
341
|
-
if (cfg.polishBaseUrl) {
|
|
342
|
-
const headers = { 'content-type': 'application/json' }
|
|
343
|
-
if (cfg.polishKeyEnv) {
|
|
344
|
-
const key = await resolveKey(cfg.polishKeyEnv)
|
|
345
|
-
if (key) headers.authorization = 'Bearer ' + key
|
|
346
|
-
}
|
|
347
|
-
const res = await fetch(
|
|
348
|
-
String(cfg.polishBaseUrl).replace(/\/+$/, '') + '/chat/completions',
|
|
349
|
-
{ method: 'POST', headers, signal, body: JSON.stringify({
|
|
350
|
-
model: cfg.polishModel || 'local-model',
|
|
351
|
-
messages: [{ role: 'user', content: ask }],
|
|
352
|
-
}) })
|
|
353
|
-
if (!res.ok) return text
|
|
354
|
-
const data = await res.json().catch(() => null)
|
|
355
|
-
const pick = data && data.choices && data.choices[0] && data.choices[0].message
|
|
356
|
-
&& data.choices[0].message.content
|
|
357
|
-
return (pick && String(pick).trim()) || text
|
|
358
|
-
}
|
|
359
|
-
// Harness model (DSH 0.1.2-alpha.1 API).
|
|
360
|
-
const llm = ctx.llm
|
|
361
|
-
if (!llm || typeof llm.stream !== 'function') return text
|
|
362
|
-
// provider/model are required in GenerateOptions; fall back to
|
|
363
|
-
// agentDefaultModel when not set explicitly.
|
|
364
|
-
const sel = (ctx.get && ctx.get('agentDefaultModel') && ctx.get('agentDefaultModel').currentSelection)
|
|
365
|
-
? ctx.get('agentDefaultModel').currentSelection()
|
|
366
|
-
: null
|
|
367
|
-
const provider = cfg.polishProvider || (sel && sel.provider) || ''
|
|
368
|
-
const model = cfg.polishModelId || (sel && sel.model) || ''
|
|
369
|
-
if (!provider || !model) return text
|
|
370
|
-
let acc = ''
|
|
371
|
-
for await (const chunk of llm.stream({
|
|
372
|
-
provider, model,
|
|
373
|
-
messages: [createUserMessage({ content: [{ type: 'text', text: ask }], source: { kind: 'user' } })],
|
|
374
|
-
signal,
|
|
375
|
-
})) {
|
|
376
|
-
if (chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string') acc += chunk.text
|
|
377
|
-
}
|
|
378
|
-
const clean = acc.trim()
|
|
379
|
-
return clean || text
|
|
380
|
-
} catch { return text }
|
|
381
|
-
}
|
|
304
|
+
const polishText = createPolishText({
|
|
305
|
+
resolveKey,
|
|
306
|
+
fetchImpl: (...args) => fetch(...args),
|
|
307
|
+
getConfig: live,
|
|
308
|
+
llm: ctx.llm,
|
|
309
|
+
createUserMessage,
|
|
310
|
+
getAgentDefaultModel: () => (ctx.get && ctx.get('agentDefaultModel') && ctx.get('agentDefaultModel').currentSelection)
|
|
311
|
+
? ctx.get('agentDefaultModel').currentSelection()
|
|
312
|
+
: null,
|
|
313
|
+
})
|
|
382
314
|
|
|
383
|
-
//
|
|
384
|
-
// Matched on the host so they never enter the composer text.
|
|
385
|
-
const SESSION_COMMANDS = [
|
|
386
|
-
{ re: /^(отправь|отправить|пошли|send)\s*[.!?]*$/i, cmd: 'send' },
|
|
387
|
-
{ re: /^(отмени|отмена|cancel|отменить)\s*[.!?]*$/i, cmd: 'cancel' },
|
|
388
|
-
{ re: /^(стоп|stop|хватит)\s*[.!?]*$/i, cmd: 'stop' },
|
|
389
|
-
{ re: /^(продолжи|continue|продолжай)\s*[.!?]*$/i, cmd: 'continue' },
|
|
390
|
-
]
|
|
391
|
-
function sessionCommand(text) {
|
|
392
|
-
const t = String(text || '').trim().toLowerCase()
|
|
393
|
-
if (!t) return null
|
|
394
|
-
for (const { re, cmd } of SESSION_COMMANDS) {
|
|
395
|
-
if (re.test(t)) return cmd
|
|
396
|
-
}
|
|
397
|
-
return null
|
|
398
|
-
}
|
|
315
|
+
// sessionCommand imported from ./transcribe-core.js
|
|
399
316
|
|
|
400
317
|
ctx.effect(() => ctx.webServer.register({
|
|
401
318
|
kind: 'exact',
|
package/lib/polish.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Transcript polish. Pure factory: inject fetch, key resolver, LLM, config.
|
|
2
|
+
// On any failure return the original text — polish must never block input.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {object} deps
|
|
6
|
+
* @param {(ref: string) => Promise<string>} deps.resolveKey
|
|
7
|
+
* @param {typeof fetch} deps.fetchImpl
|
|
8
|
+
* @param {() => object} deps.getConfig live plugin config
|
|
9
|
+
* @param {{ stream: Function } | null} deps.llm harness LLM service (optional)
|
|
10
|
+
* @param {() => ({provider: string, model: string} | null)} [deps.getAgentDefaultModel]
|
|
11
|
+
*/
|
|
12
|
+
export function createPolishText({ resolveKey, fetchImpl, getConfig, llm, getAgentDefaultModel, createUserMessage: createUserMessageFn }) {
|
|
13
|
+
return async function polishText(text, modeCfg, signal) {
|
|
14
|
+
const enable = modeCfg && (modeCfg.polish === true || modeCfg.polishSend === true)
|
|
15
|
+
if (!enable || !text) return text
|
|
16
|
+
const cfg = getConfig() || {}
|
|
17
|
+
const ask =
|
|
18
|
+
'Fix the punctuation and spelling of this dictated text and split it into '
|
|
19
|
+
+ 'paragraphs where the speaker changes topic. Remove filler words ("um", "uh", '
|
|
20
|
+
+ '"ээ", "ну", "как бы"). Keep the original language, wording and meaning. '
|
|
21
|
+
+ 'Reply with the polished text only:\n\n' + text
|
|
22
|
+
try {
|
|
23
|
+
// Local OpenAI-compatible endpoint when configured.
|
|
24
|
+
if (cfg.polishBaseUrl) {
|
|
25
|
+
const headers = { 'content-type': 'application/json' }
|
|
26
|
+
if (cfg.polishKeyEnv) {
|
|
27
|
+
const key = await resolveKey(cfg.polishKeyEnv)
|
|
28
|
+
if (key) headers.authorization = 'Bearer ' + key
|
|
29
|
+
}
|
|
30
|
+
const res = await fetchImpl(
|
|
31
|
+
String(cfg.polishBaseUrl).replace(/\/+$/, '') + '/chat/completions',
|
|
32
|
+
{
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers,
|
|
35
|
+
signal,
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
model: cfg.polishModel || 'local-model',
|
|
38
|
+
messages: [{ role: 'user', content: ask }],
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
if (!res.ok) return text
|
|
43
|
+
const data = await res.json().catch(() => null)
|
|
44
|
+
const pick = data && data.choices && data.choices[0] && data.choices[0].message
|
|
45
|
+
&& data.choices[0].message.content
|
|
46
|
+
return (pick && String(pick).trim()) || text
|
|
47
|
+
}
|
|
48
|
+
// Harness model.
|
|
49
|
+
if (!llm || typeof llm.stream !== 'function') return text
|
|
50
|
+
const sel = (typeof getAgentDefaultModel === 'function' && getAgentDefaultModel()) || null
|
|
51
|
+
const provider = cfg.polishProvider || (sel && sel.provider) || ''
|
|
52
|
+
const model = cfg.polishModelId || (sel && sel.model) || ''
|
|
53
|
+
if (!provider || !model) return text
|
|
54
|
+
let acc = ''
|
|
55
|
+
// createUserMessage comes from @deepseek-ai/dsh-llm. Inject it here so
|
|
56
|
+
// tests can exercise the harness path without installing peer deps;
|
|
57
|
+
// production wires the real helper via index.js.
|
|
58
|
+
const makeUser = createUserMessageFn
|
|
59
|
+
|| (await import('@deepseek-ai/dsh-llm')).createUserMessage
|
|
60
|
+
for await (const chunk of llm.stream({
|
|
61
|
+
provider,
|
|
62
|
+
model,
|
|
63
|
+
messages: [makeUser({ content: [{ type: 'text', text: ask }], source: { kind: 'user' } })],
|
|
64
|
+
signal,
|
|
65
|
+
})) {
|
|
66
|
+
if (chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string') acc += chunk.text
|
|
67
|
+
}
|
|
68
|
+
const clean = acc.trim()
|
|
69
|
+
return clean || text
|
|
70
|
+
} catch {
|
|
71
|
+
return text
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
package/lib/providers.js
CHANGED
|
@@ -113,6 +113,14 @@ async function readErrorDetail(res, defaultLabel) {
|
|
|
113
113
|
return `${defaultLabel} ${detail}`
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
async function safeJson(res, defaultLabel) {
|
|
117
|
+
try {
|
|
118
|
+
return await res.json()
|
|
119
|
+
} catch {
|
|
120
|
+
throw new Error(`${defaultLabel}: invalid JSON response`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
116
124
|
export function makeProviders(deps, req) {
|
|
117
125
|
const { resolveKey, fetchImpl, cfg } = deps
|
|
118
126
|
const { bytes, mime, lang, signal, models } = req
|
|
@@ -134,7 +142,10 @@ export function makeProviders(deps, req) {
|
|
|
134
142
|
signal,
|
|
135
143
|
})
|
|
136
144
|
if (!res.ok) throw new Error(await readErrorDetail(res, 'Deepgram'))
|
|
137
|
-
|
|
145
|
+
let data
|
|
146
|
+
try { data = await safeJson(res, 'Deepgram') } catch (e) {
|
|
147
|
+
return { ok: false, provider: 'deepgram', reason: e.message }
|
|
148
|
+
}
|
|
138
149
|
const text = (data?.results?.channels?.[0]?.alternatives?.[0]?.transcript || '').trim()
|
|
139
150
|
return { ok: text.length > 0, provider: 'deepgram', text, reason: text ? '' : 'empty transcript' }
|
|
140
151
|
}
|
|
@@ -154,7 +165,10 @@ export function makeProviders(deps, req) {
|
|
|
154
165
|
signal,
|
|
155
166
|
})
|
|
156
167
|
if (!res.ok) throw new Error(await readErrorDetail(res, 'Groq'))
|
|
157
|
-
|
|
168
|
+
let data
|
|
169
|
+
try { data = await safeJson(res, 'Groq') } catch (e) {
|
|
170
|
+
return { ok: false, provider: 'groq', reason: e.message }
|
|
171
|
+
}
|
|
158
172
|
const text = (data?.text || '').trim()
|
|
159
173
|
return { ok: text.length > 0, provider: 'groq', text, reason: text ? '' : 'empty transcript' }
|
|
160
174
|
}
|
|
@@ -170,7 +184,10 @@ export function makeProviders(deps, req) {
|
|
|
170
184
|
signal,
|
|
171
185
|
})
|
|
172
186
|
if (!res.ok) throw new Error(await readErrorDetail(res, 'HF'))
|
|
173
|
-
|
|
187
|
+
let data
|
|
188
|
+
try { data = await safeJson(res, 'HF') } catch (e) {
|
|
189
|
+
return { ok: false, provider: 'hf', reason: e.message }
|
|
190
|
+
}
|
|
174
191
|
const text = (data?.text || '').trim()
|
|
175
192
|
return { ok: text.length > 0, provider: 'hf', text, reason: text ? '' : 'empty transcript' }
|
|
176
193
|
}
|
|
@@ -210,7 +227,10 @@ export function makeProviders(deps, req) {
|
|
|
210
227
|
try { const e = await res.json(); if (e?.error) detail = e.error } catch { /* body is not json */ }
|
|
211
228
|
return { ok: false, provider: 'local-whisper', reason: `local whisper: ${detail}` }
|
|
212
229
|
}
|
|
213
|
-
|
|
230
|
+
let data
|
|
231
|
+
try { data = await safeJson(res, 'local whisper') } catch (e) {
|
|
232
|
+
return { ok: false, provider: 'local-whisper', reason: e.message }
|
|
233
|
+
}
|
|
214
234
|
const text = (data?.text || '').trim()
|
|
215
235
|
return { ok: text.length > 0, provider: 'local-whisper', text, reason: text ? '' : 'empty transcript' }
|
|
216
236
|
}
|
|
@@ -298,7 +318,7 @@ export function makeProviders(deps, req) {
|
|
|
298
318
|
method: 'POST', headers, body: form, signal,
|
|
299
319
|
})
|
|
300
320
|
if (!res.ok) throw new Error(await readErrorDetail(res, label))
|
|
301
|
-
const data = await res
|
|
321
|
+
const data = await safeJson(res, label)
|
|
302
322
|
return (data?.text || '').trim()
|
|
303
323
|
}
|
|
304
324
|
|
|
@@ -331,7 +351,7 @@ export function makeProviders(deps, req) {
|
|
|
331
351
|
signal,
|
|
332
352
|
})
|
|
333
353
|
if (!res.ok) throw new Error(await readErrorDetail(res, label))
|
|
334
|
-
const data = await res
|
|
354
|
+
const data = await safeJson(res, label)
|
|
335
355
|
return String(data?.choices?.[0]?.message?.content || '').trim()
|
|
336
356
|
}
|
|
337
357
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Pure helpers for the transcribe path: chain selection and session commands.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build provider order for a mode, honouring localOnly.
|
|
5
|
+
* @param {object} args
|
|
6
|
+
* @param {boolean} args.localOnly
|
|
7
|
+
* @param {{provider: string, model?: string}[]} args.chain
|
|
8
|
+
* @param {string[]} args.customKeys
|
|
9
|
+
* @param {string[]} args.knownKeys
|
|
10
|
+
* @param {Record<string, string>} args.defaultModels
|
|
11
|
+
* @returns {{order: string[], models: Record<string, string>}}
|
|
12
|
+
*/
|
|
13
|
+
export function buildProviderOrder({ localOnly, chain, customKeys, knownKeys, defaultModels }) {
|
|
14
|
+
const source = localOnly
|
|
15
|
+
? (chain || []).filter((e) => e && e.provider === 'local-whisper')
|
|
16
|
+
: (chain || [])
|
|
17
|
+
const models = {}
|
|
18
|
+
const order = []
|
|
19
|
+
for (const entry of source) {
|
|
20
|
+
if (!entry || !entry.provider) continue
|
|
21
|
+
if (!knownKeys.includes(entry.provider) && !customKeys.includes(entry.provider)) continue
|
|
22
|
+
order.push(entry.provider)
|
|
23
|
+
models[entry.provider] = entry.model || defaultModels[entry.provider] || ''
|
|
24
|
+
}
|
|
25
|
+
if (localOnly && order.length === 0) {
|
|
26
|
+
throw new Error('localOnly mode is on, but local-whisper is not in the chain')
|
|
27
|
+
}
|
|
28
|
+
return { order, models }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SESSION_COMMANDS = [
|
|
32
|
+
{ re: /^(отправь|отправить|пошли|send)\s*[.!?]*$/i, cmd: 'send' },
|
|
33
|
+
{ re: /^(отмени|отмена|cancel|отменить)\s*[.!?]*$/i, cmd: 'cancel' },
|
|
34
|
+
{ re: /^(стоп|stop|хватит)\s*[.!?]*$/i, cmd: 'stop' },
|
|
35
|
+
{ re: /^(продолжи|continue|продолжай)\s*[.!?]*$/i, cmd: 'continue' },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} text
|
|
40
|
+
* @returns {'send'|'cancel'|'stop'|'continue'|null}
|
|
41
|
+
*/
|
|
42
|
+
export function sessionCommand(text) {
|
|
43
|
+
const t = String(text || '').trim().toLowerCase()
|
|
44
|
+
if (!t) return null
|
|
45
|
+
for (const { re, cmd } of SESSION_COMMANDS) {
|
|
46
|
+
if (re.test(t)) return cmd
|
|
47
|
+
}
|
|
48
|
+
return null
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.23",
|
|
4
4
|
"description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|