dsh-speak 1.7.1 → 1.7.3
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 +9 -4
- package/README.zh-CN.md +6 -2
- package/adapters/dsh/install.ps1 +1 -1
- package/adapters/dsh/speech-hook.js +3 -1
- package/client/client.js +15 -4
- package/docs/DESIGN.md +1 -1
- package/docs/DESIGN.zh-CN.md +1 -1
- package/engine/speak.ps1 +5 -0
- package/engine/speak.sh +3 -1
- package/engine/speech-prompt.ps1 +1 -1
- package/engine/speech-summary.ps1 +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,9 +71,14 @@ Windows:
|
|
|
71
71
|
|
|
72
72
|
- Windows 10 or 11, PowerShell (any recent version).
|
|
73
73
|
- Natural voices:
|
|
74
|
-
- **Windows 11**: natural voice packs are built into the system —
|
|
75
|
-
installation. Enable/switch them in *Settings → Accessibility →
|
|
76
|
-
*Settings → Time & Language → Speech*.
|
|
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.
|
|
77
82
|
- **Windows 10**: install
|
|
78
83
|
[NaturalVoiceSAPIAdapter](https://github.com/gexgd0419/NaturalVoiceSAPIAdapter)
|
|
79
84
|
and use its VoiceDownloader to download the natural voice pack(s) you want
|
|
@@ -315,7 +320,7 @@ document):
|
|
|
315
320
|
| `longTextMessage` | `本次播报内容较长,请自行阅读。` | the fixed prompt spoken for over-long text in `message` mode (editable in the UI) |
|
|
316
321
|
| `maxChars` | platform | per-utterance ceiling. **macOS default 0 (`say` has no ceiling); Windows default 300** (SAPI fails silently beyond ~375-470) |
|
|
317
322
|
| `volume` | `50` | Windows only (0-100); macOS volume follows the system |
|
|
318
|
-
| `rate` | `0` |
|
|
323
|
+
| `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) |
|
|
319
324
|
| `announceTurnEnd` | `false` | announce "第 N 轮对话完成/中断/异常结束" on turn end (`turn/end`) |
|
|
320
325
|
| `announceCommandDone` | `false` | announce when a command finishes or fails (`command/done`) |
|
|
321
326
|
| `announceGoalChange` | `false` | announce goal created/updated/completed/paused/resumed (`goal/change`, objective head) |
|
package/README.zh-CN.md
CHANGED
|
@@ -64,8 +64,12 @@ harness 事件(DSH 会话事件 / Claude Code Stop hook / 任意方式)
|
|
|
64
64
|
|
|
65
65
|
- Windows 10 或 11,任意较新的 PowerShell。
|
|
66
66
|
- 自然语音:
|
|
67
|
-
- **Windows 11
|
|
67
|
+
- **Windows 11(21H2–23H2)**:系统已内置自然语音包,无需额外安装——在
|
|
68
68
|
*设置 → 辅助功能 → 讲述人* 或 *设置 → 时间和语言 → 语音* 中启用/切换即可。
|
|
69
|
+
- **Windows 11 24H2/25H2**:自然语音已改为 MSIX 应用包,`System.Speech` 可能
|
|
70
|
+
枚举不到(`SpeechSynthesizer` 找不到自然语音、回退到机械音)——与 Windows 10
|
|
71
|
+
相同,需安装 [NaturalVoiceSAPIAdapter](https://github.com/gexgd0419/NaturalVoiceSAPIAdapter)
|
|
72
|
+
桥接。
|
|
69
73
|
- **Windows 10**:需要安装
|
|
70
74
|
[NaturalVoiceSAPIAdapter](https://github.com/gexgd0419/NaturalVoiceSAPIAdapter),
|
|
71
75
|
并用它的 VoiceDownloader 手动下载你需要的中文或其他语言的自然语音包。
|
|
@@ -295,7 +299,7 @@ speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次
|
|
|
295
299
|
| `longTextMessage` | `本次播报内容较长,请自行阅读。` | `message` 模式下超长文本改念的固定提示语(UI 可编辑) |
|
|
296
300
|
| `maxChars` | 平台相关 | 引擎单次朗读上限。**macOS 默认 0(`say` 无上限);Windows 默认 300**(SAPI 超过约 375-470 字会静默失败) |
|
|
297
301
|
| `volume` | `50` | 仅 Windows(0-100);macOS 音量跟随系统 |
|
|
298
|
-
| `rate` | `0` |
|
|
302
|
+
| `rate` | `0` | 语速:Windows SAPI 刻度(-10 到 10,0 = 正常,推荐 0 / 稍快 1-3);macOS words-per-minute(默认 175,稍快 200) |
|
|
299
303
|
| `announceTurnEnd` | `false` | 回合结束时播报"第 N 轮对话完成/中断/异常结束"(`turn/end`) |
|
|
300
304
|
| `announceCommandDone` | `false` | 命令执行完成/失败时播报(`command/done`) |
|
|
301
305
|
| `announceGoalChange` | `false` | 目标创建/更新/完成/暂停/恢复时播报(`goal/change`,含目标标题前 40 字) |
|
package/adapters/dsh/install.ps1
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# install.ps1 — one-command installer for the DSH adapter
|
|
1
|
+
# install.ps1 — one-command installer for the DSH adapter
|
|
2
2
|
# ========================================================
|
|
3
3
|
# 1. copies engine/*.ps1 to ~/.dsh/hooks/
|
|
4
4
|
# 2. copies speech-hook.js to ~/.dsh/profiles/web/plugins/
|
|
@@ -264,7 +264,9 @@ module.exports = {
|
|
|
264
264
|
if (fullRead) args.push('-F')
|
|
265
265
|
child = spawn('/bin/bash', [cfg.engine].concat(args), { detached: true, stdio: 'ignore' })
|
|
266
266
|
} else {
|
|
267
|
-
|
|
267
|
+
// Windows 语速 = SAPI 刻度(-10 到 10,0 = 正常;负值变慢),直接透传,
|
|
268
|
+
// 不要把 0/负值替换成 1(否则无法回到正常语速/减慢)
|
|
269
|
+
child = spawn('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', cfg.engine, '-File', tmp, '-Volume', String(cfg.volume), '-Rate', String(cfg.rate), '-MaxChars', String(cfg.maxChars), '-LongTextMode', cfg.longTextMode, '-LongTextMessage', cfg.longTextMessage, '-CleanMarkdownFormatting', cfg.cleanMarkdownFormatting ? '1' : '0', '-ReadInlineCode', cfg.readInlineCode ? '1' : '0', '-CodeBlocks', cfg.codeBlocks, '-CodeBlockMaxChars', String(cfg.codeBlockMaxChars), '-CodeBlockReplacementText', cfg.codeBlockReplacementText, '-FullRead', fullRead ? '1' : '0'], { windowsHide: true, stdio: 'ignore' })
|
|
268
270
|
}
|
|
269
271
|
const token = ++speechToken
|
|
270
272
|
activeSpeech = { process: child, tmp, token, item, gapMs: item.gapMs || 0 }
|
package/client/client.js
CHANGED
|
@@ -66,7 +66,8 @@ window.__ModuleLoader__.load({
|
|
|
66
66
|
volume: '音量',
|
|
67
67
|
volumeHint: '仅 Windows(0-100);macOS 音量跟随系统。',
|
|
68
68
|
rate: '语速',
|
|
69
|
-
|
|
69
|
+
rateHintWin: 'Windows SAPI 语速(-10 到 10,0 = 正常;推荐 0,稍快 1-3)。',
|
|
70
|
+
rateHintMac: 'words-per-minute(wpm),数值越大语速越快:默认 175,稍快 200。',
|
|
70
71
|
engine: '引擎路径',
|
|
71
72
|
engineHint: '自定义引擎脚本路径;留空自动解析(包内引擎 → ~/.dsh/hooks)。',
|
|
72
73
|
longTextBehavior: '超长文本行为',
|
|
@@ -133,7 +134,8 @@ window.__ModuleLoader__.load({
|
|
|
133
134
|
volume: 'Volume',
|
|
134
135
|
volumeHint: 'Windows only (0-100); macOS volume follows the system.',
|
|
135
136
|
rate: 'Speech Rate',
|
|
136
|
-
|
|
137
|
+
rateHintWin: 'Windows SAPI rate scale (-10 to 10, 0 = normal; try 1-3 for faster).',
|
|
138
|
+
rateHintMac: 'words-per-minute (wpm) — higher = faster: default 175, 200 is a bit faster.',
|
|
137
139
|
engine: 'Engine Path',
|
|
138
140
|
engineHint: 'Custom engine script path; empty = auto-resolve (package engine → ~/.dsh/hooks).',
|
|
139
141
|
longTextBehavior: 'Long Text Behavior',
|
|
@@ -274,7 +276,15 @@ window.__ModuleLoader__.load({
|
|
|
274
276
|
return e('div', { className: 'dsh-speak-field' }, inline ? e('div', { className: 'dsh-speak-inline-field' }, e('div', { className: 'dsh-speak-field-label' }, label), children) : e('div', { className: 'dsh-speak-field-label' }, label), inline ? null : children, e('p', { className: 'dsh-speak-field-hint' }, hint))
|
|
275
277
|
}
|
|
276
278
|
function Toggle({ label, value, disabled, onChange, hint }) { return e(Field, { label: `${label}:`, hint, inline: true }, e('div', { className: 'dsh-speak-option-row' }, e(Button, { variant: 'outline', size: 'sm', disabled, 'aria-pressed': value, onClick: () => onChange(!value) }, value ? t('toggleOn') : t('toggleOff')))) }
|
|
277
|
-
function SettingInput({ label, value, disabled, numeric, onChange, hint }) {
|
|
279
|
+
function SettingInput({ label, value, disabled, numeric, onChange, hint }) {
|
|
280
|
+
const id = `dsh-speak-${label.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`
|
|
281
|
+
// 缓冲输入:受控 value 来自配置,直接绑定会让"-"这类中间态被弹回
|
|
282
|
+
// (Number('-') = NaN 拒绝后 value 不变),负数字段无法输入。这里用
|
|
283
|
+
// 局部 state 缓冲,输入过程自由,onChange 里校验合法后才写配置。
|
|
284
|
+
const [text, setText] = React.useState(String(value))
|
|
285
|
+
React.useEffect(() => { setText(String(value)) }, [value])
|
|
286
|
+
return e(Field, { label: `${label}:`, hint }, e('div', { className: 'dsh-speak-input-row' }, e(Input, { id, value: text, disabled, inputMode: numeric ? 'numeric' : undefined, onChange: event => { setText(event.target.value); onChange(event.target.value) } })))
|
|
287
|
+
}
|
|
278
288
|
function Options({ label, value, disabled, onChange, hint, options }) { return e(Field, { label, hint }, e('div', { className: 'dsh-speak-option-row' }, ...options.map(option => e(Button, { key: option.value, variant: value === option.value ? 'primary' : 'outline', size: 'sm', disabled, 'aria-pressed': value === option.value, onClick: () => onChange(option.value) }, option.label)))) }
|
|
279
289
|
function MarkdownCleaning({ value, clean, disabled, set }) {
|
|
280
290
|
const [open, setOpen] = React.useState(true); const controlsDisabled = disabled || !clean
|
|
@@ -290,6 +300,7 @@ window.__ModuleLoader__.load({
|
|
|
290
300
|
const [eventsOpen, setEventsOpen] = React.useState(false)
|
|
291
301
|
const snapshot = useSettings(); if (snapshot.status !== 'ready' || !snapshot.value) return null
|
|
292
302
|
const value = snapshot.value; const disabled = !snapshot.writable; const clean = value.cleanMarkdownFormatting !== false; const set = (field, next) => { void settings.set(field, next).catch(console.error) }
|
|
303
|
+
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent || navigator.platform || '')
|
|
293
304
|
return e('section', { 'aria-label': t('settingsAria') }, e('h3', null, t('settingsTitle')), e('p', null, t('settingsIntro')),
|
|
294
305
|
e(Toggle, { label: t('masterSwitch'), value: value.enabled !== false, disabled, onChange: next => set('enabled', next), hint: t('masterSwitchHint') }),
|
|
295
306
|
e(Toggle, { label: t('automaticSpeech'), value: value.automaticSpeech !== false, disabled, onChange: next => set('automaticSpeech', next), hint: t('automaticSpeechHint') }),
|
|
@@ -299,7 +310,7 @@ window.__ModuleLoader__.load({
|
|
|
299
310
|
e(Toggle, { label: t('cleanMarkdown'), value: clean, disabled, onChange: next => set('cleanMarkdownFormatting', next), hint: t('cleanMarkdownHint') }), e(MarkdownCleaning, { value, clean, disabled, set }),
|
|
300
311
|
e(SettingInput, { label: t('maxChars'), value: value.maxChars == null ? 0 : value.maxChars, numeric: true, disabled, onChange: next => { if (/^\d+$/.test(next)) set('maxChars', Number(next)) }, hint: t('maxCharsHint') }),
|
|
301
312
|
e(SettingInput, { label: t('volume'), value: value.volume == null ? 50 : value.volume, numeric: true, disabled, onChange: next => { if (/^\d+$/.test(next)) set('volume', Number(next)) }, hint: t('volumeHint') }),
|
|
302
|
-
e(SettingInput, { label: t('rate'), value: value.rate == null ? 0 : value.rate, numeric: true, disabled, onChange: next => { if (/^-?\d*\.?\d*$/.test(next)) set('rate',
|
|
313
|
+
e(SettingInput, { label: t('rate'), value: value.rate == null ? 0 : value.rate, numeric: true, disabled, onChange: next => { if (/^-?\d*\.?\d*$/.test(next)) { const n = Number(next); if (Number.isFinite(n)) set('rate', n) } }, hint: isMac ? t('rateHintMac') : t('rateHintWin') }),
|
|
303
314
|
e(SettingInput, { label: t('engine'), value: value.engine || '', disabled, onChange: next => set('engine', next), hint: t('engineHint') }),
|
|
304
315
|
e(Options, { label: t('longTextBehavior'), value: value.longTextMode || 'message', disabled, onChange: next => set('longTextMode', next), hint: t('longTextBehaviorHint'), options: [{ value: 'message', label: t('longTextMessageOption') }, { value: 'heading', label: t('longTextHeadingOption') }] }),
|
|
305
316
|
e(SettingInput, { label: t('fixedPrompt'), value: value.longTextMessage || '本次播报内容较长,请自行阅读。', disabled: disabled || value.longTextMode !== 'message', onChange: next => set('longTextMessage', next), hint: t('fixedPromptHint') }),
|
package/docs/DESIGN.md
CHANGED
|
@@ -253,7 +253,7 @@ config:
|
|
|
253
253
|
longTextMessage: '本次播报内容较长,请自行阅读。'
|
|
254
254
|
maxChars: 300 # macOS default 0 = unlimited
|
|
255
255
|
volume: 50 # Windows only
|
|
256
|
-
rate: 0 # 0
|
|
256
|
+
rate: 0 # rate: Win SAPI scale -10..10 (0=normal) / mac wpm (175)
|
|
257
257
|
# —— optional event announcements (off by default) ——
|
|
258
258
|
announceTurnEnd: false # turn/end
|
|
259
259
|
announceCommandDone: false # command/done
|
package/docs/DESIGN.zh-CN.md
CHANGED
|
@@ -232,7 +232,7 @@ config:
|
|
|
232
232
|
longTextMessage: '本次播报内容较长,请自行阅读。'
|
|
233
233
|
maxChars: 300 # macOS 默认 0 = 不限
|
|
234
234
|
volume: 50 # 仅 Windows
|
|
235
|
-
rate: 0 # 0
|
|
235
|
+
rate: 0 # 语速:Win SAPI 刻度 -10~10(0=正常)/ mac wpm(175)
|
|
236
236
|
# —— 可选事件播报(默认全关)——
|
|
237
237
|
announceTurnEnd: false # turn/end
|
|
238
238
|
announceCommandDone: false # command/done
|
package/engine/speak.ps1
CHANGED
|
@@ -68,7 +68,12 @@ if (-not $fullReadMode -and $text.Length -gt $MaxChars -and $LongTextMode -eq 'h
|
|
|
68
68
|
$candidate = ''
|
|
69
69
|
$bestLevel = 7
|
|
70
70
|
$firstNonEmpty = ''
|
|
71
|
+
# 代码块 fence 内的行跳过:其中的 "# 注释" 不是 markdown 标题,
|
|
72
|
+
# 否则长回复里的代码注释会被误当成标题只念注释
|
|
73
|
+
$inCodeBlock = $false
|
|
71
74
|
foreach ($line in ($text -split "`n")) {
|
|
75
|
+
if ($line -match '^\s*```') { $inCodeBlock = -not $inCodeBlock; continue }
|
|
76
|
+
if ($inCodeBlock) { continue }
|
|
72
77
|
if ($line -match '^\s*#{1,6}\s+') {
|
|
73
78
|
$level = ([regex]::Match($line, '^(\s*)(#+)')).Groups[2].Value.Length
|
|
74
79
|
if ($level -lt $bestLevel) {
|
package/engine/speak.sh
CHANGED
|
@@ -56,8 +56,10 @@ fi
|
|
|
56
56
|
# -F (full read, manual replay) skips this guard entirely.
|
|
57
57
|
if [ "$FULL_READ" != "1" ] && [ "$MAX_CHARS" -gt 0 ] && [ "${#TEXT}" -gt "$MAX_CHARS" ] && [ "$LONG_MODE" = "heading" ]; then
|
|
58
58
|
TEXT=$(printf '%s' "$TEXT" | /usr/bin/perl -CSD -e '
|
|
59
|
-
my $best = 7; my $cand = ""; my $first = "";
|
|
59
|
+
my $best = 7; my $cand = ""; my $first = ""; my $inCode = 0;
|
|
60
60
|
while (<STDIN>) {
|
|
61
|
+
if (/^\s*```/) { $inCode = !$inCode; next; } # 跳过代码块内的 "# 注释"
|
|
62
|
+
next if $inCode;
|
|
61
63
|
if (/^[ \t]*(\#{1,6})[ \t]+(.*)$/) { my $n = length($1); if ($n < $best) { $best = $n; $cand = $2; } }
|
|
62
64
|
elsif ($first eq "" && /\S/) { $first = $_; }
|
|
63
65
|
}
|
package/engine/speech-prompt.ps1
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# speech-prompt.ps1 — Short prompt announcement (synchronous, blocking)
|
|
1
|
+
# speech-prompt.ps1 — Short prompt announcement (synchronous, blocking)
|
|
2
2
|
# Use when a harness/agent needs the user's attention (a question, an approval
|
|
3
3
|
# request). Reads the text through engine/speak.ps1 and waits until it finishes,
|
|
4
4
|
# so the caller knows the announcement was actually spoken.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# speech-summary.ps1 — Reply summary announcement (synchronous, blocking)
|
|
1
|
+
# speech-summary.ps1 — Reply summary announcement (synchronous, blocking)
|
|
2
2
|
# For harnesses with no "reply finished" event (e.g. DSH has no Stop hook): the
|
|
3
3
|
# agent calls this at the end of its final reply.
|
|
4
4
|
#
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-speak",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "Make your AI harness speak — voice announcements for DSH and other AI coding harnesses (Windows SAPI5 + macOS system voices)",
|
|
5
5
|
"main": "adapters/dsh/speech-hook.js",
|
|
6
6
|
"exports": {
|