dsh-speak 1.4.0 → 1.7.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 CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
8
8
 
9
+ [![npm version](https://img.shields.io/npm/v/dsh-speak)](https://www.npmjs.com/package/dsh-speak)
10
+
9
11
  Let your agent **tell you** when a long task is done — no more staring at the screen.
10
12
 
11
13
  dsh-speak reads the final assistant reply aloud through system speech synthesis —
@@ -15,53 +17,30 @@ on macOS using the built-in `say` (can follow a Siri natural voice). It was buil
15
17
  for [DeepSeek Harness](https://github.com/deepseek-ai/dsh)
16
18
  and is structured so any harness can plug in.
17
19
 
18
- > **Project status**: this project exists only to provide an **already-verified
19
- > solution** for users who want their harness to speak. Barring unexpected
20
- > circumstances, it will not be updated further.
21
-
22
- ## TL;DR — install for DSH
23
-
24
- 1. Install the package into your web profile (pick one):
25
-
26
- ```powershell
27
- dsh plugin --profile web add dsh-speak
28
- # or, without pnpm:
29
- npm install --prefix "$env:USERPROFILE\.dsh\profiles\web" dsh-speak
30
- ```
31
-
32
- On macOS (bash):
33
-
34
- ```bash
35
- npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
36
- ```
37
-
38
- 2. Append to `~/.dsh/profiles/web/cordis.patch.yml`:
39
-
40
- ```yaml
41
- - insert:
42
- - id: speech-hook
43
- name: 'dsh-speak'
44
- ```
45
-
46
- 3. Restart the DSH web app — replies are now announced aloud.
47
-
48
- > **Let your agent do it?** Paste this repo URL
49
- > (`https://github.com/Alan2Z/dsh-speak`) into your DSH session and ask it to
50
- > install the plugin — your agent follows this very README. Approving the
51
- > out-of-workspace writes (`~/.dsh`) is all that's needed.
52
-
53
- ```
54
- harness event (DSH session event / Claude Code Stop hook / anything)
55
-
56
- ▼ adapters/… (harness-specific trigger: filter, throttle, cancel)
57
- ▼ engine/speak.ps1 / speak.sh (harness-agnostic: clean text → SAPI5 / say)
58
- ▼ 🔊 you hear the final reply
59
- ```
60
-
61
20
  ## Features
62
21
 
63
22
  - **Automatic**: DSH web plugin watches the session event stream and announces the
64
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.
65
44
  - **Best-effort**: never throws, never blocks the harness, never breaks a session.
66
45
  - **Natural voices**: Windows prefers natural voices — Windows 11 built-in packs,
67
46
  or voices registered via NaturalVoiceSAPIAdapter on Windows 10 (e.g. Xiaoxiao);
@@ -72,6 +51,20 @@ harness event (DSH session event / Claude Code Stop hook / anything)
72
51
  - **Portable engine**: any process can speak with one line:
73
52
  Windows `powershell -File speak.ps1 -Text "你好"` / macOS `./speak.sh -t "你好"`.
74
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
+
75
68
  ## Prerequisites
76
69
 
77
70
  Windows:
@@ -93,9 +86,9 @@ macOS:
93
86
  - Chinese voices: see the [macOS](#macos) section (incl. the Siri natural-voice
94
87
  picker and its pitfalls).
95
88
 
96
- ## Quick start DSH
89
+ ## Install & quick start
97
90
 
98
- ### Option A npm plugin (recommended)
91
+ ### DSH — Option A: npm plugin (recommended)
99
92
 
100
93
  ```powershell
101
94
  # 1. install the plugin into your web profile (adds dsh-speak to
@@ -117,11 +110,22 @@ dsh plugin --profile web add dsh-speak
117
110
  > ```powershell
118
111
  > npm install --prefix "$env:USERPROFILE\.dsh\profiles\web" dsh-speak
119
112
  > ```
113
+ >
114
+ > On macOS (bash):
115
+ >
116
+ > ```bash
117
+ > npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
118
+ > ```
120
119
 
121
120
  The engine ships inside the package (`node_modules/dsh-speak/engine/`), so no extra
122
121
  copying is needed.
123
122
 
124
- ### Option B file install (no npm needed)
123
+ > **Let your agent do it?** Paste this repo URL
124
+ > (`https://github.com/Alan2Z/dsh-speak`) into your DSH session and ask it to
125
+ > install the plugin — your agent follows this very README. Approving the
126
+ > out-of-workspace writes (`~/.dsh`) is all that's needed.
127
+
128
+ ### DSH — Option B: file install (no npm needed)
125
129
 
126
130
  ```powershell
127
131
  # 1. clone
@@ -145,14 +149,12 @@ What the file installer did:
145
149
  | `adapters/dsh/speech-hook.js` | `%USERPROFILE%\.dsh\profiles\web\plugins\` |
146
150
  | registration entry | appended to `%USERPROFILE%\.dsh\profiles\web\cordis.patch.yml` (backed up first) |
147
151
 
148
- ## macOS
152
+ ### macOS
149
153
 
150
154
  The same adapter runs natively on macOS — the plugin auto-detects the platform and
151
155
  calls `engine/speak.sh` (the built-in `say` command) instead of `speak.ps1`.
152
156
  **Since 1.2.0 the macOS engine ships in the npm package** — no extra software.
153
157
 
154
- ### Install (npm — same as Windows)
155
-
156
158
  ```bash
157
159
  # 1. install into your web profile (no pnpm needed — only `dsh plugin` requires it)
158
160
  npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
@@ -162,13 +164,13 @@ npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
162
164
  # - id: speech-hook
163
165
  # name: 'dsh-speak'
164
166
 
165
- # 3. no restart needed — the patch watcher hot-reloads; pure-text replies are
166
- # announced after ~1.5 s (tool-calling replies are intentionally not announced)
167
+ # 3. no restart needed — the patch watcher hot-reloads; replies are announced
168
+ # after the throttle (~1.5 s); tool-calling replies are announced at turn end
167
169
  ```
168
170
 
169
171
  > With pnpm installed, `dsh plugin --profile web add dsh-speak` works identically.
170
172
 
171
- ### Voices (important — two pitfalls)
173
+ #### Voices (important — two pitfalls)
172
174
 
173
175
  - By default the engine follows the **system reading voice** (*Settings →
174
176
  Accessibility → Spoken Content → System Voice*). On **macOS 26** that picker has
@@ -187,7 +189,7 @@ npm install --prefix "$HOME/.dsh/profiles/web" dsh-speak
187
189
  - Use `-v Eddy|Flo|Tingting` to force a specific voice (`say -v '?'` lists them).
188
190
  - `say` has no volume flag — volume follows the system output volume.
189
191
 
190
- ### Test the engine alone (no DSH needed)
192
+ #### Test the engine alone (no DSH needed)
191
193
 
192
194
  ```bash
193
195
  curl -sfL -o ~/speak.sh "https://cdn.jsdelivr.net/gh/Alan2Z/dsh-speak@main/engine/speak.sh"
@@ -196,7 +198,7 @@ chmod +x ~/speak.sh
196
198
  ~/speak.sh -t "测试" -v Eddy -r 200 # explicit voice + rate
197
199
  ```
198
200
 
199
- ## Quick start — Claude Code
201
+ ### Claude Code
200
202
 
201
203
  Register the Stop hook in `~/.claude/settings.json`:
202
204
 
@@ -217,7 +219,7 @@ Register the Stop hook in `~/.claude/settings.json`:
217
219
  }
218
220
  ```
219
221
 
220
- ## Quick start — any other harness
222
+ ### Any other harness
221
223
 
222
224
  Call the engine directly from your agent / wrapper / script:
223
225
 
@@ -225,7 +227,7 @@ Call the engine directly from your agent / wrapper / script:
225
227
  # announce a one-liner
226
228
  powershell -NoProfile -ExecutionPolicy Bypass -File engine\speak.ps1 -Text "构建完成"
227
229
 
228
- # announce a long summary (from a file)
230
+ # announce a long summary (blocking, returns when done)
229
231
  powershell -NoProfile -ExecutionPolicy Bypass -File engine\speech-summary.ps1 -Text "…"
230
232
 
231
233
  # ask for user attention (blocking, for prompts/approvals)
@@ -234,18 +236,104 @@ powershell -NoProfile -ExecutionPolicy Bypass -File engine\speech-prompt.ps1 -Te
234
236
 
235
237
  ## Configuration
236
238
 
237
- Engine parameters (see [docs/DESIGN.md](docs/DESIGN.md#5-configuration-reference)):
239
+ ### Engine parameters
240
+
241
+ See [docs/DESIGN.md §5 configuration reference](docs/DESIGN.md#5-configuration-reference):
238
242
 
239
243
  ```powershell
240
244
  speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次播报内容较长,请自行阅读。"
241
245
  ```
242
246
 
243
- DSH plugin environment variables:
247
+ ### DSH plugin config
248
+
249
+ **Either way works, and they stay in sync** (both write the same settings
250
+ document):
251
+
252
+ 1. **Web UI (1.7.0, recommended)**: a dedicated Settings → dsh-speak settings
253
+ page. Every option is editable and saved there (visible in `dsh --dump-config`,
254
+ per-profile, survives npm updates).
255
+ 2. **Profile patch `config` block** (equivalent):
256
+
257
+ ```yaml
258
+ # ~/.dsh/profiles/web/cordis.patch.yml
259
+ - insert:
260
+ - id: speech-hook
261
+ name: 'dsh-speak'
262
+ config:
263
+ enabled: true # master switch: false silences everything
264
+ automaticSpeech: true # auto-speak final replies
265
+ queueAllMessages: false # true = enqueue every assistant message as it arrives
266
+ replayFullRead: false # true = manual replay skips the long-text truncation, reads everything
267
+ cleanMarkdownFormatting: true # convert Markdown to natural speech
268
+ readInlineCode: true # read inline code without backticks
269
+ codeBlocks: smart # all | smart | replace (fenced code blocks)
270
+ codeBlockMaxChars: 300 # smart-mode code block character limit
271
+ codeBlockReplacementText: 'You can see the code in our history.' # replace-mode text
272
+ throttleMs: 1500 # merge delay before announcing (ms)
273
+ engine: '' # engine path override; '' = auto-resolve
274
+ announceApprovals: true # speak approval requests
275
+ announceQuestions: true # speak ask_user_question content
276
+ stripApprovalPrefix: true # strip "escalate sandbox to ...: " prefix
277
+ questionGapMs: 2000 # pause between multiple question announcements (ms)
278
+ longTextMode: message # message | heading (speak largest md heading)
279
+ longTextMessage: '本次播报内容较长,请自行阅读。' # fixed prompt for message mode
280
+ maxChars: 300 # per-utterance ceiling (macOS default 0 = unlimited)
281
+ volume: 50 # Windows only
282
+ rate: 0 # 0 = engine default (Windows SAPI scale / macOS wpm)
283
+ # —— optional event announcements (1.6.0, all off by default) ——
284
+ announceTurnEnd: false # turn/end — "第 N 轮对话完成"
285
+ announceCommandDone: false # command/done — command finished/failed
286
+ announceGoalChange: false # goal/change — goal created/updated/completed
287
+ announceToolErrors: false # tool/result error — announce (english dropped)
288
+ announceTodoWrite: false # todo/write — todo list updated
289
+ ```
244
290
 
245
- | var | default | meaning |
246
- | --- | ------- | ------- |
247
- | `DSH_SPEAK_ENGINE` | empty (auto-resolved) | engine path override; otherwise resolved as `<package>/engine/<platform script>` → `~/.dsh/hooks/<platform script>` (Windows `speak.ps1` / macOS `speak.sh`) |
248
- | `DSH_SPEAK_THROTTLE_MS` | `1500` | merge delay before announcing |
291
+ > Resolution order: schema default patch `config` → UI user settings. Fields
292
+ > written in YAML show up in the UI too. Platform note: `maxChars` defaults to
293
+ > 0 on macOS (`say` has no ceiling) and 300 on Windows (SAPI safe limit).
294
+
295
+ #### Option reference
296
+
297
+ | option | default | effect |
298
+ | ------ | ------- | ------ |
299
+ | `enabled` | `true` | **master switch**: when off, nothing is ever announced (final reply / approvals / questions / optional events / replay) |
300
+ | `automaticSpeech` | `true` | auto-speak final replies; manual replay always remains available |
301
+ | `queueAllMessages` | `false` | `true` enqueues every assistant message as it arrives (intermediate messages spoken too, FIFO); default only speaks the throttled final reply |
302
+ | `replayFullRead` | `false` | `true` makes manual replay skip the long-text heading truncation (`longTextMode: heading`) and read everything in chunks |
303
+ | `cleanMarkdownFormatting` | `true` | converts Markdown into natural speech text (link labels kept, URLs/heading/emphasis cleaned) |
304
+ | `readInlineCode` | `true` | read inline code without backtick markers |
305
+ | `codeBlocks` | `smart` | fenced code blocks: `all` read / `smart` (read when ≤ `codeBlockMaxChars`) / `replace` with the replacement text |
306
+ | `codeBlockMaxChars` | `300` | code block character limit for `smart` mode |
307
+ | `codeBlockReplacementText` | `You can see the code in our history.` | replacement spoken in `replace` mode (or over-limit `smart`) |
308
+ | `throttleMs` | `1500` | how long a reply's text waits before being announced (merges multi-step messages) |
309
+ | `engine` | `''` | explicit engine script path; `''` auto-resolves: `<package>/engine/<platform>` → `~/.dsh/hooks/<platform>` |
310
+ | `announceApprovals` | `true` | announce `approval/asked` events (reason, or the fixed prompt) |
311
+ | `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 |
312
+ | `questionGapMs` | `2000` | pause between multiple question announcements (ms); 0 = no pause |
313
+ | `stripApprovalPrefix` | `true` | strip the fixed English template prefix (`escalate sandbox to danger-full-access: `) from approval reasons, keeping the human explanation |
314
+ | `longTextMode` | `message` | `message` = fixed prompt for over-long text; `heading` = speak the largest markdown heading instead (see below) |
315
+ | `longTextMessage` | `本次播报内容较长,请自行阅读。` | the fixed prompt spoken for over-long text in `message` mode (editable in the UI) |
316
+ | `maxChars` | platform | per-utterance ceiling. **macOS default 0 (`say` has no ceiling); Windows default 300** (SAPI fails silently beyond ~375-470) |
317
+ | `volume` | `50` | Windows only (0-100); macOS volume follows the system |
318
+ | `rate` | `0` | `0` = engine default (Windows SAPI scale, e.g. 1; macOS words-per-minute, e.g. 175) |
319
+ | `announceTurnEnd` | `false` | announce "第 N 轮对话完成/中断/异常结束" on turn end (`turn/end`) |
320
+ | `announceCommandDone` | `false` | announce when a command finishes or fails (`command/done`) |
321
+ | `announceGoalChange` | `false` | announce goal created/updated/completed/paused/resumed (`goal/change`, objective head) |
322
+ | `announceToolErrors` | `false` | announce "工具调用出错" when a tool call returns an error (`tool/result` with `error` or an `isError` content block; English details / technical codes dropped, Chinese details kept) |
323
+ | `announceTodoWrite` | `false` | announce "待办已更新:n/m 完成" when the agent updates its todos (`todo/write`) |
324
+
325
+ #### Long-text modes
326
+
327
+ When cleaned text exceeds `maxChars`:
328
+
329
+ - **`message`** (default): speak `longTextMessage` (`本次播报内容较长,请自行阅读。`,
330
+ editable in the UI or YAML).
331
+ - **`heading`**: pick the *largest* markdown heading in the raw text — fewest `#`
332
+ wins, tie → first; if there is no heading line, the first non-empty line is used.
333
+ The chosen candidate is still cleaned and subject to the `maxChars` ceiling,
334
+ falling back to the message if it is itself too long.
335
+
336
+ Full architecture and design rationale: [docs/DESIGN.md](docs/DESIGN.md).
249
337
 
250
338
  ## Customizing (survives npm updates)
251
339
 
@@ -259,20 +347,24 @@ You can tune behavior without forking, and your changes **survive `npm update`**
259
347
  Copy-Item "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-speak\engine\speak.ps1" "$env:USERPROFILE\.dsh\hooks\my-speak.ps1"
260
348
  # macOS
261
349
  cp ~/.dsh/profiles/web/node_modules/dsh-speak/engine/speak.sh ~/.dsh/hooks/my-speak.sh
262
- # edit my-speak.ps1 / my-speak.sh to taste, then point the plugin at it:
263
- setx DSH_SPEAK_ENGINE "$env:USERPROFILE\.dsh\hooks\my-speak.ps1" # Windows
264
- echo 'export DSH_SPEAK_ENGINE=~/.dsh/hooks/my-speak.sh' >> ~/.zshrc # macOS
265
350
  ```
266
351
 
267
- The plugin resolves the engine as `DSH_SPEAK_ENGINE` package engine `~/.dsh/hooks/`,
268
- so your copy wins. `npm update` only touches the package — your engine stays.
352
+ Then point the plugin at your copy in the `config` block:
269
353
 
270
- 2. **Environment variables** (no code changes): `DSH_SPEAK_ENGINE` (engine path),
271
- `DSH_SPEAK_THROTTLE_MS` (announcement merge delay).
354
+ ```yaml
355
+ - insert:
356
+ - id: speech-hook
357
+ name: 'dsh-speak'
358
+ config:
359
+ engine: 'C:/Users/<you>/.dsh/hooks/my-speak.ps1' # or ~/.dsh/hooks/my-speak.sh on macOS
360
+ ```
361
+
362
+ The plugin resolves the engine as `config.engine` → package engine → `~/.dsh/hooks/`,
363
+ so your copy wins. `npm update` only touches the package — your engine stays.
272
364
 
273
- 3. **Edit the file inside `node_modules`** — works, but the next `npm update` overwrites it.
365
+ 2. **Edit the file inside `node_modules`** — works, but the next `npm update` overwrites it.
274
366
 
275
- 4. **Fork the repo** — full control, publish your own package if you want.
367
+ 3. **Fork the repo** — full control, publish your own package if you want.
276
368
 
277
369
  ## Troubleshooting
278
370
 
@@ -296,10 +388,12 @@ engine/ harness-agnostic speech engine (PowerShell + SAPI5 / ba
296
388
  speech-summary.ps1 blocking reply-summary announcement
297
389
  adapters/
298
390
  dsh/ DSH web plugin + one-command installer
299
- speech-hook.js session-event trigger (throttle + tool-call cancel)
391
+ speech-hook.js session-event trigger (throttle/cancel + optional events + FIFO speech queue + WebSocket + settings registration)
300
392
  install.ps1 copies + registers + backs up
301
393
  claude-code/
302
394
  stop-hook.ps1 Claude Code Stop hook trigger
395
+ client/
396
+ client.js DSH browser bundle: turn-tail Speak/Stop button + Settings → dsh-speak settings page
303
397
  docs/
304
398
  DESIGN.md full design rationale, pitfalls, extension guide
305
399
  ```