把GPT打造成听得见的完美助教

把GPT打造成听得见的完美助教

ChatGPT 网页版的“朗读”很好用,但有两个小遗憾:不能中途暂停思考,也不能自由拖动进度。

我的需求看起来很简单:

听到不懂的地方,能暂停;想通以后,继续。
需要重复的地方,可以随意的移动进度条,反复的揣摩。

最后,我做了一个 Chrome 插件。它不仅能暂停、恢复、快进、快退和拖动,还解决了流式音频后台下载、完整缓存和多次朗读串台的问题。
效果如下:


image.png

把GPT打造成听得见的完美助教

ChatGPT 网页版的“朗读”很好用,但有两个小遗憾:不能中途暂停思考,也不能自由拖动进度。

我的需求看起来很简单:

听到不懂的地方,能暂停;想通以后,继续。
需要重复的地方,可以随意的移动进度条,反复的揣摩。

最后,我做了一个 Chrome 插件。它不仅能暂停、恢复、快进、快退和拖动,还解决了流式音频后台下载、完整缓存和多次朗读串台的问题。
效果如下:

[图片上传中...(image-4b33d3-1787997223011-0)]

项目地址:https://github.com/STEPHENXING/chrome-GPT-voice-complete-player

第一个误会:Blob URL 不是音频下载地址

开发者工具里能看到这样的地址:

<pre class="hljs code__pre" style="color: #c9d1d9; background: #0d1117; font-size: 90%; overflow-x: auto; border-radius: 8px; line-height: 1.5; margin: 10px 8px; box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); padding: 0 !important;">blob:https://chatgpt.com/...</pre>

它看起来像一个 URL,但它不是服务器上的 MP3 地址,更像浏览器内存里的一张“临时门牌号”。

继续观察日志后,我发现 ChatGPT 使用的是:

  • MediaSource
  • SourceBuffer
  • audio/aac
  • • 分块 appendBuffer()

而且为了节省内存,ChatGPT 会不断调用 SourceBuffer.remove(),删除已经播放过的旧区间。

这解释了最早的 Bug:播放器显示可以回到 8 秒,但真正的缓存已经从 9 秒开始。游标能过去,声音回不去。

核心设计:不要让一个游标干两份工作

最初的方案只有一个播放器:

  • • 暂停时把它静音,让下载继续;
  • • 恢复时把 currentTime 拉回暂停位置。

表面上像有两个游标,实际上还是同一个。用户一倒退,负责向 ChatGPT 请求后续音频的游标也跟着倒退,长音频就可能卡住。

最终方案是两个真正独立的播放器:

<pre class="hljs code__pre" style="color: #c9d1d9; background: #0d1117; font-size: 90%; overflow-x: auto; border-radius: 8px; line-height: 1.5; margin: 10px 8px; box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); padding: 0 !important;">ChatGPT 原生播放器(Producer) │ │ 复制 AAC 数据块 ▼ 插件独立播放器(Consumer)</pre>

Producer 只做一件事:静音、持续向前,让 ChatGPT 把整段音频生成完。

Consumer 也只做一件事:面向用户播放,负责暂停、恢复、快进、快退和进度拖动。

这样,用户可以停在 20 秒慢慢思考,Producer 已经在后台跑到 60 秒;恢复时 Consumer 从 20 秒继续,Producer 完全不受影响。

这其实是一个很通用的工程原则:

数据生产和用户消费节奏不同,就不要强迫它们共用同一个状态。

Session 隔离:最难的不是播放,而是不串台

ChatGPT 会复用同一个 <audio> 元素,也可能在不同时间使用:

  • • 新的 MediaSource;
  • • 完整的 AAC Blob;
  • • 已经结束的同一个音频再次播放。

因此,Session 不能简单等于“一个 DOM 元素”,也不能等于“一个 GPT 对话”。

插件最终把 Session 定义为:

用户发起的一次独立播放实例。

每次新播放都会:

  1. 1. 分配新的 sessionId
  2. 2. 清理旧 Consumer 和旧缓存;
  3. 3. 把进度重置为 0:00
  4. 4. 绑定当前音频源;
  5. 5. 阻止旧事件修改新 Session。

这里还遇到过一个很隐蔽的竞态:ChatGPT 复用了同一个 audio 元素,我们销毁旧 Session 时调用了 pause(),结果暂停的已经是新音频。

最终原则变成:

插件只管理自己的 Consumer,不替页面暂停 Producer。

另外几个值得保留的设计

1. 先成功,再复制

只有页面原生 appendBuffer() 成功后,数据块才进入插件 Consumer,避免保存页面自己都没有接受的数据。

2. 页面 URL 和插件 URL 分开

ChatGPT 可以随时 revoke 自己的 Blob URL。插件为 Consumer 创建独立 URL,并且只撤销自己拥有的 URL。

3. 进度条永远从零开始

原生流媒体可能只剩一个滚动缓存区间,例如 [5:08, 6:08]。插件展示的是自己的完整时间轴,因此左端固定为 0:00

拖动时只更新预览,松手后执行一次 seek,避免鼠标每移动一个像素就干扰播放器状态。

4. 日志就是产品的一部分

每个版本和 Session 都有明确日志:

  • buildVersion
  • sessionId
  • producerTime
  • consumerTime
  • copiedBytes
  • appendedBytes

很多问题不是“猜”出来的,而是通过事件顺序被证明的。

我真正学到的:自主学习不是让 AI 一次写完

