@goodandready/dsh-tts 0.3.22 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/README.ru.md +13 -10
- package/README.zh.md +10 -10
- package/lib/cache.js +4 -5
- package/lib/client.js +105 -88
- package/lib/engines/f5.js +45 -33
- package/lib/engines/kokoro.js +21 -29
- package/lib/engines/types.js +26 -13
- package/lib/http-util.js +27 -0
- package/lib/index.js +70 -467
- package/lib/providers.js +105 -25
- package/lib/routes.js +400 -0
- package/lib/stream-hub.js +31 -0
- package/lib/text.js +46 -19
- package/package.json +6 -3
- package/scripts/f5_daemon.py +40 -0
- package/lib/client/worklet.js +0 -71
- package/lib/router.js +0 -4
package/lib/client.js
CHANGED
|
@@ -5,9 +5,8 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports
|
|
6
6
|
const React = require('react')
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// поворачивает её классом.
|
|
8
|
+
// Chevron comes from core UI primitives; local SVG is a fallback for
|
|
9
|
+
// builds without the package. Icon always points down; open state rotates.
|
|
11
10
|
let ChevronIcon = null
|
|
12
11
|
try {
|
|
13
12
|
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
@@ -23,25 +22,27 @@ window.__ModuleLoader__.load({
|
|
|
23
22
|
const CLOUD = {
|
|
24
23
|
openai: 1, elevenlabs: 1, google: 1, azure: 1, groq: 1, deepgram: 1, openrouter: 1, custom: 1,
|
|
25
24
|
}
|
|
25
|
+
// Hints stay generic: concrete provider model IDs rot quickly.
|
|
26
26
|
const MODEL_HINT = {
|
|
27
|
-
openai: '
|
|
28
|
-
elevenlabs: '
|
|
29
|
-
google: '
|
|
30
|
-
azure: 'region voice
|
|
31
|
-
groq: '
|
|
32
|
-
deepgram: '
|
|
33
|
-
openrouter: '
|
|
34
|
-
edge: 'ru-RU-SvetlanaNeural',
|
|
27
|
+
openai: 'provider default',
|
|
28
|
+
elevenlabs: 'provider default',
|
|
29
|
+
google: 'provider default',
|
|
30
|
+
azure: 'region voice name',
|
|
31
|
+
groq: 'provider default',
|
|
32
|
+
deepgram: 'provider default (English-only voices)',
|
|
33
|
+
openrouter: 'provider/model id',
|
|
34
|
+
edge: 'BCP-47 voice, e.g. ru-RU-SvetlanaNeural',
|
|
35
35
|
piper: 'path to .onnx',
|
|
36
|
-
espeak: 'ru',
|
|
36
|
+
espeak: 'language code, e.g. ru',
|
|
37
|
+
kokoro: 'not bundled — offline neural runtime unavailable',
|
|
38
|
+
f5: 'not bundled — offline neural runtime unavailable',
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const SET_CSS =
|
|
40
42
|
'.dts-wrap{display:flex;flex-direction:column;gap:22px;padding:4px 0;max-width:720px}' +
|
|
41
43
|
'.dts-block{display:flex;flex-direction:column;gap:10px}' +
|
|
42
44
|
'.dts-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
43
|
-
//
|
|
44
|
-
// (issue #19), цвета только переменными темы.
|
|
45
|
+
// Plugin-settings card matches the core card pattern; theme variables only.
|
|
45
46
|
'.dts-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
46
47
|
'.dts-head{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}' +
|
|
47
48
|
'.dts-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
@@ -54,7 +55,7 @@ window.__ModuleLoader__.load({
|
|
|
54
55
|
'.dts-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}' +
|
|
55
56
|
'.dts-foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px}' +
|
|
56
57
|
'.dts-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 14px;font-size:13px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}' +
|
|
57
|
-
//
|
|
58
|
+
// Provider-chain form helpers.
|
|
58
59
|
'.dts-entry{display:flex;flex-direction:column;gap:8px;padding:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}' +
|
|
59
60
|
'.dts-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
60
61
|
'.dts-row .dts-model{flex:1;min-width:120px}' +
|
|
@@ -67,20 +68,19 @@ window.__ModuleLoader__.load({
|
|
|
67
68
|
'.dts-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0}'
|
|
68
69
|
|
|
69
70
|
const setCssId = 'dsh-tts/settings.module.css'
|
|
70
|
-
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin
|
|
71
|
+
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-tts"]')) {
|
|
71
72
|
const tag = document.createElement('style')
|
|
72
73
|
tag.textContent = SET_CSS
|
|
73
|
-
tag.setAttribute('data-plugin', 'dsh-tts')
|
|
74
|
+
tag.setAttribute('data-dsh-plugin', 'dsh-tts')
|
|
74
75
|
tag.dataset.pluginCss = setCssId
|
|
75
76
|
document.head.appendChild(tag)
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
//
|
|
79
|
+
// Player.
|
|
79
80
|
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// закончилось предыдущее.
|
|
81
|
+
// Each incoming chunk used to replace the previous one immediately; while reading
|
|
82
|
+
// For full replies this was invisible; with speak-as-it-goes only the last
|
|
83
|
+
// chunk would be heard. Now items queue: the next starts after the previous ends.
|
|
84
84
|
const player = {
|
|
85
85
|
audio: null,
|
|
86
86
|
after: '',
|
|
@@ -111,8 +111,7 @@ window.__ModuleLoader__.load({
|
|
|
111
111
|
return player
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
//
|
|
115
|
-
// хранить и настраивать.
|
|
114
|
+
// Short chime is synthesized in-browser: no file to download, store, or configure.
|
|
116
115
|
function playChime(kind) {
|
|
117
116
|
return new Promise((resolve) => {
|
|
118
117
|
try {
|
|
@@ -139,8 +138,8 @@ window.__ModuleLoader__.load({
|
|
|
139
138
|
})
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
141
|
+
// Provider failed — fall back to the browser voice. Worse quality, but silence
|
|
142
|
+
// is worse: the user would not know the reply is ready.
|
|
144
143
|
function speakInBrowser(text) {
|
|
145
144
|
return new Promise((resolve) => {
|
|
146
145
|
try {
|
|
@@ -154,24 +153,47 @@ window.__ModuleLoader__.load({
|
|
|
154
153
|
})
|
|
155
154
|
}
|
|
156
155
|
|
|
156
|
+
function prepareAudioItem(item) {
|
|
157
|
+
if (!item || !item.audioBase64 || item.prepared) return item
|
|
158
|
+
try {
|
|
159
|
+
const bin = atob(item.audioBase64)
|
|
160
|
+
const bytes = new Uint8Array(bin.length)
|
|
161
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
|
162
|
+
const blob = new Blob([bytes], { type: item.mime || 'audio/mpeg' })
|
|
163
|
+
const url = URL.createObjectURL(blob)
|
|
164
|
+
const audio = new Audio(url)
|
|
165
|
+
audio.preload = 'auto'
|
|
166
|
+
item.prepared = { audio, url }
|
|
167
|
+
} catch (err) { /* decode error */ }
|
|
168
|
+
return item
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function prebufferNext() {
|
|
172
|
+
for (const next of player.queue) {
|
|
173
|
+
if (next && next.audioBase64 && !next.prepared) {
|
|
174
|
+
prepareAudioItem(next)
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
157
180
|
function playAudio(item) {
|
|
158
181
|
return new Promise((resolve) => {
|
|
159
182
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const blob = new Blob([bytes], { type: item.mime || 'audio/mpeg' })
|
|
164
|
-
const url = URL.createObjectURL(blob)
|
|
165
|
-
const audio = new Audio(url)
|
|
183
|
+
prepareAudioItem(item)
|
|
184
|
+
if (!item.prepared) return resolve()
|
|
185
|
+
const { audio, url } = item.prepared
|
|
166
186
|
audio.playbackRate = Math.max(0.5, Math.min(2, player.rate || 1))
|
|
167
187
|
player.audio = audio
|
|
168
188
|
playerChanged()
|
|
189
|
+
prebufferNext()
|
|
169
190
|
let doneCalled = false
|
|
170
191
|
const done = () => {
|
|
171
192
|
if (doneCalled) return
|
|
172
193
|
doneCalled = true
|
|
173
194
|
currentAudioResolve = null
|
|
174
|
-
URL.revokeObjectURL(url)
|
|
195
|
+
try { URL.revokeObjectURL(url) } catch (_) {}
|
|
196
|
+
item.prepared = null
|
|
175
197
|
player.audio = null
|
|
176
198
|
playerChanged()
|
|
177
199
|
resolve()
|
|
@@ -180,8 +202,7 @@ window.__ModuleLoader__.load({
|
|
|
180
202
|
audio.onended = done
|
|
181
203
|
audio.onerror = done
|
|
182
204
|
audio.play().catch(() => {
|
|
183
|
-
|
|
184
|
-
// поломкой, просто ничего не звучит.
|
|
205
|
+
// Autoplay without a user gesture is blocked — not a bug; just no sound.
|
|
185
206
|
done()
|
|
186
207
|
})
|
|
187
208
|
} catch (cannotDecode) {
|
|
@@ -211,6 +232,12 @@ window.__ModuleLoader__.load({
|
|
|
211
232
|
}
|
|
212
233
|
|
|
213
234
|
function stopPlayback() {
|
|
235
|
+
for (const item of player.queue) {
|
|
236
|
+
if (item && item.prepared && item.prepared.url) {
|
|
237
|
+
try { URL.revokeObjectURL(item.prepared.url) } catch (_) {}
|
|
238
|
+
item.prepared = null
|
|
239
|
+
}
|
|
240
|
+
}
|
|
214
241
|
player.queue.length = 0
|
|
215
242
|
player.paused = false
|
|
216
243
|
if (currentAudioResolve) {
|
|
@@ -243,11 +270,10 @@ window.__ModuleLoader__.load({
|
|
|
243
270
|
}
|
|
244
271
|
}
|
|
245
272
|
|
|
246
|
-
//
|
|
273
|
+
// User started talking — stop playback.
|
|
247
274
|
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
// окно, а не чужую службу, ровно поэтому.
|
|
275
|
+
// The event may come from a voice-input plugin, but there is no hard dependency:
|
|
276
|
+
// without that plugin the event simply never fires. We listen on window on purpose.
|
|
251
277
|
function listenForVoice() {
|
|
252
278
|
if (typeof window === 'undefined') return () => {}
|
|
253
279
|
const onSpeaking = (event) => {
|
|
@@ -277,8 +303,8 @@ window.__ModuleLoader__.load({
|
|
|
277
303
|
const data = await res.json()
|
|
278
304
|
const items = data && Array.isArray(data.items) ? data.items : []
|
|
279
305
|
for (const item of items) {
|
|
280
|
-
|
|
281
|
-
|
|
306
|
+
// Queue slot reserved but synthesis still running — wait for the next poll
|
|
307
|
+
// or phrase order would break.
|
|
282
308
|
if (item.kind === 'reserved') break
|
|
283
309
|
player.after = item.id
|
|
284
310
|
if (seenIds.has(item.id)) continue
|
|
@@ -294,7 +320,7 @@ window.__ModuleLoader__.load({
|
|
|
294
320
|
} catch (hostRestarting) { /* хост перезапускается */ }
|
|
295
321
|
}
|
|
296
322
|
|
|
297
|
-
//
|
|
323
|
+
// Recent utterances: last reply texts and favorites (localStorage).
|
|
298
324
|
function recentStore() {
|
|
299
325
|
const KEY = 'dsh-tts/recent'
|
|
300
326
|
let st
|
|
@@ -320,7 +346,7 @@ window.__ModuleLoader__.load({
|
|
|
320
346
|
}
|
|
321
347
|
const recent = recentStore()
|
|
322
348
|
|
|
323
|
-
//
|
|
349
|
+
// One-shot playback outside the queue: replay from recents.
|
|
324
350
|
function playStandalone(text) {
|
|
325
351
|
fetch('/dsh-tts/speak', {
|
|
326
352
|
method: 'POST',
|
|
@@ -341,7 +367,7 @@ window.__ModuleLoader__.load({
|
|
|
341
367
|
}
|
|
342
368
|
|
|
343
369
|
function ChainEditor(props) {
|
|
344
|
-
//
|
|
370
|
+
// Translator comes from the card: the slot passes props.t only to it.
|
|
345
371
|
const t = props.t || ((key) => key)
|
|
346
372
|
const rows = Array.isArray(props.value) ? props.value : []
|
|
347
373
|
const change = (i, patch) => {
|
|
@@ -408,9 +434,8 @@ window.__ModuleLoader__.load({
|
|
|
408
434
|
)
|
|
409
435
|
}
|
|
410
436
|
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
// на него же приходится откат, если перевода нет.
|
|
437
|
+
// Card strings live in the locale registry so a separate package can translate
|
|
438
|
+
// them without touching this plugin. English is the source language and fallback.
|
|
414
439
|
const en = {
|
|
415
440
|
'general': 'General',
|
|
416
441
|
'loading': 'Loading\u2026',
|
|
@@ -644,7 +669,7 @@ window.__ModuleLoader__.load({
|
|
|
644
669
|
'removeRegex': 'Регулярка вырезания',
|
|
645
670
|
'removeRegexHint': 'Глобальная регулярка: совпадения удаляются перед синтезом.',
|
|
646
671
|
'customBaseUrl': 'Свой TTS endpoint',
|
|
647
|
-
'customBaseUrlHint': 'OpenAI
|
|
672
|
+
'customBaseUrlHint': 'OpenAI-compatible base URL, e.g. http://localhost:8880/v1; empty disables it.',
|
|
648
673
|
'customKeyEnv': 'Имя ключа для endpoint',
|
|
649
674
|
'customKeyEnvHint': 'Имя credential для Bearer-ключа своего endpointа.',
|
|
650
675
|
|
|
@@ -775,7 +800,7 @@ window.__ModuleLoader__.load({
|
|
|
775
800
|
}
|
|
776
801
|
|
|
777
802
|
function TtsSection(props) {
|
|
778
|
-
//
|
|
803
|
+
// Translator comes from the slot because its registration sets locale.
|
|
779
804
|
const t = (props && props.t) || ((key) => key)
|
|
780
805
|
const [draft, setDraft] = React.useState(null)
|
|
781
806
|
const [credentials, setCredentials] = React.useState({})
|
|
@@ -801,12 +826,12 @@ window.__ModuleLoader__.load({
|
|
|
801
826
|
return () => { alive = false }
|
|
802
827
|
}, [scope])
|
|
803
828
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
829
|
+
// Snapshot status matters more than the value:
|
|
830
|
+
// loading — host has not answered yet;
|
|
831
|
+
// unavailable — host answered but the settings namespace is not ready;
|
|
832
|
+
// ready — values are present.
|
|
833
|
+
// On unavailable, writable defaults true; without a status check the card
|
|
834
|
+
// looks like a working empty form.
|
|
810
835
|
const ready = !scope || (!!snap && snap.status === 'ready')
|
|
811
836
|
const writable = !scope ? true : (ready && snap.writable !== false)
|
|
812
837
|
|
|
@@ -946,7 +971,7 @@ window.__ModuleLoader__.load({
|
|
|
946
971
|
}),
|
|
947
972
|
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
948
973
|
|
|
949
|
-
//
|
|
974
|
+
// The card header draws the title — do not duplicate a second title block.
|
|
950
975
|
const titleLine = props && props.compact ? null : React.createElement('div', { className: 'dts-h' }, t('title'))
|
|
951
976
|
return React.createElement('div', { className: 'dts-wrap' },
|
|
952
977
|
React.createElement('div', { className: 'dts-block' },
|
|
@@ -1096,7 +1121,7 @@ window.__ModuleLoader__.load({
|
|
|
1096
1121
|
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
1097
1122
|
]
|
|
1098
1123
|
|
|
1099
|
-
//
|
|
1124
|
+
// Pronunciation dictionary editor: rules apply top-down.
|
|
1100
1125
|
function PronEditor(props) {
|
|
1101
1126
|
const t = props.t || ((key) => key)
|
|
1102
1127
|
const rows = Array.isArray(props.value) ? props.value : []
|
|
@@ -1229,7 +1254,7 @@ window.__ModuleLoader__.load({
|
|
|
1229
1254
|
style: {
|
|
1230
1255
|
padding: '10px 14px',
|
|
1231
1256
|
borderRadius: '8px',
|
|
1232
|
-
border: '1px solid var(--dsw-alias-state-warning-primary
|
|
1257
|
+
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1233
1258
|
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1234
1259
|
display: 'flex',
|
|
1235
1260
|
flexDirection: 'column',
|
|
@@ -1237,7 +1262,7 @@ window.__ModuleLoader__.load({
|
|
|
1237
1262
|
fontSize: '13px',
|
|
1238
1263
|
},
|
|
1239
1264
|
},
|
|
1240
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary
|
|
1265
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1241
1266
|
'⚠️ ' + t('voiceNotInstalledTitle')
|
|
1242
1267
|
),
|
|
1243
1268
|
React.createElement('span', { className: 'dts-sub' },
|
|
@@ -1271,7 +1296,7 @@ window.__ModuleLoader__.load({
|
|
|
1271
1296
|
style: {
|
|
1272
1297
|
padding: '10px 14px',
|
|
1273
1298
|
borderRadius: '8px',
|
|
1274
|
-
border: '1px solid var(--dsw-alias-state-warning-primary
|
|
1299
|
+
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1275
1300
|
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1276
1301
|
display: 'flex',
|
|
1277
1302
|
flexDirection: 'column',
|
|
@@ -1279,7 +1304,7 @@ window.__ModuleLoader__.load({
|
|
|
1279
1304
|
fontSize: '13px',
|
|
1280
1305
|
},
|
|
1281
1306
|
},
|
|
1282
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary
|
|
1307
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1283
1308
|
'⚠️ ' + t('messengerNotInstalledTitle')
|
|
1284
1309
|
),
|
|
1285
1310
|
React.createElement('span', { className: 'dts-sub' },
|
|
@@ -1301,7 +1326,7 @@ window.__ModuleLoader__.load({
|
|
|
1301
1326
|
)
|
|
1302
1327
|
}
|
|
1303
1328
|
|
|
1304
|
-
//
|
|
1329
|
+
// Synthesis stats panel: live host counters.
|
|
1305
1330
|
function StatsPanel(props) {
|
|
1306
1331
|
const t = props.t || ((key) => key)
|
|
1307
1332
|
const [data, setData] = React.useState(null)
|
|
@@ -1322,9 +1347,8 @@ window.__ModuleLoader__.load({
|
|
|
1322
1347
|
)
|
|
1323
1348
|
}
|
|
1324
1349
|
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
// тело с чертой-разделителем. Шеврон — наш, ядро его не даёт.
|
|
1350
|
+
// Plugin-settings tab card follows the shared card pattern: li in the core list,
|
|
1351
|
+
// theme-reset header, body with a divider. Chevron is ours; core does not provide it.
|
|
1328
1352
|
function TtsCard(props) {
|
|
1329
1353
|
const t = (props && props.t) || ((key) => key)
|
|
1330
1354
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1357,12 +1381,10 @@ window.__ModuleLoader__.load({
|
|
|
1357
1381
|
)
|
|
1358
1382
|
}
|
|
1359
1383
|
|
|
1360
|
-
//
|
|
1361
|
-
// открыть недавние озвучки для реплая.
|
|
1384
|
+
// Input-dock button: shows what is playing, stop control, and recents for replay.
|
|
1362
1385
|
function SpeakerControl(props) {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
// не донесёт: без него кнопки остались бы без подписей вовсе.
|
|
1386
|
+
// A slot with `locale` injects translations into props; the fallback is the
|
|
1387
|
+
// binder created at registration, in case the slot drops translations.
|
|
1366
1388
|
const t = (props && props.t) || fallbackDockText
|
|
1367
1389
|
const p = usePlayer()
|
|
1368
1390
|
const [showList, setShowList] = React.useState(false)
|
|
@@ -1421,8 +1443,8 @@ window.__ModuleLoader__.load({
|
|
|
1421
1443
|
let fallbackDockText = (key) => key
|
|
1422
1444
|
|
|
1423
1445
|
function registerSpeaker(ctx) {
|
|
1424
|
-
|
|
1425
|
-
|
|
1446
|
+
// `locale` in the slot descriptor is how core injects translations into props.
|
|
1447
|
+
// Without it the dock would show raw keys only.
|
|
1426
1448
|
fallbackDockText = ctx.locale.bind(NS)
|
|
1427
1449
|
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register(
|
|
1428
1450
|
{
|
|
@@ -1437,14 +1459,10 @@ window.__ModuleLoader__.load({
|
|
|
1437
1459
|
}
|
|
1438
1460
|
|
|
1439
1461
|
function registerSettings(ctx) {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
// load plugins» с перечнем ни в чём не повинных соседей.
|
|
1445
|
-
//
|
|
1446
|
-
// Поэтому каждый язык объявляется отдельно и по-хорошему: заняли до нас —
|
|
1447
|
-
// уступаем, свой английский при этом всё равно встаёт на место.
|
|
1462
|
+
// Dictionary packages may also register languages for other namespaces.
|
|
1463
|
+
// Core throws on duplicate namespace+language; an unguarded call used to
|
|
1464
|
+
// take down the whole plugin ("Failed to load plugins"). Register each
|
|
1465
|
+
// language separately: skip if taken; English still lands.
|
|
1448
1466
|
const addLocale = (locale, dictionary) => {
|
|
1449
1467
|
try {
|
|
1450
1468
|
return ctx.locale.register(NS, locale, dictionary)
|
|
@@ -1456,13 +1474,12 @@ window.__ModuleLoader__.load({
|
|
|
1456
1474
|
const undo = [addLocale('en', en), addLocale('ru', ru)]
|
|
1457
1475
|
return () => { for (const off of undo) off() }
|
|
1458
1476
|
}, 'dsh-tts: словари')
|
|
1459
|
-
|
|
1460
|
-
|
|
1477
|
+
// The sidebar draws the section label, not our component: props.t never
|
|
1478
|
+
// reaches it, so bind the translator to the namespace ourselves.
|
|
1461
1479
|
const t = ctx.locale.bind(NS)
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
// не появится молча, без единой ошибки в журнале.
|
|
1480
|
+
// Primary settings home is the plugin-settings tab card. The tab looks up
|
|
1481
|
+
// a slot by entryKey equal to the namespace name: key must equal NS or
|
|
1482
|
+
// the card never appears, with no log error.
|
|
1466
1483
|
const tryPluginItem = () => {
|
|
1467
1484
|
try {
|
|
1468
1485
|
const res = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
@@ -1481,8 +1498,8 @@ window.__ModuleLoader__.load({
|
|
|
1481
1498
|
}
|
|
1482
1499
|
}
|
|
1483
1500
|
if (tryPluginItem()) return
|
|
1484
|
-
|
|
1485
|
-
|
|
1501
|
+
// Fallback for builds without settings.plugin.item: keep the sidebar section
|
|
1502
|
+
// so settings cannot disappear.
|
|
1486
1503
|
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
1487
1504
|
{
|
|
1488
1505
|
name: 'settings.section',
|
package/lib/engines/f5.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
|
|
3
|
+
function collectPing(pythonBin, daemonPath) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const proc = spawn(pythonBin, [daemonPath, '--ping'])
|
|
6
|
+
let out = ''
|
|
7
|
+
let settled = false
|
|
8
|
+
const done = (ok) => {
|
|
9
|
+
if (settled) return
|
|
10
|
+
settled = true
|
|
11
|
+
resolve(ok)
|
|
12
|
+
}
|
|
13
|
+
proc.stdout.on('data', (c) => { out += c })
|
|
14
|
+
proc.on('error', () => done(false))
|
|
15
|
+
proc.on('close', (code) => {
|
|
16
|
+
if (code !== 0) {
|
|
17
|
+
done(false)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const res = JSON.parse(out)
|
|
22
|
+
done(!!res.ok)
|
|
23
|
+
} catch {
|
|
24
|
+
done(false)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
3
30
|
export class F5Engine {
|
|
4
31
|
constructor({ daemonPath = 'scripts/f5_daemon.py', pythonBin = 'python3', modelPath = '' } = {}) {
|
|
5
32
|
this.daemonPath = daemonPath
|
|
@@ -7,39 +34,24 @@ export class F5Engine {
|
|
|
7
34
|
this.modelPath = modelPath
|
|
8
35
|
}
|
|
9
36
|
|
|
37
|
+
/** Resolve true only after a successful daemon handshake. */
|
|
10
38
|
async ping() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
resolve(code === 0)
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
resolve(true)
|
|
34
|
-
})
|
|
35
|
-
proc.on('close', (code) => {
|
|
36
|
-
try {
|
|
37
|
-
const res = JSON.parse(out)
|
|
38
|
-
resolve(!!res.ok)
|
|
39
|
-
} catch {
|
|
40
|
-
resolve(code === 0)
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
})
|
|
39
|
+
const primary = await collectPing(this.pythonBin, this.daemonPath)
|
|
40
|
+
if (primary) return true
|
|
41
|
+
if (this.pythonBin === 'python3') {
|
|
42
|
+
return collectPing('python', this.daemonPath)
|
|
43
|
+
}
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isRuntimeAvailable() {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async synthesize() {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'F5-TTS runtime is not bundled in @goodandready/dsh-tts. ' +
|
|
54
|
+
'Use Edge, Piper, or eSpeak for offline TTS.',
|
|
55
|
+
)
|
|
44
56
|
}
|
|
45
57
|
}
|
package/lib/engines/kokoro.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Local Kokoro engine wrapper.
|
|
5
|
+
*
|
|
6
|
+
* Real ONNX inference is intentionally not bundled in this package: shipping
|
|
7
|
+
* onnxruntime + voice tensors would blow the DSH Store size budget and couple
|
|
8
|
+
* the plugin to native binaries. Until a supported external runtime is wired,
|
|
9
|
+
* synthesis must fail honestly — never emit a synthetic tone.
|
|
10
|
+
*/
|
|
3
11
|
export class KokoroEngine {
|
|
4
12
|
constructor({ modelPath = '' } = {}) {
|
|
5
13
|
this.modelPath = modelPath
|
|
@@ -10,39 +18,23 @@ export class KokoroEngine {
|
|
|
10
18
|
return !!(this.modelPath && fs.existsSync(this.modelPath))
|
|
11
19
|
}
|
|
12
20
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @returns {AsyncGenerator<Float32Array>}
|
|
18
|
-
*/
|
|
19
|
-
async *synthesize(text, voice = 'af_bella') {
|
|
20
|
-
if (!text || !text.trim()) return
|
|
21
|
+
/** True only when a real inference backend can produce speech. */
|
|
22
|
+
isRuntimeAvailable() {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
chunk[i] = Math.sin((i * 440 * 2 * Math.PI) / this.sampleRate) * 0.2 * (1 - i / samples)
|
|
28
|
-
}
|
|
29
|
-
yield chunk
|
|
26
|
+
async *synthesize() {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Kokoro ONNX runtime is not bundled in @goodandready/dsh-tts. ' +
|
|
29
|
+
'Install weights does not enable speech. Use Edge, Piper, or eSpeak for offline TTS.',
|
|
30
|
+
)
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
async synthesizeWav(
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
chunks.push(chunk)
|
|
36
|
-
}
|
|
37
|
-
let totalLen = 0
|
|
38
|
-
for (const c of chunks) totalLen += c.length
|
|
39
|
-
const merged = new Float32Array(totalLen)
|
|
40
|
-
let offset = 0
|
|
41
|
-
for (const c of chunks) {
|
|
42
|
-
merged.set(c, offset)
|
|
43
|
-
offset += c.length
|
|
33
|
+
async synthesizeWav() {
|
|
34
|
+
for await (const _ of this.synthesize()) {
|
|
35
|
+
// unreachable: synthesize always throws
|
|
44
36
|
}
|
|
45
|
-
|
|
37
|
+
throw new Error('Kokoro synthesis unavailable')
|
|
46
38
|
}
|
|
47
39
|
}
|
|
48
40
|
|