DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

loveandbeepbrother /

loveandbeepbrother/dsh-tts-service

Verified

Multi-provider text-to-speech (TTS) service for DeepSeek Harness (dsh): Doubao, Alibaba Bailian and OpenAI-compatible providers, user-chosen voices, per-segment tone.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: master@bec25f79

dsh-tts-service

English | 简体中文

Multi-provider text-to-speech (TTS) service for DSH: Doubao TTS 2.0, Alibaba Bailian and OpenAI-compatible providers register independently and the user picks the voice; the agent can speak with per-segment tone, and other plugins reuse the same service.

One package, several plugin entries that you switch on and off separately on the DSH plugin page:

Entry id What it does On by default
tts-service The service ctx.ttsService: provider registry, selection (provider, voice per provider, language), synthesis, voice listing. yes
tts-doubao Provider: Doubao (Volcengine) TTS 2.0. no
tts-bailian Provider: Alibaba Bailian / Model Studio (CosyVoice, Qwen-Audio-TTS, Qwen3-TTS). no
tts-openai-compatible Provider: any endpoint compatible with POST /audio/speech. no
tts-say Agent tools tts_service_say, tts_service_voices and tts_service_select, plus the output registry ctx.ttsSay. yes
tts-speaker-windows Output: play on the PC speakers when you are at the computer (Windows only; registers nothing elsewhere). yes

There is no default provider and no default voice. Until you pick one, tts_service_say fails with VOICE_NOT_SELECTED and tells the agent where to pick it.

This version synthesizes whole clips (no streaming).

Requirements

  • DSH >=0.2.1-rc.3 <0.3.0-0 (prereleases included). Tests run against the 0.2.1-rc.5 packages.
  • Node.js 22 or newer (the one that ships with DSH is fine).
  • An account and API key for at least one provider (see below). Synthesis is billed by the provider.
  • For the speaker output: Windows with Windows PowerShell (powershell.exe, present by default).

Install

dsh plugin add @loveandbeepbrother/dsh-tts-service

or from GitHub: dsh plugin add github:loveandbeepbrother/dsh-tts-service.

Quick start

  1. On the plugin page, switch on one provider entry (tts-doubao, tts-bailian or tts-openai-compatible) and fill in its form.
  2. Store its API key in DSH's credential store (see Keys).
  3. Pick a voice: ask the agent "list the available voices" and then "use voice X", or set it yourself in the settings of Text-to-Speech Service (tts-service): provider = the provider id (e.g. doubao), and an entry in voices: provider id → voice id. See Choosing a voice.
  4. Ask the agent to say something.

Keys

Configuration never contains secret values, only key names. The value comes from DSH's credential service (ctx.credentials, the key store on the settings page) or, when no credential service is loaded, from the environment DSH was launched with. Keys are read on every request, so a rotated key applies immediately. The settings page shows only whether a key is configured.

Entry Config field Default key name Used for
tts-doubao apiKeyEnv VOLCENGINE_TTS_API_KEY Synthesis (an API key from the Volcengine console, "API Key 管理").
tts-doubao accessKeyEnv, secretKeyEnv VOLCENGINE_ACCESS_KEY, VOLCENGINE_SECRET_KEY Optional: your Volcengine account AK/SK, only to list the complete voice catalogue.
tts-bailian apiKeyEnv DASHSCOPE_API_KEY Synthesis and listing cloned voices (same name as the official DashScope SDK).
tts-openai-compatible apiKeyEnv OPENAI_COMPATIBLE_TTS_API_KEY Sent as Authorization: Bearer. Leave the field empty for local endpoints that need no key.

You can rename any of them, e.g. to keep keys for two endpoints apart.

Choosing a voice

The selection lives in the tts-service settings and is saved to your profile without a restart:

Field Meaning
provider Default provider id. Empty means none.
voices Default voice per provider: provider id → voice id. Voice ids only make sense within their provider, so switching from Doubao to Bailian and back keeps both choices.
language Default language code, auto by default. Must be one the provider declares.

There are three ways to set it; all of them write the same settings and take effect immediately:

  1. Settings page: edit provider and voices in the Text-to-Speech Service settings.
  2. Ask the agent: "which voices are there?" (tts_service_voices), then "switch to …" (tts_service_select). The agent's reply states the old and new voice and what to enter to change it back; you can also change it back on the settings page.
  3. From a plugin: await ctx.ttsService.configure({ provider: 'doubao', voices: { doubao: 'voice-id' } }).

