从写代码,到创作下一幕

探索 字节跳动 - 火山方舟 的 AI 编程与视频创作活动。

火山方舟 Agent Plan 与 Coding Plan 活动,价格 ¥9.9 起

Agent Plan & Coding Plan

一站体验多款热门模型,为 AI 编程与智能体开发提供更多选择。新用户可联系(微信: goo_lvyouyou)免费体验 9.9 agent plan。

查看套餐优惠
Seedance 2.5 视频创作活动:30 秒视频、多模态参考、局部编辑

Seedance 2.5

让创意,跃然成片。探索 30 秒视频、多模态参考与局部编辑,把脑海中的画面变成下一支作品。

探索视频创作

文章同步朗读

查看 Markdown

本示例使用 Grok API 构建文章朗读播放器,将文章转换为音频,并随播放逐词高亮。制作字幕、实现卡拉 OK 式高亮或跳转到音频中的任意位置,都需要这样的逐词时间信息。

粘贴文章链接或正文后,播放器会朗读文章,每读到一个词就高亮显示。点击任意词即可跳到对应位置。文章按段落拆分,其余段落还在录制时,首段就能开始播放。

你将学到什么

  • 从 client.voice.speak() 获取逐字符时间戳,并据此计算每个词的起止时间

  • 将文章拆成段落,在其余段落仍在录制时播放首段

  • 将每个词的时间与音频当前播放时间比对,高亮正在朗读的词,并支持跳转到任意词

  • 通过服务器发送事件(SSE)将每个段落从 Node 服务器流式传到网页,按字节范围提供音频,以便浏览器跳转播放

运行示例

需要 Node.js 22.13 或更高版本。将 API 密钥写入 .env(位于仓库根目录),或导出环境变量 XAI_API_KEY。

Bash

cd examples/article-to-read-along/typescript
npm install
npm run web

打开 http://localhost:3000 并点击 Try the sample,或粘贴文章链接或正文,再点击 Read it to me。页面的用法与朗读应用相同:全文会立即显示,几秒后开始播放第一段,并高亮正在朗读的词。点击任意词即可跳转,也可使用进度条、跳转按钮和调速按钮。右侧面板会在录制过程中列出各个段落,并显示当前词及其前后各词的起止时间。点击 Stop 可取消尚未完成的录制。

要在终端中朗读文章,请传入链接、文本文件路径或已保存网页的路径:

Bash

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:

  1. readArticle() 抓取链接,保留网页标题及其中 <p> 元素的文本。对于大多数网站,这会排除菜单、图片说明和脚注标记。如果网页未将正文放在 <p> 元素中,请直接粘贴文本。fromText() 按空行拆分粘贴的文本;没有空行时则按换行符拆分。方括号会转换成圆括号,以免语音 API 将 [pause] 或 [sic] 误作语音标签。

  2. recordParagraph() 录制单个段落,使用 client.voice.speak() 和 with_timestamps: true。响应为 JSON,包含经 base64 编码的音频和两个数组,而非原始音频字节:graph_chars 逐个保存文本字符,graph_times 为每个字符保存一对 [start, end] 时间,单位为秒。

  3. toWords() 将字符及其时间转换为词。它逐个遍历字符,遇到空格就开始一个新词。每个词从首字符开始,到末字符结束。graph_chars 为每个 Unicode 字符保存一项,因此一个表情符号在这里占一项,而在 JavaScript 字符串中占两个位置。这就是代码遍历 graph_chars 而不直接按索引读取原文的原因。

  4. 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() 可以列出内置声音。