@goodandready/dsh-tts 0.4.26 → 0.4.28
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/CHANGELOG.md +40 -1
- package/README.md +9 -5
- package/lib/cache.js +70 -40
- package/lib/client.js +257 -160
- package/lib/engines/manager.js +103 -69
- package/lib/index.js +277 -54
- package/lib/local.js +1 -1
- package/lib/providers/cloud.js +36 -5
- package/lib/providers/constants.js +4 -4
- package/lib/providers/local.js +1 -1
- package/lib/providers.js +11 -2
- package/lib/routes.js +102 -36
- package/lib/stream-hub.js +51 -7
- package/lib/text.js +18 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.4.
|
|
3
|
+
## 0.4.28
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Settings Contracts & Schemastery Validation** (#151, #152, #153, #155, #187, #188, #189):
|
|
7
|
+
- Made all 9 previously static fields volatile and ensured living Volatile boxes are unwrapped dynamically in live() without stale snapshots.
|
|
8
|
+
- Constrained maxQueue with .step(1).min(0) to reject non-integer and negative queue limits.
|
|
9
|
+
- Added optimistic revision validation and rollback on conflict (HTTP 409) in settings saving.
|
|
10
|
+
- Enforced isTrustedSettingsRequest auth guards on GET /dsh-tts/config and GET /dsh-tts/status.
|
|
11
|
+
- Enforced contiguous sequence ordering in SSE synthesis chunks.
|
|
12
|
+
|
|
13
|
+
- **Audio Pipeline, Cache & Concurrency** (#154, #156, #157, #166, #173, #174, #175, #186, #190):
|
|
14
|
+
- Sanitized and normalized text once in prepareSpeechText() before character limits or summary generation, eliminating double-clean pronunciation distortions.
|
|
15
|
+
- Fixed subagent voice override gating with autoDetectSubagent !== false.
|
|
16
|
+
- Extended cache and dedup fingerprints with endpoint, models, voices, role, and language.
|
|
17
|
+
- Added admission concurrency throttle and semaphore for maxQueue synthesis reservations.
|
|
18
|
+
- Implemented SSE subscriber backpressure tracking and dropped slow lagging subscribers (>512 KB buffer) with drain handling.
|
|
19
|
+
- Switched cache index to an in-memory Map to eliminate disk scans on eviction.
|
|
20
|
+
- Ensured speak_text tool returns metadata and delivers audio to pending queue/SSE without dumping base64 into LLM context.
|
|
21
|
+
|
|
22
|
+
- **Client Player & UI Parity** (#158, #159, #160, #161, #162, #163, #164, #165, #167, #168, #169, #176):
|
|
23
|
+
- Unified EventSource listener connection in client, preventing duplicate audio channels.
|
|
24
|
+
- Ensured skipCurrent() halts active playback immediately, and preview/replay route through the unified player.
|
|
25
|
+
- Gracefully handled browser autoplay restrictions (NotAllowedError), pausing the queue until user interaction.
|
|
26
|
+
- Kept compact recent/favorites history control visible in idle state.
|
|
27
|
+
- Registered updater routes inside ctx.effect() to properly unbind on plugin disposal.
|
|
28
|
+
- Enabled all 16 active providers in client UI, with secure key management and read-only indicators when settings scope is missing.
|
|
29
|
+
- Fixed sibling integration detection using Cordis services without relying on obsolete hasRoute.
|
|
30
|
+
- Re-anchored canonical defaults to English (en, en-US-AriaNeural) and achieved 100% parity between EN and ZH dictionaries.
|
|
31
|
+
|
|
32
|
+
- **Model Weights Manager & System Hygiene** (#177, #178, #179, #180, #181, #182, #183, #184, #185):
|
|
33
|
+
- Stream chunk downloads now properly manage EventEmitter listeners without leaking error or drain handlers.
|
|
34
|
+
- Added synchronous concurrency lock per engine to share in-flight downloads and used unique nonce temporary file paths.
|
|
35
|
+
- Added minimum file size validation (minBytes / 1024 bytes) to reject truncated or error HTML downloads.
|
|
36
|
+
- Validated engine parameter in /dsh-tts/models/install and /dsh-tts/models/delete, returning HTTP 400 for unknown engines.
|
|
37
|
+
- Replaced tautological regex and source inspection tests with true runtime behavior tests.
|
|
38
|
+
- Isolated cache tests in temporary directories with automated cleanup.
|
|
39
|
+
- Synchronized README documentation with reality and strictly excluded internal dev docs (AGENTS.md, index.md, plans) from package distributions.
|
|
40
|
+
- Closes #151, #152, #153, #154, #155, #156, #157, #158, #159, #160, #161, #162, #163, #164, #165, #166, #167, #168, #169, #170, #171, #172, #173, #174, #175, #176, #177, #178, #179, #180, #181, #182, #183, #184, #185, #186, #187, #188, #189, #190, #191.
|
|
41
|
+
|
|
42
|
+
## 0.4.27
|
|
4
43
|
|
|
5
44
|
### Fixed
|
|
6
45
|
- **Cloud Timeout Cancellation Forwarding** (#145): Fixed unquoted `abort` identifier in `lib/providers.js:21` inside `withCloudTimeout()`. Passing an active `AbortSignal` in `synthesize()` now cleanly attaches cancellation listeners without throwing `ReferenceError`.
|
package/README.md
CHANGED
|
@@ -101,10 +101,10 @@ graph LR
|
|
|
101
101
|
|
|
102
102
|
### 4. 📚 Built-in IT Terminology Pronunciation Dictionary
|
|
103
103
|
* **Pre-configured Lexicon**: Correct phonetic pronunciation for common technical abbreviations and developer terms:
|
|
104
|
-
- `SQL` $\rightarrow$ "
|
|
105
|
-
- `Nginx` $\rightarrow$ "
|
|
106
|
-
- `Kubernetes` / `K8s` $\rightarrow$ "
|
|
107
|
-
- `Docker` $\rightarrow$ "
|
|
104
|
+
- `SQL` $\rightarrow$ "sequel"
|
|
105
|
+
- `Nginx` $\rightarrow$ "engine-x"
|
|
106
|
+
- `Kubernetes` / `K8s` $\rightarrow$ "coo-ber-net-ees" / "kates"
|
|
107
|
+
- `Docker` $\rightarrow$ "docker", `API` $\rightarrow$ "A-P-I", `JSON` $\rightarrow$ "jason", `YAML` $\rightarrow$ "yaml"
|
|
108
108
|
- `GUI`, `CLI`, `CI/CD`, `PR`, `Regex`, `OAuth`, `HTTP`, `HTTPS`, `CPU`, `GPU`, `RAM`
|
|
109
109
|
* **Interactive UI Editor**: Edit rules, preview phonetic substitutions with the **▶ Listen** button, and populate standard IT terms with one click.
|
|
110
110
|
|
|
@@ -135,7 +135,7 @@ graph LR
|
|
|
135
135
|
| `f5` | Local F5-TTS GPU Daemon | `F5-TTS` | Default | *None* | Daemon ping only; **GPU inference not bundled** — fails honestly |
|
|
136
136
|
| `elevenlabs` | ElevenLabs API | `eleven_multilingual_v2` | `Rachel` | `ELEVENLABS_API_KEY` | Ultra-realistic, emotional nuance |
|
|
137
137
|
| `openai` | OpenAI Audio | `gpt-4o-mini-tts` / `tts-1` | `alloy` | `OPENAI_API_KEY` | High-quality industry standard |
|
|
138
|
-
| `edge` | Microsoft Edge Online | `
|
|
138
|
+
| `edge` | Microsoft Edge Online | `en-US-AriaNeural` | `en-US-AriaNeural` | *None* | **Free, high-fidelity neural TTS without API keys** |
|
|
139
139
|
| `siliconflow` | SiliconFlow CosyVoice | `FunAudioLLM/CosyVoice2-0.5B` | Default | `SILICONFLOW_API_KEY` | State-of-the-art CosyVoice2 neural engine |
|
|
140
140
|
| `deepinfra` | DeepInfra Kokoro | `hexgrad/Kokoro-82M` | Default | `DEEPINFRA_API_KEY` | Fast open-weights Kokoro synthesis |
|
|
141
141
|
| `fireworks` | Fireworks AI | `kokoro` | Default | `FIREWORKS_API_KEY` | Ultra-low latency Kokoro inference |
|
|
@@ -225,6 +225,10 @@ Config-only (not in the card): `*KeyEnv` fields (`openaiKeyEnv`, `elevenlabsKeyE
|
|
|
225
225
|
* `DELETE /dsh-tts/models/delete` — `{ engine: 'kokoro' | 'f5' }` → Removes local model files.
|
|
226
226
|
* `GET /dsh-tts/integrations` — Status of sibling plugins (`dsh-voice`, `dsh-messenger-gateway`).
|
|
227
227
|
* `GET /dsh-tts/status` — Returns active chain state, cache statistics, and engine readiness.
|
|
228
|
+
* `DELETE /dsh-tts/cache` — Clears cached audio files.
|
|
229
|
+
* `GET /dsh-tts/config` & `PUT /dsh-tts/config` — Retrieve and update settings revision.
|
|
230
|
+
* `GET /dsh-tts/stats` & `DELETE /dsh-tts/stats` — Performance statistics and metrics reset.
|
|
231
|
+
* `PUT /dsh-tts/credential` & `DELETE /dsh-tts/credential` — Securely store or remove cloud provider API keys.
|
|
228
232
|
|
|
229
233
|
---
|
|
230
234
|
|
package/lib/cache.js
CHANGED
|
@@ -41,56 +41,78 @@ export function createSpeechCache({ root, maxBytes }) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// In-memory index of disk cache entries to avoid O(N) readdir/stat/readFile per write (#190)
|
|
45
|
+
const diskEntries = new Map() // key -> { size, at }
|
|
46
|
+
let diskTotalBytes = 0
|
|
47
|
+
let indexLoaded = false
|
|
48
|
+
|
|
49
|
+
async function ensureIndex() {
|
|
45
50
|
await ensure()
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
if (indexLoaded) return
|
|
52
|
+
diskEntries.clear()
|
|
53
|
+
diskTotalBytes = 0
|
|
54
|
+
let files = []
|
|
55
|
+
try {
|
|
56
|
+
files = await fs.promises.readdir(dir)
|
|
57
|
+
} catch {
|
|
58
|
+
files = []
|
|
59
|
+
}
|
|
60
|
+
for (const name of files) {
|
|
48
61
|
if (!name.endsWith(AUDIO_EXT)) continue
|
|
62
|
+
const key = name.slice(0, -AUDIO_EXT.length)
|
|
49
63
|
const full = path.join(dir, name)
|
|
50
|
-
let st
|
|
51
|
-
try {
|
|
52
|
-
st = await fs.promises.stat(full)
|
|
53
|
-
} catch {
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
let at = st.mtimeMs
|
|
57
64
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
const st = await fs.promises.stat(full)
|
|
66
|
+
let at = st.mtimeMs
|
|
67
|
+
try {
|
|
68
|
+
const meta = JSON.parse(await fs.promises.readFile(path.join(dir, key + '.json'), 'utf8'))
|
|
69
|
+
if (meta && typeof meta.at === 'number') at = meta.at
|
|
70
|
+
} catch { /* no meta */ }
|
|
71
|
+
diskEntries.set(key, { size: st.size, at })
|
|
72
|
+
diskTotalBytes += st.size
|
|
73
|
+
} catch { /* skip unreadable */ }
|
|
62
74
|
}
|
|
63
|
-
|
|
75
|
+
indexLoaded = true
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
async function evict() {
|
|
79
|
+
await ensureIndex()
|
|
67
80
|
const limit = typeof maxBytes === 'function' ? maxBytes() : Number(maxBytes)
|
|
68
|
-
if (!(limit > 0)) return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
for (const
|
|
78
|
-
if (
|
|
79
|
-
await fs.promises.rm(
|
|
80
|
-
await fs.promises.rm(path.join(dir,
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
if (!(limit > 0) || diskTotalBytes <= limit) return
|
|
82
|
+
|
|
83
|
+
// Sort in-memory entries by timestamp ascending (oldest first)
|
|
84
|
+
const sorted = Array.from(diskEntries.entries()).sort((a, b) => {
|
|
85
|
+
const atA = l1Map.get(a[0])?.at ?? a[1].at
|
|
86
|
+
const atB = l1Map.get(b[0])?.at ?? b[1].at
|
|
87
|
+
return atA - atB
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
for (const [key, meta] of sorted) {
|
|
91
|
+
if (diskTotalBytes <= limit) break
|
|
92
|
+
await fs.promises.rm(path.join(dir, key + AUDIO_EXT), { force: true }).catch(() => {})
|
|
93
|
+
await fs.promises.rm(path.join(dir, key + '.json'), { force: true }).catch(() => {})
|
|
94
|
+
diskEntries.delete(key)
|
|
95
|
+
diskTotalBytes -= meta.size
|
|
96
|
+
l1Map.delete(key)
|
|
83
97
|
}
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
return {
|
|
87
101
|
async put(key, mime, audio) {
|
|
88
|
-
await
|
|
102
|
+
await ensureIndex()
|
|
89
103
|
const buf = Buffer.isBuffer(audio) ? audio : Buffer.from(audio)
|
|
90
104
|
putL1(key, mime, buf)
|
|
91
105
|
await fs.promises.writeFile(path.join(dir, key + AUDIO_EXT), buf)
|
|
92
|
-
|
|
93
|
-
await
|
|
106
|
+
const now = Date.now()
|
|
107
|
+
await fs.promises.writeFile(path.join(dir, key + '.json'), JSON.stringify({ mime, at: now }))
|
|
108
|
+
const prev = diskEntries.get(key)
|
|
109
|
+
if (prev) diskTotalBytes -= prev.size
|
|
110
|
+
diskEntries.set(key, { size: buf.length, at: now })
|
|
111
|
+
diskTotalBytes += buf.length
|
|
112
|
+
const limit = typeof maxBytes === 'function' ? maxBytes() : Number(maxBytes)
|
|
113
|
+
if (limit > 0 && diskTotalBytes > limit) {
|
|
114
|
+
await evict()
|
|
115
|
+
}
|
|
94
116
|
},
|
|
95
117
|
|
|
96
118
|
async get(key) {
|
|
@@ -100,17 +122,22 @@ export function createSpeechCache({ root, maxBytes }) {
|
|
|
100
122
|
const now = Date.now()
|
|
101
123
|
l1Map.delete(key) // refresh LRU order
|
|
102
124
|
l1Map.set(key, { ...hit, at: now })
|
|
125
|
+
const d = diskEntries.get(key)
|
|
126
|
+
if (d) d.at = now
|
|
103
127
|
fs.promises.writeFile(path.join(dir, key + '.json'), JSON.stringify({ mime: hit.mime, at: now })).catch(() => {})
|
|
104
128
|
return { mime: hit.mime || 'audio/mpeg', audio: hit.audio }
|
|
105
129
|
}
|
|
106
130
|
|
|
107
131
|
// 2. L2 Disk Hit
|
|
132
|
+
await ensureIndex()
|
|
108
133
|
try {
|
|
109
134
|
const meta = JSON.parse(await fs.promises.readFile(path.join(dir, key + '.json'), 'utf8'))
|
|
110
135
|
const audio = await fs.promises.readFile(path.join(dir, key + AUDIO_EXT))
|
|
111
136
|
putL1(key, meta.mime, audio)
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
const now = Date.now()
|
|
138
|
+
const d = diskEntries.get(key)
|
|
139
|
+
if (d) d.at = now
|
|
140
|
+
fs.promises.writeFile(path.join(dir, key + '.json'), JSON.stringify({ mime: meta.mime, at: now })).catch(() => {})
|
|
114
141
|
return { mime: meta.mime || 'audio/mpeg', audio }
|
|
115
142
|
} catch (miss) {
|
|
116
143
|
return null
|
|
@@ -119,12 +146,15 @@ export function createSpeechCache({ root, maxBytes }) {
|
|
|
119
146
|
|
|
120
147
|
async clear() {
|
|
121
148
|
l1Map.clear()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
await fs.promises.rm(path.join(dir,
|
|
149
|
+
await ensureIndex()
|
|
150
|
+
const count = diskEntries.size
|
|
151
|
+
for (const key of diskEntries.keys()) {
|
|
152
|
+
await fs.promises.rm(path.join(dir, key + AUDIO_EXT), { force: true }).catch(() => {})
|
|
153
|
+
await fs.promises.rm(path.join(dir, key + '.json'), { force: true }).catch(() => {})
|
|
126
154
|
}
|
|
127
|
-
|
|
155
|
+
diskEntries.clear()
|
|
156
|
+
diskTotalBytes = 0
|
|
157
|
+
return count
|
|
128
158
|
},
|
|
129
159
|
}
|
|
130
160
|
}
|