dsh-speak 1.7.4 → 1.8.1
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/LICENSE +21 -21
- package/README.md +462 -417
- package/README.zh-CN.md +433 -394
- package/adapters/dsh/install.ps1 +81 -81
- package/adapters/dsh/speech-hook.js +614 -561
- package/client/client.js +357 -340
- package/docs/DESIGN.md +60 -14
- package/docs/DESIGN.zh-CN.md +48 -12
- package/engine/speak.ps1 +214 -155
- package/engine/speak.sh +36 -6
- package/engine/speech-prompt.ps1 +24 -22
- package/engine/speech-summary.ps1 +27 -27
- package/package.json +82 -85
package/README.md
CHANGED
|
@@ -1,417 +1,462 @@
|
|
|
1
|
-
# dsh-speak 🔊 — Voice announcements for AI coding harnesses
|
|
2
|
-
|
|
3
|
-
**English** · [中文](README.zh-CN.md)
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
[](https://awesome-dsh-plugin.com)
|
|
8
|
-
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-speak)
|
|
10
|
-
|
|
11
|
-
Let your agent **tell you** when a long task is done — no more staring at the screen.
|
|
12
|
-
|
|
13
|
-
dsh-speak reads the final assistant reply aloud through system speech synthesis —
|
|
14
|
-
on Windows using natural voices (Windows 11 built-in, or
|
|
15
|
-
[NaturalVoiceSAPIAdapter] on Windows 10) with graceful fallback to stock voices;
|
|
16
|
-
on macOS using the built-in `say` (can follow a Siri natural voice). It was built
|
|
17
|
-
for [DeepSeek Harness](https://github.com/deepseek-ai/dsh)
|
|
18
|
-
and is structured so any harness can plug in.
|
|
19
|
-
|
|
20
|
-
## Features
|
|
21
|
-
|
|
22
|
-
- **Automatic**: DSH web plugin watches the session event stream and announces the
|
|
23
|
-
final reply (skips reasoning/tool-call narration, merges multi-step messages).
|
|
24
|
-
- **Gets your attention**: announces approval requests (hears "需要你的审批" when
|
|
25
|
-
the agent is waiting on you) and questions the agent asks via `ask_user_question`.
|
|
26
|
-
- **Final-reply replay** (1.7.0): every final reply (turn tail) has a 🔊 button
|
|
27
|
-
in its action bar — click to replay that message, click again to stop, click
|
|
28
|
-
another to switch. Speech execution stays fully owned by the DSH host (keeps
|
|
29
|
-
speaking even with the browser closed).
|
|
30
|
-
- **Host speech queue** (1.7.0): only one native speech process runs at a time;
|
|
31
|
-
queued items continue automatically. A WebSocket syncs the live state (which
|
|
32
|
-
message is speaking, queue length) to the UI.
|
|
33
|
-
- **Optional event announcements** (1.6.0): turn end, command done, goal changes,
|
|
34
|
-
tool errors, and todo updates can each be announced, toggled independently
|
|
35
|
-
(off by default).
|
|
36
|
-
- **Visual configuration** (1.7.0): a dedicated Settings → dsh-speak settings
|
|
37
|
-
page — every option (master switch, automatic speech, Markdown cleaning, code
|
|
38
|
-
blocks, event toggles, fixed prompt, …) is editable from the Web UI, no
|
|
39
|
-
hand-edited YAML.
|
|
40
|
-
- **Master switch** (1.6.0): silence everything with one toggle.
|
|
41
|
-
- **Bundle auto-registration** (1.3.0): declare the package in `dsh.profile.bundles`
|
|
42
|
-
and the plugin registers itself via the bundled `cordis.patch.yml` — no manual
|
|
43
|
-
patch entry needed.
|
|
44
|
-
- **Best-effort**: never throws, never blocks the harness, never breaks a session.
|
|
45
|
-
- **Natural voices**: Windows prefers natural voices — Windows 11 built-in packs,
|
|
46
|
-
or voices registered via NaturalVoiceSAPIAdapter on Windows 10 (e.g. Xiaoxiao);
|
|
47
|
-
macOS uses the system reading voice (Siri natural voices on recent macOS). Both
|
|
48
|
-
fall back to any installed voice.
|
|
49
|
-
- **Robust text cleaning**: strips markdown/URLs/emoji that make speech synthesis
|
|
50
|
-
fail silently, and guards the adapter's per-utterance character ceiling.
|
|
51
|
-
- **Portable engine**: any process can speak with one line:
|
|
52
|
-
Windows `powershell -File speak.ps1 -Text "你好"` / macOS `./speak.sh -t "你好"`.
|
|
53
|
-
|
|
54
|
-
## How it works
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
harness event (DSH session event / Claude Code Stop hook / anything)
|
|
58
|
-
│
|
|
59
|
-
▼ adapters/… (harness-specific trigger: filter, throttle, cancel)
|
|
60
|
-
▼ engine/speak.ps1 / speak.sh (harness-agnostic: clean text → SAPI5 / say)
|
|
61
|
-
▼ 🔊 you hear the final reply
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
The adapter turns harness-specific events into engine calls; the engine cleans the
|
|
65
|
-
text and speaks it, fully decoupled from any harness. Full design:
|
|
66
|
-
[docs/DESIGN.md](docs/DESIGN.md).
|
|
67
|
-
|
|
68
|
-
## Prerequisites
|
|
69
|
-
|
|
70
|
-
Windows:
|
|
71
|
-
|
|
72
|
-
- Windows 10 or 11, PowerShell (any recent version).
|
|
73
|
-
- Natural voices:
|
|
74
|
-
- **Windows 11 (21H2–23H2)**: natural voice packs are built into the system —
|
|
75
|
-
no extra installation. Enable/switch them in *Settings → Accessibility →
|
|
76
|
-
Narrator* or *Settings → Time & Language → Speech*.
|
|
77
|
-
- **Windows 11 24H2/25H2**: natural voices moved to MSIX app packages, which
|
|
78
|
-
`System.Speech` may not enumerate (falls back to a robotic stock voice). As
|
|
79
|
-
on Windows 10, install
|
|
80
|
-
[NaturalVoiceSAPIAdapter](https://github.com/gexgd0419/NaturalVoiceSAPIAdapter)
|
|
81
|
-
to bridge them.
|
|
82
|
-
- **Windows 10**: install
|
|
83
|
-
[NaturalVoiceSAPIAdapter](https://github.com/gexgd0419/NaturalVoiceSAPIAdapter)
|
|
84
|
-
and use its VoiceDownloader to download the natural voice pack(s) you want
|
|
85
|
-
(Chinese or any other language).
|
|
86
|
-
- Without natural voices, the engine falls back to a stock voice (e.g. Huihui).
|
|
87
|
-
|
|
88
|
-
macOS:
|
|
89
|
-
|
|
90
|
-
- macOS (Apple Silicon or Intel), built-in `say` command — **no extra software**.
|
|
91
|
-
- Chinese voices: see the [macOS](#macos) section (incl. the Siri natural-voice
|
|
92
|
-
picker and its pitfalls).
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
> **
|
|
129
|
-
>
|
|
130
|
-
>
|
|
131
|
-
>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
|
319
|
-
|
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
323
|
-
| `
|
|
324
|
-
| `
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
328
|
-
| `
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1
|
+
# dsh-speak 🔊 — Voice announcements for AI coding harnesses
|
|
2
|
+
|
|
3
|
+
**English** · [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
[](https://awesome-dsh-plugin.com)
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-speak)
|
|
10
|
+
|
|
11
|
+
Let your agent **tell you** when a long task is done — no more staring at the screen.
|
|
12
|
+
|
|
13
|
+
dsh-speak reads the final assistant reply aloud through system speech synthesis —
|
|
14
|
+
on Windows using natural voices (Windows 11 built-in, or
|
|
15
|
+
[NaturalVoiceSAPIAdapter] on Windows 10) with graceful fallback to stock voices;
|
|
16
|
+
on macOS using the built-in `say` (can follow a Siri natural voice). It was built
|
|
17
|
+
for [DeepSeek Harness](https://github.com/deepseek-ai/dsh)
|
|
18
|
+
and is structured so any harness can plug in.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Automatic**: DSH web plugin watches the session event stream and announces the
|
|
23
|
+
final reply (skips reasoning/tool-call narration, merges multi-step messages).
|
|
24
|
+
- **Gets your attention**: announces approval requests (hears "需要你的审批" when
|
|
25
|
+
the agent is waiting on you) and questions the agent asks via `ask_user_question`.
|
|
26
|
+
- **Final-reply replay** (1.7.0): every final reply (turn tail) has a 🔊 button
|
|
27
|
+
in its action bar — click to replay that message, click again to stop, click
|
|
28
|
+
another to switch. Speech execution stays fully owned by the DSH host (keeps
|
|
29
|
+
speaking even with the browser closed).
|
|
30
|
+
- **Host speech queue** (1.7.0): only one native speech process runs at a time;
|
|
31
|
+
queued items continue automatically. A WebSocket syncs the live state (which
|
|
32
|
+
message is speaking, queue length) to the UI.
|
|
33
|
+
- **Optional event announcements** (1.6.0): turn end, command done, goal changes,
|
|
34
|
+
tool errors, and todo updates can each be announced, toggled independently
|
|
35
|
+
(off by default).
|
|
36
|
+
- **Visual configuration** (1.7.0): a dedicated Settings → dsh-speak settings
|
|
37
|
+
page — every option (master switch, automatic speech, Markdown cleaning, code
|
|
38
|
+
blocks, event toggles, fixed prompt, …) is editable from the Web UI, no
|
|
39
|
+
hand-edited YAML.
|
|
40
|
+
- **Master switch** (1.6.0): silence everything with one toggle.
|
|
41
|
+
- **Bundle auto-registration** (1.3.0): declare the package in `dsh.profile.bundles`
|
|
42
|
+
and the plugin registers itself via the bundled `cordis.patch.yml` — no manual
|
|
43
|
+
patch entry needed.
|
|
44
|
+
- **Best-effort**: never throws, never blocks the harness, never breaks a session.
|
|
45
|
+
- **Natural voices**: Windows prefers natural voices — Windows 11 built-in packs,
|
|
46
|
+
or voices registered via NaturalVoiceSAPIAdapter on Windows 10 (e.g. Xiaoxiao);
|
|
47
|
+
macOS uses the system reading voice (Siri natural voices on recent macOS). Both
|
|
48
|
+
fall back to any installed voice.
|
|
49
|
+
- **Robust text cleaning**: strips markdown/URLs/emoji that make speech synthesis
|
|
50
|
+
fail silently, and guards the adapter's per-utterance character ceiling.
|
|
51
|
+
- **Portable engine**: any process can speak with one line:
|
|
52
|
+
Windows `powershell -File speak.ps1 -Text "你好"` / macOS `./speak.sh -t "你好"`.
|
|
53
|
+
|
|
54
|
+
## How it works
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
harness event (DSH session event / Claude Code Stop hook / anything)
|
|
58
|
+
│
|
|
59
|
+
▼ adapters/… (harness-specific trigger: filter, throttle, cancel)
|
|
60
|
+
▼ engine/speak.ps1 / speak.sh (harness-agnostic: clean text → SAPI5 / say)
|
|
61
|
+
▼ 🔊 you hear the final reply
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The adapter turns harness-specific events into engine calls; the engine cleans the
|
|
65
|
+
text and speaks it, fully decoupled from any harness. Full design:
|
|
66
|
+
[docs/DESIGN.md](docs/DESIGN.md).
|
|
67
|
+
|
|
68
|
+
## Prerequisites
|
|
69
|
+
|
|
70
|
+
Windows:
|
|
71
|
+
|
|
72
|
+
- Windows 10 or 11, PowerShell (any recent version).
|
|
73
|
+
- Natural voices:
|
|
74
|
+
- **Windows 11 (21H2–23H2)**: natural voice packs are built into the system —
|
|
75
|
+
no extra installation. Enable/switch them in *Settings → Accessibility →
|
|
76
|
+
Narrator* or *Settings → Time & Language → Speech*.
|
|
77
|
+
- **Windows 11 24H2/25H2**: natural voices moved to MSIX app packages, which
|
|
78
|
+
`System.Speech` may not enumerate (falls back to a robotic stock voice). As
|
|
79
|
+
on Windows 10, install
|
|
80
|
+
[NaturalVoiceSAPIAdapter](https://github.com/gexgd0419/NaturalVoiceSAPIAdapter)
|
|
81
|
+
to bridge them.
|
|
82
|
+
- **Windows 10**: install
|
|
83
|
+
[NaturalVoiceSAPIAdapter](https://github.com/gexgd0419/NaturalVoiceSAPIAdapter)
|
|
84
|
+
and use its VoiceDownloader to download the natural voice pack(s) you want
|
|
85
|
+
(Chinese or any other language).
|
|
86
|
+
- Without natural voices, the engine falls back to a stock voice (e.g. Huihui).
|
|
87
|
+
|
|
88
|
+
macOS:
|
|
89
|
+
|
|
90
|
+
- macOS (Apple Silicon or Intel), built-in `say` command — **no extra software**.
|
|
91
|
+
- Chinese voices: see the [macOS](#macos) section (incl. the Siri natural-voice
|
|
92
|
+
picker and its pitfalls).
|
|
93
|
+
|
|
94
|
+
DSH web app:
|
|
95
|
+
|
|
96
|
+
- Tested against **DSH 0.1.5-rc.1**. Two host/client APIs changed after 0.1.1, both
|
|
97
|
+
handled here (1.8.0):
|
|
98
|
+
- `@deepseek-ai/dsh-settings` deleted the `installSettingsSection` /
|
|
99
|
+
`settingsNamespace` helpers — the plugin now registers its namespace through
|
|
100
|
+
the `settings` **service**. On those older releases the plugin aborted the
|
|
101
|
+
host boot (`settingsNamespace is not a function`); a missing settings provider
|
|
102
|
+
now just leaves the composed patch `config` in force.
|
|
103
|
+
- the Session snapshot stopped carrying Conversation target data — the 🔊 button
|
|
104
|
+
resolves the clicked message through the Chat target hook `useChat`.
|
|
105
|
+
- The host floor lives where dsh-market reads it: `engines.dsh` in `package.json`
|
|
106
|
+
(`>=0.1.5-rc.1`). The catalog card and its "compatible with current DSH" filter
|
|
107
|
+
read exactly that field, so the floor moves only after a release has been
|
|
108
|
+
verified against the new host.
|
|
109
|
+
|
|
110
|
+
## Install & quick start
|
|
111
|
+
|
|
112
|
+
### DSH — Option A: npm plugin (recommended)
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
# 1. install the plugin into your web profile (adds dsh-speak to
|
|
116
|
+
# ~/.dsh/profiles/web/package.json dependencies)
|
|
117
|
+
dsh plugin --profile web add dsh-speak
|
|
118
|
+
|
|
119
|
+
# 2. register it in ~/.dsh/profiles/web/cordis.patch.yml
|
|
120
|
+
# (for npm packages the bare package name is used — no file:/// URL needed):
|
|
121
|
+
# - insert:
|
|
122
|
+
# - id: speech-hook
|
|
123
|
+
# name: 'dsh-speak'
|
|
124
|
+
|
|
125
|
+
# 3. restart the DSH web app — replies are now announced automatically
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
> **No pnpm?** `dsh plugin` forwards to pnpm, which is not installed on every
|
|
129
|
+
> machine. The exact same install can be done with npm directly:
|
|
130
|
+
>
|
|
131
|
+
> ```powershell
|
|
132
|
+
> npm install --prefix "$env:USERPROFILE\.dsh\profiles\web" dsh-speak
|
|
133
|
+
> ```
|
|
134
|
+
>
|
|
135
|
+
> On macOS (bash):
|
|
136
|
+
>
|
|
137
|
+
> ```bash
|
|
138
|
+
> npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
|
|
139
|
+
> ```
|
|
140
|
+
|
|
141
|
+
The engine ships inside the package (`node_modules/dsh-speak/engine/`), so no extra
|
|
142
|
+
copying is needed.
|
|
143
|
+
|
|
144
|
+
> **Let your agent do it?** Paste this repo URL
|
|
145
|
+
> (`https://github.com/Alan2Z/dsh-speak`) into your DSH session and ask it to
|
|
146
|
+
> install the plugin — your agent follows this very README. Approving the
|
|
147
|
+
> out-of-workspace writes (`~/.dsh`) is all that's needed.
|
|
148
|
+
|
|
149
|
+
### DSH — Option B: file install (no npm needed)
|
|
150
|
+
|
|
151
|
+
```powershell
|
|
152
|
+
# 1. clone
|
|
153
|
+
git clone https://github.com/Alan2Z/dsh-speak.git
|
|
154
|
+
cd dsh-speak
|
|
155
|
+
|
|
156
|
+
# 2. one-command install: copies engine + plugin, registers in cordis.patch.yml
|
|
157
|
+
powershell.exe -NoProfile -ExecutionPolicy Bypass -File adapters\dsh\install.ps1
|
|
158
|
+
|
|
159
|
+
# 3. verify the engine speaks
|
|
160
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "$env:USERPROFILE\.dsh\hooks\speak.ps1" -Text "你好,语音播报已就绪。"
|
|
161
|
+
|
|
162
|
+
# 4. restart the DSH web app — replies are now announced automatically
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
What the file installer did:
|
|
166
|
+
|
|
167
|
+
| file | destination |
|
|
168
|
+
| ---- | ----------- |
|
|
169
|
+
| `engine/*.ps1` | `%USERPROFILE%\.dsh\hooks\` |
|
|
170
|
+
| `adapters/dsh/speech-hook.js` | `%USERPROFILE%\.dsh\profiles\web\plugins\` |
|
|
171
|
+
| registration entry | appended to `%USERPROFILE%\.dsh\profiles\web\cordis.patch.yml` (backed up first) |
|
|
172
|
+
|
|
173
|
+
### macOS
|
|
174
|
+
|
|
175
|
+
The same adapter runs natively on macOS — the plugin auto-detects the platform and
|
|
176
|
+
calls `engine/speak.sh` (the built-in `say` command) instead of `speak.ps1`.
|
|
177
|
+
**Since 1.2.0 the macOS engine ships in the npm package** — no extra software.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# 1. install into your web profile (no pnpm needed — only `dsh plugin` requires it)
|
|
181
|
+
npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
|
|
182
|
+
|
|
183
|
+
# 2. register in ~/.dsh/profiles/web/cordis.patch.yml (bare package name — no file:/// URL):
|
|
184
|
+
# - insert:
|
|
185
|
+
# - id: speech-hook
|
|
186
|
+
# name: 'dsh-speak'
|
|
187
|
+
|
|
188
|
+
# 3. no restart needed — the patch watcher hot-reloads; replies are announced
|
|
189
|
+
# after the throttle (~1.5 s); tool-calling replies are announced at turn end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
> With pnpm installed, `dsh plugin --profile web add dsh-speak` works identically.
|
|
193
|
+
|
|
194
|
+
#### Voices (important — two pitfalls)
|
|
195
|
+
|
|
196
|
+
- By default the engine follows the **system reading voice** (*Settings →
|
|
197
|
+
Accessibility → Spoken Content → System Voice*). On **macOS 26** that picker has
|
|
198
|
+
an **ⓘ circle icon** next to it — click it for the full voice list; the plain
|
|
199
|
+
dropdown does **not** contain the Siri natural voices. Pick e.g. "普通话 Siri
|
|
200
|
+
声音1(男声)" there.
|
|
201
|
+
- **Siri voice** (*Settings → Siri → Voice*) and the system reading voice are
|
|
202
|
+
**two independent settings**; Siri voices are not exposed to `say -v '?'` and
|
|
203
|
+
cannot be selected by name — they only work as the system default.
|
|
204
|
+
- ⚠️ **Pitfall 1 (reproduced)**: opening the "Spoken Content / Siri Voice" settings
|
|
205
|
+
pane — **even without changing anything** — drifts/resets the system voice to the
|
|
206
|
+
classic "婷婷 (Tingting)". If the voice suddenly changes, re-pick it via the ⓘ
|
|
207
|
+
entry.
|
|
208
|
+
- ⚠️ **Pitfall 2**: the log lives at `$TMPDIR/dsh-speech-hook.log`
|
|
209
|
+
(`os.tmpdir()` — **not** `/tmp`).
|
|
210
|
+
- Use `-v Eddy|Flo|Tingting` to force a specific voice (`say -v '?'` lists them).
|
|
211
|
+
- `say` has no volume flag — volume follows the system output volume.
|
|
212
|
+
|
|
213
|
+
#### Test the engine alone (no DSH needed)
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
curl -sfL -o ~/speak.sh "https://cdn.jsdelivr.net/gh/Alan2Z/dsh-speak@main/engine/speak.sh"
|
|
217
|
+
chmod +x ~/speak.sh
|
|
218
|
+
~/speak.sh -t "你好,Mac 版语音播报测试"
|
|
219
|
+
~/speak.sh -t "测试" -v Eddy -r 200 # explicit voice + rate
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Claude Code
|
|
223
|
+
|
|
224
|
+
Register the Stop hook in `~/.claude/settings.json`:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"hooks": {
|
|
229
|
+
"Stop": [
|
|
230
|
+
{
|
|
231
|
+
"hooks": [
|
|
232
|
+
{
|
|
233
|
+
"type": "command",
|
|
234
|
+
"command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\\path\\to\\dsh-speak\\adapters\\claude-code\\stop-hook.ps1"
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Any other harness
|
|
244
|
+
|
|
245
|
+
Call the engine directly from your agent / wrapper / script:
|
|
246
|
+
|
|
247
|
+
```powershell
|
|
248
|
+
# announce a one-liner
|
|
249
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File engine\speak.ps1 -Text "构建完成"
|
|
250
|
+
|
|
251
|
+
# announce a long summary (blocking, returns when done)
|
|
252
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File engine\speech-summary.ps1 -Text "…"
|
|
253
|
+
|
|
254
|
+
# ask for user attention (blocking, for prompts/approvals)
|
|
255
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File engine\speech-prompt.ps1 -Text "请做出选择"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Configuration
|
|
259
|
+
|
|
260
|
+
### Engine parameters
|
|
261
|
+
|
|
262
|
+
See [docs/DESIGN.md §5 configuration reference](docs/DESIGN.md#5-configuration-reference):
|
|
263
|
+
|
|
264
|
+
```powershell
|
|
265
|
+
speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次播报内容较长,请自行阅读。"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### DSH plugin config
|
|
269
|
+
|
|
270
|
+
**Either way works, and they stay in sync** (both write the same settings
|
|
271
|
+
document):
|
|
272
|
+
|
|
273
|
+
1. **Web UI (1.7.0, recommended)**: a dedicated Settings → dsh-speak settings
|
|
274
|
+
page. Every option is editable and saved there (visible in `dsh --dump-config`,
|
|
275
|
+
per-profile, survives npm updates).
|
|
276
|
+
2. **Profile patch `config` block** (equivalent):
|
|
277
|
+
|
|
278
|
+
```yaml
|
|
279
|
+
# ~/.dsh/profiles/web/cordis.patch.yml
|
|
280
|
+
- insert:
|
|
281
|
+
- id: speech-hook
|
|
282
|
+
name: 'dsh-speak'
|
|
283
|
+
config:
|
|
284
|
+
enabled: true # master switch: false silences everything
|
|
285
|
+
automaticSpeech: true # auto-speak final replies
|
|
286
|
+
queueAllMessages: false # true = enqueue every assistant message as it arrives
|
|
287
|
+
replayFullRead: false # true = manual replay skips the long-text truncation, reads everything
|
|
288
|
+
cleanMarkdownFormatting: true # convert Markdown to natural speech
|
|
289
|
+
readInlineCode: true # read inline code without backticks
|
|
290
|
+
codeBlocks: smart # all | smart | replace (fenced code blocks)
|
|
291
|
+
codeBlockMaxChars: 300 # smart-mode code block character limit
|
|
292
|
+
codeBlockReplacementText: 'You can see the code in our history.' # replace-mode text
|
|
293
|
+
throttleMs: 1500 # merge delay before announcing (ms)
|
|
294
|
+
engine: '' # engine path override; '' = auto-resolve
|
|
295
|
+
announceApprovals: true # speak approval requests
|
|
296
|
+
announceQuestions: true # speak ask_user_question content
|
|
297
|
+
stripApprovalPrefix: true # strip "escalate sandbox to ...: " prefix
|
|
298
|
+
questionGapMs: 2000 # pause between multiple question announcements (ms)
|
|
299
|
+
longTextMode: message # message | heading (speak largest md heading)
|
|
300
|
+
longTextMessage: '本次播报内容较长,请自行阅读。' # fixed prompt for message mode
|
|
301
|
+
maxChars: 300 # per-utterance ceiling (macOS default 0 = unlimited)
|
|
302
|
+
volume: 50 # Windows only
|
|
303
|
+
rate: 0 # 0 = engine default (Windows SAPI scale / macOS wpm)
|
|
304
|
+
# —— optional event announcements (1.6.0, all off by default) ——
|
|
305
|
+
announceTurnEnd: false # turn/end — "第 N 轮对话完成"
|
|
306
|
+
announceCommandDone: false # command/done — command finished/failed
|
|
307
|
+
announceGoalChange: false # goal/change — goal created/updated/completed
|
|
308
|
+
announceToolErrors: false # tool/result error — announce (english dropped)
|
|
309
|
+
announceTodoWrite: false # todo/write — todo list updated
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
> Resolution order: schema default → patch `config` → UI user settings. Fields
|
|
313
|
+
> written in YAML show up in the UI too. Platform note: `maxChars` defaults to
|
|
314
|
+
> 0 on macOS (`say` has no ceiling) and 300 on Windows (SAPI safe limit).
|
|
315
|
+
|
|
316
|
+
#### Option reference
|
|
317
|
+
|
|
318
|
+
| option | default | effect |
|
|
319
|
+
| ------ | ------- | ------ |
|
|
320
|
+
| `enabled` | `true` | **master switch**: when off, nothing is ever announced (final reply / approvals / questions / optional events / replay) |
|
|
321
|
+
| `automaticSpeech` | `true` | auto-speak final replies; manual replay always remains available |
|
|
322
|
+
| `queueAllMessages` | `false` | `true` enqueues every assistant message as it arrives (intermediate messages spoken too, FIFO); default only speaks the throttled final reply |
|
|
323
|
+
| `replayFullRead` | `false` | `true` makes manual replay skip the long-text heading truncation (`longTextMode: heading`) and read everything in chunks |
|
|
324
|
+
| `cleanMarkdownFormatting` | `true` | converts Markdown into natural speech text (link labels kept, URLs/heading/emphasis cleaned) |
|
|
325
|
+
| `readInlineCode` | `true` | read inline code without backtick markers |
|
|
326
|
+
| `codeBlocks` | `smart` | fenced code blocks: `all` read / `smart` (read when ≤ `codeBlockMaxChars`) / `replace` with the replacement text |
|
|
327
|
+
| `codeBlockMaxChars` | `300` | code block character limit for `smart` mode |
|
|
328
|
+
| `codeBlockReplacementText` | `You can see the code in our history.` | replacement spoken in `replace` mode (or over-limit `smart`) |
|
|
329
|
+
| `throttleMs` | `1500` | how long a reply's text waits before being announced (merges multi-step messages) |
|
|
330
|
+
| `engine` | `''` | explicit engine script path; `''` auto-resolves: `<package>/engine/<platform>` → `~/.dsh/hooks/<platform>` |
|
|
331
|
+
| `announceApprovals` | `true` | announce `approval/asked` events (reason, or the fixed prompt) |
|
|
332
|
+
| `announceQuestions` | `true` | announce `ask_user_question`: each question spoken separately with a "问题N" prefix (when several) and "选项N" prefixes matching the UI numbering; a `questionGapMs` pause between questions |
|
|
333
|
+
| `questionGapMs` | `2000` | pause between multiple question announcements (ms); 0 = no pause |
|
|
334
|
+
| `stripApprovalPrefix` | `true` | strip the fixed English template prefix (`escalate sandbox to danger-full-access: `) from approval reasons, keeping the human explanation |
|
|
335
|
+
| `longTextMode` | `message` | `message` = fixed prompt for over-long text; `heading` = speak the largest markdown heading instead (see below) |
|
|
336
|
+
| `longTextMessage` | `本次播报内容较长,请自行阅读。` | the fixed prompt spoken for over-long text in `message` mode (editable in the UI) |
|
|
337
|
+
| `maxChars` | platform | per-utterance ceiling. **macOS default 0 (`say` has no ceiling); Windows default 300** (SAPI fails silently beyond ~375-470) |
|
|
338
|
+
| `volume` | `50` | Windows only (0-100); macOS volume follows the system |
|
|
339
|
+
| `rate` | `0` | speech rate: Windows SAPI scale (-10 to 10, 0 = normal; try 1-3 for faster); macOS words-per-minute (default 175, 200 is a bit faster) |
|
|
340
|
+
| `announceTurnEnd` | `false` | announce "第 N 轮对话完成/中断/异常结束" on turn end (`turn/end`) |
|
|
341
|
+
| `announceCommandDone` | `false` | announce when a command finishes or fails (`command/done`) |
|
|
342
|
+
| `announceGoalChange` | `false` | announce goal created/updated/completed/paused/resumed (`goal/change`, objective head) |
|
|
343
|
+
| `announceToolErrors` | `false` | announce "工具调用出错" when a tool call returns an error: `tool/result` carrying `error` (structured failure identity) or a result block with `isError === true`. A **non-zero shell exit does NOT count** — pwsh/bash report `exit code: N` as result data by design, so only infrastructure failures (spawn errors, aborts) and structured tool failures (e.g. fs) set `isError` (English details / technical codes dropped, Chinese details kept) |
|
|
344
|
+
| `announceTodoWrite` | `false` | announce "待办已更新:n/m 完成" when the agent updates its todos (`todo/write`) |
|
|
345
|
+
|
|
346
|
+
#### Long-text modes
|
|
347
|
+
|
|
348
|
+
When cleaned text exceeds `maxChars`:
|
|
349
|
+
|
|
350
|
+
- **`message`** (default): speak `longTextMessage` (`本次播报内容较长,请自行阅读。`,
|
|
351
|
+
editable in the UI or YAML).
|
|
352
|
+
- **`heading`**: pick the *largest* markdown heading in the raw text — fewest `#`
|
|
353
|
+
wins, tie → first. When there is **no heading at all**, speak a coherent opening
|
|
354
|
+
instead of just the first line: the leading `maxChars` window, trimmed back to
|
|
355
|
+
its last sentence end, and kept whole when that would drop more than half the
|
|
356
|
+
window. Sentence ends are recognised bilingually: full-width `。!?;` and `…`
|
|
357
|
+
always count, while half-width `.!?;` only count when followed by whitespace, a
|
|
358
|
+
closing quote/bracket, or (for the very last character) one read past the window —
|
|
359
|
+
so an English `period + space` at the edge still lands, but a decimal point such
|
|
360
|
+
as `Version 0.1.` does not. (Before 1.8.0 this fallback spoke the first non-empty
|
|
361
|
+
line only, which sounded like the narration stopped after line 1.) The chosen
|
|
362
|
+
candidate is still cleaned and subject to the `maxChars` ceiling, falling back to
|
|
363
|
+
the message if it is itself too long.
|
|
364
|
+
|
|
365
|
+
Full architecture and design rationale: [docs/DESIGN.md](docs/DESIGN.md).
|
|
366
|
+
|
|
367
|
+
## Customizing (survives npm updates)
|
|
368
|
+
|
|
369
|
+
You can tune behavior without forking, and your changes **survive `npm update`**:
|
|
370
|
+
|
|
371
|
+
1. **Copy the engine out and edit it** (recommended — this is where defaults live: volume,
|
|
372
|
+
rate, `MaxChars`, `LongTextMessage`, voice logic):
|
|
373
|
+
|
|
374
|
+
```powershell
|
|
375
|
+
# Windows
|
|
376
|
+
Copy-Item "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-speak\engine\speak.ps1" "$env:USERPROFILE\.dsh\hooks\my-speak.ps1"
|
|
377
|
+
# macOS
|
|
378
|
+
cp ~/.dsh/profiles/web/node_modules/dsh-speak/engine/speak.sh ~/.dsh/hooks/my-speak.sh
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Then point the plugin at your copy in the `config` block:
|
|
382
|
+
|
|
383
|
+
> **Windows: keep the file's UTF-8 BOM.** `speak.ps1` is a UTF-8 script and Windows
|
|
384
|
+
> PowerShell 5.1 only knows that from the 3-byte BOM (`EF BB BF`) at the start; an
|
|
385
|
+
> editor that saves it without one makes the system ANSI code page decode it instead,
|
|
386
|
+
> and Chinese text inside the script turns to mojibake — the symptom is **silence or
|
|
387
|
+
> wrong trimming, with no error**. The shipped script keeps all of its *logic* ASCII-only
|
|
388
|
+
> for that reason, so a lost BOM only garbles the Chinese comments and the default
|
|
389
|
+
> prompt. After editing, check with
|
|
390
|
+
> `Get-Content -Encoding Byte -TotalCount 3 your-speak.ps1` (expect `239 187 191`), or
|
|
391
|
+
> run `node scripts/test-engine-static.js`.
|
|
392
|
+
|
|
393
|
+
```yaml
|
|
394
|
+
- insert:
|
|
395
|
+
- id: speech-hook
|
|
396
|
+
name: 'dsh-speak'
|
|
397
|
+
config:
|
|
398
|
+
engine: 'C:/Users/<you>/.dsh/hooks/my-speak.ps1' # or ~/.dsh/hooks/my-speak.sh on macOS
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
The plugin resolves the engine as `config.engine` → package engine → `~/.dsh/hooks/`,
|
|
402
|
+
so your copy wins. `npm update` only touches the package — your engine stays.
|
|
403
|
+
|
|
404
|
+
2. **Edit the file inside `node_modules`** — works, but the next `npm update` overwrites it.
|
|
405
|
+
|
|
406
|
+
3. **Fork the repo** — full control, publish your own package if you want.
|
|
407
|
+
|
|
408
|
+
## Troubleshooting
|
|
409
|
+
|
|
410
|
+
| symptom | cause | fix |
|
|
411
|
+
| ------- | ----- | --- |
|
|
412
|
+
| No sound at all, no error | no natural voice enabled/installed | Win11: enable a natural voice in *Settings → Narrator / Speech*; Win10: install NaturalVoiceSAPIAdapter + a voice pack. Test `speak.ps1` directly |
|
|
413
|
+
| Long replies never spoken | adapter per-`Speak` character ceiling | already guarded at 300 chars — lower `-MaxChars` if needed |
|
|
414
|
+
| Narration stops after the first line | with `longTextMode: heading`, text over `maxChars` and no markdown heading made the engine speak only the first non-empty line (pre-1.8.0) | fixed in 1.8.0 (speaks a coherent opening instead); to change the policy use `message` mode or raise `maxChars` |
|
|
415
|
+
| `工具调用出错:Error: cannot read …` spoken | the "is this Chinese?" detail filter only checked for the presence of a CJK character, so a Chinese directory name inside an English error passed it (1.8.0 regression) | fixed in 1.8.0 — the detail now needs more Chinese characters than Latin letters |
|
|
416
|
+
| Emoji-heavy text silent | SAPI fails silently on emoji | already stripped by the engine |
|
|
417
|
+
| Plugin not loading | raw Windows path as plugin name | use the `file:///C:/…` URL form (installer does this) |
|
|
418
|
+
| macOS: voice suddenly became "婷婷" | opening the "Spoken Content / Siri Voice" pane drifted the system voice | re-pick via Settings → Accessibility → Spoken Content → System Voice → ⓘ entry |
|
|
419
|
+
| macOS: no log at `/tmp` | `os.tmpdir()` is `/var/folders/.../T`, not `/tmp` | log is at `$TMPDIR/dsh-speech-hook.log` |
|
|
420
|
+
|
|
421
|
+
Plugin diagnostics: Windows `%TEMP%\dsh-speech-hook.log`; macOS `$TMPDIR/dsh-speech-hook.log`
|
|
422
|
+
|
|
423
|
+
## Repository layout
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
engine/ harness-agnostic speech engine (PowerShell + SAPI5 / bash + say)
|
|
427
|
+
speak.ps1 / speak.sh clean + speak (the only seam any adapter needs)
|
|
428
|
+
speech-prompt.ps1 blocking short announcement
|
|
429
|
+
speech-summary.ps1 blocking reply-summary announcement
|
|
430
|
+
adapters/
|
|
431
|
+
dsh/ DSH web plugin + one-command installer
|
|
432
|
+
speech-hook.js session-event trigger (throttle/cancel + optional events + FIFO speech queue + WebSocket + settings registration)
|
|
433
|
+
install.ps1 copies + registers + backs up
|
|
434
|
+
claude-code/
|
|
435
|
+
stop-hook.ps1 Claude Code Stop hook trigger
|
|
436
|
+
client/
|
|
437
|
+
client.js DSH browser bundle: turn-tail Speak/Stop button + Settings → dsh-speak settings page
|
|
438
|
+
docs/
|
|
439
|
+
DESIGN.md full design rationale, pitfalls, extension guide
|
|
440
|
+
scripts/ tests + manual dev helpers (not shipped in the npm package)
|
|
441
|
+
test-engine-static.js engine invariants: .ps1 BOM + PowerShell parse, .sh LF (also run by prepublishOnly)
|
|
442
|
+
test-engine-longtext.js long-text guard contract for BOTH engines (speak.ps1 -DryRun / speak.sh's perl)
|
|
443
|
+
test-speech-hook.js host plugin: event triggers, queue, tool-error detail filter
|
|
444
|
+
test-client-bundle.js browser bundle: slot registration + component rendering
|
|
445
|
+
test-settings-integration.js settings-service wiring + removed-API guard
|
|
446
|
+
session-log-dump.js read a DSH session log (manual: what text reached the engine)
|
|
447
|
+
settings-ui-check.py Playwright UI check (manual: needs a running, authenticated dsh)
|
|
448
|
+
dsh-events-check.py Playwright disclosure check (manual)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Writing a new adapter
|
|
452
|
+
|
|
453
|
+
Three reference patterns exist: **event-stream** (DSH), **stop-hook** (Claude Code),
|
|
454
|
+
**agent-called** (`speech-summary.ps1` from a shell). In every case the adapter only
|
|
455
|
+
needs to: capture the *final reply text* → invoke the engine. See
|
|
456
|
+
[docs/DESIGN.md §7](docs/DESIGN.md#7-extending).
|
|
457
|
+
|
|
458
|
+
## License
|
|
459
|
+
|
|
460
|
+
MIT — see [LICENSE](LICENSE).
|
|
461
|
+
|
|
462
|
+
[NaturalVoiceSAPIAdapter]: https://github.com/gexgd0419/NaturalVoiceSAPIAdapter
|