← 返回
未分类 Key

Doubao Podcast TTS

Use when calling Doubao/ByteDance podcast TTS API to generate audio, parsing WebSocket binary frames, handling streaming audio chunks, extracting audio_url f...
用于调用豆宝/字节跳动播客TTSAPI生成音频,解析WebSocket二进制帧,处理流式音频块,提取audio_url...
mileszhang001-boom mileszhang001-boom 来源
未分类 clawhub v1.0.0 1 版本 100000 Key: 需要
★ 0
Stars
📥 351
下载
💾 1
安装
1
版本
#latest

概述

豆包播客 TTS API 集成指南

基于 7 篇微信长文 POC + 线上生产环境的实战经验。覆盖从建连到拿到 mp3 的全流程,以及 11 条踩坑记录。

1. 接口概览

---------
协议WebSocket 二进制协议 v3
地址wss://openspeech.bytedance.com/api/v3/sami/podcasttts
鉴权4 个 Header:X-Api-App-IdX-Api-Access-KeyX-Api-Resource-IdX-Api-App-Key
Resource IDvolc.service_type.10050
输出MP3, 96kbps, 24kHz mono(固定)

2. 二进制协议

豆包使用自定义二进制帧,不是标准 JSON WebSocket。

帧头(固定 4 字节)

[0x11, 0x14, 0x10, 0x00]
  • 第 2 字节高 4 位 = message_type(0xF = 错误帧)
  • 第 2 字节低 4 位 = flags(0x04 = 含 session_id)
  • 第 3 字节高 4 位 = serialization(1=JSON, 0=binary audio)

两种帧格式

Pre-connection:  header(4) + event_type(4, big-endian) + payload_size(4) + payload
Post-connection: header(4) + event_type(4) + sid_len(4) + session_id + payload_size(4) + payload

构造/解析代码

Python 版本见 scripts/generate_podcast.py。Node.js 版本:

const HEADER = Buffer.from([0x11, 0x14, 0x10, 0x00]);

function preFrame(event, payload) {
  const p = Buffer.from(JSON.stringify(payload));
  const e = Buffer.alloc(4); e.writeUInt32BE(event);
  const l = Buffer.alloc(4); l.writeUInt32BE(p.length);
  return Buffer.concat([HEADER, e, l, p]);
}

function postFrame(event, sid, payload) {
  const sb = Buffer.from(sid);
  const p = Buffer.from(JSON.stringify(payload));
  const e = Buffer.alloc(4); e.writeUInt32BE(event);
  const sl = Buffer.alloc(4); sl.writeUInt32BE(sb.length);
  const pl = Buffer.alloc(4); pl.writeUInt32BE(p.length);
  return Buffer.concat([HEADER, e, sl, sb, pl, p]);
}

function parseEvent(data) {
  const buf = Buffer.from(data);
  if (buf.length < 8) return { eventType: null, payload: {} };
  const mt = (buf[1] >> 4) & 0xF;
  const fl = buf[1] & 0xF;
  const ser = (buf[2] >> 4) & 0xF;
  if (mt === 0xF) { /* 错误帧 */ return { eventType: -1, payload: {} }; }
  const evt = buf.readUInt32BE(4);
  let off = 8, payload = {};
  if (fl & 0x04) {
    const sl = buf.readUInt32BE(off); off += 4 + sl;  // 跳过 session_id
    const pl = buf.readUInt32BE(off); off += 4;
    if (pl > 0 && ser === 1) try { payload = JSON.parse(buf.slice(off, off + pl).toString()); } catch {}
  }
  return { eventType: evt, payload };
}

3. 握手流程与事件表

