深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
中
ESC

Adding an AI Podcast to the Blog: Turning Articles into Two-Host Dialogue Audio with Doubao TTS

English 阅读中文版

Introduction

While tidying up the blog last weekend, a question suddenly came to mind: no matter how long an article is, readers can realistically only finish the first few hundred words. This is especially true for technical articles—thousands of words long, and many people swipe away halfway through. If I could turn articles into audio, letting visitors listen during their commute or while doing chores, wouldn’t that be a better way to consume content?

With this idea, I surveyed a range of speech synthesis options and finally settled on the Doubao Podcast TTS from Volcano Engine. Its feature is that it can generate a two-host podcast format, where two AI hosts discuss the article’s content in a question-and-answer style, which sounds far more natural than a monotonous single-voice reading. This article documents the complete integration process and the pitfalls I hit.

Adding an AI Podcast to the Blog: Turning Articles into Two-Host Dialogue Audio with Doubao TTS

Solution Selection and Core Approach

Why Choose Doubao Podcast TTS

There are quite a few TTS vendors on the market; I mainly compared these:

Solution Features Drawbacks
Alibaba Cloud Speech Synthesis Stable, cheap Single-voice reading only, no sense of dialogue
iFlytek Voiceover Rich emotion Billed per character, costly for long text
Doubao Podcast TTS Two-host dialogue, high naturalness Only a WebSocket interface, slightly complex to integrate

The reason I ultimately chose Doubao is simple: it can generate a true “podcast,” not dry reading. The two hosts are named Liu Fei and Xiao Lei—one leads the topic, the other asks questions and adds details—and the listening experience is very close to a real podcast show.

Overall Pipeline Design

The whole feature’s pipeline is like this:

graph LR
A[Markdown article] --> B[Extract plain text]
B --> C[Call Doubao API to generate dialogue text]
C --> D[Fix the opening and redundant content]
D --> E[Generate audio from the dialogue text]
E --> F[Upload MP3 to OSS]
F --> G[Insert the podcast link into the article front matter]

The whole flow is wrapped in a Python script: running python3 tools/generate_podcast.py source/_posts/xxx.md completes it in one command.

Key Technology: Parsing the WebSocket Binary Protocol

Doubao Podcast TTS’s interface is WebSocket, but it isn’t ordinary JSON over WebSocket—it’s a proprietary binary frame protocol. This was also the most troublesome part of the integration.

Protocol Frame Structure

Each frame consists of four parts: Header (4 bytes) + Optional (optional) + PayloadLength (4 bytes) + Payload.

def build_frame(message_type, event, session_id, payload_bytes, flags=0b0100):
# Header: 4 bytes
# byte0: version(4bit) + header_size(4bit) = 0x11
# byte1: message_type(4bit) + flags(4bit)
# byte2: serial_method(4bit) + compression(4bit)
# byte3: reserved
header = bytes([0x11, (message_type << 4) | flags, 0x10, 0x00])

# Optional: event(4 bytes) + session_id length(4 bytes) + session_id
optional = bytearray()
if event is not None:
optional.extend(struct.pack(">I", event))
if session_id is not None:
sid_bytes = session_id.encode('utf-8')
optional.extend(struct.pack(">I", len(sid_bytes)))
optional.extend(sid_bytes)

# Payload
frame = bytearray(header)
frame.extend(optional)
if payload_bytes is not None:
frame.extend(struct.pack(">I", len(payload_bytes)))
frame.extend(payload_bytes)

return bytes(frame)

There are several key points to note:

  1. Big-endian: all multi-byte fields are big-endian (>I means big-endian unsigned int).
  2. The event field: only when the flag FLAG_WITH_EVENT = 0b0100 is set will the frame carry the event field.
  3. message_type: requests are 0b0001, audio responses 0b1011, text responses 0b1001, errors 0b1111.

Connection and Authentication

The WebSocket handshake must carry authentication headers:

ws_headers = [
f"X-Api-App-Id: {app_id}",
f"X-Api-Access-Key: {access_key}",
f"X-Api-Resource-Id: volc.service_type.10050",
f"X-Api-App-Key: aGjiRDfUWi",
f"X-Api-Request-Id: {uuid.uuid4()}"
]

