凌晨一点,第 47 篇工具页刚发布完。我盯着终端日志里躺着的一段文字发呆——那是 Agent 在 plan 阶段写好的逐屏讲解稿,写得比页面正文还顺口。然后它就被扔进了日志,再也没人看过。页面本身?一个哑巴。
这是「Agent 全链路」系列第 5 篇。上一篇我们把发布流水线跑通了,这次补上最不该缺的一环:把已经存在的讲解稿,变成页面里能点开就播的语音。
先说人话:所谓 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:一个逆向了 Edge 浏览器“大声朗读”功能的 Python 库,调的是微软云端接口,不花一分钱。
音色直接选 `zh-CN-XiaoxiaoNeural`,中文断句和语气在这个价位(零元)里没有对手。为什么不用付费 TTS API?一篇稿子几毛钱听着不多,但你要发几百上千个页面,那就是每月固定的真金白银——对独立站来说这是硬伤。
零成本是不是就躺赚了?是,但有代价:这是非官方接口,微软哪天想封就封。所以别赌运气,所有对它的依赖都必须是“可失败的”,这就引出下一节。
设计 tts 节点时只守一条原则:语音是增强,不是依赖。
具体做法:tts 节点跑失败,只打一行 warn 日志、记个失败清单,退出码返回 0,发布流水线当无事发生。前端那边同样——JS 请求 mp3 拿到 404,播放条直接不渲染,页面安静但完整。
那没语音的页面还有价值吗?有。narration 文本还在 `data-narration` 里,最坏情况大不了后续手动补跑一次合成,或者干脆让用户自己读。语音挂掉最多是“少了个播放键”,而阻断发布是“整个页面没了”,孰轻孰重一目了然。
这个坑我必须单开一节,因为它害我查了半小时。
需求很简单:把 mp3 输出到页面同目录下。我顺手写了 `path.join(job.path, 'audio')`,心想这不就是拼个目录吗。结果第一跑就翻车:mp3 全写到了 `index.html/audio/` 这种鬼路径下,报的还是“目录不存在”,看着跟权限问题似的,极其迷惑。
根因:`job.path` 是 `/dist/pages/xxx/index.html`,带文件名。拿它当目录拼子路径,拼出来就是 `index.html/audio/` 这种废路径。正解一行:
```python
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 到底编码了啥,比在终端里瞎猜快得多。
edge-tts 会不会哪天突然不能用了?
会,它是逆向接口,没有 SLA,别赌运气。所以降级逻辑不是可选项,是保命符——挂了最多没声音,绝不能挂了没页面。
mp3 是发布时同步合成,还是事后异步补?
异步。tts 节点在发布动作完成之后单独跑,成败都不碰发布结果,必要时可以拿失败清单单独重跑。
narration 以后会不会跟页面内容对不上?
会漂。所以真源只有一份——`data-narration` 里那段文本,页面改版必然重渲染属性,tts 节点每次按属性内容重新合成,自然保持同步。凡是需要两处手工同步的方案,都是未来的坑。
评论列表 (0条)