tts_service_voices (or ctx.ttsService.voices(id) for plugins) lists voices with their languages and whether they follow tone descriptions. Lists that cannot be complete say so; you can always type a voice id by hand.

Errors are explicit: a provider that is not registered, a language it does not support, text longer than its maxChars, or a format it cannot produce all fail. The service never silently switches to another provider.

Providers

Doubao TTS 2.0 (tts-doubao, provider id doubao)

Field Default Notes
providerId doubao Change it if you run two copies.
resourceId seed-tts-2.0 Sent as X-Api-Resource-Id.
apiKeyEnv, accessKeyEnv, secretKeyEnv see Keys
sampleRate 24000 16-bit mono PCM.
maxChars 300 Per request.
timeoutMs 60000
  • Tone descriptions and the "context" (the user's last sentence) are combined into one voice instruction and sent as additions.context_texts. Only Doubao 2.0 system voices (ids containing uranus) follow it; cloned voices do not.
  • Voice listing: without AK/SK you get a static table of 2.0 system voices transcribed from the official voice list, marked incomplete. With AK/SK the plugin calls the official ListSpeakers API (signed with HMAC-SHA256, service speech_saas_prod, region cn-beijing).
  • Languages: auto, zh, en, ja (sent as explicit_language).

Alibaba Bailian (tts-bailian, provider id bailian)

Field Default Notes
model cosyvoice-v3-flash e.g. cosyvoice-v3-flash, cosyvoice-v3-plus, cosyvoice-v2, cosyvoice-v3.5-flash, qwen-audio-3.1-tts-flash, qwen3-tts-flash. Voices only work with their own model.
region cn-beijing cn-beijing, ap-southeast-1, cn-hongkong, us-east-1, eu-central-1, ap-northeast-1. Keys, workspaces and models are per region.
workspaceId — The first label of your API host (e.g. llm-xxx). When set, the workspace domain {workspaceId}.{region}.maas.aliyuncs.com is used; otherwise the older DashScope domain. Required for eu-central-1 and ap-northeast-1. Alibaba Cloud says the DashScope domain gets no new features after 2026-09-30, so setting it is recommended.
apiKeyEnv DASHSCOPE_API_KEY
sampleRate 24000 CosyVoice / Qwen-Audio-TTS; Qwen3-TTS is fixed at 24000.
maxChars 600 for Qwen3-TTS, 2000 otherwise
timeoutMs 60000 Covers the synthesis call and the audio download.
  • CosyVoice and Qwen-Audio-TTS use /api/v1/services/audio/tts/SpeechSynthesizer; Qwen3-TTS uses /api/v1/services/aigc/multimodal-generation/generation. Both return a 24-hour audio URL, which the plugin downloads.

  • Tone descriptions (instruction), per the official docs:

    Model Follows free-form tone descriptions
    qwen-audio-* yes, system, cloned and designed voices (a few English voices excepted)
    cosyvoice-v3.5-* yes (this model only has cloned and designed voices)
    cosyvoice-v3-flash cloned and designed voices only; system voices accept only fixed instruction templates
    cosyvoice-v3-plus, cosyvoice-v2 no
    qwen3-tts-instruct-* yes (sent as instructions)
    qwen3-tts-flash no

    CosyVoice instructions are limited to 100 characters (CJK characters count as two).

  • Voice listing: system voices from a static table transcribed from the official CosyVoice, Qwen-Audio-TTS and Qwen3-TTS lists; your cloned and designed voices for the configured model come from the official voice-enrollment list_voice API.

OpenAI-compatible endpoints (tts-openai-compatible, provider id openai-compatible)

No vendor, URL or voice is preset. Fill in:

Field Default Notes
name OpenAI 兼容接口 Display name.
baseUrl required e.g. https://example.com/v1; the plugin calls {baseUrl}/audio/speech.
model required
apiKeyEnv OPENAI_COMPATIBLE_TTS_API_KEY Empty: no Authorization header.
instructions false Set to true if the endpoint accepts the instructions parameter.
voices [] Hand-written voice table, used when GET {baseUrl}/audio/voices is not available. Each entry can set instructions: false.
languages ["auto"] Only used to validate the selected language; not sent.
maxChars 4096
timeoutMs 60000

The plugin always requests response_format: "wav" and needs 16-bit PCM WAV back. To use two endpoints, add a second row to your profile with another providerId.

This is not "the OpenAI integration". Note that OpenAI's own three speech models are scheduled to be retired on 2027-01-06; check the vendor's current model list before pointing this entry at it.

The tools

tts_service_say

Parameter Description
segments 1–12 segments, spoken in order. Each has text and an optional tone (one short natural-language description, at most 40 characters).
context Optional: the user's last sentence (at most 60 characters). Providers that support it use it to pick the tone.
target auto (default) or the id of one output.

Each segment is synthesized separately as 16-bit PCM, joined with 0.25 s of silence into one WAV file and handed to an output. The result (a JSON string) has route (the output id, or kept), file, seconds, detail, the outputs that were unavailable and why, and tone: when a segment's tone was not applied it says so, so the agent can stop writing tones for that voice.

tts-say settings:

Field Default Description
outputs ["speaker-windows"] Outputs tried in order for target: "auto"; the first available one is used.
maxChars 300 Total characters per call.
retentionDays 0 Keep audio of the last N days (today included); 0 keeps everything.

Audio files are stored at <DSH home>/tts-say/audio/<YYYY-MM-DD>/<HHMMSS>-<id>.wav (local date) and are not deleted automatically unless you set retentionDays. <DSH home> is ~/.dsh unless you set DSH_HOME.

tts_service_voices

Lists the current selection and, per provider, its voices. Optional parameters: provider, language, search (substring of id, name or description) and refresh. At most 60 voices per provider are shown. Pressing stop ends it right away, even while a provider is still answering.

tts_service_select

Changes the voice for the user. The tool description tells the agent to use it only when you ask to change the voice or explicitly agree to one; because that is only an instruction to the model, the change is also made visible and reversible in the result itself.

Parameter Description
voice Required: the voice id.
provider Optional: the provider id; defaults to the current default provider (an error asks for it when there is none).

Before saving it checks the provider's voice list. If the list is complete and the id is not in it, nothing is saved and the agent is told to look it up with tts_service_voices. If the list is incomplete or cannot be fetched, the id is saved as typed, with a note that it is not in the known list. The result always states the selection before and after (with voice names when known), that it is already in effect, that you can see and change it in the Text-to-Speech Service settings, and the exact values that restore the previous state.

Outputs

speaker-windows counts as available when there was keyboard or mouse input in the last presentMinutes (default 5) and the screen is not locked (Windows PowerShell: GetLastInputInfo, LogonUI.exe). Playback uses System.Media.SoundPlayer, one clip at a time; the tool does not wait for it to finish.

Write your own provider

A provider is a plugin that injects ttsService and registers an object implementing TtsProvider (types in index.d.ts):

export const name = 'my-tts'
export const inject = ['ttsService']

export function apply(ctx) {
  ctx.effect(() => ctx.ttsService.register({
    info: {
      id: 'my-tts', name: 'My TTS', location: 'cloud',
      languages: ['auto', 'en'], formats: ['pcm16', 'wav'],
      instructions: false, context: false, maxChars: 1000, voiceListing: true,
    },
    async listVoices(query, signal) {
      return { voices: [{ id: 'v1', name: 'Voice 1', languages: ['en'] }], complete: true }
    },
    async synthesize(input, signal) {
      // input: { text, voice, language, instructions?, context?, formats }
      // Pick the first of input.formats you can produce; honour `signal`.
      const pcm = await callMyApi(input, signal)
      return { audio: pcm, format: 'pcm16', sampleRate: 24000, channels: 1, seconds: pcm.length / 48000, instructionsApplied: false }
    },
  }))
}
  • register throws if the id is taken and returns an async disposer; ctx.effect calls it when your plugin unloads. Unregistering rejects new requests, aborts the signal of running ones and waits for them.
  • Report instructionsApplied: true only when the tone description was actually used.
  • The service does not convert formats. The bundled tool asks for pcm16.

Calling the service from another plugin: inject = ['ttsService'], then const audio = await ctx.ttsService.synthesize(ctx.ttsService.resolve({ text, formats: ['wav'] }), signal).

Write your own output

An output is a plugin that injects ttsSay and registers a VoiceOutput, for example to send the clip to a chat app:

export const name = 'my-voice-output'
export const inject = ['ttsSay']

export function apply(ctx) {
  ctx.effect(() => ctx.ttsSay.registerOutput({
    id: 'my-chat',
    name: 'My chat app',
    async available(session) {
      return isBound(session.id) ? { yes: true } : { yes: false, why: 'no chat bound to this session' }
    },
    async deliver(session, file, { seconds }) {
      await sendVoice(session.id, file)   // file: absolute path of a 16-bit PCM WAV
      return { ok: true, detail: 'sent to my chat app' }
    },
  }))
}

Then add its id to outputs in the tts-say settings, e.g. ["speaker-windows", "my-chat"].

Tested combinations

Real synthesis was checked on 2026-11-04 with these combinations, all returning pcm16 at 24 kHz:

Provider Model / resource Voice Note
Doubao seed-tts-2.0 ICL_uranus_zh_female_wenrounvshen_tob tone description applied
Alibaba Bailian cosyvoice-v3-flash system voice
Alibaba Bailian qwen3-tts-flash system voice
Alibaba Bailian qwen-audio-3.1-tts-flash system voice

Known limitations

These can only be confirmed by real use; reports are welcome.

  • Bailian qwen3-tts-instruct-*: where instructions goes in the HTTP body (input.instructions) is inferred from the official SDK example, not from the HTTP reference.
  • Bailian without workspaceId: whether the old DashScope domain still serves newer models after 2026-09-30 is not confirmed. Set workspaceId if a model is rejected.
  • Per-request character limits are conservative guesses where the docs give none: Doubao 300, Bailian SpeechSynthesizer (CosyVoice, Qwen-Audio-TTS) 2000. Both can be changed with maxChars.
  • Doubao: the accepted explicit_language values (zh-cn, en, ja are sent) and the format of the language field in ListSpeakers responses are not confirmed.
  • The static voice tables are transcribed from the official pages and may be incomplete or contain mistakes, so lists built from them (Doubao without AK/SK, all Bailian lists) are always marked incomplete and tts_service_select accepts ids that are not in them. You can always type a voice id by hand.

Pricing and terms

Every provider bills you directly under its own terms; this plugin adds no fees.

  • Doubao / Volcengine: billing overview, Volcengine Terms of Service.
  • Alibaba Bailian (Model Studio): model pricing, agreements.
  • OpenAI-compatible endpoints: whatever the endpoint's operator charges and requires.

Privacy

The text, tone descriptions and context are sent to the provider you selected. Doubao requests go to openspeech.bytedance.com (and open.volcengineapi.com for listing); Bailian requests go to your region's DashScope or workspace domain, and the audio is downloaded from the URL it returns; OpenAI-compatible requests go to your baseUrl. Nothing else leaves the machine unless an output sends the audio.

When DSH ships its own textToSpeech service

DSH's speech-recognition docs list speech synthesis as future work. If it ships, it will almost certainly be called ctx.textToSpeech. Cordis service names are global, so this package uses ctx.ttsService and both can run side by side. We will then publish a major version that:

  1. registers the three providers on the upstream service, keeping capabilities the upstream provider interface lacks (tone descriptions, context, voice listing) as extensions until upstream adds them;
  2. makes the tools and outputs call the upstream service;
  3. retires ctx.ttsService with no forwarding layer. Your selection moves over as follows, and a one-time migration script will be provided:
Here (tts-service settings and provider entries) Upstream
provider the upstream default provider
voices (per provider) the upstream per-provider voice setting
language the upstream language
each provider's apiKeyEnv (and Doubao's AK/SK names) the same key names; stored values in the credential store need no change

The interface here deliberately mirrors the upstream speech-recognition service, so provider implementations should change very little.

Development

npm install
npm test

Tests use node --test. Every network call goes through a fake fetch, and PowerShell is stubbed, so the tests make no network requests and play no sound. The Volcengine signature is checked against the worked examples in the official signing guide.

License

MIT © 2026 loveandbeepbrother

DSH is developed by DeepSeek. This plugin is an independent project and is not affiliated with DeepSeek, Volcengine, Alibaba Cloud or OpenAI.

—/ 5

No ratings yet

Verified DSH bundle

Commit bec25f79d8d1

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