客户端                                豆包 API
  │── StartConnection(event=1) ────→│
  │←── ConnectionStarted(event=50) ─│  ← session_id 在二进制帧中
  │── StartSession(event=100) ─────→│  ← 携带播客参数
  │←── SessionStarted(event=150) ───│
  │  ┌── 流式循环 ──────────────────┐
  │←─┤ 360: RoundStart (JSON)      │  ← 文案文本(第1轮为空!见坑10)
  │←─┤ 361: RoundResp (binary)     │  ← 音频块 ~4.6KB/chunk
  │←─┤ 362: RoundEnd (JSON)        │
  │  └─────────────────────────────┘
  │←── 363: PodcastEnd (JSON) ─────│  ← audio_url(⚠️ duration_sec 可能为 0,见坑9)
  │←── 152: SessionFinished ───────│  ← ⚠️ 经常不来,见坑2
  │── FinishConnection(event=2) ──→│

事件速查

event名称payload关键字段
-------------------------------
1/2Start/FinishConnection{}
50ConnectionStarted二进制session_id 在帧中提取
100StartSessionJSON播客参数
360RoundStartJSONtext(第 1 轮为空)
361RoundRespbinary音频块
363PodcastEndJSONmeta_info.audio_url

提取 session_id(ConnectionStarted 帧)

const buf = Buffer.from(data);
let off = 8;
const sidLen = buf.readUInt32BE(off); off += 4;
const sessionId = buf.slice(off, off + sidLen).toString();

4. 两种输入模式

input_url(URL 文章)

{
  input_info: { input_url: "https://...", return_audio_url: true },  // ⚠️ url 在 input_info 内
  use_head_music: true, use_tail_music: false,
  audio_config: { format: "mp3", sample_rate: 24000, speech_rate: 0 },
  speaker_info: { random_order: true, speakers: ["zh_male_dayixiansheng_v2_saturn_bigtts", "zh_female_mizaitongxue_v2_saturn_bigtts"] }
}

input_text(短文本 < 200 字)

{
  input_text: "文本内容...",  // ⚠️ text 在顶层,不是 input_info
  audio_config: { ... },
  speaker_info: { ... }
}

5. 实战踩坑记录(11 条)

坑 1:input_url vs input_text 参数位置

❌ {"input_url": "https://..."}                  → 顶层没这字段,静默失败
✅ {"input_info": {"input_url": "https://..."}}  → URL 模式正确用法
✅ {"input_text": "你好世界"}                     → 短文本模式正确用法

搞反了不会报错,只会得到空结果。

坑 2:PodcastEnd 之后不要等 SessionFinished

SessionFinished(152) 经常不来或等 10 分钟+。PodcastEnd(363) 拿到 audio_url 后立即 break

if (eventType === 363) {
  const url = payload.meta_info?.audio_url || '';
  ws.close();  // 立即关闭!
  resolve({ audioUrl: url });
}

不加 break,5 分钟播客可能跑 15 分钟。这是最致命的坑。

坑 3:超时设 900s

文章长度播客时长生成耗时
---------------------------
~2000字~5 min~2.5 min
~5000字~10 min~4 min
~25000字~30 min~10 min

统一设 TIMEOUT = 900(15 分钟)。

坑 4:WebSocket 连接参数

Python: ping_timeout=120,不设的话长文生成时会心跳超时断开。

Node.js: ws 库默认无 ping,不需要额外设置。

坑 5:连接断开但已有音频块

音频块是完整 MP3 片段,直接拼接就是可播放文件。断连时不要丢弃已收到的块。

坑 6:audio_url 24 小时过期

CDN URL 签名过期后返回 403。生成完成后立即下载 mp3 到自己的存储

坑 7:Python nohup 输出缓冲

后台运行加 python -u(unbuffered)才能看到实时日志。

坑 8:浏览器 WebSocket 不支持自定义 Header ⭐

浏览器 new WebSocket(url) 无法设置 HTTP Header。豆包需要 4 个鉴权 Header → 必须走服务端代理

推荐架构:

浏览器 ── POST /api/podcast ──→ 服务端 ── WSS + Header ──→ 豆包
       ← SSE 流式进度 ────────         ← 二进制帧 ──────

服务端用 Node.js ws 库(支持自定义 Header)连接豆包,通过 SSE(Server-Sent Events)把进度推给浏览器。凭证存在服务端,不暴露给前端。

