← 全部作品

Work — 2026-08

Zifo 知否 · 课文逐词朗读

课文 mp3 同步逐词高亮 + 波形进度,像 Apple Music 歌词那样听课本。

已上线可试玩Vue 3Web AudioCanvasTTMLWhisper静态资源驱动

手机竖屏应用。演示入口与主应用同源,但绕过登录守卫,点开即可直接试玩朗读功能。

把课本音频做成“能逐词跟读”的形态——播到哪个词,哪个词就亮起来,点任意一句还能跳回去重听。

怎么长出来的

这个项目一开始不是做朗读的。

2026-06-06,立项。 当时它是一个“帮小学生记课本单词”的 Web PWA:后端有 SM-2 间隔重复算法,前端有单词卡与学习记录,还配了管理后台和 JWT 登录。

6 月 7~11 日,单词线成型。 管理后台、学习记录、PWA 离线,一周内跑通。

7 月 13 日,主线换了。 这一天加进来的是课文朗读:五年级的音频、AudioView.vue、页面到 Unit 的映射。第二天补上离线生成的时间轴、点文本跳转,并把自写的歌词滚动换成了 AMLL。7 月 15 日改解析 TTML,逐词时间轴从此定型。

7 月 24 日到 9 月 14 日,逐单元补内容。 一个单元一个单元地加音频与字幕,中间还专门提交过一次人工修正 TTML 的分段与词级时间。

为什么转向,仓库里没有记载——设计文档停在 6 月 6 日,之后的提交信息只说做了什么、没说为什么。我能确认的是:单词学习那部分代码还在仓库里,只是 7 月 13 日之后再没被碰过。这段动机只有我自己知道,改天补上。

和主线的结合点

我的主线是「探索面向 AI 的交互边界」。这块作品在这条线上站的位置不太一样,得说清楚。

它运行时没有任何 AI。 全仓没有一处模型调用。唯一真实的 AI 在离线脚本里:用 Whisper(stable-ts)把课文音频对齐成词级时间轴。

所以它不是“AI 在系统里有位置”的探针。 按三条判据——产出可继续编辑的结构、能主动发起、和人共用工作台——后两条完全不成立,产品里根本没有 AI 这个角色。

它回答的是同一个问题的另一面:AI 该放在哪里。 这里给出的答案是把重活放在离线,把结果固化成人和机器都能继续改的中间格式(TTML)。判据里第一条在这里是弱形式:TTML 是可读、可人工继续修改的结构,有提交为证——确实有人工修正分段与词级时间的那一次。

这条取舍很具体:用户点开就要立刻能听,不能等模型。 代价是新增课文要跑一次离线脚本;换来的是运行时零推理、朗读页首启约 1 秒。

和白板放在一起看会更有意思:那块白板把 AI 放进系统内部、和人的操作走同一条路;这块把 AI 挡在运行时之外。同一个“位置”问题,两个相反的答案。

难点

Whisper 的输出和课本的结构对不上。 它吐出的是整段词流,而课本要按句分组。解法是按原句的词数把词流切回去;切不齐时会出现零时长的词,还得插值补上——否则那一段的歌词会卡住不动。

TTML 比想象中松。 它用 itunes 命名空间标记 timing="word",时间还有 0.2s00:00:01.540 两种写法。前端用 DOMParsergetElementsByTagNameNS 解析,两种都认;解析失败就降级成空,不让整页挂掉。

滚动会打架。 用户手动滚歌词时,播放器仍在按时间推高亮,两者抢同一个滚动位置,画面会一跳一跳。解法是在用户滚动期间冻结传给歌词组件的时间输入,等 1.5 秒惯性结束再恢复。

配置写了两份。 页面到 Unit 的映射在前端 audio-pages.ts 里,离线脚本又“抄”了一份(脚本注释里自己也承认了)。加一个 Unit 必须两边同时改——这是一个明确的债,还没还。

iframe 里的自动播放。 演示入口希望点开就播,但浏览器要求宿主委派 allow="autoplay" 并且顶层文档有过用户手势;而选页和音频解码又都是异步的。最后是轮询重试,直到暂停图标真的出现为止。

亮点

静态资源驱动。 音频与时间轴走一套路径约定,加一课只需要放两个文件、改一个配置数组,不碰后端。这也是它能以纯静态形态嵌进来的前提。

TTML 当人机共用的中间格式。 它既是离线脚本的产物,也是人能打开来改的文件——前面那次人工修正就是这么做的。前端不需要自己写歌词滚动,直接喂给现成的词级组件。

波形自己算自己画。AudioContext 解码,按 300ms 分桶取最大振幅,画在 canvas 上——比逐帧采样快一个数量级,也没有引第三方的波形库。canvas 还按 devicePixelRatio 缩放过,Retina 屏上不发虚。

演示入口零侵入。 独立入口加一次多入口构建就够了:装自己的路由、不挂登录守卫、只写偏好不写 token。主应用逻辑一行没改,登录流程也完全不受影响。

核心功能

  • 年级 → Unit 1~10 → 课文,逐层进入后点播放
  • 播放时歌词按单词粒度高亮,时间轴来自离线对齐
  • 点击任意句子立即跳转回听,音频 seek 与歌词滚动同步
  • 波形进度条:实时解码 mp3,300ms 颗粒度取振幅
  • 手动滚动歌词时冻结播放时间,避免滚动与高亮互相打架

现状

朗读功能已经可以页内试玩,线上地址就是主站 iframe 指向的那个演示入口。

它一度做不到,但原因不是“技术架构不允许”,而是被登录守卫挡着/audio 路由声明了 requiresAuth,全局守卫检查 localStorage 里的 token,未登录直接跳登录页。

核对源码后发现三件事:

  • AudioView.vue 运行时只 fetch 时间轴与音频,没有任何后端调用
  • 音频与时间轴都在 public/ 下随产物发布(59 个音频、57 个时间轴)
  • 而那个守卫只是前端判断——只看 token 存不存在,并不校验有效性

所以解法不是“改造架构”,而是给它加一个独立演示入口。已实现并验证:首屏约 197 KB(gzip),运行时零后端请求,在 iframe 内能正常播放并逐词高亮。

因为是手机竖屏应用,这里用 9:16 的框展示。