DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-tts-service

d

dsh-tts-service

Manifest valid

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

hasBundlePatch

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 idWhat it doesOn by default
tts-serviceThe service ctx.ttsService: provider registry, selection (provider, voice per provider, language), synthesis, voice listing.yes
tts-doubaoProvider: Doubao (Volcengine) TTS 2.0.no
tts-bailianProvider: Alibaba Bailian / Model Studio (CosyVoice, Qwen-Audio-TTS, Qwen3-TTS).no
tts-openai-compatibleProvider: any endpoint compatible with POST /audio/speech.no
tts-sayAgent tools tts_service_say, tts_service_voices and tts_service_select, plus the output registry ctx.ttsSay.yes
tts-speaker-windowsOutput: 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.

EntryConfig fieldDefault key nameUsed for
tts-doubaoapiKeyEnvVOLCENGINE_TTS_API_KEYSynthesis (an API key from the Volcengine console, "API Key 管理").
tts-doubaoaccessKeyEnv, secretKeyEnvVOLCENGINE_ACCESS_KEY, VOLCENGINE_SECRET_KEYOptional: your Volcengine account AK/SK, only to list the complete voice catalogue.
tts-bailianapiKeyEnvDASHSCOPE_API_KEYSynthesis and listing cloned voices (same name as the official DashScope SDK).
tts-openai-compatibleapiKeyEnvOPENAI_COMPATIBLE_TTS_API_KEYSent 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:

FieldMeaning
providerDefault provider id. Empty means none.
voicesDefault 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.
languageDefault 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)

FieldDefaultNotes
providerIddoubaoChange it if you run two copies.
resourceIdseed-tts-2.0Sent as X-Api-Resource-Id.
apiKeyEnv, accessKeyEnv, secretKeyEnvsee Keys
sampleRate2400016-bit mono PCM.
maxChars300Per request.
timeoutMs60000
  • 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)

FieldDefaultNotes
modelcosyvoice-v3-flashe.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.
regioncn-beijingcn-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.
apiKeyEnvDASHSCOPE_API_KEY
sampleRate24000CosyVoice / Qwen-Audio-TTS; Qwen3-TTS is fixed at 24000.
maxChars600 for Qwen3-TTS, 2000 otherwise
timeoutMs60000Covers 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:

    ModelFollows 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-flashcloned and designed voices only; system voices accept only fixed instruction templates
    cosyvoice-v3-plus, cosyvoice-v2no
    qwen3-tts-instruct-*yes (sent as instructions)
    qwen3-tts-flashno

    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:

FieldDefaultNotes
nameOpenAI 兼容接口Display name.
baseUrlrequirede.g. https://example.com/v1; the plugin calls {baseUrl}/audio/speech.
modelrequired
apiKeyEnvOPENAI_COMPATIBLE_TTS_API_KEYEmpty: no Authorization header.
instructionsfalseSet 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.
maxChars4096
timeoutMs60000

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

ParameterDescription
segments1–12 segments, spoken in order. Each has text and an optional tone (one short natural-language description, at most 40 characters).
contextOptional: the user's last sentence (at most 60 characters). Providers that support it use it to pick the tone.
targetauto (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:

FieldDefaultDescription
outputs["speaker-windows"]Outputs tried in order for target: "auto"; the first available one is used.
maxChars300Total characters per call.
retentionDays0Keep 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.

ParameterDescription
voiceRequired: the voice id.
providerOptional: 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:

ProviderModel / resourceVoiceNote
Doubaoseed-tts-2.0ICL_uranus_zh_female_wenrounvshen_tobtone description applied
Alibaba Bailiancosyvoice-v3-flashsystem voice
Alibaba Bailianqwen3-tts-flashsystem voice
Alibaba Bailianqwen-audio-3.1-tts-flashsystem 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.

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
providerthe upstream default provider
voices (per provider)the upstream per-provider voice setting
languagethe 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.

Comments

Loading…