ws = websocket.create_connection(
"wss://openspeech.bytedance.com/api/v3/sami/podcasttts",
timeout=60,
header=ws_headers
)

Here app_id and access_key need to be obtained after creating an application in the Volcano Engine console.

Three Steps: From Text to Audio

The whole generation flow has three steps, each corresponding to an action parameter.

Step One: Generate the Dialogue Text (action=0)

First, have the AI generate a podcast dialogue script from the article content. There’s a trick here: add only_nlp_text=True, and the API will directly return the text of each round of dialogue instead of audio.

payload = {
"input_text": text, # full article text
"action": 0,
"input_info": {
"only_nlp_text": True,
"input_text_max_length": 12000
},
"speaker_info": {
"random_order": True,
"speakers": [
"zh_male_liufei_v2_saturn_bigtts",
"zh_male_xiaolei_v2_saturn_bigtts"
]
}
}

The returned data format is a JSON array, each element containing speaker and text. I usually print it out first to see the effect, and only move on to the next step after confirming the dialogue logic is smooth.

Step Two: Fix the Dialogue Content

Sometimes the Doubao-generated dialogue lacks an opening, or contains some overly brief transitions. I added a layer of post-processing:

def modify_nlp_texts(nlp_texts, title=""):
# Check whether the first 3 rounds contain an opening
has_opening = any("Welcome to" in t["text"] or "I'm Awen" in t["text"]
for t in nlp_texts[:3])

if not has_opening:
first_speaker = nlp_texts[0]["speaker"]
opening = f"Welcome to Awen's podcast, I'm Awen. Today I want to talk with you about this article—《{title}》."
nlp_texts.insert(0, {"speaker": first_speaker, "text": opening})

# Filter out overly short redundant sentences
filtered = [item for item in nlp_texts if len(item["text"].strip()) >= 10]

return filtered

Here it’s forced that the podcast begins with “Welcome to Awen’s podcast, I’m Awen……” to strengthen the branding.

Step Three: Generate the Audio (action=3)

Once you have the corrected dialogue text, call the WebSocket again—this time with action=3, carrying the complete dialogue list:

payload = {
"action": 3,
"nlp_texts": nlp_texts,
"use_head_music": True, # opening music
"use_tail_music": True, # ending music
"audio_config": {
"format": "mp3",
"sample_rate": 24000,
"speech_rate": 0
},
"speaker_info": {
"random_order": True,
"speakers": ["zh_male_liufei_v2_saturn_bigtts", "zh_male_xiaolei_v2_saturn_bigtts"]
}
}

The API returns MP3-format binary audio data. Each round of dialogue triggers three events: PODCAST_ROUND_START, PODCAST_ROUND_RESPONSE (the audio data), and PODCAST_ROUND_END. You need to collect, in order, the payloads from all PODCAST_ROUND_RESPONSE events and finally merge them into a complete MP3 file.

Here’s a point to note: the Doubao API has a limit on the number of rounds per batch of dialogue—at most 25 rounds per call. If an article is long and the dialogue exceeds 25 rounds, you need to generate in batches and finally splice the multiple MP3 segments together. MP3 is a streaming format, so direct byte concatenation works—no re-encoding needed.

Preprocessing the Article Text

To make the podcast sound better, I did a lot of cleanup when extracting text from Markdown:

def extract_article_text(file_path):
# Remove front matter
content = re.sub(r'^---\s*\n.*?\n---\s*\n', '', content, flags=re.DOTALL)

# Remove images, links, code blocks, inline code
content = re.sub(r'!\[([^\]]*)\]\([^\)]+\)', '', content)
content = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', content)
content = re.sub(r'```.*?```', '', content, flags=re.DOTALL)
content = re.sub(r'`[^`]+`', '', content)

# Remove heading markers, bold/italic, HTML tags
content = re.sub(r'^#+\s*', '', content, flags=re.MULTILINE)
content = re.sub(r'[*_]{1,2}', '', content)
content = re.sub(r'<[^>]+>', '', content)

return content.strip()

In addition, to let the AI generate a better podcast, I added a piece of guiding text (a prompt) before the article text, explicitly telling the AI that the two hosts should discuss the article from a third-person perspective and must not assume the author’s identity. This prompt has a big impact on the final effect—it directly determines whether the podcast is a “discussion” or a “reading.”

