文章同步朗读
本示例使用 Grok API 构建文章朗读播放器,将文章转换为音频,并随播放逐词高亮。制作字幕、实现卡拉 OK 式高亮或跳转到音频中的任意位置,都需要这样的逐词时间信息。
粘贴文章链接或正文后,播放器会朗读文章,每读到一个词就高亮显示。点击任意词即可跳到对应位置。文章按段落拆分,其余段落还在录制时,首段就能开始播放。
你将学到什么
从
client.voice.speak()获取逐字符时间戳,并据此计算每个词的起止时间将文章拆成段落,在其余段落仍在录制时播放首段
将每个词的时间与音频当前播放时间比对,高亮正在朗读的词,并支持跳转到任意词
通过服务器发送事件(SSE)将每个段落从 Node 服务器流式传到网页,按字节范围提供音频,以便浏览器跳转播放
运行示例
需要 Node.js 22.13 或更高版本。将 API 密钥写入 .env(位于仓库根目录),或导出环境变量 XAI_API_KEY。
cd examples/article-to-read-along/typescript
npm install
npm run web打开 http://localhost:3000 并点击 Try the sample,或粘贴文章链接或正文,再点击 Read it to me。页面的用法与朗读应用相同:全文会立即显示,几秒后开始播放第一段,并高亮正在朗读的词。点击任意词即可跳转,也可使用进度条、跳转按钮和调速按钮。右侧面板会在录制过程中列出各个段落,并显示当前词及其前后各词的起止时间。点击 Stop 可取消尚未完成的录制。
要在终端中朗读文章,请传入链接、文本文件路径或已保存网页的路径:
npm start -- https://en.wikipedia.org/wiki/Voyager_Golden_Record不传参数时读取 sample-article.txt,这是一篇介绍火箭助推器如何自行着陆的短文。音频将保存到 output/<title>/article.mp3,每个词的起止时间则保存在旁边的 timings.json 文件中。
语音费用为每百万字符 15 美元,因此一篇 10,000 字符的文章约需 0.15 美元。示例文章有 2,483 个字符,实测花费 0.04 美元。点击后 2 至 6 秒开始播放首段;七段共 2 分 35 秒的音频在 11 至 21 秒内全部录制完成。每次最多朗读 20,000 个字符,相当于约 20 分钟音频,费用约 0.30 美元。要调整上限,请修改 MAX_CHARACTERS,其所在文件为 src/readalong.ts。语音 API 不返回单次请求的费用,应用根据字符数计算。
工作原理
共享代码位于 src/readalong.ts:
readArticle()抓取链接,保留网页标题及其中<p>元素的文本。对于大多数网站,这会排除菜单、图片说明和脚注标记。如果网页未将正文放在<p>元素中,请直接粘贴文本。fromText()按空行拆分粘贴的文本;没有空行时则按换行符拆分。方括号会转换成圆括号,以免语音 API 将[pause]或[sic]误作语音标签。recordParagraph()录制单个段落,使用client.voice.speak()和with_timestamps: true。响应为 JSON,包含经 base64 编码的音频和两个数组,而非原始音频字节:graph_chars逐个保存文本字符,graph_times为每个字符保存一对[start, end]时间,单位为秒。toWords()将字符及其时间转换为词。它逐个遍历字符,遇到空格就开始一个新词。每个词从首字符开始,到末字符结束。graph_chars为每个 Unicode 字符保存一项,因此一个表情符号在这里占一项,而在 JavaScript 字符串中占两个位置。这就是代码遍历graph_chars而不直接按索引读取原文的原因。recordAll()会优先单独录制首段,让听众尽快开始收听,之后每次并行录制四段,每段完成后立即通知。录制速度通常是实际播放速度的数倍,因此播放时很少需要等待。
src/server.ts 是一个小型 node:http 服务器。EventSource 只能发送 GET 请求,而粘贴的文本可能过长,无法放入 URL。因此网页先将待朗读内容 POST 到 /api/articles,然后打开 /api/read 并传入返回的 ID。服务器一读完原文就发送文章,每录完一段便发送该段的音频 URL 和词序列。音频保存在内存中并支持按字节范围获取,否则 Chrome 会无法跳转播放。关闭网页或点击 Stop 会中止正在进行的录制。
public/index.html 使用原生 HTML 和 JavaScript,通过一个 Audio 元素依次播放各段。播放时,每一帧动画都会检查当前时间,并高亮最近开始朗读的词。点击某个词会将播放时间设为它的起点。每个词都是一个 span,文字与空格都保持原样,音频就绪时页面不会跳动。
src/index.ts 在终端中完成同样的工作,并将所有音频片段合并为一个 MP3。每段实际时长比 API 报告值长约 50 毫秒,整篇文章累积起来会产生偏差。因此,程序按前面各片段的文件大小计算每段在合并文件中的起点。在默认的 128 kbps 下,MP3 每秒为 16,000 字节。
要更换声音,请修改 VOICE,其所在文件为 src/readalong.ts。client.voice.list() 可以列出内置声音。