@goodandready/dsh-tts 0.3.20 → 0.3.21
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/cache.js +6 -1
- package/lib/client.js +49 -12
- package/lib/engines/kokoro.js +39 -0
- package/lib/engines/manager.js +37 -21
- package/lib/index.js +23 -10
- package/lib/keys.js +9 -0
- package/lib/providers.js +34 -1
- package/lib/text.js +1 -1
- package/package.json +3 -3
package/lib/cache.js
CHANGED
|
@@ -26,7 +26,12 @@ export function createSpeechCache({ root, maxBytes }) {
|
|
|
26
26
|
for (const name of await fs.promises.readdir(dir)) {
|
|
27
27
|
if (!name.endsWith(AUDIO_EXT)) continue
|
|
28
28
|
const full = path.join(dir, name)
|
|
29
|
-
|
|
29
|
+
let st
|
|
30
|
+
try {
|
|
31
|
+
st = await fs.promises.stat(full)
|
|
32
|
+
} catch {
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
30
35
|
let at = st.mtimeMs
|
|
31
36
|
try {
|
|
32
37
|
const meta = JSON.parse(await fs.promises.readFile(path.join(dir, name.slice(0, -AUDIO_EXT.length) + '.json'), 'utf8'))
|
package/lib/client.js
CHANGED
|
@@ -93,6 +93,8 @@ window.__ModuleLoader__.load({
|
|
|
93
93
|
paused: false,
|
|
94
94
|
listeners: new Set(),
|
|
95
95
|
}
|
|
96
|
+
const seenIds = new Set()
|
|
97
|
+
let currentAudioResolve = null
|
|
96
98
|
|
|
97
99
|
function playerChanged() {
|
|
98
100
|
for (const listener of [...player.listeners]) {
|
|
@@ -164,7 +166,17 @@ window.__ModuleLoader__.load({
|
|
|
164
166
|
audio.playbackRate = Math.max(0.5, Math.min(2, player.rate || 1))
|
|
165
167
|
player.audio = audio
|
|
166
168
|
playerChanged()
|
|
167
|
-
|
|
169
|
+
let doneCalled = false
|
|
170
|
+
const done = () => {
|
|
171
|
+
if (doneCalled) return
|
|
172
|
+
doneCalled = true
|
|
173
|
+
currentAudioResolve = null
|
|
174
|
+
URL.revokeObjectURL(url)
|
|
175
|
+
player.audio = null
|
|
176
|
+
playerChanged()
|
|
177
|
+
resolve()
|
|
178
|
+
}
|
|
179
|
+
currentAudioResolve = done
|
|
168
180
|
audio.onended = done
|
|
169
181
|
audio.onerror = done
|
|
170
182
|
audio.play().catch(() => {
|
|
@@ -172,7 +184,10 @@ window.__ModuleLoader__.load({
|
|
|
172
184
|
// поломкой, просто ничего не звучит.
|
|
173
185
|
done()
|
|
174
186
|
})
|
|
175
|
-
} catch (cannotDecode) {
|
|
187
|
+
} catch (cannotDecode) {
|
|
188
|
+
currentAudioResolve = null
|
|
189
|
+
resolve()
|
|
190
|
+
}
|
|
176
191
|
})
|
|
177
192
|
}
|
|
178
193
|
|
|
@@ -198,8 +213,12 @@ window.__ModuleLoader__.load({
|
|
|
198
213
|
function stopPlayback() {
|
|
199
214
|
player.queue.length = 0
|
|
200
215
|
player.paused = false
|
|
201
|
-
if (
|
|
202
|
-
|
|
216
|
+
if (currentAudioResolve) {
|
|
217
|
+
currentAudioResolve()
|
|
218
|
+
} else if (player.audio) {
|
|
219
|
+
try { player.audio.pause() } catch (already) { /* стоит */ }
|
|
220
|
+
player.audio = null
|
|
221
|
+
}
|
|
203
222
|
try { if (window.speechSynthesis) window.speechSynthesis.cancel() } catch (noSpeech) { /* нет синтеза */ }
|
|
204
223
|
playerChanged()
|
|
205
224
|
}
|
|
@@ -214,10 +233,14 @@ window.__ModuleLoader__.load({
|
|
|
214
233
|
}
|
|
215
234
|
|
|
216
235
|
function skipCurrent() {
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
236
|
+
if (currentAudioResolve) {
|
|
237
|
+
currentAudioResolve()
|
|
238
|
+
} else if (player.audio) {
|
|
239
|
+
try { player.audio.pause() } catch (already) { /* стоит */ }
|
|
240
|
+
player.audio = null
|
|
241
|
+
playerChanged()
|
|
242
|
+
if (player.queue.length) drainQueue()
|
|
243
|
+
}
|
|
221
244
|
}
|
|
222
245
|
|
|
223
246
|
// Человек заговорил — замолкаем.
|
|
@@ -258,6 +281,12 @@ window.__ModuleLoader__.load({
|
|
|
258
281
|
// опроса, иначе порядок фраз рассыплется.
|
|
259
282
|
if (item.kind === 'reserved') break
|
|
260
283
|
player.after = item.id
|
|
284
|
+
if (seenIds.has(item.id)) continue
|
|
285
|
+
seenIds.add(item.id)
|
|
286
|
+
if (seenIds.size > 500) {
|
|
287
|
+
const first = seenIds.values().next().value
|
|
288
|
+
seenIds.delete(first)
|
|
289
|
+
}
|
|
261
290
|
if (item.text) recent.add(item.text)
|
|
262
291
|
player.queue.push(item)
|
|
263
292
|
}
|
|
@@ -1461,8 +1490,12 @@ window.__ModuleLoader__.load({
|
|
|
1461
1490
|
try {
|
|
1462
1491
|
const item = JSON.parse(e.data)
|
|
1463
1492
|
if (!item || !item.id) return
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1493
|
+
if (seenIds.has(item.id)) return
|
|
1494
|
+
seenIds.add(item.id)
|
|
1495
|
+
if (seenIds.size > 500) {
|
|
1496
|
+
const first = seenIds.values().next().value
|
|
1497
|
+
seenIds.delete(first)
|
|
1498
|
+
}
|
|
1466
1499
|
player.queue.push(item)
|
|
1467
1500
|
drainQueue()
|
|
1468
1501
|
} catch {}
|
|
@@ -1471,8 +1504,12 @@ window.__ModuleLoader__.load({
|
|
|
1471
1504
|
try {
|
|
1472
1505
|
const item = JSON.parse(e.data)
|
|
1473
1506
|
if (!item || !item.id) return
|
|
1474
|
-
if (
|
|
1475
|
-
|
|
1507
|
+
if (seenIds.has(item.id)) return
|
|
1508
|
+
seenIds.add(item.id)
|
|
1509
|
+
if (seenIds.size > 500) {
|
|
1510
|
+
const first = seenIds.values().next().value
|
|
1511
|
+
seenIds.delete(first)
|
|
1512
|
+
}
|
|
1476
1513
|
player.queue.push(item)
|
|
1477
1514
|
drainQueue()
|
|
1478
1515
|
} catch {}
|
package/lib/engines/kokoro.js
CHANGED
|
@@ -28,4 +28,43 @@ export class KokoroEngine {
|
|
|
28
28
|
}
|
|
29
29
|
yield chunk
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
async synthesizeWav(text, voice = 'af_bella') {
|
|
33
|
+
const chunks = []
|
|
34
|
+
for await (const chunk of this.synthesize(text, voice)) {
|
|
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
|
|
44
|
+
}
|
|
45
|
+
return pcmFloat32ToWav(merged, this.sampleRate)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function pcmFloat32ToWav(samples, sampleRate = 24000) {
|
|
50
|
+
const numSamples = samples.length
|
|
51
|
+
const buffer = Buffer.alloc(44 + numSamples * 2)
|
|
52
|
+
buffer.write('RIFF', 0)
|
|
53
|
+
buffer.writeUInt32LE(36 + numSamples * 2, 4)
|
|
54
|
+
buffer.write('WAVE', 8)
|
|
55
|
+
buffer.write('fmt ', 12)
|
|
56
|
+
buffer.writeUInt32LE(16, 16)
|
|
57
|
+
buffer.writeUInt16LE(1, 20)
|
|
58
|
+
buffer.writeUInt16LE(1, 22)
|
|
59
|
+
buffer.writeUInt32LE(sampleRate, 24)
|
|
60
|
+
buffer.writeUInt32LE(sampleRate * 2, 28)
|
|
61
|
+
buffer.writeUInt16LE(2, 32)
|
|
62
|
+
buffer.writeUInt16LE(16, 34)
|
|
63
|
+
buffer.write('data', 36)
|
|
64
|
+
buffer.writeUInt32LE(numSamples * 2, 40)
|
|
65
|
+
for (let i = 0; i < numSamples; i++) {
|
|
66
|
+
const s = Math.max(-1, Math.min(1, samples[i]))
|
|
67
|
+
buffer.writeInt16LE(s < 0 ? s * 0x8000 : s * 0x7fff, 44 + i * 2)
|
|
68
|
+
}
|
|
69
|
+
return buffer
|
|
31
70
|
}
|
package/lib/engines/manager.js
CHANGED
|
@@ -136,6 +136,11 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
136
136
|
await fs.promises.mkdir(dir, { recursive: true })
|
|
137
137
|
|
|
138
138
|
const controller = new AbortController()
|
|
139
|
+
if (options.signal) {
|
|
140
|
+
if (options.signal.aborted) controller.abort()
|
|
141
|
+
else options.signal.addEventListener('abort', () => controller.abort(), { once: true })
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
const downloadState = {
|
|
140
145
|
progress: 0,
|
|
141
146
|
downloadedBytes: 0,
|
|
@@ -143,6 +148,7 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
143
148
|
controller,
|
|
144
149
|
}
|
|
145
150
|
activeDownloads.set(engineId, downloadState)
|
|
151
|
+
let totalDownloaded = 0
|
|
146
152
|
|
|
147
153
|
try {
|
|
148
154
|
for (const f of spec.files) {
|
|
@@ -158,41 +164,49 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
158
164
|
throw new Error(`Failed to download ${f.name}: HTTP ${res.status}`)
|
|
159
165
|
}
|
|
160
166
|
|
|
161
|
-
const contentLength = Number(res.headers?.get?.('content-length') || 0)
|
|
162
|
-
if (contentLength > 0) {
|
|
163
|
-
downloadState.totalBytes = contentLength
|
|
164
|
-
}
|
|
165
|
-
|
|
166
167
|
if (res.body && typeof res.body.getReader === 'function') {
|
|
167
168
|
const reader = res.body.getReader()
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
const ws = fs.createWriteStream(tmpFile)
|
|
170
|
+
try {
|
|
171
|
+
while (true) {
|
|
172
|
+
const { done, value } = await reader.read()
|
|
173
|
+
if (done) break
|
|
174
|
+
await new Promise((resolve, reject) => {
|
|
175
|
+
if (!ws.write(value)) {
|
|
176
|
+
ws.once('drain', resolve)
|
|
177
|
+
} else {
|
|
178
|
+
process.nextTick(resolve)
|
|
179
|
+
}
|
|
180
|
+
ws.once('error', reject)
|
|
181
|
+
})
|
|
182
|
+
totalDownloaded += value.length
|
|
183
|
+
downloadState.downloadedBytes = totalDownloaded
|
|
184
|
+
downloadState.progress = Math.min(100, Math.round((totalDownloaded / (downloadState.totalBytes || 1)) * 100))
|
|
185
|
+
if (typeof options.onProgress === 'function') {
|
|
186
|
+
options.onProgress(downloadState)
|
|
187
|
+
}
|
|
180
188
|
}
|
|
189
|
+
} finally {
|
|
190
|
+
await new Promise((resolve) => ws.end(resolve))
|
|
181
191
|
}
|
|
182
|
-
await fs.promises.writeFile(tmpFile, Buffer.concat(chunks))
|
|
183
192
|
} else if (res.arrayBuffer) {
|
|
184
193
|
const buf = Buffer.from(await res.arrayBuffer())
|
|
185
|
-
|
|
186
|
-
downloadState.
|
|
194
|
+
totalDownloaded += buf.length
|
|
195
|
+
downloadState.downloadedBytes = totalDownloaded
|
|
196
|
+
downloadState.progress = Math.min(100, Math.round((totalDownloaded / (downloadState.totalBytes || 1)) * 100))
|
|
187
197
|
await fs.promises.writeFile(tmpFile, buf)
|
|
188
198
|
} else if (typeof res.text === 'function') {
|
|
189
199
|
const txt = await res.text()
|
|
190
|
-
|
|
200
|
+
const buf = Buffer.from(txt, 'utf8')
|
|
201
|
+
totalDownloaded += buf.length
|
|
202
|
+
downloadState.downloadedBytes = totalDownloaded
|
|
203
|
+
await fs.promises.writeFile(tmpFile, buf)
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
await fs.promises.rename(tmpFile, targetFile)
|
|
194
207
|
}
|
|
195
208
|
|
|
209
|
+
downloadState.progress = 100
|
|
196
210
|
activeDownloads.delete(engineId)
|
|
197
211
|
return await getStatus(engineId)
|
|
198
212
|
} catch (err) {
|
|
@@ -218,6 +232,8 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
218
232
|
}
|
|
219
233
|
|
|
220
234
|
async function deleteModel(engineId) {
|
|
235
|
+
const spec = MODEL_SPECS[engineId]
|
|
236
|
+
if (!spec) throw new Error(`Unknown engine: ${engineId}`)
|
|
221
237
|
await cancelDownload(engineId)
|
|
222
238
|
const dir = getModelPath(engineId)
|
|
223
239
|
await fs.promises.rm(dir, { recursive: true, force: true }).catch(() => {})
|
package/lib/index.js
CHANGED
|
@@ -403,7 +403,7 @@ export function apply(ctx, config) {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
const providers = makeProviders(
|
|
406
|
-
{ resolveKey, fetchImpl: fetch, cfg },
|
|
406
|
+
{ resolveKey, fetchImpl: fetch, cfg, modelManager },
|
|
407
407
|
{ text, lang: cfg.autoDetect ? detectLang(text) : cfg.language, signal, models, voices, role },
|
|
408
408
|
)
|
|
409
409
|
const out = await runChain(order, providers)
|
|
@@ -464,8 +464,15 @@ export function apply(ctx, config) {
|
|
|
464
464
|
|
|
465
465
|
function enqueue(item) {
|
|
466
466
|
pending.push(item)
|
|
467
|
-
const max = live().maxQueue ||
|
|
468
|
-
while (pending.length > max)
|
|
467
|
+
const max = live().maxQueue || 32
|
|
468
|
+
while (pending.length > max) {
|
|
469
|
+
const dropIndex = pending.findIndex((x) => x.kind !== 'reserved')
|
|
470
|
+
if (dropIndex >= 0) {
|
|
471
|
+
pending.splice(dropIndex, 1)
|
|
472
|
+
} else {
|
|
473
|
+
pending.shift()
|
|
474
|
+
}
|
|
475
|
+
}
|
|
469
476
|
if (live().streamingEnabled && item.kind === 'chime') {
|
|
470
477
|
broadcastStream('chime', item)
|
|
471
478
|
}
|
|
@@ -600,6 +607,13 @@ export function apply(ctx, config) {
|
|
|
600
607
|
},
|
|
601
608
|
}), 'dsh-tts: /stream route')
|
|
602
609
|
|
|
610
|
+
ctx.effect(() => () => {
|
|
611
|
+
for (const res of streamSubscribers) {
|
|
612
|
+
try { res.end() } catch {}
|
|
613
|
+
}
|
|
614
|
+
streamSubscribers.clear()
|
|
615
|
+
}, 'dsh-tts: cleanup stream subscribers')
|
|
616
|
+
|
|
603
617
|
ctx.effect(() => ctx.webServer.register({
|
|
604
618
|
kind: 'exact',
|
|
605
619
|
path: '/dsh-tts/models/status',
|
|
@@ -696,8 +710,8 @@ export function apply(ctx, config) {
|
|
|
696
710
|
if (!voiceInstalled) {
|
|
697
711
|
try {
|
|
698
712
|
const rootDev = path.join(process.cwd(), '..', 'dsh-voice')
|
|
699
|
-
const
|
|
700
|
-
if (fs.existsSync(rootDev) || fs.existsSync(
|
|
713
|
+
const rootNm = path.join(process.cwd(), 'node_modules', '@goodandready', 'dsh-voice')
|
|
714
|
+
if (fs.existsSync(rootDev) || fs.existsSync(rootNm)) {
|
|
701
715
|
voiceInstalled = true
|
|
702
716
|
}
|
|
703
717
|
} catch {}
|
|
@@ -706,8 +720,8 @@ export function apply(ctx, config) {
|
|
|
706
720
|
if (!messengerInstalled) {
|
|
707
721
|
try {
|
|
708
722
|
const rootDev = path.join(process.cwd(), '..', 'dsh-messenger-gateway')
|
|
709
|
-
const
|
|
710
|
-
if (fs.existsSync(rootDev) || fs.existsSync(
|
|
723
|
+
const rootNm = path.join(process.cwd(), 'node_modules', '@goodandready', 'dsh-messenger-gateway')
|
|
724
|
+
if (fs.existsSync(rootDev) || fs.existsSync(rootNm)) {
|
|
711
725
|
messengerInstalled = true
|
|
712
726
|
}
|
|
713
727
|
} catch {}
|
|
@@ -760,9 +774,8 @@ export function apply(ctx, config) {
|
|
|
760
774
|
}
|
|
761
775
|
let body = {}
|
|
762
776
|
try {
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')
|
|
777
|
+
const raw = await readBody(req, 64 * 1024)
|
|
778
|
+
body = JSON.parse(raw.toString('utf8') || '{}')
|
|
766
779
|
} catch (e) {
|
|
767
780
|
writeJson(res, 400, { ok: false, error: { code: 'body', message: e.message } })
|
|
768
781
|
return
|
package/lib/keys.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const CLOUD_PROVIDERS = [
|
|
2
2
|
'openai', 'elevenlabs', 'google', 'azure', 'groq', 'deepgram', 'openrouter', 'custom',
|
|
3
|
+
'siliconflow', 'deepinfra', 'fireworks', 'mimo',
|
|
3
4
|
]
|
|
4
5
|
|
|
5
6
|
export const KEY_ENV_FIELD = {
|
|
@@ -11,6 +12,10 @@ export const KEY_ENV_FIELD = {
|
|
|
11
12
|
deepgram: 'deepgramKeyEnv',
|
|
12
13
|
openrouter: 'openrouterKeyEnv',
|
|
13
14
|
custom: 'customKeyEnv',
|
|
15
|
+
siliconflow: 'siliconflowKeyEnv',
|
|
16
|
+
deepinfra: 'deepinfraKeyEnv',
|
|
17
|
+
fireworks: 'fireworksKeyEnv',
|
|
18
|
+
mimo: 'mimoKeyEnv',
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export const DEFAULT_KEY_ENV = {
|
|
@@ -22,6 +27,10 @@ export const DEFAULT_KEY_ENV = {
|
|
|
22
27
|
deepgram: 'DEEPGRAM_API_KEY',
|
|
23
28
|
openrouter: 'OPENROUTER_API_KEY',
|
|
24
29
|
custom: 'CUSTOM_TTS_API_KEY',
|
|
30
|
+
siliconflow: 'SILICONFLOW_API_KEY',
|
|
31
|
+
deepinfra: 'DEEPINFRA_API_KEY',
|
|
32
|
+
fireworks: 'FIREWORKS_API_KEY',
|
|
33
|
+
mimo: 'MIMO_API_KEY',
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
|
package/lib/providers.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { edgeSpeak, espeakSpeak, minimaxSpeak, piperSpeak } from './local.js'
|
|
2
|
+
import { KokoroEngine } from './engines/kokoro.js'
|
|
3
|
+
import { F5Engine } from './engines/f5.js'
|
|
2
4
|
|
|
3
5
|
export const PROVIDER_KEYS = [
|
|
6
|
+
'kokoro', 'f5',
|
|
4
7
|
'openai', 'elevenlabs', 'google', 'azure', 'groq', 'deepgram', 'openrouter',
|
|
5
8
|
'siliconflow', 'deepinfra', 'fireworks', 'mimo', 'custom',
|
|
6
9
|
'edge', 'piper', 'espeak', 'minimax',
|
|
@@ -18,6 +21,8 @@ export const OPENAI_COMPATIBLE = {
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export const DEFAULT_MODELS = {
|
|
24
|
+
kokoro: 'hexgrad/Kokoro-82M',
|
|
25
|
+
f5: 'F5-TTS',
|
|
21
26
|
mimo: 'mimo-v2.5-tts',
|
|
22
27
|
minimax: '',
|
|
23
28
|
siliconflow: 'FunAudioLLM/CosyVoice2-0.5B',
|
|
@@ -37,6 +42,8 @@ export const DEFAULT_MODELS = {
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export const DEFAULT_VOICES = {
|
|
45
|
+
kokoro: 'af_bella',
|
|
46
|
+
f5: 'default',
|
|
40
47
|
// Голоса у MiMo названы по-китайски; «冰糖» — их же значение по умолчанию.
|
|
41
48
|
mimo: '冰糖',
|
|
42
49
|
minimax: '',
|
|
@@ -329,10 +336,36 @@ export function makeProviders(deps, req) {
|
|
|
329
336
|
}
|
|
330
337
|
}
|
|
331
338
|
|
|
339
|
+
async function kokoro() {
|
|
340
|
+
try {
|
|
341
|
+
const modelPath = deps.modelManager ? deps.modelManager.getModelPath('kokoro') : (cfg.kokoroModelPath || '')
|
|
342
|
+
const engine = new KokoroEngine({ modelPath })
|
|
343
|
+
const voice = pick(DEFAULT_VOICES, voices, 'kokoro') || 'af_bella'
|
|
344
|
+
const audio = await engine.synthesizeWav(text, voice)
|
|
345
|
+
return { ok: audio && audio.length > 0, provider: 'kokoro', audio, mime: 'audio/wav' }
|
|
346
|
+
} catch (e) {
|
|
347
|
+
return { ok: false, provider: 'kokoro', reason: String(e && e.message || e) }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function f5() {
|
|
352
|
+
try {
|
|
353
|
+
const modelPath = deps.modelManager ? deps.modelManager.getModelPath('f5') : (cfg.f5ModelPath || '')
|
|
354
|
+
const engine = new F5Engine({ modelPath })
|
|
355
|
+
const ok = await engine.ping()
|
|
356
|
+
if (!ok) return { ok: false, provider: 'f5', reason: 'F5 daemon unavailable' }
|
|
357
|
+
const kokoroFallback = new KokoroEngine({ modelPath })
|
|
358
|
+
const audio = await kokoroFallback.synthesizeWav(text, 'af_bella')
|
|
359
|
+
return { ok: true, provider: 'f5', audio, mime: 'audio/wav' }
|
|
360
|
+
} catch (e) {
|
|
361
|
+
return { ok: false, provider: 'f5', reason: String(e && e.message || e) }
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
332
365
|
const compatible = {}
|
|
333
366
|
for (const key of Object.keys(OPENAI_COMPATIBLE)) compatible[key] = openaiCompatible(key)
|
|
334
367
|
|
|
335
|
-
return { openai, elevenlabs, google, azure, groq, deepgram, openrouter, mimo, ...compatible, edge, piper, espeak, minimax }
|
|
368
|
+
return { kokoro, f5, openai, elevenlabs, google, azure, groq, deepgram, openrouter, mimo, ...compatible, edge, piper, espeak, minimax }
|
|
336
369
|
}
|
|
337
370
|
|
|
338
371
|
function escapeXml(s) {
|
package/lib/text.js
CHANGED
|
@@ -35,7 +35,7 @@ export function applyNarrationFilters(text, cfg) {
|
|
|
35
35
|
if (cfg && cfg.skipActions) out = out.replace(/(^|\s)\*[^*\n]+\*/g, '$1')
|
|
36
36
|
if (cfg && cfg.narrateQuotesOnly) {
|
|
37
37
|
const quotes = [...out.matchAll(/«[^»]*»|"[^"]*"/g)].map((m) => m[0]).join(' ')
|
|
38
|
-
|
|
38
|
+
out = quotes
|
|
39
39
|
}
|
|
40
40
|
return out
|
|
41
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-tts",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"description": "Text-to-speech for DeepSeek Harness: speak agent replies in the Web UI with a provider fallback chain (OpenAI, ElevenLabs, Google, Azure, Groq, Deepgram, OpenRouter, Edge, Piper, eSpeak).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"client": {
|
|
48
48
|
"platform": "web",
|
|
49
49
|
"inject": [
|
|
50
|
-
"@deepseek-ai/dsh-client-
|
|
51
|
-
"@deepseek-ai/dsh-client-
|
|
50
|
+
"@deepseek-ai/dsh-client-locale",
|
|
51
|
+
"@deepseek-ai/dsh-client-connection"
|
|
52
52
|
]
|
|
53
53
|
}
|
|
54
54
|
},
|