dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
English | 中文
Read assistant replies aloud in DeepSeek Harness, one continuous voice per reply.
Each finished assistant message gets a play control in its action row. Press it and the reply is spoken through a text-to-speech service; press it again and it stops. A settings row in General owns the service, the voice, and the tuning.
The whole reply goes out in one request and the host streams the audio back while the service is still synthesizing it. Playback starts once a few seconds are buffered — about a second for a typical reply — and from there it is one continuous voice to the last word: the pre-buffer is what keeps it from running dry in the middle of a sentence.
Service · Key needed · Voices Microsoft Edge (default) · no · the service publishes its full catalog, every language it serves
Adding a provider means adding one folder under src/providers/ and one line in src/providers/registry.ts — the settings screen builds its fields from what the provider declares.
Every handshake carries a short-lived Sec-MS-GEC token: the current time in Windows file-time ticks, rounded down to five minutes, digested together with the well-known client token. In October 2026 the endpoint moved to a second generation of that token — a patched SHA-256 paired with a 64-bit checksum — and began answering the first generation with a bare 403 (Edge TTS WebSocket error: Unexpected server response: 403), which is what a reply that never speaks looks like from the browser. src/providers/edge/gec.ts reproduces the accepted generation; server.ts hands it to msedge-tts by replacing the library's private URL builder, which is also what pins the token version the endpoint now expects. If read-aloud stops working again, test that token first: it is the only part of this plugin that can expire without the code changing.
dsh web, or the desktop app — version 0.2.0-rc.2 or newer.The version floor is not decorative. The plugin's preferences are its own Cordis entry config, exposed to the settings screen as a form through ctx.configForms. Releases before 0.2.0-rc.2 had plugins register settings namespaces themselves (settings.register) and handed the browser a ctx.settingsScope; both services are gone.
This package declares dsh.bundle.patch, so it installs as a profile bundle: the package manager links it, the launcher appends it to the profile's dsh.profile.bundles, and the bundle's own patch layer inserts the plugin row. Installing also enables it — in DeepSeek Harness, installing a plugin and enabling it in a profile are separate steps, and a bundle collapses the two.
Desktop app — sidebar → Plugins → Add plugin, then give one of:
@nakamuraia/dsh-plugin-speech (the npm registry)github:NakamuraIA/dsh-plugin-speechFrom the CLI — the same operation, profile-scoped:
dsh plugin --profile web add github:NakamuraIA/dsh-plugin-speechRestart the surface afterwards. The host half claims its HTTP routes when the plugin loads, a desktop profile is owned by the app, and a newly installed out-of-tree bundle is not mounted by a live recomposition — an app-internal row added to a patch does reload immediately, a package that has to come from the profile's node_modules does not.
cordis.patch.yml is the same patch the bundle applies, so it also works as a temporary command-line overlay:
dsh web --patch ./cordis.patch.ymlDo not do both. A profile patch that inserts ui-speech while the bundle is selected collides with duplicate loader entry id; the profile layer is for overriding the row's config, which is what the settings screen writes.
The row name is the package name and also the browser module id the built bundle registers under; the row id (ui-speech) is the settings namespace the config form is keyed by. Nothing works if either one drifts, so change the two together.
Settings opens on General, where a Read aloud row owns:
The row edits the plugin's own entry config, so the chosen values persist in the profile patch beside the entry rather than in a settings document. Resetting a field restores the schema default, and a write the Host refuses leaves the stored value untouched while the row says so.
Tables and pasted spreadsheets are never read, whichever way Skip code is set. That switch decides only whether fenced and inline code are read: with it off, the code itself is spoken and just the fences go. Links keep their label and lose their target; emoji, quotes, and standalone symbols are dropped. Sentence punctuation is kept, because the voice uses it for pauses.
browser host service
─────── ──── ───────
click ─ ▶ POST /speech/speak { text }
select provider from
its own entry config
────────────────────────── ▶ synthesize
◀ ─ audio bytes stream back ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
buffer four seconds, then play while it keeps arrivingSynthesis lives on the host because a browser cannot hold an API key and cannot reach most services across CORS. The browser only posts text and plays bytes. Every editable leaf of the entry config is .volatile(), so a settings write lands in the running host without reloading it — the host reads the same value the browser wrote.
Playback goes through MediaSource and starts once four seconds of audio are buffered, then plays while the rest arrives. That pre-buffer is the whole design: streaming straight away stalled whenever the service synthesized slower than the voice spoke, and waiting for the whole file was seamless but could take ten seconds. Four seconds is early enough to feel immediate — measured at about a second on a warm connection — and late enough that nothing runs dry mid-sentence. When a link in that chain is missing (no MediaSource, a container the engine refuses, a buffer that dies mid-reply) the reply is still played, as one whole file assembled from the bytes in hand.
Only a reply larger than one request may carry (20 000 characters) is spoken sentence by sentence, which is where a pause between sentences can still be heard.
A voice can also end every sentence with a long silence of its own — pt-BR-AntonioNeural, the schema default, leaves about 0.9 s at each one, while the multilingual voices leave none. That silence is in the audio the service returns, not in how this plugin plays it.
This repository builds on its own against the published @deepseek-ai/* packages; no harness checkout is involved:
pnpm install
pnpm build # tsc → lib/types, tsdown → lib/index.js + lib/client.js
pnpm test # vitest
pnpm typecheck # sources and specsbuild/plugin-bundle.ts is the client-bundle preset. It emits the window.__ModuleLoader__.load handoff, keeps every module-table row a require, compiles CSS Modules into one injected style tag, and fails the build on a cross-plugin value import. It is a self-contained copy of the harness preset, which is what lets the package be built here.
Three specs under tests/checkout/ compose the real client registries, which only exist as sources inside a harness checkout; tests/checkout/README.md explains why and how to run them. Everything else, including the client wiring, runs through pnpm test.
Config — the schemastery schema with .volatile() leaves — and no longer calls settings.register; the synthesis route reads the entry config it was handed at apply time.ctx.configForms.get('ui-speech') instead of ctx.settingsScope.bind(...), and a write reports acceptance directly (Promise) instead of being checked against the settled section.@deepseek-ai/dsh-client-ui-primitives are named by weight (IconPlayOutlineRegular), not by drawn size (IconPlayOutline16).MIT. The Edge provider drives Microsoft's read-aloud endpoint through msedge-tts (MIT); that endpoint is not a documented, supported Microsoft API, so treat availability as best effort.