dsh-speak 1.4.0 → 1.5.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 +40 -14
- package/README.zh-CN.md +38 -13
- package/adapters/dsh/speech-hook.js +81 -31
- package/docs/CUSTOMIZATION.md +89 -0
- package/docs/CUSTOMIZATION.zh-CN.md +88 -0
- package/docs/DESIGN.md +17 -6
- package/docs/DESIGN.zh-CN.md +17 -6
- package/engine/speak.ps1 +27 -2
- package/engine/speak.sh +25 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://awesome-dsh-plugin.com)
|
|
8
8
|
|
|
9
|
+
[](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 —
|
|
@@ -62,6 +64,11 @@ harness event (DSH session event / Claude Code Stop hook / anything)
|
|
|
62
64
|
|
|
63
65
|
- **Automatic**: DSH web plugin watches the session event stream and announces the
|
|
64
66
|
final reply (skips reasoning/tool-call narration, merges multi-step messages).
|
|
67
|
+
- **Gets your attention**: announces approval requests (hears "需要你的审批" when
|
|
68
|
+
the agent is waiting on you) and questions the agent asks via `ask_user_question`.
|
|
69
|
+
- **Bundle auto-registration** (1.3.0): declare the package in `dsh.profile.bundles`
|
|
70
|
+
and the plugin registers itself via the bundled `cordis.patch.yml` — no manual
|
|
71
|
+
patch entry needed.
|
|
65
72
|
- **Best-effort**: never throws, never blocks the harness, never breaks a session.
|
|
66
73
|
- **Natural voices**: Windows prefers natural voices — Windows 11 built-in packs,
|
|
67
74
|
or voices registered via NaturalVoiceSAPIAdapter on Windows 10 (e.g. Xiaoxiao);
|
|
@@ -240,12 +247,27 @@ Engine parameters (see [docs/DESIGN.md](docs/DESIGN.md#5-configuration-reference
|
|
|
240
247
|
speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次播报内容较长,请自行阅读。"
|
|
241
248
|
```
|
|
242
249
|
|
|
243
|
-
DSH plugin
|
|
250
|
+
DSH plugin configuration — **prefer the profile patch `config` block** (visible in
|
|
251
|
+
`dsh --dump-config`, per-profile, survives npm updates):
|
|
252
|
+
|
|
253
|
+
```yaml
|
|
254
|
+
# ~/.dsh/profiles/web/cordis.patch.yml
|
|
255
|
+
- insert:
|
|
256
|
+
- id: speech-hook
|
|
257
|
+
name: 'dsh-speak'
|
|
258
|
+
config:
|
|
259
|
+
throttleMs: 1500 # merge delay before announcing (ms)
|
|
260
|
+
engine: '' # engine path override; '' = auto-resolve
|
|
261
|
+
announceApprovals: true # speak approval requests
|
|
262
|
+
announceQuestions: true # speak ask_user_question content
|
|
263
|
+
stripApprovalPrefix: true # strip the "escalate sandbox to ...: " prefix
|
|
264
|
+
longTextMode: message # message | heading (speak largest md heading)
|
|
265
|
+
maxChars: 300 # engine per-utterance ceiling
|
|
266
|
+
volume: 50 # Windows only
|
|
267
|
+
rate: 0 # 0 = engine default (Windows SAPI scale / macOS wpm)
|
|
268
|
+
```
|
|
244
269
|
|
|
245
|
-
|
|
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 |
|
|
270
|
+
See [docs/CUSTOMIZATION.md](docs/CUSTOMIZATION.md) for the full customization guide.
|
|
249
271
|
|
|
250
272
|
## Customizing (survives npm updates)
|
|
251
273
|
|
|
@@ -259,20 +281,24 @@ You can tune behavior without forking, and your changes **survive `npm update`**
|
|
|
259
281
|
Copy-Item "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-speak\engine\speak.ps1" "$env:USERPROFILE\.dsh\hooks\my-speak.ps1"
|
|
260
282
|
# macOS
|
|
261
283
|
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
284
|
```
|
|
266
285
|
|
|
267
|
-
|
|
268
|
-
|
|
286
|
+
Then point the plugin at your copy in the `config` block:
|
|
287
|
+
|
|
288
|
+
```yaml
|
|
289
|
+
- insert:
|
|
290
|
+
- id: speech-hook
|
|
291
|
+
name: 'dsh-speak'
|
|
292
|
+
config:
|
|
293
|
+
engine: 'C:/Users/<you>/.dsh/hooks/my-speak.ps1' # or ~/.dsh/hooks/my-speak.sh on macOS
|
|
294
|
+
```
|
|
269
295
|
|
|
270
|
-
|
|
271
|
-
`
|
|
296
|
+
The plugin resolves the engine as `config.engine` → package engine → `~/.dsh/hooks/`,
|
|
297
|
+
so your copy wins. `npm update` only touches the package — your engine stays.
|
|
272
298
|
|
|
273
|
-
|
|
299
|
+
2. **Edit the file inside `node_modules`** — works, but the next `npm update` overwrites it.
|
|
274
300
|
|
|
275
|
-
|
|
301
|
+
3. **Fork the repo** — full control, publish your own package if you want.
|
|
276
302
|
|
|
277
303
|
## Troubleshooting
|
|
278
304
|
|
package/README.zh-CN.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://awesome-dsh-plugin.com)
|
|
6
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/dsh-speak)
|
|
8
|
+
|
|
7
9
|
让 Agent 在长任务完成时**开口告诉你**——不用再盯着屏幕等。
|
|
8
10
|
|
|
9
11
|
dsh-speak 通过系统语音合成把 Agent 的最终回复朗读出来——Windows 上优先使用自然
|
|
@@ -58,6 +60,10 @@ harness 事件(DSH 会话事件 / Claude Code Stop hook / 任意方式)
|
|
|
58
60
|
|
|
59
61
|
- **全自动**:DSH web 插件监听会话事件流,自动播报最终回复
|
|
60
62
|
(跳过 reasoning/工具调用旁白,合并同一回复的多步消息)。
|
|
63
|
+
- **提醒你**:审批请求(Agent 等你操作时会播"需要你的审批")和 Agent 通过
|
|
64
|
+
`ask_user_question` 提出的问题都会播报。
|
|
65
|
+
- **Bundle 自动注册**(1.3.0):把包声明进 `dsh.profile.bundles`,插件通过包内
|
|
66
|
+
自带的 `cordis.patch.yml` 自动注册,无需手动写 patch 条目。
|
|
61
67
|
- **尽力而为**:绝不抛错、绝不阻塞 harness、绝不破坏会话。
|
|
62
68
|
- **自然语音**:Windows 优先使用自然语音——Windows 11 内置语音包,或 Windows 10
|
|
63
69
|
上经 NaturalVoiceSAPIAdapter 注册的语音(如晓晓);macOS 使用系统朗读声音
|
|
@@ -225,12 +231,26 @@ powershell -NoProfile -ExecutionPolicy Bypass -File engine\speech-prompt.ps1 -Te
|
|
|
225
231
|
speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次播报内容较长,请自行阅读。"
|
|
226
232
|
```
|
|
227
233
|
|
|
228
|
-
DSH
|
|
234
|
+
DSH 插件配置——**优先用 profile patch 的 `config` 块**(`dsh --dump-config` 可见、按 profile 隔离、升级不丢):
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
# ~/.dsh/profiles/web/cordis.patch.yml
|
|
238
|
+
- insert:
|
|
239
|
+
- id: speech-hook
|
|
240
|
+
name: 'dsh-speak'
|
|
241
|
+
config:
|
|
242
|
+
throttleMs: 1500 # 播报前的合并延迟(毫秒)
|
|
243
|
+
engine: '' # 引擎路径覆盖;'' = 自动解析
|
|
244
|
+
announceApprovals: true # 播报审批请求
|
|
245
|
+
announceQuestions: true # 播报 ask_user_question 提问内容
|
|
246
|
+
stripApprovalPrefix: true # 剥离审批原因里的 "escalate sandbox to ...: " 前缀
|
|
247
|
+
longTextMode: message # message | heading(念最大字号 markdown 标题)
|
|
248
|
+
maxChars: 300 # 引擎单次朗读字数上限
|
|
249
|
+
volume: 50 # 仅 Windows
|
|
250
|
+
rate: 0 # 0 = 引擎默认(Windows SAPI 刻度 / macOS wpm)
|
|
251
|
+
```
|
|
229
252
|
|
|
230
|
-
|
|
231
|
-
| --- | ------- | ---- |
|
|
232
|
-
| `DSH_SPEAK_ENGINE` | 空(自动解析) | 引擎路径覆盖;否则按"包内 `engine/<平台脚本>` → `~/.dsh/hooks/<平台脚本>`"顺序解析(Windows `speak.ps1` / macOS `speak.sh`) |
|
|
233
|
-
| `DSH_SPEAK_THROTTLE_MS` | `1500` | 播报前的合并延迟(毫秒) |
|
|
253
|
+
完整自定义指南见 [docs/CUSTOMIZATION.md](docs/CUSTOMIZATION.zh-CN.md)。
|
|
234
254
|
|
|
235
255
|
## 自定义(升级不丢)
|
|
236
256
|
|
|
@@ -243,19 +263,24 @@ DSH 插件环境变量:
|
|
|
243
263
|
Copy-Item "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-speak\engine\speak.ps1" "$env:USERPROFILE\.dsh\hooks\my-speak.ps1"
|
|
244
264
|
# macOS
|
|
245
265
|
cp ~/.dsh/profiles/web/node_modules/dsh-speak/engine/speak.sh ~/.dsh/hooks/my-speak.sh
|
|
246
|
-
# 按喜好编辑 my-speak.ps1 / my-speak.sh,然后让插件指向它:
|
|
247
|
-
setx DSH_SPEAK_ENGINE "$env:USERPROFILE\.dsh\hooks\my-speak.ps1" # Windows
|
|
248
|
-
echo 'export DSH_SPEAK_ENGINE=~/.dsh/hooks/my-speak.sh' >> ~/.zshrc # macOS
|
|
249
266
|
```
|
|
250
267
|
|
|
251
|
-
|
|
252
|
-
|
|
268
|
+
然后在 config 块里指向你的副本:
|
|
269
|
+
|
|
270
|
+
```yaml
|
|
271
|
+
- insert:
|
|
272
|
+
- id: speech-hook
|
|
273
|
+
name: 'dsh-speak'
|
|
274
|
+
config:
|
|
275
|
+
engine: 'C:/Users/<你>/.dsh/hooks/my-speak.ps1' # macOS 用 ~/.dsh/hooks/my-speak.sh
|
|
276
|
+
```
|
|
253
277
|
|
|
254
|
-
|
|
278
|
+
插件按 `config.engine` → 包内引擎 → `~/.dsh/hooks/` 的顺序解析引擎,所以你的副本
|
|
279
|
+
优先生效;`npm update` 只动包本身,你的引擎安然无恙。
|
|
255
280
|
|
|
256
|
-
|
|
281
|
+
2. **直接改 `node_modules` 里的文件**——能改,但下次 `npm update` 会被覆盖。
|
|
257
282
|
|
|
258
|
-
|
|
283
|
+
3. **fork 仓库**——完全掌控,想发自己的包也行。
|
|
259
284
|
|
|
260
285
|
## 排障
|
|
261
286
|
|
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
// Trigger semantics:
|
|
9
9
|
// * only events with a `text` block are announced (reasoning / tool_use blocks
|
|
10
10
|
// are skipped)
|
|
11
|
-
// *
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// prompt) since approvals are time-sensitive
|
|
11
|
+
// * a tool/call to `ask_user_question` announces the parsed question
|
|
12
|
+
// (title + single/multi + options); other tool calls cancel the pending
|
|
13
|
+
// announcement (that round's assistant text is process narration)
|
|
14
|
+
// * `approval/asked` is announced immediately (reason with the fixed English
|
|
15
|
+
// template prefix stripped, or a fixed prompt)
|
|
17
16
|
// * a final reply with no following tool/call is announced after a throttle
|
|
18
17
|
// delay (merges multi-step messages from the same reply)
|
|
19
18
|
//
|
|
@@ -24,11 +23,20 @@
|
|
|
24
23
|
// name: 'file:///C:/Users/<your-username>/.../speech-hook.js' # repo/file install (replace <your-username>)
|
|
25
24
|
// (run adapters/dsh/install.ps1 to do this automatically for the file install)
|
|
26
25
|
//
|
|
27
|
-
// Configuration (
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
26
|
+
// Configuration — prefer the profile patch `config` block (see docs/CUSTOMIZATION.md):
|
|
27
|
+
// - insert:
|
|
28
|
+
// - id: speech-hook
|
|
29
|
+
// name: 'dsh-speak'
|
|
30
|
+
// config:
|
|
31
|
+
// throttleMs: 1500 # merge delay before announcing (ms)
|
|
32
|
+
// engine: '' # engine path override; '' = auto-resolve
|
|
33
|
+
// announceApprovals: true # speak approval requests
|
|
34
|
+
// announceQuestions: true # speak ask_user_question content
|
|
35
|
+
// stripApprovalPrefix: true # strip "escalate sandbox to ...: " prefix
|
|
36
|
+
// longTextMode: message # message | heading (speak largest md heading)
|
|
37
|
+
// maxChars: 300 # engine per-utterance ceiling
|
|
38
|
+
// volume: 50 # Windows only
|
|
39
|
+
// rate: 0 # 0 = engine default (Windows SAPI scale / macOS wpm)
|
|
32
40
|
'use strict'
|
|
33
41
|
const { spawn } = require('child_process')
|
|
34
42
|
const fs = require('fs')
|
|
@@ -43,27 +51,40 @@ function log(...args) {
|
|
|
43
51
|
} catch (e) { /* ignore */ }
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
const THROTTLE_MS = Number(process.env.DSH_SPEAK_THROTTLE_MS) || 1500
|
|
47
54
|
const ENGINE_NAME = process.platform === 'darwin' ? 'speak.sh' : 'speak.ps1'
|
|
48
55
|
|
|
49
56
|
/**
|
|
50
57
|
* Locate the engine script:
|
|
51
|
-
* 1. explicit
|
|
58
|
+
* 1. explicit override (config `engine`)
|
|
52
59
|
* 2. <this package>/engine/<speak.ps1|speak.sh> — works both when running from
|
|
53
60
|
* a repo checkout and when installed into a profile's node_modules
|
|
54
61
|
* 3. legacy file-copy location (~/.dsh/hooks/<speak.ps1|speak.sh>)
|
|
55
62
|
*/
|
|
56
|
-
function resolveEngine() {
|
|
57
|
-
if (
|
|
63
|
+
function resolveEngine(override) {
|
|
64
|
+
if (override) return override
|
|
58
65
|
const bundled = path.join(__dirname, '..', '..', 'engine', ENGINE_NAME)
|
|
59
66
|
if (fs.existsSync(bundled)) return bundled
|
|
60
67
|
return path.join(os.homedir(), '.dsh', 'hooks', ENGINE_NAME)
|
|
61
68
|
}
|
|
62
|
-
const SPEAK_ENGINE = resolveEngine()
|
|
63
69
|
|
|
64
70
|
module.exports = {
|
|
65
|
-
apply(ctx) {
|
|
66
|
-
|
|
71
|
+
apply(ctx, config) {
|
|
72
|
+
config = config || {}
|
|
73
|
+
// resolved settings: config > default
|
|
74
|
+
const cfg = {
|
|
75
|
+
throttleMs: Number(config.throttleMs != null ? config.throttleMs : 1500) || 1500,
|
|
76
|
+
engine: resolveEngine(config.engine || ''),
|
|
77
|
+
announceApprovals: config.announceApprovals !== false,
|
|
78
|
+
announceQuestions: config.announceQuestions !== false,
|
|
79
|
+
stripApprovalPrefix: config.stripApprovalPrefix !== false,
|
|
80
|
+
longTextMode: config.longTextMode || 'message',
|
|
81
|
+
maxChars: Number(config.maxChars != null ? config.maxChars : 300) || 300,
|
|
82
|
+
volume: Number(config.volume != null ? config.volume : 50) || 50,
|
|
83
|
+
rate: Number(config.rate != null ? config.rate : 0) || 0,
|
|
84
|
+
}
|
|
85
|
+
log('plugin apply 执行(加载成功); engine=', cfg.engine, '; throttle=', cfg.throttleMs,
|
|
86
|
+
'; longTextMode=', cfg.longTextMode, '; maxChars=', cfg.maxChars)
|
|
87
|
+
|
|
67
88
|
let timer = null
|
|
68
89
|
let pendingText = ''
|
|
69
90
|
|
|
@@ -86,11 +107,18 @@ module.exports = {
|
|
|
86
107
|
let ps
|
|
87
108
|
if (process.platform === 'darwin') {
|
|
88
109
|
// macOS: run the say-based engine through bash
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
const args = ['-f', tmp, '-m', String(cfg.maxChars), '-M', cfg.longTextMode]
|
|
111
|
+
if (cfg.rate > 0) args.push('-r', String(cfg.rate))
|
|
112
|
+
ps = spawn('/bin/bash', [cfg.engine].concat(args), { stdio: 'ignore' })
|
|
113
|
+
log('spawn bash (macOS engine) 已发起:', args.join(' '))
|
|
91
114
|
} else {
|
|
92
115
|
ps = spawn('powershell.exe',
|
|
93
|
-
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
|
|
116
|
+
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', cfg.engine,
|
|
117
|
+
'-File', tmp,
|
|
118
|
+
'-Volume', String(cfg.volume),
|
|
119
|
+
'-Rate', String(cfg.rate > 0 ? cfg.rate : 1),
|
|
120
|
+
'-MaxChars', String(cfg.maxChars),
|
|
121
|
+
'-LongTextMode', cfg.longTextMode],
|
|
94
122
|
{ windowsHide: true, stdio: 'ignore' })
|
|
95
123
|
log('spawn powershell 已发起')
|
|
96
124
|
}
|
|
@@ -105,14 +133,31 @@ module.exports = {
|
|
|
105
133
|
if (type !== 'assistant/chunk') {
|
|
106
134
|
log('事件 type=', type, 'surfaceOp=', event && event.surfaceOp, 'seq=', event && event.seq)
|
|
107
135
|
}
|
|
108
|
-
// tool-call round: a call to ask_user_question
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// announcement (that round's assistant text is process narration)
|
|
136
|
+
// tool-call round: a call to ask_user_question announces the parsed
|
|
137
|
+
// question (title + mode + options); any other tool call cancels the
|
|
138
|
+
// pending announcement (that round's assistant text is narration)
|
|
112
139
|
if (type === 'tool/call') {
|
|
113
140
|
const toolName = event.data && event.data.name
|
|
114
|
-
if (toolName === 'ask_user_question') {
|
|
115
|
-
|
|
141
|
+
if (toolName === 'ask_user_question' && cfg.announceQuestions) {
|
|
142
|
+
let spoken = ''
|
|
143
|
+
try {
|
|
144
|
+
const args = JSON.parse((event.data && event.data.arguments) || '{}')
|
|
145
|
+
const qs = Array.isArray(args.questions) ? args.questions : []
|
|
146
|
+
spoken = qs.map((q) => {
|
|
147
|
+
const mode = q.multi_select ? '多选' : '单选'
|
|
148
|
+
const labels = Array.isArray(q.options)
|
|
149
|
+
? q.options.map((o) => o.label).filter(Boolean).join('、')
|
|
150
|
+
: ''
|
|
151
|
+
return (q.question || '') + '(' + mode + ')' + (labels ? ',选项:' + labels : '')
|
|
152
|
+
}).filter(Boolean).join(';')
|
|
153
|
+
} catch (e) { /* arguments 解析失败则回退原逻辑 */ }
|
|
154
|
+
if (spoken) {
|
|
155
|
+
cancelPending()
|
|
156
|
+
log('提问播报:', spoken.slice(0, 120))
|
|
157
|
+
speak(spoken)
|
|
158
|
+
} else {
|
|
159
|
+
log('提问工具调用(ask_user_question)— 保留待播报文本')
|
|
160
|
+
}
|
|
116
161
|
return
|
|
117
162
|
}
|
|
118
163
|
cancelPending()
|
|
@@ -120,10 +165,15 @@ module.exports = {
|
|
|
120
165
|
}
|
|
121
166
|
// approval requested: announce it right away (time-sensitive), using
|
|
122
167
|
// the approval reason if present
|
|
123
|
-
if (type === 'approval/asked') {
|
|
168
|
+
if (type === 'approval/asked' && cfg.announceApprovals) {
|
|
124
169
|
cancelPending()
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
let reason = (event.data && event.data.reason) || ''
|
|
171
|
+
if (cfg.stripApprovalPrefix) {
|
|
172
|
+
// strip the fixed English template prefix (e.g. "escalate sandbox
|
|
173
|
+
// to danger-full-access: "), keep the human explanation
|
|
174
|
+
reason = reason.replace(/^escalate sandbox to danger-full-access\s*:\s*/i, '').trim()
|
|
175
|
+
}
|
|
176
|
+
const text = reason || '需要你的审批,请查看界面。'
|
|
127
177
|
log('审批请求,播报:', text.slice(0, 60))
|
|
128
178
|
speak(text)
|
|
129
179
|
return
|
|
@@ -154,7 +204,7 @@ module.exports = {
|
|
|
154
204
|
speak(pendingText)
|
|
155
205
|
pendingText = ''
|
|
156
206
|
timer = null
|
|
157
|
-
},
|
|
207
|
+
}, cfg.throttleMs)
|
|
158
208
|
} catch (e) {
|
|
159
209
|
log('事件处理异常:', e.message)
|
|
160
210
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# CUSTOMIZATION.md — customizing dsh-speak
|
|
2
|
+
|
|
3
|
+
(中文版:docs/CUSTOMIZATION.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
dsh-speak is deliberately small, but it exposes three levels of customization:
|
|
6
|
+
**configuration** (no code), **engine override** (copy & edit), and **extension**
|
|
7
|
+
(new backends / adapters). Everything below survives `npm update`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Configuration (recommended)
|
|
12
|
+
|
|
13
|
+
Set options in the profile patch `config` block — visible in
|
|
14
|
+
`dsh --dump-config`, per-profile, and never overwritten by npm:
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
# ~/.dsh/profiles/web/cordis.patch.yml
|
|
18
|
+
- insert:
|
|
19
|
+
- id: speech-hook
|
|
20
|
+
name: 'dsh-speak'
|
|
21
|
+
config:
|
|
22
|
+
throttleMs: 1500 # merge delay before announcing (ms)
|
|
23
|
+
engine: '' # engine path override; '' = auto-resolve
|
|
24
|
+
announceApprovals: true # speak approval requests
|
|
25
|
+
announceQuestions: true # speak ask_user_question content
|
|
26
|
+
stripApprovalPrefix: true # strip "escalate sandbox to ...: " prefix
|
|
27
|
+
longTextMode: message # message | heading (speak largest md heading)
|
|
28
|
+
maxChars: 300 # engine per-utterance ceiling
|
|
29
|
+
volume: 50 # Windows only
|
|
30
|
+
rate: 0 # 0 = engine default (Windows SAPI scale / macOS wpm)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Option reference
|
|
34
|
+
|
|
35
|
+
| option | default | effect |
|
|
36
|
+
| ------ | ------- | ------ |
|
|
37
|
+
| `throttleMs` | `1500` | how long a reply's text waits before being announced (merges multi-step messages) |
|
|
38
|
+
| `engine` | `''` | explicit engine script path; `''` auto-resolves: `<package>/engine/<platform>` → `~/.dsh/hooks/<platform>` |
|
|
39
|
+
| `announceApprovals` | `true` | announce `approval/asked` events (reason, or the fixed prompt) |
|
|
40
|
+
| `announceQuestions` | `true` | announce `ask_user_question` calls as "question(单选/多选),选项:…" |
|
|
41
|
+
| `stripApprovalPrefix` | `true` | strip the fixed English template prefix (`escalate sandbox to danger-full-access: `) from approval reasons, keeping the human explanation |
|
|
42
|
+
| `longTextMode` | `message` | `message` = fixed prompt for over-long text; `heading` = speak the largest markdown heading instead (see below) |
|
|
43
|
+
| `maxChars` | `300` | engine per-utterance ceiling (SAPI/NVSAPIAdapter fails silently beyond ~375-470) |
|
|
44
|
+
| `volume` | `50` | Windows only (0-100); macOS volume follows the system |
|
|
45
|
+
| `rate` | `0` | `0` = engine default (Windows SAPI scale, e.g. 1; macOS words-per-minute, e.g. 175) |
|
|
46
|
+
|
|
47
|
+
### Long-text modes
|
|
48
|
+
|
|
49
|
+
When cleaned text exceeds `maxChars`:
|
|
50
|
+
|
|
51
|
+
- **`message`** (default): speak `LongTextMessage` (`本次播报内容较长,请自行阅读。`, overridable via engine param `-LongTextMessage` / `-l`).
|
|
52
|
+
- **`heading`**: pick the *largest* markdown heading in the raw text — fewest `#` wins, tie → first; if there is no heading line, the first non-empty line is used. The chosen candidate is still cleaned and subject to the `maxChars` ceiling, falling back to the message if it is itself too long.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 2. Engine override (copy & edit)
|
|
57
|
+
|
|
58
|
+
To change what is actually spoken (voice selection, cleaning rules, defaults),
|
|
59
|
+
copy the engine out of the package and point the plugin at your copy:
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
# Windows
|
|
63
|
+
Copy-Item "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-speak\engine\speak.ps1" "$env:USERPROFILE\.dsh\hooks\my-speak.ps1"
|
|
64
|
+
# macOS
|
|
65
|
+
cp ~/.dsh/profiles/web/node_modules/dsh-speak/engine/speak.sh ~/.dsh/hooks/my-speak.sh
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then set the engine path in config:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
config:
|
|
72
|
+
engine: 'C:/Users/<you>/.dsh/hooks/my-speak.ps1' # or ~/.dsh/hooks/my-speak.sh on macOS
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`npm update` never touches your copy.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 3. Extension
|
|
80
|
+
|
|
81
|
+
- **New engine backend**: the engine is the single seam. A new backend
|
|
82
|
+
(`speak-edge.ps1` for edge-tts, `speak-piper.ps1` for a local model, ...) keeps
|
|
83
|
+
the same parameter contract and cleaning pipeline — adapters never change.
|
|
84
|
+
See [DESIGN.md §7](DESIGN.md#7-extending).
|
|
85
|
+
- **New harness adapter**: capture the final reply text → call the engine. The
|
|
86
|
+
DSH (event stream), Claude Code (Stop hook), and agent-called
|
|
87
|
+
(`speech-summary.ps1`) patterns are the three references.
|
|
88
|
+
- **Your own published variant**: fork this repo, adjust, publish your own npm
|
|
89
|
+
package — the `dsh.bundle` manifest already makes it `dsh plugin add`-installable.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# CUSTOMIZATION.zh-CN.md — dsh-speak 自定义指南
|
|
2
|
+
|
|
3
|
+
(English: docs/CUSTOMIZATION.md)
|
|
4
|
+
|
|
5
|
+
dsh-speak 刻意保持小巧,但提供三层自定义:**配置**(不改代码)、**引擎覆盖**(复制后改)、
|
|
6
|
+
**扩展**(新后端 / 新适配器)。以下所有方式都不会被 `npm update` 覆盖。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 一、配置(推荐)
|
|
11
|
+
|
|
12
|
+
在 profile patch 的 `config` 块里设置——`dsh --dump-config` 可见、按 profile 隔离、
|
|
13
|
+
npm 更新永不覆盖:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
# ~/.dsh/profiles/web/cordis.patch.yml
|
|
17
|
+
- insert:
|
|
18
|
+
- id: speech-hook
|
|
19
|
+
name: 'dsh-speak'
|
|
20
|
+
config:
|
|
21
|
+
throttleMs: 1500 # 播报前的合并延迟(毫秒)
|
|
22
|
+
engine: '' # 引擎路径覆盖;'' = 自动解析
|
|
23
|
+
announceApprovals: true # 播报审批请求
|
|
24
|
+
announceQuestions: true # 播报 ask_user_question 提问内容
|
|
25
|
+
stripApprovalPrefix: true # 剥离审批原因里的 "escalate sandbox to ...: " 前缀
|
|
26
|
+
longTextMode: message # message | heading(念最大字号 markdown 标题)
|
|
27
|
+
maxChars: 300 # 引擎单次朗读字数上限
|
|
28
|
+
volume: 50 # 仅 Windows
|
|
29
|
+
rate: 0 # 0 = 引擎默认(Windows SAPI 刻度 / macOS wpm)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 选项说明
|
|
33
|
+
|
|
34
|
+
| 选项 | 默认值 | 效果 |
|
|
35
|
+
| ---- | ------ | ---- |
|
|
36
|
+
| `throttleMs` | `1500` | 回复文本等待多久才播报(合并同一回复的多步消息) |
|
|
37
|
+
| `engine` | `''` | 显式引擎脚本路径;`''` 自动解析:包内 `engine/<平台>` → `~/.dsh/hooks/<平台>` |
|
|
38
|
+
| `announceApprovals` | `true` | 播报 `approval/asked` 事件(审批原因,或固定提示语) |
|
|
39
|
+
| `announceQuestions` | `true` | 把 `ask_user_question` 调用播报成"问题(单选/多选),选项:…" |
|
|
40
|
+
| `stripApprovalPrefix` | `true` | 剥离审批原因里的固定英文模板前缀(`escalate sandbox to danger-full-access: `),保留中文说明 |
|
|
41
|
+
| `longTextMode` | `message` | `message` = 超长念固定提示语;`heading` = 改念最大字号 markdown 标题(规则见下) |
|
|
42
|
+
| `maxChars` | `300` | 引擎单次朗读上限(SAPI/NVSAPIAdapter 超过约 375-470 字会静默失败) |
|
|
43
|
+
| `volume` | `50` | 仅 Windows(0-100);macOS 音量跟随系统 |
|
|
44
|
+
| `rate` | `0` | `0` = 引擎默认(Windows SAPI 刻度如 1;macOS words-per-minute 如 175) |
|
|
45
|
+
|
|
46
|
+
### 超长文本模式
|
|
47
|
+
|
|
48
|
+
清洗后文本超过 `maxChars` 时:
|
|
49
|
+
|
|
50
|
+
- **`message`**(默认):念 `LongTextMessage`(`本次播报内容较长,请自行阅读。`,
|
|
51
|
+
可用引擎参数 `-LongTextMessage` / `-l` 覆盖)。
|
|
52
|
+
- **`heading`**:在原始文本里挑**最大字号**的 markdown 标题——`#` 数量最少者优先,
|
|
53
|
+
并列取第一个;没有标题行则取第一个非空行。选中的候选仍会清洗并受 `maxChars`
|
|
54
|
+
上限约束,若其本身仍超长则回退提示语。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 二、引擎覆盖(复制后改)
|
|
59
|
+
|
|
60
|
+
想改"实际念出来的内容"(音色选择、清洗规则、默认参数),把引擎复制出包并指向你的副本:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
# Windows
|
|
64
|
+
Copy-Item "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-speak\engine\speak.ps1" "$env:USERPROFILE\.dsh\hooks\my-speak.ps1"
|
|
65
|
+
# macOS
|
|
66
|
+
cp ~/.dsh/profiles/web/node_modules/dsh-speak/engine/speak.sh ~/.dsh/hooks/my-speak.sh
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
然后在 config 里设引擎路径:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
config:
|
|
73
|
+
engine: 'C:/Users/<你>/.dsh/hooks/my-speak.ps1' # macOS 用 ~/.dsh/hooks/my-speak.sh
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`npm update` 永远不会碰你的副本。
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 三、扩展
|
|
81
|
+
|
|
82
|
+
- **新引擎后端**:引擎是唯一接缝。新后端(`speak-edge.ps1` 封装 edge-tts、
|
|
83
|
+
`speak-piper.ps1` 接本地模型……)保持同样的参数契约与清洗管线——适配层不用改。
|
|
84
|
+
见 [DESIGN.zh-CN.md §7](DESIGN.zh-CN.md#7-扩展)。
|
|
85
|
+
- **新 harness 适配层**:拿到最终回复文本 → 调引擎。DSH(事件流)、Claude Code
|
|
86
|
+
(Stop hook)、Agent 自调用(`speech-summary.ps1`)是三种参考范式。
|
|
87
|
+
- **发布自己的变体**:fork 本仓库、按需调整、发布自己的 npm 包——`dsh.bundle`
|
|
88
|
+
manifest 已让它天然支持 `dsh plugin add` 安装。
|
package/docs/DESIGN.md
CHANGED
|
@@ -168,13 +168,24 @@ returns immediately. (Async spawning is safe here — the nested-spawn restricti
|
|
|
168
168
|
| `-Rate` | `1` | speech rate (SAPI scale) |
|
|
169
169
|
| `-MaxChars` | `300` | beyond this, replaced by `LongTextMessage` |
|
|
170
170
|
| `-LongTextMessage`| `本次播报内容较长,请自行阅读。` | spoken instead of over-long text |
|
|
171
|
+
| `-LongTextMode` | `message` | `message` (fixed prompt) \| `heading` (speak the largest markdown heading) |
|
|
171
172
|
|
|
172
|
-
### DSH plugin (
|
|
173
|
+
### DSH plugin (profile `config` — recommended; env vars are legacy fallbacks)
|
|
173
174
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
```yaml
|
|
176
|
+
config:
|
|
177
|
+
throttleMs: 1500
|
|
178
|
+
engine: '' # '' = auto-resolve
|
|
179
|
+
announceApprovals: true
|
|
180
|
+
announceQuestions: true
|
|
181
|
+
stripApprovalPrefix: true
|
|
182
|
+
longTextMode: message # message | heading
|
|
183
|
+
maxChars: 300
|
|
184
|
+
volume: 50 # Windows only
|
|
185
|
+
rate: 0 # 0 = engine default
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Full guide: docs/CUSTOMIZATION.md.
|
|
178
189
|
|
|
179
190
|
## 6. Pitfalls (hard-won; do not "fix" casually)
|
|
180
191
|
|
|
@@ -227,7 +238,7 @@ dependencies in the profile). This repository is prepared for that path:
|
|
|
227
238
|
|
|
228
239
|
`speech-hook.js` locates `engine/speak.ps1` in this order:
|
|
229
240
|
|
|
230
|
-
1. `
|
|
241
|
+
1. `config.engine` override;
|
|
231
242
|
2. `<package>/engine/speak.ps1` resolved relative to the plugin file — covers
|
|
232
243
|
both a repo checkout and `node_modules/dsh-speak/` after `npm install`;
|
|
233
244
|
3. legacy `%USERPROFILE%\.dsh\hooks\speak.ps1` (the file-install location).
|
package/docs/DESIGN.zh-CN.md
CHANGED
|
@@ -155,13 +155,24 @@ Claude Code *确实*有 Stop hook。hook JSON(含 `transcript_path`)从 stdi
|
|
|
155
155
|
| `-Rate` | `1` | 语速(SAPI 刻度) |
|
|
156
156
|
| `-MaxChars` | `300` | 超过此长度时替换为 `LongTextMessage` |
|
|
157
157
|
| `-LongTextMessage` | `本次播报内容较长,请自行阅读。` | 超长文本时改念这句 |
|
|
158
|
+
| `-LongTextMode` | `message` | `message`(固定提示语)\| `heading`(念最大字号 markdown 标题) |
|
|
158
159
|
|
|
159
|
-
### DSH
|
|
160
|
+
### DSH 插件(profile `config` —— 推荐)
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
```yaml
|
|
163
|
+
config:
|
|
164
|
+
throttleMs: 1500
|
|
165
|
+
engine: '' # '' = 自动解析
|
|
166
|
+
announceApprovals: true
|
|
167
|
+
announceQuestions: true
|
|
168
|
+
stripApprovalPrefix: true
|
|
169
|
+
longTextMode: message # message | heading
|
|
170
|
+
maxChars: 300
|
|
171
|
+
volume: 50 # 仅 Windows
|
|
172
|
+
rate: 0 # 0 = 引擎默认
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
完整指南:docs/CUSTOMIZATION.zh-CN.md。
|
|
165
176
|
|
|
166
177
|
## 6. 踩坑记录(来之不易;不要随意"修复")
|
|
167
178
|
|
|
@@ -212,7 +223,7 @@ DSH 的插件机制基于 Cordis,官方安装树外插件的路径是
|
|
|
212
223
|
|
|
213
224
|
`speech-hook.js` 按以下顺序定位 `engine/speak.ps1`:
|
|
214
225
|
|
|
215
|
-
1. `
|
|
226
|
+
1. `config.engine` 覆盖;
|
|
216
227
|
2. 相对插件文件解析 `<包>/engine/speak.ps1`——同时覆盖仓库检出和
|
|
217
228
|
`npm install` 后的 `node_modules/dsh-speak/`;
|
|
218
229
|
3. 旧的 `%USERPROFILE%\.dsh\hooks\speak.ps1`(文件安装的位置)。
|
package/engine/speak.ps1
CHANGED
|
@@ -29,7 +29,9 @@ param(
|
|
|
29
29
|
[int]$Volume = 50,
|
|
30
30
|
[int]$Rate = 1,
|
|
31
31
|
[int]$MaxChars = 300,
|
|
32
|
-
[string]$LongTextMessage = '本次播报内容较长,请自行阅读。'
|
|
32
|
+
[string]$LongTextMessage = '本次播报内容较长,请自行阅读。',
|
|
33
|
+
[ValidateSet('message', 'heading')]
|
|
34
|
+
[string]$LongTextMode = 'message'
|
|
33
35
|
)
|
|
34
36
|
|
|
35
37
|
# ---------- input: pick text source ----------
|
|
@@ -41,6 +43,29 @@ if ($File) {
|
|
|
41
43
|
}
|
|
42
44
|
if (-not $text -or -not $text.Trim()) { exit 0 }
|
|
43
45
|
|
|
46
|
+
# ---------- length guard: adapter per-Speak ceiling ----------
|
|
47
|
+
# 'message': fixed prompt. 'heading': speak the largest markdown heading instead
|
|
48
|
+
# (fewest '#' wins, tie -> first; no heading -> first non-empty line; the
|
|
49
|
+
# cleaned candidate is still subject to the ceiling below).
|
|
50
|
+
if ($text.Length -gt $MaxChars -and $LongTextMode -eq 'heading') {
|
|
51
|
+
$candidate = ''
|
|
52
|
+
$bestLevel = 7
|
|
53
|
+
$firstNonEmpty = ''
|
|
54
|
+
foreach ($line in ($text -split "`n")) {
|
|
55
|
+
if ($line -match '^\s*#{1,6}\s+') {
|
|
56
|
+
$level = ([regex]::Match($line, '^(\s*)(#+)')).Groups[2].Value.Length
|
|
57
|
+
if ($level -lt $bestLevel) {
|
|
58
|
+
$bestLevel = $level
|
|
59
|
+
$candidate = $line -replace '^\s*#+\s*', ''
|
|
60
|
+
}
|
|
61
|
+
} elseif (-not $firstNonEmpty -and $line.Trim()) {
|
|
62
|
+
$firstNonEmpty = $line
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (-not $candidate) { $candidate = $firstNonEmpty }
|
|
66
|
+
if ($candidate) { $text = $candidate }
|
|
67
|
+
}
|
|
68
|
+
|
|
44
69
|
# ---------- clean: markdown -> plain speech text ----------
|
|
45
70
|
# code blocks, inline code, markdown links, bare URLs, emphasis/marker chars
|
|
46
71
|
$text = $text -replace '```[\s\S]*?```', ' '
|
|
@@ -54,7 +79,7 @@ $text = [regex]::Replace($text, '[^一-龥 -〿- - -~]', '')
|
|
|
54
79
|
$text = $text -replace '\s+', ' '
|
|
55
80
|
$text = $text.Trim()
|
|
56
81
|
|
|
57
|
-
# ----------
|
|
82
|
+
# ---------- final ceiling (also catches over-long heading candidates) ----------
|
|
58
83
|
if ($text.Length -gt $MaxChars) { $text = $LongTextMessage }
|
|
59
84
|
|
|
60
85
|
# ---------- speak ----------
|
package/engine/speak.sh
CHANGED
|
@@ -37,13 +37,14 @@ VOICE=""
|
|
|
37
37
|
RATE=175 # words per minute (say default)
|
|
38
38
|
MAX_CHARS=300
|
|
39
39
|
LONG_MSG="本次播报内容较长,请自行阅读。"
|
|
40
|
+
LONG_MODE="message" # message | heading
|
|
40
41
|
|
|
41
42
|
usage() {
|
|
42
|
-
echo "usage: speak.sh [-t text | -f file] [-v voice] [-r wpm] [-m maxchars] [-l longmsg]" >&2
|
|
43
|
+
echo "usage: speak.sh [-t text | -f file] [-v voice] [-r wpm] [-m maxchars] [-l longmsg] [-M message|heading]" >&2
|
|
43
44
|
exit 1
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
while getopts "t:f:v:r:m:l:h" opt; do
|
|
47
|
+
while getopts "t:f:v:r:m:l:M:h" opt; do
|
|
47
48
|
case "$opt" in
|
|
48
49
|
t) TEXT="$OPTARG" ;;
|
|
49
50
|
f) FILE="$OPTARG" ;;
|
|
@@ -51,6 +52,7 @@ while getopts "t:f:v:r:m:l:h" opt; do
|
|
|
51
52
|
r) RATE="$OPTARG" ;;
|
|
52
53
|
m) MAX_CHARS="$OPTARG" ;;
|
|
53
54
|
l) LONG_MSG="$OPTARG" ;;
|
|
55
|
+
M) LONG_MODE="$OPTARG" ;;
|
|
54
56
|
h) usage ;;
|
|
55
57
|
*) usage ;;
|
|
56
58
|
esac
|
|
@@ -63,6 +65,26 @@ if [ -n "$FILE" ]; then
|
|
|
63
65
|
fi
|
|
64
66
|
if [ -z "$TEXT" ]; then exit 0; fi
|
|
65
67
|
|
|
68
|
+
# ---------- length guard / long-text mode ----------
|
|
69
|
+
# 'message': fixed prompt. 'heading': speak the largest markdown heading instead
|
|
70
|
+
# (fewest '#' wins, tie -> first; no heading -> first non-empty line; the
|
|
71
|
+
# cleaned candidate is still subject to the ceiling below).
|
|
72
|
+
if [ "${#TEXT}" -gt "$MAX_CHARS" ] && [ "$LONG_MODE" = "heading" ]; then
|
|
73
|
+
TEXT=$(printf '%s' "$TEXT" | /usr/bin/perl -CSD -e '
|
|
74
|
+
my $best = 7; my $cand = ""; my $first = "";
|
|
75
|
+
while (<STDIN>) {
|
|
76
|
+
if (/^[ \t]*(\#{1,6})[ \t]+(.*)$/) {
|
|
77
|
+
my $n = length($1);
|
|
78
|
+
if ($n < $best) { $best = $n; $cand = $2; }
|
|
79
|
+
} elsif ($first eq "" && /\S/) {
|
|
80
|
+
$first = $_;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
$cand = $first if $cand eq "";
|
|
84
|
+
print $cand;
|
|
85
|
+
')
|
|
86
|
+
fi
|
|
87
|
+
|
|
66
88
|
# ---------- clean (mirrors speak.ps1) ----------
|
|
67
89
|
TEXT=$(printf '%s' "$TEXT" | /usr/bin/perl -CSD -pe '
|
|
68
90
|
s/```[\s\S]*?```/ /g; # code blocks
|
|
@@ -75,7 +97,7 @@ TEXT=$(printf '%s' "$TEXT" | /usr/bin/perl -CSD -pe '
|
|
|
75
97
|
')
|
|
76
98
|
TEXT=$(printf '%s' "$TEXT" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
77
99
|
|
|
78
|
-
# ----------
|
|
100
|
+
# ---------- final ceiling (also catches over-long heading candidates) ----------
|
|
79
101
|
if [ "${#TEXT}" -gt "$MAX_CHARS" ]; then
|
|
80
102
|
TEXT="$LONG_MSG"
|
|
81
103
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-speak",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Make your AI harness speak — verified voice announcements for DSH and other AI coding harnesses (Windows SAPI5 + macOS system voices)",
|
|
5
5
|
"main": "adapters/dsh/speech-hook.js",
|
|
6
6
|
"files": [
|