dsh-speak 1.7.0 → 1.7.2

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
@@ -315,7 +315,7 @@ document):
315
315
  | `longTextMessage` | `本次播报内容较长,请自行阅读。` | the fixed prompt spoken for over-long text in `message` mode (editable in the UI) |
316
316
  | `maxChars` | platform | per-utterance ceiling. **macOS default 0 (`say` has no ceiling); Windows default 300** (SAPI fails silently beyond ~375-470) |
317
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) |
318
+ | `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
319
  | `announceTurnEnd` | `false` | announce "第 N 轮对话完成/中断/异常结束" on turn end (`turn/end`) |
320
320
  | `announceCommandDone` | `false` | announce when a command finishes or fails (`command/done`) |
321
321
  | `announceGoalChange` | `false` | announce goal created/updated/completed/paused/resumed (`goal/change`, objective head) |
package/README.zh-CN.md CHANGED
@@ -295,7 +295,7 @@ speak.ps1 -Text "…" -Volume 50 -Rate 1 -MaxChars 300 -LongTextMessage "本次
295
295
  | `longTextMessage` | `本次播报内容较长,请自行阅读。` | `message` 模式下超长文本改念的固定提示语(UI 可编辑) |
296
296
  | `maxChars` | 平台相关 | 引擎单次朗读上限。**macOS 默认 0(`say` 无上限);Windows 默认 300**(SAPI 超过约 375-470 字会静默失败) |
297
297
  | `volume` | `50` | 仅 Windows(0-100);macOS 音量跟随系统 |
298
- | `rate` | `0` | `0` = 引擎默认(Windows SAPI 刻度如 1macOS words-per-minute 175) |
298
+ | `rate` | `0` | 语速:Windows SAPI 刻度(-10 到 10,0 = 正常,推荐 0 / 稍快 1-3);macOS words-per-minute(默认 175,稍快 200) |
299
299
  | `announceTurnEnd` | `false` | 回合结束时播报"第 N 轮对话完成/中断/异常结束"(`turn/end`) |
300
300
  | `announceCommandDone` | `false` | 命令执行完成/失败时播报(`command/done`) |
301
301
  | `announceGoalChange` | `false` | 目标创建/更新/完成/暂停/恢复时播报(`goal/change`,含目标标题前 40 字) |
@@ -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
- child = spawn('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', cfg.engine, '-File', tmp, '-Volume', String(cfg.volume), '-Rate', String(cfg.rate > 0 ? cfg.rate : 1), '-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' })
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
@@ -45,6 +45,8 @@ window.__ModuleLoader__.load({
45
45
  replayFullReadHint: '手动重播时跳过超长文本的标题截断,完整朗读。',
46
46
  queueAllMessages: '入队所有消息',
47
47
  queueAllMessagesHint: '每一条 assistant 消息到达即入队朗读(中间消息也读,FIFO),而不只读最终回复。',
48
+ throttleMs: '合并延迟(毫秒)',
49
+ throttleMsHint: '回复文本等待多久才播报(合并同一回复的多步消息)。',
48
50
  cleanMarkdown: '清理 Markdown',
49
51
  cleanMarkdownHint: '把 Markdown 转成自然的语音文本。',
50
52
  markdownCleaning: 'Markdown 清理',
@@ -61,6 +63,13 @@ window.__ModuleLoader__.load({
61
63
  codeBlockReplacementTextHint: '代码块被替换时朗读的文本。',
62
64
  maxChars: '最大朗读字数',
63
65
  maxCharsHint: 'macOS 上 0 = 不限;Windows 保留安全默认值。',
66
+ volume: '音量',
67
+ volumeHint: '仅 Windows(0-100);macOS 音量跟随系统。',
68
+ rate: '语速',
69
+ rateHintWin: 'Windows SAPI 语速(-10 到 10,0 = 正常;推荐 0,稍快 1-3)。',
70
+ rateHintMac: 'words-per-minute(wpm),数值越大语速越快:默认 175,稍快 200。',
71
+ engine: '引擎路径',
72
+ engineHint: '自定义引擎脚本路径;留空自动解析(包内引擎 → ~/.dsh/hooks)。',
64
73
  longTextBehavior: '超长文本行为',
65
74
  longTextBehaviorHint: '超过正数上限时如何处理。',
66
75
  longTextMessageOption: '朗读替换提示语',
@@ -69,6 +78,8 @@ window.__ModuleLoader__.load({
69
78
  fixedPromptHint: '超长文本(message 模式)时朗读的提示语。',
70
79
  announceApprovals: '播报审批',
71
80
  announceApprovalsHint: '朗读审批请求。',
81
+ stripApprovalPrefix: '剥离审批前缀',
82
+ stripApprovalPrefixHint: '剥离审批原因里的固定英文模板前缀(escalate sandbox to …: )。',
72
83
  announceQuestions: '播报提问',
73
84
  announceQuestionsHint: '朗读 ask_user_question 问题。',
74
85
  questionGap: '问题间隔(毫秒)',
@@ -102,6 +113,8 @@ window.__ModuleLoader__.load({
102
113
  replayFullReadHint: 'When replaying, read the full text instead of the heading fallback.',
103
114
  queueAllMessages: 'Queue All Messages',
104
115
  queueAllMessagesHint: 'Also speaks intermediate assistant messages in a FIFO queue, not only the final reply.',
116
+ throttleMs: 'Merge Delay (ms)',
117
+ throttleMsHint: 'How long a reply waits before being announced (merges multi-step messages).',
105
118
  cleanMarkdown: 'Clean Markdown Formatting',
106
119
  cleanMarkdownHint: 'Converts Markdown into natural speech text.',
107
120
  markdownCleaning: 'Markdown cleaning',
@@ -118,6 +131,13 @@ window.__ModuleLoader__.load({
118
131
  codeBlockReplacementTextHint: 'Used when a code block is replaced.',
119
132
  maxChars: 'Max Speech Characters',
120
133
  maxCharsHint: '0 is unlimited on macOS; Windows keeps its safe default.',
134
+ volume: 'Volume',
135
+ volumeHint: 'Windows only (0-100); macOS volume follows the system.',
136
+ rate: 'Speech Rate',
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.',
139
+ engine: 'Engine Path',
140
+ engineHint: 'Custom engine script path; empty = auto-resolve (package engine → ~/.dsh/hooks).',
121
141
  longTextBehavior: 'Long Text Behavior',
122
142
  longTextBehaviorHint: 'When a positive maximum is exceeded.',
123
143
  longTextMessageOption: 'Read replacement message',
@@ -126,6 +146,8 @@ window.__ModuleLoader__.load({
126
146
  fixedPromptHint: 'The prompt spoken when the text is too long (message mode).',
127
147
  announceApprovals: 'Announce Approvals',
128
148
  announceApprovalsHint: 'Announces approval requests.',
149
+ stripApprovalPrefix: 'Strip Approval Prefix',
150
+ stripApprovalPrefixHint: 'Strip the fixed English template prefix (escalate sandbox to …:) from approval reasons.',
129
151
  announceQuestions: 'Announce Questions',
130
152
  announceQuestionsHint: 'Announces ask-user questions.',
131
153
  questionGap: 'Question Gap (ms)',
@@ -254,7 +276,15 @@ window.__ModuleLoader__.load({
254
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))
255
277
  }
256
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')))) }
257
- function SettingInput({ label, value, disabled, numeric, onChange, hint }) { const id = `dsh-speak-${label.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`; return e(Field, { label: `${label}:`, hint }, e('div', { className: 'dsh-speak-input-row' }, e(Input, { id, value: String(value), disabled, inputMode: numeric ? 'numeric' : undefined, onChange: event => onChange(event.target.value) }))) }
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
+ }
258
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)))) }
259
289
  function MarkdownCleaning({ value, clean, disabled, set }) {
260
290
  const [open, setOpen] = React.useState(true); const controlsDisabled = disabled || !clean
@@ -270,16 +300,22 @@ window.__ModuleLoader__.load({
270
300
  const [eventsOpen, setEventsOpen] = React.useState(false)
271
301
  const snapshot = useSettings(); if (snapshot.status !== 'ready' || !snapshot.value) return null
272
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 || '')
273
304
  return e('section', { 'aria-label': t('settingsAria') }, e('h3', null, t('settingsTitle')), e('p', null, t('settingsIntro')),
274
305
  e(Toggle, { label: t('masterSwitch'), value: value.enabled !== false, disabled, onChange: next => set('enabled', next), hint: t('masterSwitchHint') }),
275
306
  e(Toggle, { label: t('automaticSpeech'), value: value.automaticSpeech !== false, disabled, onChange: next => set('automaticSpeech', next), hint: t('automaticSpeechHint') }),
276
307
  e(Toggle, { label: t('replayFullRead'), value: value.replayFullRead === true, disabled, onChange: next => set('replayFullRead', next), hint: t('replayFullReadHint') }),
277
308
  e(Toggle, { label: t('queueAllMessages'), value: value.queueAllMessages === true, disabled: disabled || value.automaticSpeech === false, onChange: next => set('queueAllMessages', next), hint: t('queueAllMessagesHint') }),
309
+ e(SettingInput, { label: t('throttleMs'), value: value.throttleMs == null ? 1500 : value.throttleMs, numeric: true, disabled, onChange: next => { if (/^\d+$/.test(next)) set('throttleMs', Number(next)) }, hint: t('throttleMsHint') }),
278
310
  e(Toggle, { label: t('cleanMarkdown'), value: clean, disabled, onChange: next => set('cleanMarkdownFormatting', next), hint: t('cleanMarkdownHint') }), e(MarkdownCleaning, { value, clean, disabled, set }),
279
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') }),
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') }),
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') }),
314
+ e(SettingInput, { label: t('engine'), value: value.engine || '', disabled, onChange: next => set('engine', next), hint: t('engineHint') }),
280
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') }] }),
281
316
  e(SettingInput, { label: t('fixedPrompt'), value: value.longTextMessage || '本次播报内容较长,请自行阅读。', disabled: disabled || value.longTextMode !== 'message', onChange: next => set('longTextMessage', next), hint: t('fixedPromptHint') }),
282
317
  e(Toggle, { label: t('announceApprovals'), value: value.announceApprovals !== false, disabled, onChange: next => set('announceApprovals', next), hint: t('announceApprovalsHint') }),
318
+ e(Toggle, { label: t('stripApprovalPrefix'), value: value.stripApprovalPrefix !== false, disabled: disabled || value.announceApprovals === false, onChange: next => set('stripApprovalPrefix', next), hint: t('stripApprovalPrefixHint') }),
283
319
  e(Toggle, { label: t('announceQuestions'), value: value.announceQuestions !== false, disabled, onChange: next => set('announceQuestions', next), hint: t('announceQuestionsHint') }),
284
320
  e(SettingInput, { label: t('questionGap'), value: value.questionGapMs == null ? 2000 : value.questionGapMs, numeric: true, disabled: disabled || value.announceQuestions === false, onChange: next => { if (/^\d+$/.test(next)) set('questionGapMs', Number(next)) }, hint: t('questionGapHint') }),
285
321
  e(DisclosureRow, { icon: null, title: t('optionalEvents'), open: eventsOpen, expandable: true, onToggle: () => setEventsOpen(!eventsOpen), expandOnRowClick: true }, e('div', { style: { paddingLeft: '22px' } },
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 = engine default
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
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-speak",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
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": {