首页 独立开发实战内容详情

给 Agent 产物自动配音:edge-tts 全链路实录

2026-11-03 6

给 Agent 产物自动配音:edge-tts 全链路实录

凌晨一点,第 47 篇工具页刚发布完。我盯着终端日志里躺着的一段文字发呆——那是 Agent 在 plan 阶段写好的逐屏讲解稿,写得比页面正文还顺口。然后它就被扔进了日志,再也没人看过。页面本身?一个哑巴。

这是「Agent 全链路」系列第 5 篇。上一篇我们把发布流水线跑通了,这次补上最不该缺的一环:把已经存在的讲解稿,变成页面里能点开就播的语音。

核心要点

  • 讲解稿早就有了:plan 阶段 Agent 产出的 narration 一直被当垃圾丢掉,数据已存在,只是没人去接
  • 四步链路:plan narration → HTML data-narration 属性 → 发布后 tts 节点用 edge-tts 合成 mp3 → 前端 JS 播放条
  • edge-tts 零成本:白嫖 Edge 浏览器朗读接口,中文音色够用,一篇稿子一分钱不花
  • 降级是底线:语音合成挂了只记 warn,绝不阻断发布,页面照发
  • 最大的坑:job.path 带文件名,拼输出目录必须先 path.dirname(),否则直接翻车

一、被扔掉的讲解稿:最贵的数据往往已经在手里

先说人话:所谓 narration,就是 Agent 做页面规划时,顺手给每个区块写的口语化讲解稿。

之前我一直当它是过程产物,日志里一滚就没了。为什么不重新调 LLM 生成一遍?没必要——plan 里那份本来就是按“念给人听”的语气写的,重新生成反而多花一次 token,还可能跟页面结构对不上。

数据复用:比起再造一份数据,把已经在手里的一份数据接到链路上,成本是零,收益是整个页面活了。这种现成的便宜不占,属于典型的被坑惨了还不知道。

二、注入链路:四步把哑巴页面变成有声书

整条链路长这样,每一步都很短:

1. plan 阶段:Agent 输出结构化 plan,每个区块带 narration 字段,一两句话讲这个区块是什么

2. 模板渲染:narration 原样写进 HTML,挂在对应区块的 data-narration 属性上

3. tts 节点:发布完成后,流水线追加一个独立节点,扫描所有 data-narration,调 edge-tts 合成 mp3,落盘到页面同级的 audio/ 目录

4. 播放条 JS:前端脚本检测 mp3 是否存在,存在就渲染播放条,播放时滚动定位到对应区块并高亮

为什么把文本塞进 HTML 属性,而不是单独存一份 JSON?因为 data-narration 跟着 DOM 走,tts 节点和前端 JS 读的是同一份真源,不用维护两边的映射关系。映射这种东西,迟早会漂。

三、edge-tts:免费 TTS 里的突击套利

edge-tts:一个逆向了 Edge 浏览器“大声朗读”功能的 Python 库,调的是微软云端接口,不花一分钱。

音色直接选 zh-CN-XiaoxiaoNeural,中文断句和语气在这个价位(零元)里没有对手。为什么不用付费 TTS API?一篇稿子几毛钱听着不多,但你要发几百上千个页面,那就是每月固定的真金白银——对独立站来说这是硬伤。

零成本是不是就躺赚了?是,但有代价:这是非官方接口,微软哪天想封就封。所以别赌运气,所有对它的依赖都必须是“可失败的”,这就引出下一节。

四、失败降级:语音挂了,页面照发

设计 tts 节点时只守一条原则:语音是增强,不是依赖。

具体做法:tts 节点跑失败,只打一行 warn 日志、记个失败清单,退出码返回 0,发布流水线当无事发生。前端那边同样——JS 请求 mp3 拿到 404,播放条直接不渲染,页面安静但完整。

那没语音的页面还有价值吗?有。narration 文本还在 data-narration 里,最坏情况大不了后续手动补跑一次合成,或者干脆让用户自己读。语音挂掉最多是“少了个播放键”,而阻断发布是“整个页面没了”,孰轻孰重一目了然。

五、踩坑实录:job.path 里的文件名陷阱

这个坑我必须单开一节,因为它害我查了半小时。

需求很简单:把 mp3 输出到页面同目录下。我顺手写了 path.join(job.path, 'audio'),心想这不就是拼个目录吗。结果第一跑就翻车:mp3 全写到了 index.html/audio/ 这种鬼路径下,报的还是“目录不存在”,看着跟权限问题似的,极其迷惑。

根因:job.path 是 /dist/pages/xxx/index.html,带文件名。拿它当目录拼子路径,拼出来就是 index.html/audio/ 这种废路径。正解一行:

out_dir = Path(job.path).parent / 'audio' # 或 os.path.join(os.path.dirname(job.path), 'audio')

先 dirname 剥掉文件名再拼,完事。一行代码的坑,报错信息却完全不指向真相,这就是典型的那种“被坑惨了才记住”的知识点。

顺带一提:narration 是长文本,直接塞 HTML 属性容易被引号转义折腾得死去活来。我后来干脆让 Agent 输出时把文本做 base64 编码,挂在 data-narration-b64 上,前端 atob 一下再用, tts 节点同样解码,转义问题彻底消失。调试时对着编码串肉眼看不出内容,我是直接用站里的 Base64/URL 编码互转工具 贴进去解一下,确认 Agent 到底编码了啥,比在终端里瞎猜快得多。

FAQ / 常见问题

edge-tts 会不会哪天突然不能用了?

会,它是逆向接口,没有 SLA,别赌运气。所以降级逻辑不是可选项,是保命符——挂了最多没声音,绝不能挂了没页面。

mp3 是发布时同步合成,还是事后异步补?

异步。tts 节点在发布动作完成之后单独跑,成败都不碰发布结果,必要时可以拿失败清单单独重跑。

narration 以后会不会跟页面内容对不上?

会漂。所以真源只有一份——data-narration 里那段文本,页面改版必然重渲染属性,tts 节点每次按属性内容重新合成,自然保持同步。凡是需要两处手工同步的方案,都是未来的坑。

  • 评论列表 (0条)

 暂无评论,快来抢沙发吧~

发布评论