坑 9:duration_sec 经常返回 0 ⭐

PodcastEnd 的 meta_info.duration_sec 实测经常为 0 或不返回

备用方案:根据音频大小估算(96kbps = 12KB/s):

const durationSec = meta.duration_sec || Math.round(totalAudioBytes / 12000);

坑 10:RoundStart 第一轮文案为空 ⭐

第 1 轮是片头音乐,text 字段为空字符串。第 2 轮开始才有实际文案内容。

如果需要从文案中提取标题/主题,从 Round 2+ 的文案中取。

坑 11:微信文章标题无法从服务端抓取 ⭐

从中国云服务器 fetch 微信文章会返回"环境异常"页面(反爬)。标题提取备用方案:

  1. 优先:fetch 文章页面提取 </code> 或 <code>og:title</code>(非微信 URL 有效)</li><li>备用:从 RoundStart 第 2 轮文案提取主题(去口语化前缀如"今天我们聊的是")</li><li>兜底:使用 fallback 文本</li></ol><h2>6. 内容存储与去重(生产架构)</h2><p>豆包 token 昂贵,必须避免重复生成。推荐架构:</p><pre><code>POST /generate {url} → 查 store:相同 url 已有? → YES:直接返回缓存(0 token) → NO:调豆包生成 → 下载 mp3 到本地 → 写入 store → 返回 </code></pre><p>Store 数据模型:</p><pre><code>{ "id": "gen_xxx", "source_url": "https://...", "title": "...", "audio_file": "gen_xxx.mp3", "duration_sec": 682 } </code></pre><p>音频下载到本地后不再依赖豆包 CDN(24h 过期)。</p><h2>7. 性能基线</h2><table><thead><tr><th>指标</th><th>值</th></tr></thead><tbody><tr><td>------</td><td>-----</td></tr><tr><td>生成速度</td><td>2.7× 实时(1 分钟播客 ≈ 22s 生成)</td></tr><tr><td>首 token</td><td>avg 6s</td></tr><tr><td>Token 消耗</td><td>~800 字/分钟播客,~3000 token/分钟</td></tr><tr><td>音频块</td><td>~4.6KB/chunk</td></tr><tr><td>总测试量</td><td>7 篇 + 线上多篇,全部成功</td></tr></tbody></table><h2>8. 错误处理</h2><table><thead><tr><th>场景</th><th>表现</th><th>处理</th></tr></thead><tbody><tr><td>------</td><td>------</td><td>------</td></tr><tr><td>建连失败</td><td>event=-1</td><td>检查凭证,重试</td></tr><tr><td>内容过滤</td><td>错误码 <code>50302102</code></td><td>提示不支持,不重试</td></tr><tr><td>心跳超时</td><td>静默断开</td><td>Python: <code>ping_timeout=120</code></td></tr><tr><td>audio_url 过期</td><td>CDN 403</td><td>用本地存储的 mp3</td></tr></tbody></table><h2>9. 参考实现</h2><table><thead><tr><th>文件</th><th>语言</th><th>说明</th></tr></thead><tbody><tr><td>------</td><td>------</td><td>------</td></tr><tr><td><code>scripts/generate_podcast.py</code></td><td>Python</td><td>批量生成 + CLI,POC 验证用</td></tr><tr><td>项目 <code>server/podcast-api.js</code></td><td>Node.js</td><td>生产环境:HTTP API + SSE + 内容存储</td></tr></tbody></table></div> </div> </div> <div id="tab-versions" class="detail-content"> <div class="detail-section"> <h2>版本历史</h2> <p style="margin-bottom:12px;font-size:14px;color:#94a3b8;">共 1 个版本</p> <ul class="version-list"> <li> <div> <span class="version-tag">v1.0.0</span> <span style="font-size:11px;color:#5b6abf;margin-left:8px;background:#eef0ff;padding:1px 8px;border-radius:10px;">当前</span> </div> <div style="font-size:12px;color:#94a3b8;"> 2026-05-07 06:14 安全 安全 </div> </li> </ul> </div> </div> <div id="tab-security" class="detail-content"> <div class="detail-section"> <h2>安全检测</h2> <div class="sec-grid"> <div class="sec-card"> <h4>腾讯云安全 (Keen)</h4> <div class="sec-status sec-safe"> 安全,无风险 </div> <a href="https://tix.qq.com/search/skill?keyword=6df89c74780280f8ef307bf75c031e5c" target="_blank">查看报告</a> </div> <div class="sec-card"> <h4>腾讯云安全 (Sanbu)</h4> <div class="sec-status sec-safe"> 安全,无风险 </div> <a href="https://static.cloudsec.tencent.com/html-report-v2/2026/05/26/437469_20edd8bf5a89c4bb87b855cd2372d225.html?q-sign-algorithm=sha1&q-ak=AKID8JMG1bzBC1dz96qNhssfFftujT1NCoFi&q-sign-time=1781528070%3B1813064070&q-key-time=1781528070%3B1813064070&q-header-list=host&q-url-param-list=&q-signature=f10df99f5fc5f02a264b7e58b4ef258810c8ffc9" target="_blank">查看报告</a> </div> </div> </div> </div> <!-- Recommended Skills --> <div style="margin-top:24px;"> <h2 style="font-size:18px;font-weight:600;margin-bottom:16px;">🔗 相关推荐</h2> <div class="rec-grid"> <div class="rec-card"> <span class="badge-cat" style="margin-bottom:8px;display:inline-block;">ai-agent</span> <h3><a href="/s/self-improving">Self-Improving + Proactive Agent</a></h3> <div class="rec-owner">ivangdavila</div> <div class="rec-desc">自我反思+自我批评+自我学习+自组织记忆。智能体评估自身工作、发现错误并持续改进。</div> <div class="rec-stats"> <span style="color:#f39c12;">★ 1,380</span> <span style="color:#5b6abf;">📥 320,631</span> </div> </div> <div class="rec-card"> <span class="badge-cat" style="margin-bottom:8px;display:inline-block;">ai-agent</span> <h3><a href="/s/skill-vetter">Skill Vetter</a></h3> <div class="rec-owner">spclaudehome</div> <div class="rec-desc">AI智能体技能安全预审工具。安装ClawdHub、GitHub等来源技能前,检查风险信号、权限范围及可疑模式。</div> <div class="rec-stats"> <span style="color:#f39c12;">★ 1,228</span> <span style="color:#5b6abf;">📥 267,998</span> </div> </div> <div class="rec-card"> <span class="badge-cat" style="margin-bottom:8px;display:inline-block;">ai-agent</span> <h3><a href="/s/self-improving-agent">self-improving agent</a></h3> <div class="rec-owner">pskoett</div> <div class="rec-desc">捕获经验教训、错误及修正内容,以实现持续改进。适用于以下场景:(1)命令或操作意外失败;(2)用户纠正Claude(如“不,那不对……”“实际上……”);(3)用户请求的功能不存在;(4)外部API或工具出现故障;(5)Claude发现自身</div> <div class="rec-stats"> <span style="color:#f39c12;">★ 4,082</span> <span style="color:#5b6abf;">📥 811,935</span> </div> </div> </div> </div> </div> <script> document.addEventListener('DOMContentLoaded',function(){ document.querySelectorAll('.detail-tab').forEach(function(btn){ btn.addEventListener('click',function(e){ var tab = this.getAttribute('data-tab'); document.querySelectorAll('.detail-tab').forEach(function(b){b.classList.remove('active')}); document.querySelectorAll('.detail-content').forEach(function(c){c.classList.remove('active')}); this.classList.add('active'); var el = document.getElementById('tab-'+tab); if(el) el.classList.add('active'); }); }); }); </script> <div class="footer"> <p>Skill工具集 © 2026</p> </div></body> </html>