🎁 Get the FREE AI Skills Starter GuideSubscribe →
BytesAgainBytesAgain
🦀 ClawHub

Doubao Podcast TTS

by @mileszhang001-boom

Use when calling Doubao/ByteDance podcast TTS API to generate audio, parsing WebSocket binary frames, handling streaming audio chunks, extracting audio_url f...

Versionv1.0.0
Downloads666
TERMINAL
clawhub install doubao-podcast

📖 About This Skill


name: doubao-podcast description: | Use when calling Doubao/ByteDance podcast TTS API to generate audio, parsing WebSocket binary frames, handling streaming audio chunks, extracting audio_url from PodcastEnd, troubleshooting podcast generation timeout or stuck issues, or designing podcast playback architecture (streaming vs non-streaming). Also use when: integrating podcast generation into a browser-based app (browser WebSocket cannot set custom headers), designing content caching to avoid redundant generation, or extracting article metadata for podcast titles. Trigger even without explicit mention of "Doubao" — any podcast TTS, ByteDance speech synthesis, or podcast generation task should use this skill.

豆包播客 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 ID | volc.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/2 | Start/FinishConnection | {} | — | | 50 | ConnectionStarted | 二进制 | session_id 在帧中提取 | | 100 | StartSession | JSON | 播客参数 | | 360 | RoundStart | JSON | text(第 1 轮为空) | | 361 | RoundResp | binary | 音频块 | | 363 | PodcastEnd | JSON | meta_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 style="background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em">og:title</code>(非微信 URL 有效) 2. 备用:从 RoundStart 第 2 轮文案提取主题(去口语化前缀如"今天我们聊的是") 3. 兜底:使用 fallback 文本</p><p style="margin:8px 0"><h3 style="color:#e5e7eb;margin:18px 0 8px;font-size:1.05em">6. 内容存储与去重(生产架构)</h3></p><p style="margin:8px 0">豆包 token 昂贵,必须避免重复生成。推荐架构:</p><p style="margin:8px 0"><pre style="background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0"><code style="color:#a5f3fc;background:none;padding:0;font-size:1em">POST /generate {url} → 查 store:相同 url 已有? → YES:直接返回缓存(0 token) → NO:调豆包生成 → 下载 mp3 到本地 → 写入 store → 返回 </code></pre></p><p style="margin:8px 0">Store 数据模型: <pre style="background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0"><code style="color:#a5f3fc;background:none;padding:0;font-size:1em">{ "id": "gen_xxx", "source_url": "https://...", "title": "...", "audio_file": "gen_xxx.mp3", "duration_sec": 682 } </code></pre></p><p style="margin:8px 0">音频下载到本地后不再依赖豆包 CDN(24h 过期)。</p><p style="margin:8px 0"><h3 style="color:#e5e7eb;margin:18px 0 8px;font-size:1.05em">7. 性能基线</h3></p><p style="margin:8px 0">| 指标 | 值 | |------|-----| | 生成速度 | 2.7× 实时(1 分钟播客 ≈ 22s 生成) | | 首 token | avg 6s | | Token 消耗 | ~800 字/分钟播客,~3000 token/分钟 | | 音频块 | ~4.6KB/chunk | | 总测试量 | 7 篇 + 线上多篇,全部成功 |</p><p style="margin:8px 0"><h3 style="color:#e5e7eb;margin:18px 0 8px;font-size:1.05em">8. 错误处理</h3></p><p style="margin:8px 0">| 场景 | 表现 | 处理 | |------|------|------| | 建连失败 | event=-1 | 检查凭证,重试 | | 内容过滤 | 错误码 <code style="background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em">50302102</code> | 提示不支持,不重试 | | 心跳超时 | 静默断开 | Python: <code style="background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em">ping_timeout=120</code> | | audio_url 过期 | CDN 403 | 用本地存储的 mp3 |</p><p style="margin:8px 0"><h3 style="color:#e5e7eb;margin:18px 0 8px;font-size:1.05em">9. 参考实现</h3></p><p style="margin:8px 0">| 文件 | 语言 | 说明 | |------|------|------| | <code style="background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em">scripts/generate_podcast.py</code> | Python | 批量生成 + CLI,POC 验证用 | | 项目 <code style="background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em">server/podcast-api.js</code> | Node.js | 生产环境:HTTP API + SSE + 内容存储 | </p></div></section></div><div class="two-col-side"></div></div></div><script> document.querySelectorAll('.copy-btn, .script-copy-btn').forEach(btn => { btn.addEventListener('click', () => { const cmd = btn.getAttribute('data-cmd'); if (!cmd) return; navigator.clipboard.writeText(cmd).then(() => { const orig = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = orig, 1500); }).catch(() => {}); }); }); </script><!--$--><!--/$--></main><footer style="background:var(--bg-primary);border-top:1px solid var(--border-secondary);margin-top:60px"><div style="border-top:1px solid var(--border-light);max-width:1200px;margin:0 auto;padding:24px 20px"><div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:24px;margin-bottom:24px"><div><div style="font-weight:700;color:var(--text-muted);margin-bottom:8px">BytesAgain</div><div style="color:var(--text-muted3);font-size:.82em;max-width:200px">Discover the best AI agent skills for your workflow.</div></div><div><div style="color:var(--text-muted);font-size:.75em;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px">Explore</div><div style="margin-bottom:6px"><a href="/skills" style="color:var(--text-muted2);text-decoration:none;font-size:.85em">Skills</a></div><div style="margin-bottom:6px"><a href="/articles" style="color:var(--text-muted2);text-decoration:none;font-size:.85em">Articles</a></div><div style="margin-bottom:6px"><a href="/use-case" style="color:var(--text-muted2);text-decoration:none;font-size:.85em">Cases</a></div></div><div><div style="color:var(--text-muted);font-size:.75em;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px">Company</div><div style="margin-bottom:6px"><a href="/about" style="color:var(--text-muted2);text-decoration:none;font-size:.85em">About</a></div><div style="margin-bottom:6px"><a href="/contact" style="color:var(--text-muted2);text-decoration:none;font-size:.85em">Contact</a></div><div style="margin-bottom:6px"><a href="/privacy-policy" style="color:var(--text-muted2);text-decoration:none;font-size:.85em">Privacy Policy</a></div><div style="margin-bottom:6px"><a href="/terms" style="color:var(--text-muted2);text-decoration:none;font-size:.85em">Terms</a></div><div style="margin-bottom:6px"><a href="/feedback" style="color:var(--text-muted2);text-decoration:none;font-size:.85em">Feedback</a></div></div></div><div style="border-top:1px solid var(--border-light);padding-top:16px"><div style="color:var(--text-muted4);font-size:.8em;margin-bottom:8px">© <!-- -->2026<!-- --> BytesAgain. All rights reserved.</div><div style="color:var(--text-muted5);font-size:.75em;line-height:1.6;max-width:720px">BytesAgain is an independent skill directory. We index and link to third-party content (ClawHub, GitHub, LobeHub, Dify, etc.) for informational purposes only. All trademarks, skill names, and content are the property of their respective owners. BytesAgain does not claim ownership of any indexed content.</div></div></div></footer><button style="position:fixed;bottom:28px;right:28px;z-index:1000;width:48px;height:48px;border-radius:50%;border:none;cursor:pointer;background:linear-gradient(135deg,#667eea,#00d4ff);color:#fff;font-size:1.3em;box-shadow:0 4px 20px #667eea66;display:flex;align-items:center;justify-content:center;transition:transform .2s">💬</button><script src="/_next/static/chunks/0ze4gu236oq96.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[62894,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"LangProvider\"]\n3:I[89220,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"ThemeProvider\"]\n4:I[16988,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"default\"]\ne:I[68027,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"default\",1]\n:HL[\"/_next/static/chunks/051nc0vy_6.rl.css?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"style\"]\n:HL[\"/_next/static/media/caa3a2e1cccd8315-s.p.09~u27dqhyhd6.woff2?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n5:Td5e,"])</script><script>self.__next_f.push([1,"[{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"BytesAgain\",\"url\":\"https://bytesagain.com\",\"description\":\"Search 60,000+ verified AI agent skills via MCP API or REST. Supports 7 languages. Free, no auth required.\",\"inLanguage\":[\"en\",\"zh\",\"es\",\"fr\",\"de\",\"ja\",\"ko\"],\"potentialAction\":{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https://bytesagain.com/skills?q={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}},{\"@context\":\"https://schema.org\",\"@type\":\"Organization\",\"name\":\"BytesAgain\",\"url\":\"https://bytesagain.com\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://bytesagain.com/og-image.png\"},\"description\":\"AI agent skill directory. Search 60,000+ skills, 1,000+ use cases, and community requests.\",\"foundingDate\":\"2026\",\"foundingLocation\":{\"@type\":\"Place\",\"name\":\"Global\"},\"sameAs\":[\"https://x.com/bytesagain\",\"https://github.com/bytesagain/ai-skills\",\"https://clawhub.ai/profile/bytesagain\"],\"contactPoint\":{\"@type\":\"ContactPoint\",\"email\":\"hello@bytesagain.com\",\"contactType\":\"customer support\"},\"numberOfEmployees\":{\"@type\":\"QuantitativeValue\",\"value\":1}},{\"@context\":\"https://schema.org\",\"@type\":\"WebApplication\",\"name\":\"BytesAgain AI Skills Search\",\"url\":\"https://bytesagain.com\",\"applicationCategory\":\"DeveloperApplication\",\"operatingSystem\":\"Web\",\"description\":\"Search engine and MCP API for 60,000+ AI agent skills. Semantic search, role recommendations, and use case packs.\",\"offers\":{\"@type\":\"Offer\",\"price\":\"0\",\"priceCurrency\":\"USD\"},\"featureList\":[\"Search 60,000+ AI agent skills\",\"Role-based recommendations for developers, creators, and traders\",\"1,000+ curated use case packs\",\"Free MCP API and REST API\",\"Multi-language search (EN, ZH, ES, FR, DE, JA, KO)\"],\"potentialAction\":{\"@type\":\"SearchAction\",\"target\":\"https://bytesagain.com/skills?q={search_term_string}\",\"query-input\":\"required name=search_term_string\"},\"dateModified\":\"2026-09-20\"},{\"@context\":\"https://schema.org\",\"@type\":\"FAQPage\",\"mainEntity\":[{\"@type\":\"Question\",\"name\":\"What is BytesAgain?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"BytesAgain is a curated directory of 60,000+ AI agent skills from ClawHub, GitHub, LobeHub, and Dify. Search skills by keyword in 7 languages, browse by role (developer, creator, trader, marketer) or by use case.\"}},{\"@type\":\"Question\",\"name\":\"How do I find AI skills on BytesAgain?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Use the search bar on BytesAgain.com to search by keyword in 7 languages. You can also browse by role (developer, creator, trader, marketer) or by use case. Each skill shows install instructions for Claude, Cursor, OpenClaw, Continue, and more.\"}},{\"@type\":\"Question\",\"name\":\"Is BytesAgain free?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Yes, BytesAgain is completely free. No registration required for searching skills. The MCP API is also free with rate limits.\"}},{\"@type\":\"Question\",\"name\":\"Does BytesAgain have an API for AI agents?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Yes! BytesAgain provides a free MCP SSE endpoint at /api/mcp/sse for AI agents, plus a REST API at /api/mcp?action=search\u0026q=\u003cquery\u003e. No authentication needed.\"}},{\"@type\":\"Question\",\"name\":\"Can I request a new AI skill on BytesAgain?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Yes! Visit the Requests page on BytesAgain.com to submit a skill request. Your request will be visible to the community and notified to the site admin.\"}}]}]"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"c\":[\"\",\"skill\",\"doubao-podcast\"],\"q\":\"\",\"i\":false,\"f\":[[[\"\",{\"children\":[\"skill\",{\"children\":[[\"slug\",\"doubao-podcast\",\"d\",null],{\"children\":[\"__PAGE__\",{}]}]}]},\"$undefined\",\"$undefined\",16],[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/051nc0vy_6.rl.css?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-1\",{\"src\":\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-2\",{\"src\":\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[[\"$\",\"head\",null,{\"children\":[[\"$\",\"link\",null,{\"rel\":\"llms\",\"href\":\"/llms.txt\"}],[\"$\",\"link\",null,{\"rel\":\"llms-full\",\"href\":\"/llms-full.txt\"}],[\"$\",\"script\",null,{\"async\":true,\"src\":\"https://www.googletagmanager.com/gtag/js?id=G-3C1MM9FWYF\"}],[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"\\n window.dataLayer = window.dataLayer || [];\\n function gtag(){dataLayer.push(arguments);}\\n gtag('js', new Date());\\n gtag('config', 'G-3C1MM9FWYF');\\n \"}}]]}],[\"$\",\"body\",null,{\"className\":\"geist_9e050971-module__05dp7a__className\",\"style\":{\"margin\":0},\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[[\"$\",\"div\",null,{\"style\":{\"width\":\"100%\",\"background\":\"var(--bg-subscribe)\",\"borderBottom\":\"1px solid var(--border-primary)\",\"padding\":\"8px 20px\",\"textAlign\":\"center\",\"fontSize\":\".82em\",\"color\":\"#818cf8\"},\"children\":[\"🎁 \",[\"$\",\"strong\",null,{\"style\":{\"color\":\"var(--text-primary)\"},\"children\":\"Get the FREE AI Skills Starter Guide\"}],\" — \",[\"$\",\"a\",null,{\"href\":\"/register\",\"style\":{\"color\":\"#00d4ff\",\"textDecoration\":\"underline\"},\"children\":\"Subscribe →\"}]]}],[\"$\",\"$L4\",null,{}],[\"$\",\"script\",null,{\"type\":\"application/ld+json\",\"dangerouslySetInnerHTML\":{\"__html\":\"$5\"}}],\"$L6\",\"$L7\",\"$L8\"]}]}]}]]}]]}],{\"children\":[\"$L9\",{\"children\":[\"$La\",{\"children\":[\"$Lb\",{},null,false,null]},null,false,\"$@c\"]},null,false,\"$@c\"]},null,false,null],\"$Ld\",false]],\"m\":\"$undefined\",\"G\":[\"$e\",[\"$Lf\"]],\"S\":true,\"h\":null,\"s\":\"$undefined\",\"l\":\"$undefined\",\"p\":\"$undefined\",\"d\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"10:I[39756,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"default\"]\n11:I[37457,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"default\"]\n12:I[22016,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0ka051yepewro.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"\"]\n13:I[90940,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"default\"]\n14:I[16397,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"default\"]\n16:I[97367,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"OutletBoundary\"]\n17:\"$Sreact.suspense\"\n1a:I[97367,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"ViewportBoundary\"]\n1c:I[97367,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"MetadataBoundary\"]\n"])</script><script>self.__next_f.push([1,"6:[\"$\",\"main\",null,{\"children\":[\"$\",\"$L10\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L11\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"main\",null,{\"style\":{\"minHeight\":\"100vh\",\"display\":\"flex\",\"alignItems\":\"center\",\"justifyContent\":\"center\",\"background\":\"#050611\",\"color\":\"#e5e7eb\"},\"children\":[[\"$\",\"style\",null,{\"children\":\"\\n .nf-box { text-align: center; padding: 60px 32px; }\\n .nf-code { font-size: 6rem; font-weight: 900; color: #22d3ee; line-height: 1; margin: 0; }\\n .nf-title { font-size: 1.8rem; font-weight: 800; margin: 12px 0 8px; }\\n .nf-desc { color: var(--text-muted2); font-size: 1rem; margin-bottom: 32px; max-width: 440px; }\\n .nf-link { display: inline-block; padding: 12px 28px; background: linear-gradient(135deg,#34d399,#22d3ee); color: #000; font-weight: 900; border-radius: 12px; text-decoration: none; }\\n \"}],[\"$\",\"div\",null,{\"className\":\"nf-box\",\"children\":[[\"$\",\"p\",null,{\"className\":\"nf-code\",\"children\":\"404\"}],[\"$\",\"h1\",null,{\"className\":\"nf-title\",\"children\":\"Page Not Found\"}],[\"$\",\"p\",null,{\"className\":\"nf-desc\",\"children\":\"The skill or page you're looking for doesn't exist or has been moved.\"}],[\"$\",\"$L12\",null,{\"className\":\"nf-link\",\"href\":\"/\",\"children\":\"Back to BytesAgain\"}]]}]]}],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]\n"])</script><script>self.__next_f.push([1,"7:[\"$\",\"$L13\",null,{}]\n8:[\"$\",\"$L14\",null,{}]\n9:[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L10\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L11\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}]\na:[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L10\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L11\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}]\nb:[\"$\",\"$1\",\"c\",{\"children\":[\"$L15\",[[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/12w5ognupk9fb.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"$L16\",null,{\"children\":[\"$\",\"$17\",null,{\"name\":\"Next.MetadataOutlet\",\"children\":\"$@18\"}]}]]}]\n19:[]\nc:\"$W19\"\nd:[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$L1a\",null,{\"children\":\"$L1b\"}],[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$L1c\",null,{\"children\":[\"$\",\"$17\",null,{\"name\":\"Next.Metadata\",\"children\":\"$L1d\"}]}]}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]]}]\nf:[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/051nc0vy_6.rl.css?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]\n"])</script><script>self.__next_f.push([1,"1b:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n"])</script><script>self.__next_f.push([1,"1e:I[27201,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"IconMark\"]\n18:null\n"])</script><script>self.__next_f.push([1,"1d:[[\"$\",\"title\",\"0\",{\"children\":\"Doubao Podcast TTS — AI Agent Skill | BytesAgain | BytesAgain\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Use when calling Doubao/ByteDance podcast TTS API to generate audio, parsing WebSocket binary frames, handling streaming audio chunks, extracting audio_url f...\"}],[\"$\",\"meta\",\"2\",{\"name\":\"robots\",\"content\":\"index, follow\"}],[\"$\",\"meta\",\"3\",{\"name\":\"googlebot\",\"content\":\"index, follow, max-image-preview:large, max-snippet:-1\"}],[\"$\",\"meta\",\"4\",{\"name\":\"llms-txt\",\"content\":\"https://bytesagain.com/llms.txt\"}],[\"$\",\"meta\",\"5\",{\"name\":\"llms-full-txt\",\"content\":\"https://bytesagain.com/llms-full.txt\"}],[\"$\",\"link\",\"6\",{\"rel\":\"canonical\",\"href\":\"https://bytesagain.com/skill/doubao-podcast\"}],[\"$\",\"meta\",\"7\",{\"name\":\"baidu-site-verification\",\"content\":\"codeva-0evUqX1TFs\"}],[\"$\",\"meta\",\"8\",{\"property\":\"og:title\",\"content\":\"Doubao Podcast TTS — AI Agent Skill | BytesAgain\"}],[\"$\",\"meta\",\"9\",{\"property\":\"og:description\",\"content\":\"Use when calling Doubao/ByteDance podcast TTS API to generate audio, parsing WebSocket binary frames, handling streaming audio chunks, extracting audio_url f...\"}],[\"$\",\"meta\",\"10\",{\"property\":\"og:url\",\"content\":\"https://bytesagain.com/skill/doubao-podcast\"}],[\"$\",\"meta\",\"11\",{\"property\":\"og:site_name\",\"content\":\"BytesAgain\"}],[\"$\",\"meta\",\"12\",{\"property\":\"og:image\",\"content\":\"https://bytesagain.com/social-preview.png\"}],[\"$\",\"meta\",\"13\",{\"property\":\"og:image:width\",\"content\":\"1200\"}],[\"$\",\"meta\",\"14\",{\"property\":\"og:image:height\",\"content\":\"630\"}],[\"$\",\"meta\",\"15\",{\"property\":\"og:type\",\"content\":\"website\"}],[\"$\",\"meta\",\"16\",{\"name\":\"twitter:card\",\"content\":\"summary_large_image\"}],[\"$\",\"meta\",\"17\",{\"name\":\"twitter:title\",\"content\":\"Doubao Podcast TTS — AI Agent Skill | BytesAgain\"}],[\"$\",\"meta\",\"18\",{\"name\":\"twitter:description\",\"content\":\"Use when calling Doubao/ByteDance podcast TTS API to generate audio, parsing WebSocket binary frames, handling streaming audio chunks, extracting audio_url f...\"}],[\"$\",\"meta\",\"19\",{\"name\":\"twitter:image\",\"content\":\"https://bytesagain.com/social-preview.png\"}],[\"$\",\"meta\",\"20\",{\"name\":\"twitter:image:width\",\"content\":\"1200\"}],[\"$\",\"meta\",\"21\",{\"name\":\"twitter:image:height\",\"content\":\"630\"}],[\"$\",\"link\",\"22\",{\"rel\":\"icon\",\"href\":\"/favicon.ico?favicon.0x3dzn~oxb6tn.ico\",\"sizes\":\"256x256\",\"type\":\"image/x-icon\"}],[\"$\",\"$L1e\",\"23\",{}]]\n"])</script><script>self.__next_f.push([1,"1f:T1562,"])</script><script>self.__next_f.push([1,"\n .skill-page { max-width: 1100px; margin: 0 auto; padding: 32px 20px 80px; }\n .two-col { display: flex; gap: 32px; align-items: flex-start; }\n .two-col-main { flex: 1; min-width: 0; }\n .two-col-side { width: 300px; flex-shrink: 0; }\n @media (max-width: 860px) {\n .two-col { flex-direction: column; }\n .two-col-side { width: 100%; }\n }\n .breadcrumb { font-size: .82em; color: var(--text-muted2); margin-bottom: 28px; }\n .breadcrumb a { color: #818cf8; text-decoration: none; }\n .breadcrumb a:hover { text-decoration: underline; }\n .skill-card { background: var(--bg-card); border: 1px solid var(--border-card); border-radius: 20px; padding: 28px; margin-bottom: 24px; }\n .skill-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }\n .skill-badges { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n .skill-top-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }\n .badge { display: inline-flex; align-items: center; gap: 5px; font-size: .75em; font-weight: 600; padding: 4px 12px; border-radius: 999px; border: 1px solid transparent; }\n .skill-title { font-size: 1.6em; font-weight: 800; color: var(--text-primary); margin: 0 0 4px; line-height: 1.2; }\n .skill-owner { font-size: .82em; color: var(--text-muted2); margin: 0 0 14px; }\n .skill-owner span { color: #818cf8; }\n .skill-desc { font-size: .92em; color: var(--text-secondary); line-height: 1.65; margin: 0 0 16px; }\n .skill-meta { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid var(--border-card); }\n .meta-item { display: flex; flex-direction: column; gap: 2px; }\n .meta-label { font-size: .7em; color: var(--text-muted5); text-transform: uppercase; letter-spacing: 1px; font-weight: 600; }\n .meta-value { font-size: .92em; color: var(--text-muted2); font-weight: 600; }\n .tags-row { display: flex; gap: 6px; flex-wrap: wrap; }\n .tag { font-size: .75em; color: #6366f1; background: #6366f115; border: 1px solid #6366f130; border-radius: 6px; padding: 3px 10px; text-decoration: none; }\n .tag:hover { background: #6366f125; }\n .install-box { background: var(--bg-deep); border: 1px solid var(--border-card); border-radius: 12px; overflow: hidden; margin-bottom: 24px; }\n .install-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid var(--border-card); }\n .install-dots { display: flex; gap: 6px; }\n .dot { width: 10px; height: 10px; border-radius: 50%; }\n .install-label { font-size: .72em; color: var(--text-muted5); font-family: monospace; letter-spacing: 1px; }\n .install-body { padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }\n .install-cmd { color: var(--text-code);\n font-family: 'Courier New', monospace; font-size: 1em; }\n .copy-btn { font-size: .75em; color: #6366f1; background: #6366f115; border: 1px solid #6366f130; border-radius: 6px; padding: 5px 12px; cursor: pointer; white-space: nowrap; transition: all .15s; }\n .copy-btn:hover { background: #6366f125; }\n .btn-secondary { display: inline-flex; align-items: center; gap: 8px; padding: 13px 24px; background: transparent; border: 1px solid var(--border-card); border-radius: 10px; color: #6b7280; text-decoration: none; font-weight: 600; font-size: .95em; transition: all .15s; }\n .btn-secondary:hover { border-color: #818cf8; color: #818cf8; }\n .ours-badge { display: inline-flex; align-items: center; gap: 6px; font-size: .72em; font-weight: 700; color: #22d3ee; background: #22d3ee10; border: 1px solid #22d3ee30; border-radius: 999px; padding: 4px 14px; }\n .section-card { background: var(--bg-card); border: 1px solid var(--border-card); border-radius: 16px; padding: 22px 24px; margin-bottom: 20px; }\n .section-title { color: var(--text-primary); font-size: 1.08em; font-weight: 800; margin: 0 0 12px; display: flex; align-items: center; gap: 8px; }\n /* Script box */\n .script-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; background: var(--bg-input); border-bottom: 1px solid var(--border-card); }\n .script-filename { font-size: .72em; color: var(--text-muted2); font-family: 'Courier New', monospace; }\n .script-copy-btn { font-size: .72em; color: #6366f1; background: none; border: 1px solid #6366f130; border-radius: 4px; padding: 2px 10px; cursor: pointer; }\n .script-copy-btn:hover { background: #6366f115; }\n .script-body { padding: 14px 16px; font-family: 'Courier New', monospace; font-size: .82em; line-height: 1.6; color: var(--text-code); overflow-x: auto; max-height: 420px; overflow-y: auto; white-space: pre; }\n /* Articles */\n .article-card { display: block; background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: 10px; padding: 14px 16px; text-decoration: none; transition: border-color .15s; }\n .article-card:hover { border-color: #6366f1; }\n @media (max-width: 600px) {\n .skill-card { padding: 20px; }\n .skill-title { font-size: 1.5em; }\n }\n "])</script><script>self.__next_f.push([1,"15:[[\"$\",\"style\",null,{\"children\":\"$1f\"}],\"$L20\",\"$L21\"]\n"])</script><script>self.__next_f.push([1,"22:I[78297,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/12w5ognupk9fb.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"default\"]\n23:T4c7a,"])</script><script>self.__next_f.push([1,"\u003cp style=\"margin:8px 0\"\u003e\u003chr style=\"border:none;border-top:1px solid #1e1e3f;margin:12px 0\"\u003e\nname: doubao-podcast\ndescription: |\n Use when calling Doubao/ByteDance podcast TTS API to generate audio, parsing WebSocket binary frames, handling streaming audio chunks, extracting audio_url from PodcastEnd, troubleshooting podcast generation timeout or stuck issues, or designing podcast playback architecture (streaming vs non-streaming).\n Also use when: integrating podcast generation into a browser-based app (browser WebSocket cannot set custom headers), designing content caching to avoid redundant generation, or extracting article metadata for podcast titles.\n Trigger even without explicit mention of \"Doubao\" — any podcast TTS, ByteDance speech synthesis, or podcast generation task should use this skill.\n\u003chr style=\"border:none;border-top:1px solid #1e1e3f;margin:12px 0\"\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch2 style=\"color:#f3f4f6;margin:20px 0 10px;font-size:1.15em\"\u003e豆包播客 TTS API 集成指南\u003c/h2\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e基于 7 篇微信长文 POC + 线上生产环境的实战经验。覆盖从建连到拿到 mp3 的全流程,以及 11 条踩坑记录。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch3 style=\"color:#e5e7eb;margin:18px 0 8px;font-size:1.05em\"\u003e1. 接口概览\u003c/h3\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e| 项 | 值 |\n|----|-----|\n| 协议 | WebSocket 二进制协议 v3 |\n| 地址 | \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003ewss://openspeech.bytedance.com/api/v3/sami/podcasttts\u003c/code\u003e |\n| 鉴权 | 4 个 Header:\u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003eX-Api-App-Id\u003c/code\u003e、\u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003eX-Api-Access-Key\u003c/code\u003e、\u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003eX-Api-Resource-Id\u003c/code\u003e、\u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003eX-Api-App-Key\u003c/code\u003e |\n| Resource ID | \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003evolc.service_type.10050\u003c/code\u003e |\n| 输出 | MP3, 96kbps, 24kHz mono(固定) |\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch3 style=\"color:#e5e7eb;margin:18px 0 8px;font-size:1.05em\"\u003e2. 二进制协议\u003c/h3\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e豆包使用自定义二进制帧,\u003cstrong style=\"color:#e5e7eb\"\u003e不是\u003c/strong\u003e标准 JSON WebSocket。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e帧头(固定 4 字节)\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003e[0x11, 0x14, 0x10, 0x00]\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cli style=\"color:#94a3b8;margin:3px 0\"\u003e第 2 字节高 4 位 = message_type(0xF = 错误帧)\u003c/li\u003e\n\u003cli style=\"color:#94a3b8;margin:3px 0\"\u003e第 2 字节低 4 位 = flags(0x04 = 含 session_id)\u003c/li\u003e\n\u003cli style=\"color:#94a3b8;margin:3px 0\"\u003e第 3 字节高 4 位 = serialization(1=JSON, 0=binary audio)\u003c/li\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e两种帧格式\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003ePre-connection: header(4) + event_type(4, big-endian) + payload_size(4) + payload\nPost-connection: header(4) + event_type(4) + sid_len(4) + session_id + payload_size(4) + payload\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e构造/解析代码\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003ePython 版本见 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003escripts/generate_podcast.py\u003c/code\u003e。Node.js 版本:\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003econst HEADER = Buffer.from([0x11, 0x14, 0x10, 0x00]);\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003efunction preFrame(event, payload) {\n const p = Buffer.from(JSON.stringify(payload));\n const e = Buffer.alloc(4); e.writeUInt32BE(event);\n const l = Buffer.alloc(4); l.writeUInt32BE(p.length);\n return Buffer.concat([HEADER, e, l, p]);\n}\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003efunction postFrame(event, sid, payload) {\n const sb = Buffer.from(sid);\n const p = Buffer.from(JSON.stringify(payload));\n const e = Buffer.alloc(4); e.writeUInt32BE(event);\n const sl = Buffer.alloc(4); sl.writeUInt32BE(sb.length);\n const pl = Buffer.alloc(4); pl.writeUInt32BE(p.length);\n return Buffer.concat([HEADER, e, sl, sb, pl, p]);\n}\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003efunction parseEvent(data) {\n const buf = Buffer.from(data);\n if (buf.length \u003c 8) return { eventType: null, payload: {} };\n const mt = (buf[1] \u003e\u003e 4) \u0026 0xF;\n const fl = buf[1] \u0026 0xF;\n const ser = (buf[2] \u003e\u003e 4) \u0026 0xF;\n if (mt === 0xF) { /* 错误帧 */ return { eventType: -1, payload: {} }; }\n const evt = buf.readUInt32BE(4);\n let off = 8, payload = {};\n if (fl \u0026 0x04) {\n const sl = buf.readUInt32BE(off); off += 4 + sl; // 跳过 session_id\n const pl = buf.readUInt32BE(off); off += 4;\n if (pl \u003e 0 \u0026\u0026 ser === 1) try { payload = JSON.parse(buf.slice(off, off + pl).toString()); } catch {}\n }\n return { eventType: evt, payload };\n}\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch3 style=\"color:#e5e7eb;margin:18px 0 8px;font-size:1.05em\"\u003e3. 握手流程与事件表\u003c/h3\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003e客户端 豆包 API\n │── StartConnection(event=1) ────→│\n │←── ConnectionStarted(event=50) ─│ ← session_id 在二进制帧中\n │── StartSession(event=100) ─────→│ ← 携带播客参数\n │←── SessionStarted(event=150) ───│\n │ ┌── 流式循环 ──────────────────┐\n │←─┤ 360: RoundStart (JSON) │ ← 文案文本(第1轮为空!见坑10)\n │←─┤ 361: RoundResp (binary) │ ← 音频块 ~4.6KB/chunk\n │←─┤ 362: RoundEnd (JSON) │\n │ └─────────────────────────────┘\n │←── 363: PodcastEnd (JSON) ─────│ ← audio_url(⚠️ duration_sec 可能为 0,见坑9)\n │←── 152: SessionFinished ───────│ ← ⚠️ 经常不来,见坑2\n │── FinishConnection(event=2) ──→│\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e事件速查\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e| event | 名称 | payload | 关键字段 |\n|-------|------|---------|---------|\n| 1/2 | Start/FinishConnection | \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003e{}\u003c/code\u003e | — |\n| 50 | ConnectionStarted | 二进制 | session_id 在帧中提取 |\n| 100 | StartSession | JSON | 播客参数 |\n| 360 | RoundStart | JSON | \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003etext\u003c/code\u003e(第 1 轮为空) |\n| 361 | RoundResp | binary | 音频块 |\n| 363 | PodcastEnd | JSON | \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003emeta_info.audio_url\u003c/code\u003e |\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e提取 session_id(ConnectionStarted 帧)\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003econst buf = Buffer.from(data);\nlet off = 8;\nconst sidLen = buf.readUInt32BE(off); off += 4;\nconst sessionId = buf.slice(off, off + sidLen).toString();\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch3 style=\"color:#e5e7eb;margin:18px 0 8px;font-size:1.05em\"\u003e4. 两种输入模式\u003c/h3\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003einput_url(URL 文章)\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003e{\n input_info: { input_url: \"https://...\", return_audio_url: true }, // ⚠️ url 在 input_info 内\n use_head_music: true, use_tail_music: false,\n audio_config: { format: \"mp3\", sample_rate: 24000, speech_rate: 0 },\n speaker_info: { random_order: true, speakers: [\"zh_male_dayixiansheng_v2_saturn_bigtts\", \"zh_female_mizaitongxue_v2_saturn_bigtts\"] }\n}\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003einput_text(短文本 \u003c 200 字)\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003e{\n input_text: \"文本内容...\", // ⚠️ text 在顶层,不是 input_info\n audio_config: { ... },\n speaker_info: { ... }\n}\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch3 style=\"color:#e5e7eb;margin:18px 0 8px;font-size:1.05em\"\u003e5. 实战踩坑记录(11 条)\u003c/h3\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 1:input_url vs input_text 参数位置\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003e❌ {\"input_url\": \"https://...\"} → 顶层没这字段,静默失败\n✅ {\"input_info\": {\"input_url\": \"https://...\"}} → URL 模式正确用法\n✅ {\"input_text\": \"你好世界\"} → 短文本模式正确用法\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e搞反了不会报错,只会得到空结果。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 2:PodcastEnd 之后不要等 SessionFinished\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003eSessionFinished(152) \u003cstrong style=\"color:#e5e7eb\"\u003e经常不来或等 10 分钟+\u003c/strong\u003e。PodcastEnd(363) 拿到 audio_url 后\u003cstrong style=\"color:#e5e7eb\"\u003e立即 break\u003c/strong\u003e。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003eif (eventType === 363) {\n const url = payload.meta_info?.audio_url || '';\n ws.close(); // 立即关闭!\n resolve({ audioUrl: url });\n}\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e不加 break,5 分钟播客可能跑 15 分钟。这是最致命的坑。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 3:超时设 900s\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e| 文章长度 | 播客时长 | 生成耗时 |\n|---------|---------|---------|\n| ~2000字 | ~5 min | ~2.5 min |\n| ~5000字 | ~10 min | ~4 min |\n| ~25000字 | ~30 min | ~10 min |\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e统一设 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003eTIMEOUT = 900\u003c/code\u003e(15 分钟)。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 4:WebSocket 连接参数\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003ePython: \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003eping_timeout=120\u003c/code\u003e,不设的话长文生成时会心跳超时断开。\nNode.js: \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003ews\u003c/code\u003e 库默认无 ping,不需要额外设置。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 5:连接断开但已有音频块\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e音频块是完整 MP3 片段,直接拼接就是可播放文件。断连时不要丢弃已收到的块。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 6:audio_url 24 小时过期\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003eCDN URL 签名过期后返回 403。\u003cstrong style=\"color:#e5e7eb\"\u003e生成完成后立即下载 mp3 到自己的存储\u003c/strong\u003e。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 7:Python nohup 输出缓冲\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e后台运行加 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003epython -u\u003c/code\u003e(unbuffered)才能看到实时日志。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 8:浏览器 WebSocket 不支持自定义 Header ⭐\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e浏览器 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003enew WebSocket(url)\u003c/code\u003e \u003cstrong style=\"color:#e5e7eb\"\u003e无法设置 HTTP Header\u003c/strong\u003e。豆包需要 4 个鉴权 Header → \u003cstrong style=\"color:#e5e7eb\"\u003e必须走服务端代理\u003c/strong\u003e。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e推荐架构:\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003e浏览器 ── POST /api/podcast ──→ 服务端 ── WSS + Header ──→ 豆包\n ← SSE 流式进度 ──────── ← 二进制帧 ──────\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e服务端用 Node.js \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003ews\u003c/code\u003e 库(支持自定义 Header)连接豆包,通过 SSE(Server-Sent Events)把进度推给浏览器。凭证存在服务端,不暴露给前端。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 9:duration_sec 经常返回 0 ⭐\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003ePodcastEnd 的 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003emeta_info.duration_sec\u003c/code\u003e \u003cstrong style=\"color:#e5e7eb\"\u003e实测经常为 0 或不返回\u003c/strong\u003e。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e备用方案:根据音频大小估算(96kbps = 12KB/s):\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003econst durationSec = meta.duration_sec || Math.round(totalAudioBytes / 12000);\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 10:RoundStart 第一轮文案为空 ⭐\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e第 1 轮是片头音乐,\u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003etext\u003c/code\u003e 字段为空字符串。\u003cstrong style=\"color:#e5e7eb\"\u003e第 2 轮开始\u003c/strong\u003e才有实际文案内容。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e如果需要从文案中提取标题/主题,从 Round 2+ 的文案中取。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch4 style=\"color:#d1d5db;margin:14px 0 6px;font-size:.95em\"\u003e坑 11:微信文章标题无法从服务端抓取 ⭐\u003c/h4\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e从中国云服务器 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003efetch\u003c/code\u003e 微信文章会返回\"环境异常\"页面(反爬)。标题提取备用方案:\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e1. 优先:\u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003efetch\u003c/code\u003e 文章页面提取 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003e\u003ctitle\u003e\u003c/code\u003e 或 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003eog:title\u003c/code\u003e(非微信 URL 有效)\n2. 备用:从 RoundStart 第 2 轮文案提取主题(去口语化前缀如\"今天我们聊的是\")\n3. 兜底:使用 fallback 文本\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch3 style=\"color:#e5e7eb;margin:18px 0 8px;font-size:1.05em\"\u003e6. 内容存储与去重(生产架构)\u003c/h3\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e豆包 token 昂贵,必须避免重复生成。推荐架构:\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003ePOST /generate {url}\n → 查 store:相同 url 已有?\n → YES:直接返回缓存(0 token)\n → NO:调豆包生成 → 下载 mp3 到本地 → 写入 store → 返回\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003eStore 数据模型:\n\u003cpre style=\"background:#0a0a1c;border:1px solid #1e1e3f;border-radius:6px;padding:10px 12px;overflow-x:auto;font-size:.9em;margin:8px 0\"\u003e\u003ccode style=\"color:#a5f3fc;background:none;padding:0;font-size:1em\"\u003e{ \"id\": \"gen_xxx\", \"source_url\": \"https://...\", \"title\": \"...\", \"audio_file\": \"gen_xxx.mp3\", \"duration_sec\": 682 }\n\u003c/code\u003e\u003c/pre\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e音频下载到本地后不再依赖豆包 CDN(24h 过期)。\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch3 style=\"color:#e5e7eb;margin:18px 0 8px;font-size:1.05em\"\u003e7. 性能基线\u003c/h3\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e| 指标 | 值 |\n|------|-----|\n| 生成速度 | 2.7× 实时(1 分钟播客 ≈ 22s 生成) |\n| 首 token | avg 6s |\n| Token 消耗 | ~800 字/分钟播客,~3000 token/分钟 |\n| 音频块 | ~4.6KB/chunk |\n| 总测试量 | 7 篇 + 线上多篇,全部成功 |\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch3 style=\"color:#e5e7eb;margin:18px 0 8px;font-size:1.05em\"\u003e8. 错误处理\u003c/h3\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e| 场景 | 表现 | 处理 |\n|------|------|------|\n| 建连失败 | event=-1 | 检查凭证,重试 |\n| 内容过滤 | 错误码 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003e50302102\u003c/code\u003e | 提示不支持,不重试 |\n| 心跳超时 | 静默断开 | Python: \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003eping_timeout=120\u003c/code\u003e |\n| audio_url 过期 | CDN 403 | 用本地存储的 mp3 |\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e\u003ch3 style=\"color:#e5e7eb;margin:18px 0 8px;font-size:1.05em\"\u003e9. 参考实现\u003c/h3\u003e\u003c/p\u003e\u003cp style=\"margin:8px 0\"\u003e| 文件 | 语言 | 说明 |\n|------|------|------|\n| \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003escripts/generate_podcast.py\u003c/code\u003e | Python | 批量生成 + CLI,POC 验证用 |\n| 项目 \u003ccode style=\"background:#0d0d1e;color:#a5f3fc;padding:1px 5px;border-radius:3px;font-size:.88em\"\u003eserver/podcast-api.js\u003c/code\u003e | Node.js | 生产环境:HTTP API + SSE + 内容存储 |\n\u003c/p\u003e"])</script><script>self.__next_f.push([1,"20:[\"$\",\"div\",null,{\"className\":\"skill-page\",\"children\":[[\"$\",\"script\",null,{\"type\":\"application/ld+json\",\"dangerouslySetInnerHTML\":{\"__html\":\"{\\\"@context\\\":\\\"https://schema.org\\\",\\\"@type\\\":\\\"SoftwareApplication\\\",\\\"name\\\":\\\"Doubao Podcast TTS\\\",\\\"description\\\":\\\"Use when calling Doubao/ByteDance podcast TTS API to generate audio, parsing WebSocket binary frames, handling streaming audio chunks, extracting audio_url f...\\\",\\\"url\\\":\\\"https://bytesagain.com/skill/doubao-podcast\\\",\\\"applicationCategory\\\":\\\"clawhub\\\",\\\"operatingSystem\\\":\\\"Any\\\",\\\"offers\\\":{\\\"@type\\\":\\\"Offer\\\",\\\"price\\\":\\\"0\\\",\\\"priceCurrency\\\":\\\"USD\\\"},\\\"publisher\\\":{\\\"@type\\\":\\\"Organization\\\",\\\"name\\\":\\\"BytesAgain\\\",\\\"url\\\":\\\"https://bytesagain.com\\\"}}\"}}],[\"$\",\"div\",null,{\"className\":\"breadcrumb\",\"children\":[[\"$\",\"a\",null,{\"href\":\"/\",\"children\":\"BytesAgain\"}],\" › \",[\"$\",\"a\",null,{\"href\":\"/skills\",\"children\":\"Skills\"}],\" › \",\"Doubao Podcast TTS\"]}],[\"$\",\"div\",null,{\"className\":\"two-col\",\"children\":[[\"$\",\"div\",null,{\"className\":\"two-col-main\",\"children\":[[\"$\",\"div\",null,{\"className\":\"skill-card\",\"children\":[[\"$\",\"div\",null,{\"className\":\"skill-header\",\"children\":[[\"$\",\"div\",null,{\"className\":\"skill-badges\",\"children\":[[\"$\",\"span\",null,{\"className\":\"badge\",\"style\":{\"color\":\"#818cf8\",\"background\":\"#818cf822\",\"borderColor\":\"#818cf844\"},\"children\":[\"🦀\",\" \",\"ClawHub\"]}],false]}],[\"$\",\"div\",null,{\"className\":\"skill-top-actions\",\"children\":[\"$\",\"$L22\",null,{\"slug\":\"doubao-podcast\"}]}]]}],[\"$\",\"h1\",null,{\"className\":\"skill-title\",\"children\":\"Doubao Podcast TTS\"}],[\"$\",\"p\",null,{\"className\":\"skill-owner\",\"children\":[\"by \",[\"$\",\"span\",null,{\"children\":[\"@\",\"mileszhang001-boom\"]}]]}],[\"$\",\"p\",null,{\"className\":\"skill-desc\",\"children\":\"Use when calling Doubao/ByteDance podcast TTS API to generate audio, parsing WebSocket binary frames, handling streaming audio chunks, extracting audio_url f...\"}],[\"$\",\"div\",null,{\"className\":\"skill-meta\",\"children\":[[\"$\",\"div\",null,{\"className\":\"meta-item\",\"children\":[[\"$\",\"span\",null,{\"className\":\"meta-label\",\"children\":\"Version\"}],[\"$\",\"span\",null,{\"className\":\"meta-value\",\"children\":[\"v\",\"1.0.0\"]}]]}],[\"$\",\"div\",null,{\"className\":\"meta-item\",\"children\":[[\"$\",\"span\",null,{\"className\":\"meta-label\",\"children\":\"Downloads\"}],[\"$\",\"span\",null,{\"className\":\"meta-value\",\"children\":\"666\"}]]}],false,false,false,[\"$\",\"div\",null,{\"className\":\"meta-item\",\"style\":{\"flexDirection\":\"row\",\"gap\":6,\"alignItems\":\"center\"},\"children\":[[\"$\",\"a\",\"audio\",{\"href\":\"/?q=audio\",\"className\":\"tag\",\"children\":[\"#\",\"audio\"]}],[\"$\",\"a\",\"legal\",{\"href\":\"/?q=legal\",\"className\":\"tag\",\"children\":[\"#\",\"legal\"]}]]}]]}],[\"$\",\"div\",null,{\"style\":{\"marginTop\":6},\"children\":[\"$\",\"a\",null,{\"href\":\"https://clawhub.ai/mileszhang001-boom/doubao-podcast\",\"target\":\"_blank\",\"rel\":\"noopener\",\"className\":\"btn-secondary\",\"style\":{\"padding\":\"6px 12px\",\"fontSize\":\".82em\",\"borderRadius\":8,\"background\":\"transparent\",\"border\":\"1px solid var(--border-card)\",\"color\":\"var(--text-muted2)\",\"textDecoration\":\"none\",\"whiteSpace\":\"nowrap\"},\"children\":[\"View on \",\"ClawHub\",\" →\"]}]}]]}],[\"$\",\"div\",null,{\"className\":\"install-box\",\"children\":[[\"$\",\"div\",null,{\"className\":\"install-header\",\"children\":[[\"$\",\"div\",null,{\"className\":\"install-dots\",\"children\":[[\"$\",\"div\",null,{\"className\":\"dot\",\"style\":{\"background\":\"#ef4444\"}}],[\"$\",\"div\",null,{\"className\":\"dot\",\"style\":{\"background\":\"#eab308\"}}],[\"$\",\"div\",null,{\"className\":\"dot\",\"style\":{\"background\":\"#22c55e\"}}]]}],[\"$\",\"span\",null,{\"className\":\"install-label\",\"children\":\"TERMINAL\"}]]}],[\"$\",\"div\",null,{\"className\":\"install-body\",\"style\":{\"flexWrap\":\"wrap\"},\"children\":[[\"$\",\"code\",null,{\"className\":\"install-cmd\",\"children\":\"clawhub install doubao-podcast\"}],[\"$\",\"button\",null,{\"className\":\"copy-btn\",\"data-cmd\":\"clawhub install doubao-podcast\",\"style\":{\"fontWeight\":700},\"children\":\"Copy\"}]]}]]}],[\"$\",\"section\",null,{\"className\":\"skill-card\",\"style\":{\"marginBottom\":20},\"children\":[[\"$\",\"h2\",null,{\"style\":{\"color\":\"#f8fafc\",\"fontSize\":\"1.2em\",\"fontWeight\":800,\"margin\":\"0 0 16px\",\"display\":\"flex\",\"alignItems\":\"center\",\"gap\":8},\"children\":\"📖 About This Skill\"}],[\"$\",\"div\",null,{\"style\":{\"fontSize\":\".92em\",\"color\":\"#94a3b8\",\"lineHeight\":1.75},\"dangerouslySetInnerHTML\":{\"__html\":\"$23\"}}]]}],null,null,null,null,null,null,null,false,false]}],\"$L24\"]}]]}]\n"])</script><script>self.__next_f.push([1,"21:[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"\\n document.querySelectorAll('.copy-btn, .script-copy-btn').forEach(btn =\u003e {\\n btn.addEventListener('click', () =\u003e {\\n const cmd = btn.getAttribute('data-cmd');\\n if (!cmd) return;\\n navigator.clipboard.writeText(cmd).then(() =\u003e {\\n const orig = btn.textContent;\\n btn.textContent = 'Copied!';\\n setTimeout(() =\u003e btn.textContent = orig, 1500);\\n }).catch(() =\u003e {});\\n });\\n });\\n \"}}]\n"])</script><script>self.__next_f.push([1,"25:I[71521,[\"/_next/static/chunks/0ph_0rx9ah5dc.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/0i_x3w546rsb3.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/06ig5gym-0n-u.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\",\"/_next/static/chunks/12w5ognupk9fb.js?dpl=dpl_9zPFH6pojPkqyspdDNz28GzCc6KQ\"],\"default\"]\n24:[\"$\",\"div\",null,{\"className\":\"two-col-side\",\"children\":[\"$\",\"$L25\",null,{\"category\":\"clawhub\",\"currentSlug\":\"doubao-podcast\",\"name\":\"Doubao Podcast TTS\",\"tags\":[\"audio\",\"legal\"]}]}]\n"])</script></body></html>