Text → audiobook mp3. Not read literally, but firstarranged: different kinds of content get different voices, voice instructions are added at key turning points, then synthesized with Volcano Engine Doubao seed-tts-2.0.
Synthesis tool:~/code/volcano-tts/tts.py(first source ~/code/.env)。
Iron rules
- Never feed text with 【】/[ ] annotations directly to the bare API — the bare API will read the annotations out verbatim (confirmed by testing). Always go through
tts.py, which splits the annotations into segments and converts them intocontext_textsvoice instructions. - Only 2.0 voices can be used (
_uranus_bigttssuffix) — moon/mars/tob voices throw an error under the seed-tts-2.0 resourceresource ID is mismatched。 - Before synthesis, show the user the arranged reading script(unless the user says to just produce it) — arrangement is re-creation, and the voice assignment and instructions are worth confirming. Skip this step in headless/automated scenarios.
Workflow
1. Get the content and read it through carefully
- Plain text/file: read directly.
- URL: WebFetch; for SPA pages (such as docs.volcengine.com), use the browse skill to render and then get the body text.
- VoiceDrop article: the voicedrop MCP's
read_article。
While reading through, mark: body narration / direct quotes (quotation marks, blockquote) / plain-language gripes and inner monologue / data, lists, tables / headings and sections.
2. Arrange and rewrite into a reading script
This is the core step; it isrewritingnot transcription:
- Remove all visual residue: markdown symbols, links, image captions, footnote numbers.
- Rewrite tables, lists, and data into spoken sentences ("three reasons: first...").
- Do not read headings word for word; fold them into transition sentences, or carry them with a pause + breath.
- Change long sentences that are too formal into colloquial ones, but keep the author's word style.
- Assign voices by the nature of the content(see the voice table): one main voice runs through the body; quotes switch to a quote voice; gripes/plain language switch to an interjection voice. Voice switching is a "format signal" for listeners, equivalent to a quote block visually.
- Do not switch voices frequently: generally cap it at 2~3 voices, and switch only where the nature of the content truly changes.
3. Add voice instructions (sparingly)
Add before the sentence [心理活动、细腻表情、肢体动作等描述], such as [放慢,一字一顿,点出要害]。
- Add only where needed: emotional turns, rhythm changes, emphasis, imitation of a quote's tone. Add none to straightforward passages; rely on global instructions as a fallback.
- Empirical density: at most one every 3~5 sentences; a calm narrative passage can have none at all.
- Each annotation is a segment split (one API call + splice point); splitting too finely makes the speech flow disjointed.
- Write annotations as performance cues addressed to the reader (inner activity/expression/action are all fine), not as explanations addressed to the listener.
- Instructions must be dramatic and emotional to be effective(tested): the model follows emotion/voice-type instructions very strongly (crying tone, whisper, excited shouting, like a judge pronouncing a sentence, changes on the order of +50% duration), and follows subtle stage directions ("the tone sinks", "with a trace of regret") and mechanically precise instructions ("pause for one second", "slow down by half") very weakly. In wording, err on the side of exaggeration: "please press the voice down to near a whisper, lean close to the mic, as if revealing a secret" is far better than "lower the voice".
4. Reading script format (tts.py --script)
# 注释行
@voice narrator zh_male_yuanboxiaoshu_uranus_bigtts
@voice quote zh_male_yizhipiannan_uranus_bigtts
@voice casual zh_male_fanjuanqingnian_uranus_bigtts
@narrator
[语气平静从容,像老朋友聊天]先讲一个真事。……他公开断言:
@quote
[带着当年的笃定与体面]股价已经站上了一个永久的高原。
@narrator
几天后,市场开始了最惨烈的下跌。
@casual
[像随口吐槽]这人判断力真差。
5. Synthesis and acceptance
source ~/code/.env
python3 ~/code/volcano-tts/tts.py -f script.txt --script -o out.mp3 \
-i "这是一段有声书朗读,自然口语化,像讲故事,不要播音腔"
-iGlobal instructions are mandatory, setting the overall tone;--speech-rate、--subtitle(word-level timestamps) as needed.- Acceptance:
afinfo out.mp3Check whether the duration matches the word count (Chinese is about 4-5 characters/second); if this is the first time you've modified a tool or you have doubts, use~/.claude/skills/wjs-transcribing-audio/scripts/volc_asr_stream.pyto spot-check a passage and confirm the annotations are not being read aloud. - Use SendUserFile to send the mp3 to the user.
Voice list (tested and available, all support voice instructions)
| Purpose | voice_type | Name |
|---|---|---|
| Main narration voice (male, default) | zh_male_yuanboxiaoshu_uranus_bigtts | 渊博小叔 |
| Main narration voice (female, alternative) | zh_female_zhixingnv_uranus_bigtts | 知性女声 |
| Narration (conversational/podcast feel) | zh_male_shenyeboke_uranus_bigtts | 深夜播客 |
| Commentary tone | zh_male_cixingjieshuonan_uranus_bigtts | 磁性解说男声 |
| Quotations/famous quotes/translations | zh_male_yizhipiannan_uranus_bigtts | 译制片男 |
| Plain speech/roasting/inner monologue | zh_female_shuangkuaisisi_uranus_bigtts | 爽快思思(用户定:大白话用女声,与男声旁白区分) |
| Plain speech (male alternative) | zh_male_fanjuanqingnian_uranus_bigtts | 反卷青年 |
| General female voice | zh_female_vv_uranus_bigtts | Vivi |
More 2.0 voices:https://www.volcengine.com/docs/6561/1257544 (only recognizes the _uranus_bigtts suffix).
Common pitfalls
- Instructions silently fail (everything sounds the same) → three confirmed pitfalls,
tts.pyall fixed: ①context_texts/section_idmust be embedded inadditions(JSON string); if placed at the top level ofreq_params, it is silently ignored; ②context_textswhen multiple are passed, only the first takes effect—in-segment instructions must be on their own line and cannot be listed side by side with global ones (a global one placed first will override all in-segment ones); ③ paraphrasing packaging ("read according to this instruction: X") dilutes the effect; sending the annotated original text directly is strongest. When in doubt, use the "extremely slow crying tone" instruction for an A/B duration comparison (should be +25% or more); baseline synthesis is deterministic (the same sentence with the same parameters has exactly the same duration); a difference of less than 5% means it did not take effect. - Annotations are read aloud → you forgot to use
tts.py's annotation parsing, or used a bracket format it does not recognize (supports【】and[ ])。 resource ID is mismatched with speaker related resource→ a non-2.0 voice was used.- Obvious sense of broken tone in the middle of the output → annotations split segments too finely; merge annotations and reduce switching.
- Hearing "asterisk" or "hashtag" → the orchestration step did not clean markdown thoroughly (tts.py does not enable markdown filtering; it relies on clearing during orchestration).