@goodandready/dsh-tts 0.4.24 → 0.4.25
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/index.js +63 -61
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
|
|
25
25
|
// Scoped identity matching package.json and cordis.patch.yml
|
|
26
26
|
export const name = '@goodandready/dsh-tts'
|
|
27
|
-
export const inject = ['tools', 'credentials', 'llm', 'webServer'
|
|
27
|
+
export const inject = ['tools', 'credentials', 'llm', 'webServer']
|
|
28
28
|
|
|
29
29
|
const NS = 'dsh-tts'
|
|
30
30
|
|
|
@@ -45,34 +45,43 @@ const RoleOverride = z.object({
|
|
|
45
45
|
ssmlStyle: z.string().default(''),
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
+
// Unwrap Volatile boxes before any caller reads a value. A volatile field holds a
|
|
49
|
+
// box rather than its value, and the Loader mutates those boxes in place.
|
|
50
|
+
export function plainConfig(value) {
|
|
51
|
+
if (value === null || typeof value !== 'object') return value
|
|
52
|
+
if (Array.isArray(value)) return value.map(plainConfig)
|
|
53
|
+
if (typeof value.get === 'function') return plainConfig(value.get())
|
|
54
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plainConfig(v)]))
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
export const Config = z.object({
|
|
49
|
-
speakReplies: z.boolean().default(false)
|
|
58
|
+
speakReplies: z.boolean().volatile().default(false)
|
|
50
59
|
.description('When on, speak each finished agent reply in the Web UI.'),
|
|
51
|
-
enableLocalEngines: z.boolean().default(false)
|
|
60
|
+
enableLocalEngines: z.boolean().volatile().default(false)
|
|
52
61
|
.description('Enable offline local TTS engines (Kokoro / F5).'),
|
|
53
|
-
kokoroEnabled: z.boolean().default(false)
|
|
62
|
+
kokoroEnabled: z.boolean().volatile().default(false)
|
|
54
63
|
.description('Use local Kokoro-82M on CPU when installed.'),
|
|
55
|
-
f5Enabled: z.boolean().default(false)
|
|
64
|
+
f5Enabled: z.boolean().volatile().default(false)
|
|
56
65
|
.description('Use local F5-TTS on GPU when installed.'),
|
|
57
|
-
streamingEnabled: z.boolean().default(true)
|
|
66
|
+
streamingEnabled: z.boolean().volatile().default(true)
|
|
58
67
|
.description('Stream audio in real-time using AudioWorklet and SSE for sub-300ms latency.'),
|
|
59
|
-
skipCode: z.boolean().default(true)
|
|
68
|
+
skipCode: z.boolean().volatile().default(true)
|
|
60
69
|
.description('Replace fenced code blocks with a short spoken notice instead of reading them aloud.'),
|
|
61
|
-
cache: z.boolean().default(true)
|
|
70
|
+
cache: z.boolean().volatile().default(true)
|
|
62
71
|
.description('Reuse synthesized audio for repeated phrases instead of paying and waiting again.'),
|
|
63
|
-
cacheMaxMb: z.number().default(100)
|
|
72
|
+
cacheMaxMb: z.number().volatile().default(100)
|
|
64
73
|
.description('Disk limit for the synthesis cache; least recently used items are evicted first.'),
|
|
65
|
-
narrateQuotesOnly: z.boolean().default(false)
|
|
74
|
+
narrateQuotesOnly: z.boolean().volatile().default(false)
|
|
66
75
|
.description('Speak only quoted fragments.'),
|
|
67
|
-
skipActions: z.boolean().default(false)
|
|
76
|
+
skipActions: z.boolean().volatile().default(false)
|
|
68
77
|
.description('Drop *asterisk action* blocks instead of reading them.'),
|
|
69
|
-
removeRegex: z.string().default('')
|
|
78
|
+
removeRegex: z.string().volatile().default('')
|
|
70
79
|
.description('Custom global regex whose matches are removed before synthesis.'),
|
|
71
|
-
autoDetect: z.boolean().default(false)
|
|
80
|
+
autoDetect: z.boolean().volatile().default(false)
|
|
72
81
|
.description('Guess ru/en per spoken piece instead of using the language setting.'),
|
|
73
|
-
customBaseUrl: z.string().default('')
|
|
82
|
+
customBaseUrl: z.string().volatile().default('')
|
|
74
83
|
.description('Origin of an OpenAI-compatible /audio/speech endpoint; empty skips it.'),
|
|
75
|
-
customKeyEnv: z.string().default('CUSTOM_TTS_API_KEY'),
|
|
84
|
+
customKeyEnv: z.string().volatile().default('CUSTOM_TTS_API_KEY'),
|
|
76
85
|
pronunciation: z.array(z.object({
|
|
77
86
|
from: z.string().default(''),
|
|
78
87
|
to: z.string().default(''),
|
|
@@ -80,29 +89,29 @@ export const Config = z.object({
|
|
|
80
89
|
lang: z.string().default(''),
|
|
81
90
|
})).default([])
|
|
82
91
|
.description('Say this instead of that. Applied top-down; /regex/ form allowed in from.'),
|
|
83
|
-
enableItDictionary: z.boolean().default(true)
|
|
92
|
+
enableItDictionary: z.boolean().volatile().default(true)
|
|
84
93
|
.description('Auto-correct common IT terminology pronunciation (SQL, Nginx, K8s, etc.).'),
|
|
85
|
-
longReply: z.string().default('truncate')
|
|
94
|
+
longReply: z.string().volatile().default('truncate')
|
|
86
95
|
.description('What to do when a reply exceeds the length limit.'),
|
|
87
|
-
summaryModel: z.string().default('')
|
|
96
|
+
summaryModel: z.string().volatile().default('')
|
|
88
97
|
.description('provider/model for the spoken retelling; empty uses the conversation default.'),
|
|
89
|
-
summarySentences: z.number().default(3)
|
|
98
|
+
summarySentences: z.number().volatile().default(3)
|
|
90
99
|
.description('Sentences in the spoken retelling.'),
|
|
91
|
-
roles: z.dict(RoleOverride).default({
|
|
100
|
+
roles: z.dict(RoleOverride).volatile().default({
|
|
92
101
|
reply: { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' },
|
|
93
102
|
approval: { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' },
|
|
94
103
|
error: { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' },
|
|
95
104
|
})
|
|
96
105
|
.description('Per-role and subagent overrides; empty fields inherit the main chain.'),
|
|
97
|
-
voiceDuplexEnabled: z.boolean().default(false)
|
|
106
|
+
voiceDuplexEnabled: z.boolean().volatile().default(false)
|
|
98
107
|
.description('Full-duplex voice conversation with dsh-voice.'),
|
|
99
|
-
vadBargeIn: z.boolean().default(true)
|
|
108
|
+
vadBargeIn: z.boolean().volatile().default(true)
|
|
100
109
|
.description('Mute speech synthesis immediately when voice activity is detected.'),
|
|
101
|
-
messengerTtsEnabled: z.boolean().default(false)
|
|
110
|
+
messengerTtsEnabled: z.boolean().volatile().default(false)
|
|
102
111
|
.description('Send synthesized voice notes to messengers via @goodandready/dsh-messenger-gateway.'),
|
|
103
|
-
autoDetectSubagent: z.boolean().default(true)
|
|
112
|
+
autoDetectSubagent: z.boolean().volatile().default(true)
|
|
104
113
|
.description('Auto-detect subagent roles in multi-agent sessions and route to dedicated voice profiles.'),
|
|
105
|
-
language: z.string().default('ru'),
|
|
114
|
+
language: z.string().volatile().default('ru'),
|
|
106
115
|
speakAsItGoes: z
|
|
107
116
|
.boolean()
|
|
108
117
|
.description('Speak each reply as it lands instead of waiting for the whole turn to finish. '
|
|
@@ -138,43 +147,43 @@ export const Config = z.object({
|
|
|
138
147
|
.string()
|
|
139
148
|
.description('Short sound before an announcement: ding, beep or none.')
|
|
140
149
|
.default('ding'),
|
|
141
|
-
maxChars: z.number().default(4000)
|
|
150
|
+
maxChars: z.number().volatile().default(4000)
|
|
142
151
|
.description('Longer replies are truncated before synthesis.'),
|
|
143
|
-
chain: z.array(ChainEntry)
|
|
152
|
+
chain: z.array(ChainEntry).volatile()
|
|
144
153
|
.default([
|
|
145
154
|
{ provider: 'edge', model: '', voice: 'ru-RU-SvetlanaNeural' },
|
|
146
155
|
{ provider: 'piper', model: '', voice: '' },
|
|
147
156
|
{ provider: 'espeak', model: '', voice: 'ru' },
|
|
148
157
|
])
|
|
149
158
|
.description('Fallback chain. Order is the order of attempts. A provider without a key is skipped.'),
|
|
150
|
-
openaiKeyEnv: z.string().default('OPENAI_API_KEY'),
|
|
151
|
-
openaiBaseUrl: z.string().default('https://api.openai.com/v1'),
|
|
152
|
-
elevenlabsKeyEnv: z.string().default('ELEVENLABS_API_KEY'),
|
|
153
|
-
googleKeyEnv: z.string().default('GEMINI_API_KEY'),
|
|
154
|
-
azureKeyEnv: z.string().default('AZURE_SPEECH_KEY'),
|
|
155
|
-
azureRegion: z.string().default('')
|
|
159
|
+
openaiKeyEnv: z.string().volatile().default('OPENAI_API_KEY'),
|
|
160
|
+
openaiBaseUrl: z.string().volatile().default('https://api.openai.com/v1'),
|
|
161
|
+
elevenlabsKeyEnv: z.string().volatile().default('ELEVENLABS_API_KEY'),
|
|
162
|
+
googleKeyEnv: z.string().volatile().default('GEMINI_API_KEY'),
|
|
163
|
+
azureKeyEnv: z.string().volatile().default('AZURE_SPEECH_KEY'),
|
|
164
|
+
azureRegion: z.string().volatile().default('')
|
|
156
165
|
.description('Azure Speech region, e.g. eastus. Required for the azure provider.'),
|
|
157
|
-
mimoKeyEnv: z.string().description('Credential holding the Xiaomi MiMo key.').default('MIMO_API_KEY'),
|
|
158
|
-
mimoBaseUrl: z.string().description('MiMo API root. Synthesis goes through its chat endpoint.').default('https://api.xiaomimimo.com/v1'),
|
|
159
|
-
mimoFormat: z.string().description('MiMo output format: mp3 or wav.').default('mp3'),
|
|
160
|
-
minimaxBin: z.string()
|
|
166
|
+
mimoKeyEnv: z.string().volatile().description('Credential holding the Xiaomi MiMo key.').default('MIMO_API_KEY'),
|
|
167
|
+
mimoBaseUrl: z.string().volatile().description('MiMo API root. Synthesis goes through its chat endpoint.').default('https://api.xiaomimimo.com/v1'),
|
|
168
|
+
mimoFormat: z.string().volatile().description('MiMo output format: mp3 or wav.').default('mp3'),
|
|
169
|
+
minimaxBin: z.string().volatile()
|
|
161
170
|
.description('MiniMax CLI, looked up in PATH unless absolute. Install and log in separately; '
|
|
162
171
|
+ 'without it the provider declines and the chain moves on.')
|
|
163
172
|
.default('mmx'),
|
|
164
|
-
siliconflowKeyEnv: z.string().description('Credential holding the SiliconFlow key.').default('SILICONFLOW_API_KEY'),
|
|
165
|
-
deepinfraKeyEnv: z.string().description('Credential holding the DeepInfra key.').default('DEEPINFRA_API_KEY'),
|
|
166
|
-
fireworksKeyEnv: z.string().description('Credential holding the Fireworks key.').default('FIREWORKS_API_KEY'),
|
|
167
|
-
groqKeyEnv: z.string().default('GROQ_API_KEY'),
|
|
168
|
-
deepgramKeyEnv: z.string().default('DEEPGRAM_API_KEY'),
|
|
169
|
-
openrouterKeyEnv: z.string().default('OPENROUTER_API_KEY'),
|
|
170
|
-
edgeBin: z.string().default('edge-tts')
|
|
173
|
+
siliconflowKeyEnv: z.string().volatile().description('Credential holding the SiliconFlow key.').default('SILICONFLOW_API_KEY'),
|
|
174
|
+
deepinfraKeyEnv: z.string().volatile().description('Credential holding the DeepInfra key.').default('DEEPINFRA_API_KEY'),
|
|
175
|
+
fireworksKeyEnv: z.string().volatile().description('Credential holding the Fireworks key.').default('FIREWORKS_API_KEY'),
|
|
176
|
+
groqKeyEnv: z.string().volatile().default('GROQ_API_KEY'),
|
|
177
|
+
deepgramKeyEnv: z.string().volatile().default('DEEPGRAM_API_KEY'),
|
|
178
|
+
openrouterKeyEnv: z.string().volatile().default('OPENROUTER_API_KEY'),
|
|
179
|
+
edgeBin: z.string().volatile().default('edge-tts')
|
|
171
180
|
.description('edge-tts CLI. Looked up in PATH unless an absolute path is given.'),
|
|
172
|
-
piperBin: z.string().default('piper'),
|
|
173
|
-
piperModel: z.string().default('')
|
|
181
|
+
piperBin: z.string().volatile().default('piper'),
|
|
182
|
+
piperModel: z.string().volatile().default('')
|
|
174
183
|
.description('Path to a Piper ONNX model. The piper provider is skipped while this is empty.'),
|
|
175
|
-
espeakBin: z.string().default('espeak-ng'),
|
|
176
|
-
timeoutMs: z.number().default(60000),
|
|
177
|
-
maxQueue: z.number().default(8),
|
|
184
|
+
espeakBin: z.string().volatile().default('espeak-ng'),
|
|
185
|
+
timeoutMs: z.number().volatile().default(60000),
|
|
186
|
+
maxQueue: z.number().volatile().default(8),
|
|
178
187
|
})
|
|
179
188
|
|
|
180
189
|
let utteranceSeq = 0
|
|
@@ -183,18 +192,11 @@ export function apply(ctx, config) {
|
|
|
183
192
|
const providerBreaker = createProviderBreaker()
|
|
184
193
|
|
|
185
194
|
let getConfig = () => config
|
|
186
|
-
const live = () => Config(structuredClone(getConfig() ?? {})) ?? config
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
settingsApi = scope
|
|
192
|
-
getConfig = () => scope.get() ?? config
|
|
193
|
-
sctx.effect(() => () => {
|
|
194
|
-
settingsApi = undefined
|
|
195
|
-
getConfig = () => config
|
|
196
|
-
})
|
|
197
|
-
})
|
|
195
|
+
const live = () => plainConfig(Config(structuredClone(getConfig() ?? {})) ?? config)
|
|
196
|
+
|
|
197
|
+
// settings.register was removed before 0.1.7-rc.2. The host hands the profile entry
|
|
198
|
+
// config to apply() directly; live() below re-resolves it through the schema and
|
|
199
|
+
// unwraps, so the Loader's in-place box updates are picked up without a service.
|
|
198
200
|
|
|
199
201
|
const pending = []
|
|
200
202
|
const collectors = new Map()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-tts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.25",
|
|
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",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
68
68
|
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
69
69
|
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
70
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
70
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
71
71
|
},
|
|
72
72
|
"publishConfig": {
|
|
73
73
|
"access": "public"
|