DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

RinChanNOWWW /

RinChanNOWWW/dsh-speak

Verified

Read 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.

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@19b3cdd5

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 with spd-say, or a browser-side speechSynthesis variant, 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)

  1. Open DSH → Plugins (Settings → Plugins, or the Plugins panel in the sidebar).

  2. Install bundle and paste the repository URL:

    https://github.com/RinChanNOWWW/dsh-speak
    

    A git URL, a local absolute path, and an npm name are all accepted.

  3. The bundle patch inserts one row (speak) and the browser half is served automatically — there is no build step and no dependencies to install.

  4. 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 plugin CLI 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.

  1. Copy the package next to the profile's patch file:

    $DSH_HOME/profiles/<profile>/plugins/dsh-speak/{package.json,index.js,client.js}
    
  2. Add one row to $DSH_HOME/profiles/<profile>/cordis.patch.yml:

    - insert:
        - id: speak
          name: ./plugins/dsh-speak/index.js
    

    The name must point at the entry file: the loader resolves a .-prefixed name with import(new URL(name, baseUrl)), so a directory cannot be imported, and the browser half is found by walking up from that file to the nearest package.json.

  3. Restart the app, or let the profile reload — an added row is picked up live.

  4. 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 its name changes, and a ?v= query in the name is rejected by the loader. deploy.sh automates 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 contenteditable regions 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 the speak tool 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/status what is playing now: source, message identity, languages, voices, sequence, progress
    GET/POST /api/dsh-speak/settings read/write preferences; lists installed voices
    GET/POST /api/dsh-speak/client the 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 in shell.overlay, and conversation.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 say process per run, started when the previous one exits: say has 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/GET and require application/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 to say.
  • 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

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 19b3cdd5ae19

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout