voyager
Nagi-ovo
Enhancement suite for Gemini, AI Studio, Claude & ChatGPT — plus a prompt manager for any websites, DeepSeek Harness included. / 面向 Gemini、AI Studio、Claude 与 ChatGPT 的增强套件;其中的提示词管理器可用于任意网站,如 DeepSeek Harness。
wangzhanchao883/dsh-hold-to-talk
Hold-to-talk voice input for the DeepSeek Harness web composer: hold the mouse on the input box, speak, release to insert the text into the draft. Local SenseVoice ASR via sherpa-onnx: no API key, offline, audio never leaves the machine. | DSH 长按说话语音输入插件:输入框上按住鼠标说话,浮层边说边出字,松手把文字写进输入框,上滑取消;识别在本机跑,免密钥、离线、音频不出本机。
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:wangzhanchao883/dsh-hold-to-talk
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
Hold-to-talk voice input for the DeepSeek Harness Web composer — the WeChat-desktop gesture: hold the mouse on the input box, speak, see the running transcript float above it, release to drop the text into the draft. Recognition runs locally (SenseVoice via sherpa-onnx): no API key, offline, audio never leaves the machine.
DSH Web 输入框的"长按说话"语音输入 —— 微信电脑版同款操作:在输入框上按住鼠标说话,浮层里边说边出字,松手把文字写进输入框,上滑取消。识别全部在本机跑(SenseVoice + sherpa-onnx),免密钥、离线、音频不出本机。
┌─ composer ──────────────────────────────────┐
│ [hold the mouse, don't move, 400ms] │
│ ↓ │
│ ┌──────────────────────────────────┐ │
│ │ ● ▁▃▅▂▇▃▁ 3s │ ← overlay, fixed above the composer
│ │ release to send · slide up to cancel │ │
│ │ 开饭时间早上9点至下午5点 │ ← live transcript (preview only)
│ └──────────────────────────────────┘ │
│ [release] → final text appended to the draft │
└──────────────────────────────────────────────┘

按住鼠标说话 → 浮层里边说边出字 → 松手把文字写进输入框(35 秒实录,完整视频)
| 1. Hold on the input box / 长按进入录音 | 2. Release, text lands in the draft / 松手写入草稿 | 3. Plugin card / 插件卡片 |
|---|---|---|
![]() |
![]() |
![]() |
| The overlay appears with "release to send · slide up to cancel" / 浮层出现,提示"松开发送 · 上滑取消" | The transcript is appended to the composer; the live preview stayed in the overlay / 识别文字写入输入框(边说边出字的预览只留在浮层里) | Listed in the plugin list after install / 安装后在插件列表可见 |
Esc cancels; a 60-second hold auto-finishes.| Gesture | Behaviour |
|---|---|
| Hold the mouse on the input box, still, for ≥400 ms | Recording starts (no microphone request and no recording indicator before this) |
| Move >8 px, or create a text selection, or release early | Treated as normal editing — nothing happens |
| Speaking | The tail window is re-decoded every 1.5 s for the overlay preview |
| Release | The full audio is decoded and the text is appended to the draft |
| Hold and slide up >60 px | Overlay turns red ("release to cancel"); releasing discards the audio |
Esc while holding |
Cancel immediately |
| Hold longer than 60 s | Auto-finish |
dsh web).>=22.19 or >=24 on the machine running DSH.http://127.0.0.1, which is a secure context, so getUserMedia
is allowed.dsh plugin --profile web add dsh-hold-to-talk
# or from a checkout / GitHub:
dsh plugin --profile web add /path/to/dsh-hold-to-talk
# then restart dsh web and hard-refresh the page (Ctrl+Shift+R)
On page load the plugin starts downloading the model in the background
(model.int8.onnx, 228 MB, from hf-mirror.com, resumable). While it downloads,
holding the composer shows "语音模型准备中 x%". To fetch it ahead of time:
npm run model:fetch
The model is cached under ~/.dsh/hold-to-talk/models/ (change with modelDir).
Namespace dsh-hold-to-talk (written to settings.yaml by the Web settings
panel), or override in the profile's cordis.patch.yml:
- id: dsh-hold-to-talk
name: dsh-hold-to-talk
config:
holdThresholdMs: 400 # long-press threshold
cancelSlidePx: 60 # slide-up-to-cancel distance
interimIntervalMs: 1500 # live-preview cadence
maxWindowSec: 6 # preview decode window; the final always uses all audio
minHoldMs: 250 # shorter holds are discarded (mis-taps)
maxHoldMs: 60000 # auto-finish limit
autoSend: false # insert only; never auto-send by default
language: auto # auto / zh / en / ja / ko / yue
useItn: true # inverse text normalization ("二零二六" -> "2026")
numThreads: 2 # native engine threads
mirror: https://hf-mirror.com
modelDir: "" # empty = ~/.dsh/hold-to-talk/models
Priority: settings panel > cordis.patch.yml > plugin defaults.
browser half (lib/client.js) host half (lib/index.js)
┌───────────────────────────────┐ HTTP ┌────────────────────────────────┐
│ gesture capture-phase events │ │ exact routes via webServer │
│ mousedown→400ms→move/select │ │ POST /asr ?mode=interim|final │
│ capture AudioWorklet→16k PCM │────────────▶│ |drop │
│ ring buffer / linear resample │ │ GET /health[?prepare=1] │
│ overlay conversation.input. │◀────────────│ GET /config │
│ overlay slot │ JSON │ per-hold buffer: interim decodes│
│ insert inputActions.setDraft │ │ the tail window, final uses all │
└───────────────────────────────┘ │ worker_threads ↓ │
│ sherpa-onnx SenseVoice (int8) │
└────────────────────────────────┘
Engineering decisions worth knowing:
sherpa-onnx-node (native, multi-threaded) is
~2.5× faster than sherpa-onnx (WASM, single-threaded) — see the table below.
WASM lives in optionalDependencies; the worker falls back automatically.AudioContext is created inside the mousedown gesture. Creating it
400 ms later gets it suspended by autoplay policy and no audio flows. The
capture node feeds a zero-gain sink before destination so it is pulled
without being audible (otherwise: feedback howl).offsetParent/getClientRects()
made long-press die permanently after the first successful dictation (v0.1.0).Decode time on the author's machine (Windows, Node 22.22):
| Audio | WASM (single thread) | Native (numThreads=2) |
|---|---|---|
| 5.6 s | 1663 ms | 704 ms |
| 11.2 s | 4403 ms | 1123 ms |
| 22.4 s | 8015 ms | 3310 ms |
Model load into the worker happens once: 2158 ms (WASM) / 3565 ms (native).
npm run check # syntax check all runtime modules
npm test # check + three offline suites
npm run model:fetch # 228MB model (needed by the full suites)
npm run bench # decode-time benchmark
None of the suites need a browser or a DSH restart:
| Script | Covers |
|---|---|
tools/client-smoke.mjs |
Client registration contract: module id, only react, exports, slot id/order, single style injection, safe without slots |
tools/host-test.mjs |
Real HTTP server over the real routes: config/health, final, interim increments, drop, short-audio skip, cross-site rejection, 405/404 |
tools/client-test.mjs |
The real interaction logic in Node (browser APIs stubbed): click-does-nothing, move-cancels, hold→preview→draft, slide-up cancel, mis-tap discard, mic denial, reuse after cancel, three consecutive holds |
tools/decode-test.mjs |
Model + worker + recognition smoke test (tools/zh.wav) |
Without the model, the model-dependent cases skip themselves, so npm test is
green on a clean clone — which is exactly what CI runs.
sec-fetch-site), so a random web page
cannot post audio at the local server.hf-mirror.com by default). No telemetry.MIT — see LICENSE.
给 DeepSeek Harness Web 界面加微信电脑版同款语音输入:在输入框上按住鼠标说话,浮层里边说边出字,松手把文字写进输入框,按住上滑则丢弃。识别在你自己的机器上跑(SenseVoice + sherpa-onnx),不需要任何 API key,音频不出本机。
Esc 取消、按满 60 秒自动定稿。| 操作 | 行为 |
|---|---|
| 按住输入框不动 ≥400ms | 进入录音(此前不申请麦克风权限、不点亮录音指示灯) |
| 按住期间移动 >8px、或产生文本选区、或提前松手 | 判定为普通编辑,完全不触发(拖选文字、点光标落位一切照旧) |
| 说话中 | 每 1.5 秒重解码一次尾部窗口,浮层实时预览 |
| 松手 | 用完整音频定稿,文字追加到输入框已有内容末尾 |
| 按住上滑 >60px | 浮层变红"松开取消",松手即丢弃 |
| 按住 Esc | 立即取消 |
| 按住超过 60 秒 | 自动定稿(防止忘记松手) |
两个刻意的设计取舍:
dsh web)。>=22.19 或 >=24。http://127.0.0.1,属安全上下文,getUserMedia 可用。dsh plugin --profile web add dsh-hold-to-talk
# 或从本地目录 / GitHub:
dsh plugin --profile web add /path/to/dsh-hold-to-talk
# 装完重启 dsh web,然后 Ctrl+Shift+R 硬刷新页面
⚠️ Windows 上跑
dshCLI 要用 DSH 自己那个 Node。CLI 入口靠import.meta.main自执行(Node 22.18+/24 才有),在旧版 Node 上会静默退出、什么都不做——看起来安装成功了,实际 profile 里没有任何变化。用运行中的 DSH 同款解释器:& 'D:\Program Files\QClaw\<版本>\resources\node\node.exe' ` 'C:\Users\Administrator\AppData\Roaming\QClaw\npm-global\node_modules\@deepseek-ai\dsh\lib\bin.js' ` plugin --profile web add D:\workout\deepseekharness\dsh-hold-to-talk
页面加载后插件会在后台自动下载模型(model.int8.onnx 228MB + tokens.txt,走 hf-mirror.com,支持断点续传)。下载期间长按会看到"语音模型准备中 x%",只会发生一次。想提前下好:
npm run model:fetch
模型缓存在 ~/.dsh/hold-to-talk/models/(可用 modelDir 改)。
设置命名空间 dsh-hold-to-talk(Web 设置面板写入 settings.yaml),或在 profile 的 cordis.patch.yml 里按 id 覆盖:
- id: dsh-hold-to-talk
name: dsh-hold-to-talk
config:
holdThresholdMs: 400 # 长按判定阈值
cancelSlidePx: 60 # 上滑取消的位移
interimIntervalMs: 1500 # 实时预览间隔
maxWindowSec: 6 # 预览解码的滑动窗口上限;定稿始终用完整音频
minHoldMs: 250 # 短于此时长直接丢弃(防误触)
maxHoldMs: 60000 # 单次长按上限,到点自动定稿
autoSend: false # 定稿后是否自动发送(默认只写进输入框)
language: auto # auto / zh / en / ja / ko / yue
useItn: true # 逆文本规整("二零二六年"→"2026年")
numThreads: 2 # 原生引擎线程数
mirror: https://hf-mirror.com
modelDir: "" # 留空 = ~/.dsh/hold-to-talk/models
优先级:设置面板 > cordis.patch.yml > 插件默认值。
Esc 直接丢弃。dsh-hold-to-talk/
├── package.json # dsh.bundle / dsh.client 声明、依赖、脚本
├── cordis.patch.yml # 宿主行注册 + 全部默认配置
├── lib/
│ ├── index.js # 宿主:三条 HTTP 路由 + 引擎管理 + 模型下载 + 会话缓冲 + 设置命名空间
│ ├── asr-worker.mjs # worker 线程内解码(原生优先、WASM 兜底)
│ ├── model-cache.js # 模型下载缓存(镜像、断点续传、进度)
│ └── client.js # 浏览器:长按手势 + AudioWorklet 采集 + 浮层 + 上屏
├── tools/ # 离线自测与开发脚本(不随 npm 包发布)
│ ├── client-smoke.mjs # 客户端注册契约
│ ├── host-test.mjs # 真实 HTTP 路由端到端
│ ├── client-test.mjs # 在 Node 里驱动真实交互逻辑
│ ├── decode-test.mjs # 模型+worker+识别冒烟
│ ├── bench.mjs # 解码耗时基准
│ ├── fetch-model.mjs # 拉模型
│ ├── market-note.mjs # 写 DSH 市场卡片备注的小工具
│ ├── market-note.txt # 上面的文案
│ └── zh.wav # 测试音频(k2-fsa sherpa-onnx 示例音频,Apache-2.0)
├── .github/workflows/test.yml
├── LICENSE
├── README.md
├── CHANGELOG.md
└── CONTRIBUTING.md
浏览器半身 (lib/client.js) 宿主半身 (lib/index.js)
┌───────────────────────────────┐ HTTP ┌────────────────────────────────┐
│ 手势层 document 捕获阶段 │ │ webServer.register 精确路由 │
│ mousedown→400ms→移动/选区取消 │ │ POST /asr ?mode=interim|final │
│ 采集层 AudioWorklet→16k f32 PCM │────────────▶│ |drop │
│ 环形缓冲/线性重采样 │ │ GET /health[?prepare=1] │
│ 浮层 conversation.input. │◀────────────│ GET /config │
│ overlay 槽 │ JSON │ 会话缓冲:interim 解码尾部窗口, │
│ 上屏 inputActions.setDraft() │ │ final 用完整音频 │
└───────────────────────────────┘ │ worker_threads ↓ │
│ sherpa-onnx SenseVoice(int8) │
└────────────────────────────────┘
几个关键工程决定:
sherpa-onnx-node(原生、可多线程)比 sherpa-onnx(WASM 单线程)快约 2.5 倍,实测见下表。WASM 放在 optionalDependencies 里,原生加载失败时 worker 自动降级。sherpa-onnx 的 WASM 构建启用了 NODERAWFS,相对路径会落到 emscripten 的虚拟 CWD 里读不到(实测)。suspended,音频流不起来。采集节点串一个 0 增益 sink 再进 destination,既被拉动又不外放(否则啸叫)。offsetParent/getClientRects() 判"我这个实例可见吗",会在第一次识别成功后长按彻底失效(v0.1.0 实测踩到这个坑)。改成"从输入框往上找同时包含浮层节点的祖先",与 CSS 无关;单实例时兜底,多实例且都对不上宁可不响应(避免把文字写进别的会话)。mousedown 被新旧两份逻辑各接一次。本机(Windows + Node 22.22)解码耗时:
| 音频长度 | WASM 单线程 | 原生 numThreads=2 |
|---|---|---|
| 5.6s | 1663ms | 704ms |
| 11.2s | 4403ms | 1123ms |
| 22.4s | 8015ms | 3310ms |
模型加载(首次进入 worker):WASM 2158ms / 原生 3565ms,只发生一次。
http://127.0.0.1 属安全上下文,可用;若通过局域网 IP 访问需 https。~/.dsh 所在盘需留出空间。npm run check # 四个运行模块的语法检查
npm test # 上面 + 三套离线测试
npm run model:fetch # 拉模型(228MB,全量测试需要)
npm run bench # 解码耗时基准
三套自测都不需要重启 DSH、不需要浏览器:
| 脚本 | 覆盖 |
|---|---|
tools/client-smoke.mjs |
client 半身注册契约:__ModuleLoader__ id、仅依赖 react、导出形态、槽位/id/order、样式只注入一次、slots 缺失不崩 |
tools/host-test.mjs |
用桩 ctx 起真 HTTP 服务打真实路由:config/health、final 定稿、interim 增量、drop、短音频跳过、跨站拒绝、405/404 |
tools/client-test.mjs |
在 Node 里驱动真实交互逻辑(桩掉 AudioContext/Worklet/getUserMedia):普通点击不触发、判定期移动取消、长按→预览→定稿入草稿、上滑取消、超短丢弃、麦克风拒绝、Esc 后状态机可复用、连续 3 轮长按 |
tools/decode-test.mjs |
模型 + worker + 识别链路冒烟(自带 tools/zh.wav) |
没有模型时依赖识别的用例会自动跳过,所以在干净克隆上 npm test 也是绿的——CI 跑的就是这个。
sec-fetch-site 拦截,任意网页无法向本机服务投递音频。hf-mirror.com)下载模型;无遥测。dsh plugin --profile web remove dsh-hold-to-talk
# 重启 dsh web;模型缓存在 ~/.dsh/hold-to-talk/,可自行删除
MIT — 见 LICENSE。
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: wechat、privacy、sensevoice、voice、voice-input、wechat-desktop。