dsh-speak
Manifest valid★ 1Read text aloud in the DeepSeek Harness (DSH): a speak tool for the agent, per-message and select-to-read buttons in the Web UI, and per-language voices — via the macOS say command.
dsh-speak
Read text aloud in the DeepSeek Harness (DSH), on the machine running it, with the macOS say command.
English | 中文
| Entry | Where | What it does |
|---|---|---|
speak tool | the model can call it | reads text aloud, or stops with stop: true |
| 🔊 message action | the action strip under every finalized assistant reply | click to read that reply, click again to stop |
| 🔊 select-to-read bubble | above any text you select on the page | "select and read", like select-to-translate |
| ⚙ reading settings | speaker-gear button in the composer dock | per-language voice, rate, and Han-character bias |
Chinese, English, Japanese, and Korean are detected per character, and every script run is read by its own system voice in its own say process. One reading plays at a time; a new request replaces it.
Audio comes out of the machine running DSH, never the browser. No network calls, no API keys, no model downloads.
Requirements
- macOS — playback shells out to
say. (Linux withspd-say, or a browser-sidespeechSynthesisvariant, would be a separate provider; not implemented.) - DSH 0.2.x — the desktop app, or any profile whose page is served to a browser.
- Node 20+ only if you want to run the offline test.
Install
A. Through the plugin manager (recommended)
-
Open DSH → Plugins (Settings → Plugins, or the Plugins panel in the sidebar).
-
Install bundle and paste the repository URL:
https://github.com/RinChanNOWWW/dsh-speakA git URL, a local absolute path, and an npm name are all accepted.
-
The bundle patch inserts one row (
speak) and the browser half is served automatically — there is no build step and no dependencies to install. -
Use it. If the buttons do not appear, reload the page once.
The in-app plugin manager is the intended path for the desktop app, which is why the
dsh pluginCLI refuses that profile. For any other profile the CLI works too — see below.
B. Manual install into a profile
Use this when you want the plugin to live outside the profile's node_modules.
-
Copy the package next to the profile's patch file:
$DSH_HOME/profiles/<profile>/plugins/dsh-speak/{package.json,index.js,client.js} -
Add one row to
$DSH_HOME/profiles/<profile>/cordis.patch.yml:- insert: - id: speak name: ./plugins/dsh-speak/index.jsThe
namemust point at the entry file: the loader resolves a.-prefixed name withimport(new URL(name, baseUrl)), so a directory cannot be imported, and the browser half is found by walking up from that file to the nearestpackage.json. -
Restart the app, or let the profile reload — an added row is picked up live.
-
To change host code later: write a new file (
index.v2.js…) and remove then re-insert the row. A mounted row is not re-imported when only itsnamechanges, and a?v=query in the name is rejected by the loader.deploy.shautomates exactly this.
C. A dsh web profile
dsh plugin --profile web add https://github.com/RinChanNOWWW/dsh-speak
That installs the package; the bundle also has to be listed in that profile's package.json → dsh.profile.bundles for its patch to be applied. The in-app plugin manager does both steps for you.
Use
Ask the assistant
"read this aloud" / "读一下这段" / "stop reading" / "别读了"
speak argument | Meaning |
|---|---|
text | text to read; markdown is stripped; required unless stop is true |
stop | true stops the current reading instead of starting one |
language | zh / en / ja / ko — biases ambiguous text (bare Han characters) and acts as the voice fallback; does not force one voice for everything |
voice | an exact macOS voice name (Tingting, Kyoko, Yuna, Samantha…); reads the whole text in that one voice |
rate | words per minute, 80–500 |
wait | wait for the whole reading instead of returning as soon as it starts |
It returns the character count, the languages found, the voices used, and the per-run sequence, e.g. zh:Tingting -> ko:Yuna -> ja:Kyoko -> en:Samantha.
From the interface
- Message action — 🔊 under a finalized reply. While it reads, the icon highlights and its sound waves pulse; click again to stop. If another reply is being read it dims (clicking switches to this one), and a failure shows red with a retry hint.
- Select to read — select text anywhere that is not an input; a small bubble appears above the selection (below it when there is no room, always clamped to the viewport). Click it to read; the bubble stays and becomes "stop reading". Selections inside inputs, textareas, and
contenteditableregions are ignored, as is anything over 20,000 characters. - Settings — the speaker-gear button in the composer dock, or the gear inside the selection bubble.
Stopping a reading
| Way | Notes |
|---|---|
| the same bubble, now "停止朗读 / Stop reading" | it no longer dismisses itself when it starts |
| the floating stop pill, bottom right | appears while anything plays — including a reading the agent started through the tool — because the page keeps a slow status heartbeat |
| ⌘⇧S (Ctrl/Cmd+Shift+S) | works anywhere, including inside the composer; rebindable in DSH's shortcut settings as speak.stop |
| ask the assistant | speak with stop: true |
Settings
The panel offers, per language (中文 / English / 日本語 / 한국어), the voices actually installed on the machine — recommended ones first — plus a rate slider, a Bare Han switch (whether Han characters without kana read as Chinese or Japanese), a preview button, and save state.
They are stored in $DSH_HOME/dsh-speak.settings.json and also drive the speak tool, so the assistant reads with the voices you picked. Explicit tool arguments override them for one reading.
curl -s $DSH_WEB_URL/api/dsh-speak/settings # installed voices + current settings
How it works
Two halves in one package (one loader row):
-
index.js— the host half. Registers thespeaktool and four routes on the composition's web server:Route Purpose POST /api/dsh-speak{action:"speak", text?/sessionId+messageId}or{action:"stop"}GET /api/dsh-speak/statuswhat is playing now: source, message identity, languages, voices, sequence, progress GET/POST /api/dsh-speak/settingsread/write preferences; lists installed voices GET/POST /api/dsh-speak/clientthe browser half's report channel (see Diagnostics) -
client.js— the browser half, a prebuilt bundle in the format the client module loader expects (window.__ModuleLoader__.load({id, factory})). It registers four slots:conversation.chat.assistant-actions, two entries inshell.overlay, andconversation.composer.dock.
Language handling:
- Kana makes a Han/kana run Japanese; Hangul is Korean; Latin is English; a kana-free Han run follows the Bare Han setting.
- Sentence-ending punctuation and script changes split runs, so
中文句子。日本語の文。keeps two voices. - Latin runs shorter than
MIN_LATIN_RUN(12 characters of their own script, default) are folded into the surrounding language, so an acronym inside Chinese prose does not flip the voice for three words. CJK and Hangul runs are never absorbed. - Playback is one
sayprocess per run, started when the previous one exits:sayhas no inline voice switch (its[[...]]commands cover rate, pitch, volume, and input mode only), and an unsupported[[voice:…]]is read out as literal text.
Diagnostics
The browser half reports what it loaded and did to GET /api/dsh-speak/client (a 200-entry ring buffer in the host): hello (build + registered entries), style, mounted, visibility, rendered (the measured box, whether it is inside the viewport, and which ancestor would break position: fixed), error (with the component stack), plus settings and playback transitions. That is how "I clicked and nothing appeared" gets answered without devtools.
curl -s $DSH_WEB_URL/api/dsh-speak/client
Troubleshooting
| Symptom | Check |
|---|---|
| nothing is read | curl -s $DSH_WEB_URL/api/dsh-speak/status — speaking should be non-null; then .../client for a browser-side failure |
| buttons missing | reload the page; a changed client.js only reaches the page when the row is recomposed |
| old host code still runs | a mounted row is not re-imported on a name change; deploy a new filename and remove/re-insert the row (deploy.sh) |
| wrong voice | the settings panel: a per-language pin wins, then the recommended voice for that language, then the system default |
| bare Han read in the wrong language | set Bare Han to 中文 or 日文 — kana-free Han text is genuinely ambiguous |
unknown session (422) | the message belongs to a session this Harness process has not loaded |
Development
pnpm install # only for @deepseek-ai/dsh-tools, used by index.js and the test
pnpm test # offline host-half suite; spawns the real `say`, so it makes noise
./deploy.sh # deploy into the running profile and wait for the reload (dev helper)
deploy.sh writes a new versioned entry file, removes and re-inserts the profile row, waits for the status route to come back, and keeps one entry file. It reads $DSH_PROFILE_DIR and falls back to $DSH_HOME/profiles/desktop.
The test suite stubs the tool registry, session store, and web server, and points the settings file at a temporary DSH_HOME, so it neither needs DSH nor touches your preferences.
Security notes
- Routes only accept
POST/GETand requireapplication/json, so a cross-origin page hits a CORS preflight it cannot pass, and a simple request is rejected with 415. Text never leaves the machine — it goes straight tosay. - The plugin deliberately does not apply DSH's connection trust fence to its routes, so they can be exercised with
curl. It only ever makes the host speak.
License
Comments
Loading…
From the same category
by awesome-dsh-plugin
A curated list of plugins for DeepSeek Harness (dsh) · DeepSeek Harness 插件精选列表
★ 18.4k
CC0-1.0
Python
Oct 11, 2026
by 0xsline
DeepSeek Harness (DSH) ecosystem: curated plugins, tools, and infrastructure from dsh-external/hub and the public dsh-plugin topic.
★ 1.2k
CC0-1.0
Python
Oct 10, 2026
by pax-beehive
Open-source CLI, schemas, resolver, and DSH agent tools for DSH Plugin Hub
★ 406
MIT
TypeScript
Oct 6, 2026
by xiajiajun516
DeepSeek Harness (DSH) backup & restore plugin — export, import, migrate and sync your complete DSH configuration, plugins, MCP servers, skills and workspace. One-click migration to another machine.
★ 177
MIT
TypeScript
Oct 11, 2026
dsh plugin --profile web add dsh-config-managerby yjh051108
推荐组件(非必须):DeepSeek Harness 运行时注入器;已随 dsh-routing-suite 单仓库化保留,本仓库继续维护/发布。
★ 165
TypeScript
Sep 18, 2026
dsh plugin --profile web add @dsh-external/dsh-super-injectorby jigjoy-ai
A CLI that turns a goal into a pull request - and a sandbox for testing concurrent AI coding agents on the Mozaik runtime.
★ 124
MIT
TypeScript
Oct 2, 2026