Complete Usage Flow

Once the script is written, daily use is very simple:

# Generate the podcast and automatically insert it into the article
python3 tools/generate_podcast.py source/_posts/2026/04/article-title.md

After running, the console outputs the progress of each step:

📝 Processing article: source/_posts/2026/04/article-title.md
============================================================

📄 Extracting article text...
Extracted 3850 characters

📝 Step 1: Generating podcast dialogue text...
✅ Generated 18 rounds of dialogue

📋 Generated podcast dialogue text:
[Liu Fei] Welcome to Awen's podcast, I'm Awen. Today I want to talk with you about this article—《Adding an AI Podcast to the Blog》...
[Xiao Lei] The author's idea is pretty interesting; turning articles into podcasts really does solve the problem of many people not having time to read long pieces...

🔧 Step 2: Checking and modifying the dialogue text...

🎙️ Step 3: Generating audio from the dialogue text...
🎵 Opening music...
🎙️ Podcast round 0: Liu Fei
🎙️ Podcast round 1: Xiao Lei
⏱️ Round duration: 12.3s
...

☁️ Uploading audio to OSS...
⬆️ Upload: oss://file201503/podcast/2026/04/24/20260424a1b2c3d4.mp3

✅ Podcast uploaded successfully: https://file.awen.me/podcast/2026/04/24/20260424a1b2c3d4.mp3?e=2106345600&st=w0i6ZMAnTZo_mq-qbIkkJQ

📝 Inserting the podcast link into the article...
✅ Added the podcast field to the front matter

============================================================
🎉 Done!

The generated podcast link is written into the article’s front matter: podcast: https://file.awen.me/podcast/.../xxx.mp3. Add a podcast player component to the front-end theme, read this field, and it will display on the article page.

Cost and Effect

Currently, Volcano Engine’s Doubao Podcast TTS has no public billing documentation (it’s in internal testing). From actual tests, generating a podcast for a 3,000-word article takes about 30–60 seconds, with audio duration of about 8–12 minutes. Estimating by Volcano’s billing standard, the cost is roughly a few fen to a tenth of a yuan—entirely acceptable for a personal blog.

In terms of effect, Doubao’s two-host dialogue mode really does sound much better than single-voice TTS. Liu Fei’s timbre is steady, suited to leading; Xiao Lei is more lively, suited to asking questions and adding details. The two also have a very natural sense of rhythm, with no obvious “machine feel.” The only minor flaw is occasional repetitive phrasing; filtering short sentences through post-processing already alleviates most of it.

Pitfalls I Stepped In

  1. WebSocket timeout: generating long articles easily times out; you need to set timeout larger (60 seconds or more) and add a retry mechanism.
  2. Binary frame parsing error: at first I parsed in little-endian, so all the payload lengths came out wrong. Only after carefully reading the docs did I discover it was big-endian.
  3. Dialogue rounds exceeding the limit: more than 25 rounds in a single batch triggers an error directly, so you must do batch processing.
  4. Audio splicing problems: MP3s generated in batches can’t be spliced directly—you need to first strip the ID3 tags from each segment. Actually, MP3 streaming concatenation is feasible, but if there’s a complete ID3v2 header in the middle it can cause problems. My approach was to ensure that only the first batch has opening music and only the last batch has ending music, with no music in the middle batches, so the spliced result is clean.

Conclusion

Adding an AI podcast to the blog is essentially about expanding the scenarios in which content is consumed. Not everyone is willing to sit down and read through a long article, but listening to a 10-minute podcast lowers the barrier a lot. Doubao Podcast TTS makes achieving this very cheap—no need for real-person recording, no need for editing; one command generates usable audio content.

If you also have a blog or a content site, why not give it a try. The technical solution isn’t complex; the core is parsing the WebSocket binary protocol and orchestrating the dialogue flow.

But a reminder as well: an AI podcast is suitable as a supplementary way to consume content and can’t fully replace reading text. Some technical details and code snippets are hard to remember after hearing them once—you still need to return to the original text and read carefully. The best approach is to provide both text and podcast entries on the article page, letting readers choose for themselves.

文章作者:阿文
文章链接: https://www.awen.me/post/b5d10b4.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客!

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️