这个项目的过程更像一个自主闭环:

<pre class="hljs code__pre" style="color: #c9d1d9; background: #0d1117; font-size: 90%; overflow-x: auto; border-radius: 8px; line-height: 1.5; margin: 10px 8px; box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); padding: 0 !important;">观察现象 → 提出假设 → 加日志 → 收集证据 → 修改最小变量 → 真实验证 → 独立 Code Review</pre>

AI 可以写代码,但真正重要的是让它接受证据约束:

  • • 不确定时先探测;
  • • 日志不支持的结论不采用;
  • • 每次修复都有可验证的成功标准;
  • • 修改后再让独立 Agent 做审查。

从“我想要一个暂停键”,到理解 MediaSource、双播放器、Session 生命周期和事件竞态,这就是我喜欢的学习方式:从真实问题出发,把知识长在解决问题的过程中。

如果这个插件对你有用,欢迎在 GitHub 点一个 Star。你的 Star,也许就是下一个功能的需求票。

项目地址:https://github.com/STEPHENXING/chrome-GPT-voice-complete-player

第一个误会:Blob URL 不是音频下载地址

开发者工具里能看到这样的地址:

blob:https://chatgpt.com/...

它看起来像一个 URL,但它不是服务器上的 MP3 地址,更像浏览器内存里的一张“临时门牌号”。

继续观察日志后,我发现 ChatGPT 使用的是:

  • MediaSource
  • SourceBuffer
  • audio/aac
  • 分块 appendBuffer()

而且为了节省内存,ChatGPT 会不断调用 SourceBuffer.remove(),删除已经播放过的旧区间。

这解释了最早的 Bug:播放器显示可以回到 8 秒,但真正的缓存已经从 9 秒开始。游标能过去,声音回不去。

核心设计:不要让一个游标干两份工作

最初的方案只有一个播放器:

  • 暂停时把它静音,让下载继续;
  • 恢复时把 currentTime 拉回暂停位置。

表面上像有两个游标,实际上还是同一个。用户一倒退,负责向 ChatGPT 请求后续音频的游标也跟着倒退,长音频就可能卡住。

最终方案是两个真正独立的播放器:

ChatGPT 原生播放器(Producer)
        │
        │ 复制 AAC 数据块
        ▼
插件独立播放器(Consumer)

Producer 只做一件事:静音、持续向前,让 ChatGPT 把整段音频生成完。

Consumer 也只做一件事:面向用户播放,负责暂停、恢复、快进、快退和进度拖动。

这样,用户可以停在 20 秒慢慢思考,Producer 已经在后台跑到 60 秒;恢复时 Consumer 从 20 秒继续,Producer 完全不受影响。

这其实是一个很通用的工程原则:

数据生产和用户消费节奏不同,就不要强迫它们共用同一个状态。

Session 隔离:最难的不是播放,而是不串台

ChatGPT 会复用同一个 <audio> 元素,也可能在不同时间使用:

  • 新的 MediaSource;
  • 完整的 AAC Blob;
  • 已经结束的同一个音频再次播放。

因此,Session 不能简单等于“一个 DOM 元素”,也不能等于“一个 GPT 对话”。

插件最终把 Session 定义为:

用户发起的一次独立播放实例。

每次新播放都会:

  1. 分配新的 sessionId
  2. 清理旧 Consumer 和旧缓存;
  3. 把进度重置为 0:00
  4. 绑定当前音频源;
  5. 阻止旧事件修改新 Session。

这里还遇到过一个很隐蔽的竞态:ChatGPT 复用了同一个 audio 元素,我们销毁旧 Session 时调用了 pause(),结果暂停的已经是新音频。

最终原则变成:

插件只管理自己的 Consumer,不替页面暂停 Producer。

另外几个值得保留的设计

1. 先成功,再复制

只有页面原生 appendBuffer() 成功后,数据块才进入插件 Consumer,避免保存页面自己都没有接受的数据。

2. 页面 URL 和插件 URL 分开

ChatGPT 可以随时 revoke 自己的 Blob URL。插件为 Consumer 创建独立 URL,并且只撤销自己拥有的 URL。

3. 进度条永远从零开始

原生流媒体可能只剩一个滚动缓存区间,例如 [5:08, 6:08]。插件展示的是自己的完整时间轴,因此左端固定为 0:00

拖动时只更新预览,松手后执行一次 seek,避免鼠标每移动一个像素就干扰播放器状态。

4. 日志就是产品的一部分

每个版本和 Session 都有明确日志:

  • buildVersion
  • sessionId
  • producerTime
  • consumerTime
  • copiedBytes
  • appendedBytes

很多问题不是“猜”出来的,而是通过事件顺序被证明的。

我真正学到的:自主学习不是让 AI 一次写完

这个项目的过程更像一个自主闭环:

观察现象 → 提出假设 → 加日志 → 收集证据
→ 修改最小变量 → 真实验证 → 独立 Code Review

AI 可以写代码,但真正重要的是让它接受证据约束:

  • 不确定时先探测;
  • 日志不支持的结论不采用;
  • 每次修复都有可验证的成功标准;
  • 修改后再让独立 Agent 做审查。

从“我想要一个暂停键”,到理解 MediaSource、双播放器、Session 生命周期和事件竞态,这就是我喜欢的学习方式:从真实问题出发,把知识长在解决问题的过程中。

如果这个插件对你有用,欢迎在 GitHub 点一个 Star。你的 Star,也许就是下一个功能的需求票。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容