@goodandready/dsh-voice 0.8.22 → 0.8.24
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/20-css.js +1 -1
- 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} +1 -253
- package/lib/client-src/73-plugin-card.js +38 -0
- package/lib/client.js +42 -42
- package/lib/http-util.js +28 -0
- package/lib/index.js +21 -104
- package/lib/polish.js +74 -0
- package/lib/transcribe-core.js +49 -0
- package/package.json +1 -1
package/lib/client-src/20-css.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
'.dvo-pill{display:flex;align-items:center;gap:10px;height:52px;border-radius:26px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);padding:0 14px;width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px rgba(0,0,0,.18);box-sizing:border-box}' +
|
|
7
7
|
'.dvo-pbtn{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:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary)}' +
|
|
8
8
|
'.dvo-pbtn:hover{background:var(--dsw-alias-bg-layer-2)}' +
|
|
9
|
-
'.dvo-wave{flex:1;height:40px;width:100%;color:var(--dsw-alias-label-primary)}' +
|
|
9
|
+
'.dvo-wave{flex:1;min-width:0;max-width:100%;height:40px;width:100%;color:var(--dsw-alias-label-primary)}' +
|
|
10
10
|
'.dvo-status{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
|
|
11
11
|
'.dvo-err{color:var(--dsw-alias-state-error-primary)}' +
|
|
12
12
|
'.dvo-count{font-variant-numeric:tabular-nums;font-size:13px;color:var(--dsw-alias-label-secondary)}' +
|
|
@@ -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
|
|
@@ -732,41 +518,3 @@
|
|
|
732
518
|
)
|
|
733
519
|
}
|
|
734
520
|
|
|
735
|
-
// Card in Settings → Plugins → Plugin settings (#18)
|
|
736
|
-
function PluginCard(props) {
|
|
737
|
-
const [open, setOpen] = React.useState(false)
|
|
738
|
-
const tt = (props && props.t) || t
|
|
739
|
-
const title = tt('title')
|
|
740
|
-
return React.createElement(ErrorBoundary, null,
|
|
741
|
-
React.createElement('li',
|
|
742
|
-
{ className: open ? 'dvo-pcard dvo-pcardOpen' : 'dvo-pcard' },
|
|
743
|
-
React.createElement('button',
|
|
744
|
-
{
|
|
745
|
-
type: 'button', className: 'dvo-phead',
|
|
746
|
-
'aria-expanded': open ? 'true' : 'false',
|
|
747
|
-
'aria-label': (open ? tt('collapse') : tt('expand')) + ': ' + title,
|
|
748
|
-
onClick: () => setOpen((v) => !v),
|
|
749
|
-
},
|
|
750
|
-
React.createElement('span', { className: 'dvo-pheadtext' },
|
|
751
|
-
React.createElement('span', { className: 'dvo-ptitle' }, title),
|
|
752
|
-
React.createElement('span', { className: 'dvo-pdesc' }, tt('cardHint')),
|
|
753
|
-
),
|
|
754
|
-
React.createElement('span', { className: 'dvo-pchev' }, chevronIcon()),
|
|
755
|
-
),
|
|
756
|
-
open ? React.createElement('div', { className: 'dvo-pbody' }, React.createElement(VoiceSection, props)) : null,
|
|
757
|
-
),
|
|
758
|
-
)
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function registerSettings(ctx) {
|
|
762
|
-
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
763
|
-
{
|
|
764
|
-
name: 'settings.plugin.item',
|
|
765
|
-
key: NS,
|
|
766
|
-
locale: NS,
|
|
767
|
-
inject: () => ({ ctx: ctx }),
|
|
768
|
-
},
|
|
769
|
-
PluginCard,
|
|
770
|
-
))
|
|
771
|
-
}
|
|
772
|
-
|
|
@@ -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
|
@@ -192,7 +192,7 @@ window.__ModuleLoader__.load({
|
|
|
192
192
|
'.dvo-pill{display:flex;align-items:center;gap:10px;height:52px;border-radius:26px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);padding:0 14px;width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px rgba(0,0,0,.18);box-sizing:border-box}' +
|
|
193
193
|
'.dvo-pbtn{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:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary)}' +
|
|
194
194
|
'.dvo-pbtn:hover{background:var(--dsw-alias-bg-layer-2)}' +
|
|
195
|
-
'.dvo-wave{flex:1;height:40px;width:100%;color:var(--dsw-alias-label-primary)}' +
|
|
195
|
+
'.dvo-wave{flex:1;min-width:0;max-width:100%;height:40px;width:100%;color:var(--dsw-alias-label-primary)}' +
|
|
196
196
|
'.dvo-status{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
|
|
197
197
|
'.dvo-err{color:var(--dsw-alias-state-error-primary)}' +
|
|
198
198
|
'.dvo-count{font-variant-numeric:tabular-nums;font-size:13px;color:var(--dsw-alias-label-secondary)}' +
|
|
@@ -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
|
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
|
+
}
|
|
@@ -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.24",
|
|
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",
|