underworld-oddball/sh-volume-shuff ↗★ 0
sh-volume-shuff
Read the newest turn aloud from the DSH composer: a speaker button in the composer tool row, left of the model selector, that reads from the start of your newest question, scrolls the page to that spot and marks it with a blinking caret on one click, stops on the next, picks any other reading start position by double-clicking the icon and then clicking the spot, and opens a vertical volume mixer (in-page media volume + system output) on press-and-drag-up. 适合需要快速语音朗读对话内容、自定义起点和调节音量的用户。
Install
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:underworld-oddball/sh-volume-shuffREADME
Read the full README ↗sh-volume-shuff
朗读 + 音量,一个按钮。从你最新那条提问的开头或者自选位置开始朗读。· Read aloud from the start of your newest question — or from any spot you pick. One button. 作者 / by 江湖老妖
DeepSeek Harness Web GUI 的输入框工具行里、模型选择框左边的一个扬声器按钮:
- 单击:按下的瞬间图标上的悬浮提示词先消失 → 松开手开始朗读(页面自动翻动到朗读起点,默认=你最新那条提问的第一个字,在起点闪烁一根光标,从那里读到最新回复结尾);再单击停止,标记彻底消失(起点仍在最新那条提问的第一个字,下次点击从那里重读)。
- 按住图标(≥400ms)并松手 → 松手时图标上方提示词变为「选择开始位置后点击 / After Pick → Tap」(按住期间提示词不变);松手后再移动鼠标,提示词才消失 → 在你想开始读的地方单击 → 光标落在那个字上,立刻从那里开始朗读。
- 松手后一直不动(不移动鼠标、也不点正文)超过 5 秒 → 自动取消本次操作,提示词收起、标记消失、鼠标指针回到普通状态。想重来把鼠标移回图标再长按一次。
- 再次单击停止 / 朗读自然结束 → 标记都彻底消失;起点始终是「你最后一次提问的第一个字」,所以下一次点击仍从那里读起。
- 按住并向上拖拽 → 调出竖式音量混音台:页内音量 + 系统音量,各自可静音。
- 点击页面任何其他地方 / Esc → 收起混音台 / 取消挑选(挑选中取消会连光标一起收掉)。
- 挑选起点期间,除了「移动鼠标」,任何其他操作都取消本次挑选:按键、滚轮、拖动、右键、窗口失焦……(详见下表)
[ …其他插件图标 ] [ 🔊 ] [ 模型选择 ▾ ] ← conversation.input.right 整体排在 .model 左边
它注册在 conversation.input.right(「提交键之前的一排紧凑控件」),整体渲染在模型选择框左边、紧挨着模型选择框——模型选择框自己占 conversation.input.model 这个固定座位,位置不随插件增删而变。同一个槽位里装了多个插件图标时,按槽位规则排队:先比 priority,再比 order,两者相同则按插件加载(安装)顺序——先装的靠左。本插件显式设了 order: 40(其他插件一般用默认 0),所以它总是贴着模型选择框待在这一排的最右边。
安装
# 从 npm 安装(推荐)
dsh plugin --profile web add sh-volume-shuff
也可以直接从 GitHub 装(等价,取的是仓库源码):
dsh plugin --profile web add github:underworld-oddball/sh-volume-shuff
收录进社区列表后,还能在 dsh-market(设置 → 插件市场)里搜到并一键安装。
装完重启一次 dsh web(插件在启动时组入 boot graph),然后刷新页面。
交互
| 手势 | 行为 |
|---|---|
| 单击图标 | 按下即收起悬浮提示词 → 松手开始朗读(翻到起点、闪烁光标);再单击 → 停止,标记彻底消失(起点仍是最新提问的第一个字) |
| 按住图标(≥400ms)→ 松手 | 挑起点:松手时图标上方提示词变为「选择开始位置后点击」(悬停气泡同时退场,两个气泡不重叠)→ 松手后移动鼠标,提示词消失 → 页面上单击 → 光标落在该处并立刻开读 |
| 按住并向上拖拽(≥18px) | 调出音量混音台(从图标上方弹出) |
| 挑选中点空(空白/代码块外) | 取消本次挑选(同「其他操作」),提示词与光标一起收起 |
| 挑选中移动鼠标 | 唯一「不取消」的动作:提示词消失,挑选继续,点正文即开读;每移动一次,下面的 5 秒空闲计时重新开始 |
| 挑选全程的鼠标形状 | 整个页面保持选择(十字)形状——移到图标、链接、按钮上也不会变回手型;选定或取消后立即恢复 |
| 挑选中一直不动(≥5 秒) | 自动取消本次操作:提示词收起、指针复原、标记消失 |
| 挑选中其他任何操作(点空白 / 按键 / 滚轮 / 拖动 / 右键 / 失焦) | 立刻取消本次挑选:提示词收起、标记消失,图标恢复为可单击状态 |
| 朗读自然结束 | 标记消失,起点仍是最后一次提问的第一个字 |
| 朗读中再次单击(停止) | 停止播放,标记消失,起点同上 |
| 点击其他地方 / Esc | 收起混音台 / 取消挑选 |
| 长按(≥400ms) | 不朗读;松手即进入挑起点(防误触,也是挑起点手势) |
| 混音台开着时单击图标 | 只收起混音台 |
| 悬停图标 | 三列双语提示(中文在上、灰色英文在下): |
点击朗读 长按选起点 上滑调音量 | |
[Tap: Speak] [Hold: Pick] [Swipe ↑: Volume](一按下就收起) | |
| Enter / Space | 朗读 / 停止 |
| Alt + ↑ | 开关混音台 |
| ← / → | 光标左右移 1 字(挑选模式下同样可微调);空闲时挪过的位置不会成为单击的起点 |
| Shift + ← / → | 光标左右移 20 字 |
朗读中图标变蓝并多一道声波弧(不会再变红——静音状态只交给混音台显示)。挑选起点时光标变琥珀色,朗读时光标是主题色。
朗读起点
- 默认起点:你最新那条提问的第一个字——每次单击/回车都从这里读起(严格规则:即使你之前用 ← / → 把光标挪到别处,单击也不会从那里读)。单击即翻到那里并闪一根光标,读的顺序是「这条提问 → 之后的助手回复(含最新那条)」。
- 自定义起点:按住图标约 0.4 秒后松手——松手的那一刻图标上方出现「选择开始位置后点击」,然后移动鼠标,提示词消失(此时十字光标仍在,挑选状态没变),再在页面上想开始读的地方单击即可。点空白(代码块外面)算「其他操作」,会直接取消这次挑选;此外按任意键、滚轮、拖动、右键或切走窗口也都取消,图标回到可单击状态、光标停止闪烁。Esc 同样取消。松手后一直没动作(约 5 秒)也会自动取消:空闲计时从松手那刻开始,鼠标每移动一次就重新计满 5 秒。取消后标记彻底消失,起点仍是最后一次提问的第一个字(下次点击从那里读)。取消的原因会写进诊断:
gesture:pick-cancel的 detail 是idle-timeout/empty-space/key/wheel/context-menu/context-click/drag/blur/hidden/escape。 - 起点按会话记录保存(
localStorage),刷新页面后仍在;出现新回复时自动回到「最新提问开头」。 - 光标只是标记,不是 MD 编辑器里的选区:它不会选中文字、不影响复制。它只在朗读/挑选期间出现:朗读中闪动(主题色)、挑选中琥珀色;朗读结束(自然读完、手动停止)或取消后一律彻底消失,不残留、不闪烁。
两个音量有什么区别
| 推子 | 管什么 | 不管什么 |
|---|---|---|
| 页内音量 | 本标签页里所有 / 元素的 volume:语音朗读、提示音、网页播放器 | 系统音量、其他 App、其他标签页 |
| 系统音量 | 系统输出音量与静音(macOS osascript,Linux pactl) | 单个页面的元素音量上限 |
两者串联:系统音量是天花板。系统静音时页内开满也没声;页内拉到 0 只静这一页。
界面提示文案
悬停图标与挑选起点时的提示都是中英双标(方便中英混用环境):中文在上(沿用原来的橙色字),英文在下、灰色。空闲气泡是三列手势对齐(每列一个手势,列内中英上下对齐,整块左对齐);挑选/停止气泡只有一列,两行居中互相对齐;两种场景的位置、大小、颜色完全一致——都浮在图标上方。
| 场景 | 中文(上) | 英文(下,灰) |
|---|---|---|
| 悬停图标(空闲) | 三列,中文在上:点击朗读 / 长按选起点 / 上滑调音量 | 每列下方灰色英文:[Tap: Speak] / [Hold: Pick] / [Swipe ↑: Volume] |
| 悬停图标(朗读中) | 点击停止 | Tap: Stop |
| 长按松手后(鼠标未动) | 选择开始位置后点击 | After Pick → Tap |
时序:按住(≥400ms,提示词保持原样)→ 松手(提示词变为「选择开始位置后点击」)→ 移动鼠标(提示词消失)→ 单击正文(开读);这条链上任何一步停住超过 5 秒、或做了别的动作,都会取消(见上手势表)。气泡只负责把操作员送到起点,动鼠标即视为已经在找位置,不该再挡住页面;此后十字光标保持,单击即定起点开读。
反过来,移动鼠标是唯一不会取消挑选的动作:思考、改主意时按个键/滚一下/点右键/点一下空白处即可退出,不会朗读、也不会留下闪烁的光标。取消后提示词收起,想重来把鼠标移回图标再长按一次即可。
朗读实现
-
文本来源是白名单,不是黑名单。真实 DSH DOM 里每个消息节点都带
[data-chat-flow],用data-chat-flow-kind标明类型;实测(dsh 0.1.7-rc.1)出现过这 5 种:kind 内容 是否朗读 user你的提问 ✅ 默认起点就是最新这条的第一个字 assistant-step回复正文( data-chat-group-part="response")✅ assistant-step思考过程( data-chat-group-part="reasoning")❌ 模型草稿,不念 turn-process「已完成工作 用时 4 秒」折叠头 ❌ turn-tail用量 301K tok 02:00❌ tool-call工具调用卡片 ❌ 所有节点都是平铺的兄弟节点(没有嵌套),所以按文档顺序拼接即可。
-
阅读范围:从起点字符起,沿「可读节点序列」一直读到末尾;更早的轮次不读。
-
每个字符都有对应的 DOM 位置:插件把节点按
innerText语义打平成文本串,同时记录「第 n 个字落在哪个文本节点的第几个字符」,所以光标、翻页、按字符微调都精确到字;节点内部的按钮(复制/重试)不计入。 -
合成:按 ~220 字切句块,逐块
POST /dsh-tts/speak(复用 dsh-tts 已配置的 provider 链,例如 Edgezh-CN-XiaoxiaoNeural),顺序播放;未安装 dsh-tts 或某个分块合成失败时,剩下的部分回退浏览器speechSynthesis(不会从头重念)。 -
点按钮时会先暂停页面上正在播放的音频,所以和 dsh-tts 的自动朗读(
speakReplies)不会重叠。
早期版本在这里翻过车:按
data-chat-flow-kind="assistant"找回复(真实值其实是assistant-step),又用「跳过 button/svg」的黑名单排除杂物,结果把turn-tail当成了起点——光标落在时间戳上。现在换成白名单,并补了针对这一条的回归测试。
结构
package.json dsh.bundle.patch + dsh.client.platform=web
cordis.patch.yml bundle 层
lib/index.js host 半:GET/POST /sh-volume-shuff/system、诊断 /sh-volume-shuff/diag
lib/client.js 浏览器半:slot 按钮 + 文本/光标定位 + 朗读器 + 竖式混音台(无需构建步骤)
scripts/test.mjs jsdom 测试:把上面这个 bundle 原样跑在仿真会话 DOM 上
开发
npm install
npm test # 33 项:文本定位、光标三态、长按挑选起点、移动只收气泡、5 秒空闲自动取消、点空白/按键/滚轮/拖动/右键/失焦取消、结束/取消后光标归位静止、双语提示、朗读、回退、混音台
测试不引入 React,而是自带一个只实现 createElement + 四个 hook 的微型运行时,并用 jsdom 跑未改动的 lib/client.js;只有 jsdom 缺的东西(布局几何、音频、语音合成、宿主 HTTP 路由)是仿真的。
要求
-
DSH
>= 0.1.5-rc.3,Web GUI。适配按你要的通道顺序推进:正式版 → next → latest(alpha 版不在适配范围内)。顺序 通道 现在这个 Tag 指向 状态 1 正式版(无后缀稳定版) 还没有——npm 上 26 个版本全是 -rc/-alpha,GitHub 上 10 个 release 全标着 prerelease(deepseek-harness@0.0.1只是占位包)出了即通过,插件无需改动 2 next0.1.7-rc.1已核对(本机 GUI 就是这一版) 3 latest0.1.5-rc.3已核对 ⚠️
latest比next旧(0.1.5-rc.3 =0.1.5-rc.3的原因:写成按「最新那版」收窄的范围(比如>=0.1.7-rc.1)会把latest关在门外;写成^0.1.5又会在 0.1.x 上永远不匹配0.1.7-rc.1,把next` 也踢掉。 正式版发出来之后,位于版本号下方的通道一律被这个范围涵盖,不需要再动。 -
插件代码里没有任何按版本分支:
window.__ModuleLoader__.load({id, factory})、ctx.slots.inject/ctx.slots.register、conversation.input.right这些接口在latest(0.1.5-rc.3) 与next(0.1.7-rc.1) 上逐个比对过,签名一致。 ⚠️ 但消息 DOM 的取值必须实测,不能只看接口名:早期版本就栽在这里——以为 kind 是user/assistant,实际是user/assistant-step/turn-process/turn-tail/tool-call。现在的取值是在真实页面上 dump 出来的(见下表)。 -
系统音量路由:macOS(
osascript,内置)/ Linux(pactl)。其他平台该行置灰并显示原因。 -
可选:dsh-tts —— 没装也能用,只是回退到浏览器语音。
已知边界
- 页内音量是母音量,会覆盖插件自己设过的
element.volume;默认 100% 时行为不变。 - 浏览器
speechSynthesis没有音量 API,只能在静音时cancel()。 - 混音台路由只接受回环地址或同源请求,避免任意网页改你机器音量。
- 「起点」是按字符记的:如果回复在起点之后被重新渲染成不同的文字(例如流式输出还没结束),起点会按同一节点重新夹取;节点整个换掉时退回默认起点(最新提问开头)。
许可
MIT © 江湖老妖