逐字稿文字預覽
大家好我是小木头 上一期视频 我们用 OpenAI 官方托管的沙箱环境 把 Agents API 跑通了 创建了 session 跑了命令 下载了 artifact 在任务执行中也加载了 Skill 官方托管的沙箱环境 只是执行环境的一种 Agents API 还支持另一种 环境类型叫 self_hosted 执行器可以跑在咱们 自己的基础设施里 OpenAI 只负责调度 这一期我分享一个刚刚 开源的小项目叫 apple-sandbox 它用 Apple 官方的 container 工具 在 Mac 上给每个 session 起一个 独立的轻量级的虚拟机 把 Agents API 的执行器跑在这里面 不需要云账号 不需要公网地址 也不需要 Docker 项目已经做过端到端的验证 在测试中能够看到真实的 session 的创建 真实的任务执行 结果文件也能够直接 落到咱们本机的磁盘上 今天的内容咱们分三块吧 先介绍这个项目是什么 架构是什么 然后重点说说它和 Agents API 之间的连接方式 尤其是没有公网的地址的情况下 它是怎么和 OpenAI 协同工作的 最后用发布版本从安装到 跑任务完整的演示一遍 apple-sandbox 是一个用 Go 语言 编写的命令行工具 没有常驻进程 目前呢也没有守护程序 它只做一件事情 把 OpenAI 的 codex exec-server 这个执行器 跑进一个隔离的虚拟机里 底层用的是 Apple 去年开源的 container 项目 它和 Docker 最大的区别是隔离级别 Docker 上所有容器共享一个 Linux 内核 Apple container 是每个容器单独 起一个轻量级的 Linux 虚拟机 基于 macOS 自带的 Virtualization.framework 所以在 apple-sandbox 里面 一个 session 就是一个 OCI 容器 也就是一台独立的虚拟机 agent 在里面装软件跑脚本 都不会离开这台虚拟机 从下往上来看看整个结构 目前呢是 4 层 最底下是 Apple Silicon 的 Mac 装好 container 工具 执行 container system start 把 它的服务启动起来 往上一层是 Apple container 本身 负责从 OCI 镜像启动虚拟机 再往上是执行器的镜像 基于 node 22 的精简版 安装了 CA 证书和 codex 这个 npm 包 入口脚本里面执行了 codex exec-server 最上面才是 apple-sandbox 这个 CLI 它负责创建工作目录 注入环境变量 调用 container run 然后呢就退出 把虚拟机留在后台跑 有一个设计方面的细节需要单独聊聊 每个 session 在本机的 sandboxes 目录下有一个自己的子目录 直接挂载成虚拟机里的 /workspace 所以文件进出不需要 任何上传下载接口 往那个目录里放一个文件 虚拟机里马上就能看到 agent 写出来的结果 本机磁盘上也能马上读到 虚拟机是随时可以销毁的 磁盘上这个目录才是真正的状态 命令一共 8 个 doctor 检查环境 build 构建执行器镜像 run 启动一个 session logs 看日志 ls 列出 session stop 和 rm 分别是停止以及删除 可以用 prune 命令按时间批量 地清理已经停掉的 session 再来看看它在整个链路里的位置 第一步 你的应用用普通的 API key 来调用 Agents API 创建一个 self_hosted 类型的 session 返回里除了 session ID 还有两个关键字段 environment.id 和一个远端的地址 remote_url 这两个值是给执行器用的 第二步在你的 OpenAI 平台上单独 建一个受限的 API key 拿普通的 API key 去注册执行器 API 会直接返回 403 通常会看到这么一个错误 说缺少 api.agents.environments.connect 这个 scope 第三步是把这个受限的 API key 放进环境变量 OPENAI_EXECUTOR_API_KEY 执行 apple-sandbox run 传入 environment.id 和 remote_url CLI 会创建工作目录 把这三个值作为环境变量注入 调用 container run 启动一台虚拟机 虚拟机里的入口脚本把受限 的 API key 转成 codex exec-server 自己要求的变量名 CODEX_API_KEY 然后呢启动 exec-server 第四步 exec-server 用这个 API key 主动向 remote_url 发起一个 WebSocket 连接 完成注册 OpenAI 那边把 session 标记成活跃 之后每一轮任务都是 OpenAI 那边发命令 虚拟机在 /workspace 里执行 结果呢再回传 你的应用通过 Agents API 提交任务 订阅事件流 看到的和 OpenAI 托管的 sandbox 完全一样 整个过程里 apple-sandbox 从头到尾没有和 OpenAI 通信 它只负责把三个参数变成 一台跑着执行器的虚拟机 然后呢就退出了 这段是这期里面我想再聊聊的一部分 首先看托管的 sandbox 什么都不用管 OpenAI 自己起环境 如果大家看过 Cloudflare 官方 给 Agents API 写的参考实现 它是云端部署的典型形态 你部署一个 worker 公网地址有了 OpenAI 通过带签名的 webhook 把 session 的事件推送 有任务要执行 状态空闲 执行失败等等 这些呢都是事件会推送过来的 而 worker 收到事件再去启动或唤醒容器 容器里的执行器向 OpenAI 建立 WebSocket 连接 也就是说在那套方案里面 webhook 和 WebSocket 各管一件事 WebSocket 是执行器和 OpenAI 之间 真正的交换命令和结果 永远都是从你这边主动出站 而 webhook 只解决一个问题 OpenAI 什么时候需要算力 怎么通知你把容器启动起来 而 apple-sandbox 跑在一台个人的 Mac 电脑上 没有公网地址 也就没法给 OpenAI 提供 一个接收 webhook 的地址 那怎么办呢 答案其实也在 OpenAI 自己文档里 self_hosted 环境 也就咱们这种自己运行的沙箱环境 有两种同样都可以 认为是正式的启动方式 一种叫 webhook-managed 就是刚才咱们谈到的像 Cloudflare 那种 OpenAI 决定什么时候要算力 推送事件给你 另一种呢叫 application-managed 你的应用创建 session 的那一刻 就已经拿到了环境的 ID 也就是 environment.id 和 remote_url 直接同步把算力呢启动起来 不需要等待 OpenAI 的通知 Cloudflare、Vercel、Modal 的集成文档都把这两种 并列成为选项 而不是主备关系 apple-sandbox 实现的正是 application-managed 创建 session 拿到两个参数 立即执行 apple-sandbox run 三步呢是在一起的 webhook 那条路解决的是规模化 场景下按需启动算力的问题 一个开发者呢 在自己的 Mac 上跑 其实呢并没有很具体的这方面的问题 真正的数据通道 两种方式是一样的 都是执行器主动出站 发起一个 WebSocket 连接 连接断了自己重连 OpenAI 从来不需要反向地连你的机器 所以少了公网地址 少的只是提前收通知这件事 那少了通知还剩一件事情要自己处理 session 什么时候结束 虚拟机什么时候该销毁 apple-sandbox 没有去轮询 OpenAI 也并不需要 exec-server 有个特性 连接临时断开它会重连 但 OpenAI 彻底关闭 session 之后 它就退出了 容器随之呢 就进入了 stopped 状态 所以 prune 命令只看一件事 哪些容器已经停了 停了多久 prune --older-than 1h 把停掉超过一小时的清理掉 手动跑或者挂一个 cron 都行 它永远呢不会碰正在运行的容器 当然了目前 apple-sandbox 还处于非常早期 在后续的开发中还会 逐渐完善这方面的功能 使它越来越健壮 把上面的机制串成状态图 一个 session 在沙箱里经历 的状态大致是这样的 执行 apple-sandbox run 容器被创建出来 这是 Created 状态 入口脚本启动 exec-server 向 OpenAI 注册 进入 Registering 状态 注册呢有两个分支 remote_url 或者 key 不对 exec-server 立即就拒绝 进入 Rejected 容器随即就停掉 错误信息呢 可以在 apple-sandbox logs 里看到 注册通过进入连接状态 Connected 之后 OpenAI 每下发一条 命令就进入 Executing 执行完回到 Connected 中间 WebSocket 掉线 进入 Reconnecting exec-server 自己重连 apple-sandbox 并不参与 OpenAI 彻底关闭 session exec-server 退出 容器呢就进入 stopped 状态 最后通过 rm 或者 prune 把它删掉 最终达到了 Removed 状态 从 CLI 的角度看 ls 只报两种状态 running 和 stopped 中间那些状态全都在虚拟机里面发生 下面我们用发布版本完整跑一遍 项目在 GitHub 上 sugarforever/apple-sandbox 在发布版本这里 releases 有一个 darwin_arm64 的压缩包 只有这个平台 因为 Apple container 呢 只能在 Apple Silicon 的 Mac 上跑 前置条件只有一个 brew install --cask container 装好 Apple 的 container 工具 然后按照 README 里的一条命令安装 从 GitHub Releases 拉取最新的压缩包 把二进制解压到用户 目录下的 .local/bin 并不需要 sudo 执行器镜像的 Dockerfile 和入口 脚本已经嵌在了二进制里 所以不需要克隆代码仓库 在任何一个空目录里都能操作 装好后先跑 apple-sandbox doctor 做 5 项检查 包含了像 Apple Silicon Hypervisor 支持 container 命令 container 的服务状态 磁盘空间 全部 PASS 就可以了 如果服务状态检查失败 那么尝试运行 container system start 启动一下 再跑一次 然后 apple-sandbox build 来构建执行器的镜像 这一步要拉取 node 镜像 装 CA 证书装 codex 第一次呢会花一点时间 镜像准备好了 现在呢创建一个 session 向 /v1/agents/sessions 发一个 POST 请求 请求的头带上 agents=v1 这个 beta 标记以及 OpenAI 的 API key 请求体里面带上 agent 和 session 模型我选择了 gpt-6-astra 系统指令是让它在 /workspace 里工作 每一步跑了什么命令都展示出来 生成的文件在结束前自己检查一遍 environment 指定类型是 self_hosted 工作目录是 /workspace API 响应应该是 201 来看看三个字段 一个呢是 ID ID 是 sess 开头的一个 session ID 另外一个是 environment.id 还包含一个 remote_url 它是 api.openai.com 下面 一个 connect 路径的地址 这两个值原样地传给沙箱就行 并不需要改动 把响应存到 session.json 后面呢用三个变量咱们把它取出来 受限的 API key 我已经在 OpenAI 平台上 的 agents 部分创建好了 把它放在本机环境 变量 OPENAI_EXECUTOR_API_KEY 里面 现在启动沙箱 运行 apple-sandbox run 给它 session ID、environment.id 和 remote_url 命令马上返回 session 的名字 容器的 ID 本机的工作目录 还有查看日志的命令 apple-sandbox ls 状态呢应该能看到它是 running apple-sandbox logs -f 可以跟一下日志 能够看到 exec-server 启动 向远程的 URL 也就是 remote_url 发起了注册 顺便验证一下它真的是一台虚拟机 container exec 进去 执行 uname -a 看到的是 Linux 内核 跑在了一台 Mac 上 给它一个任务吧 先在本机 sandboxes 目录下 这个 session 的子目录里放一个 CSV 文件 内容很简单 4 行数据 item 和 amount 两列 不需要任何上传接口 放进目录里 实际上就放进了虚拟机 里的 /workspace 目录 顺便说一句吧 OpenAI 托管的 sandbox 创建 session 的时候呢 可以用 environment.files 这个 字段内联地把文件带进去 同样的字段在 self_hosted 上 OpenAI 会返回 400 说 environment.files 是个未知参数 因此 self_hosted 类型的文件 进出只能靠你自己去挂载 API 并不支持 然后就提交任务吧 POST 到 session 的 events 接口 事件类型呢 是 agent.session.input.message 内容是用 Python 读 /workspace/amounts.csv 验证 amount 列是数字 算行数和总数 把结果按固定格式写到 /workspace/outputs/sum
本次執行流程與問題處理
下達指令
預期流程
- 採用來源字幕(略過語音辨識)
- 來源選擇與品質檢查
- 文字校正與句子單元
- 筆記與發布
- 公開網頁發布
1. 下載與語音辨識
工具:videonote-transcribe-url(主辨識管線:下載、Whisper 辨識與選稿)
處理:下載音訊、以 Whisper 辨識並選出最佳逐字稿
產出:yt_dlp_meta.json(影片元資料)、audio_16k_mono.wav(16kHz 單聲道音訊)、transcript.whisper.json(Whisper 辨識結果)、transcript.json(正式逐字稿)、transcript_selection_report.json(來源選擇報告)
耗時:14.0 秒
關係:依序執行
結果:完成
2. 全文快速校讀
工具:videonote_rapid_transcript_review.py
處理:先速讀完整逐字稿,再修正高可信突兀詞句並列出待確認項目
產出:rapid_transcript_review_report.json(產出檔案)
耗時:46.2 秒
關係:依序執行
結果:完成
3. 建立句子單元
工具:videonote-build-sentence-units
處理:將逐字稿切成帶時間軸的句子單元
產出:sentence_units.json(句子單元)
耗時:0.2 秒
關係:依序執行
結果:完成
4. 時間軸稽核
工具:videonote_timeline_audit.py
處理:檢查時間軸與音訊對齊
產出:timeline_audit_report.json(時間軸稽核)
耗時:0.2 秒
關係:依序執行
結果:完成
5. 品質檢查
工具:videonote_controlled_quality.py(檢查時間軸與品質)
處理:檢查語言、時間軸與可讀性品質
產出:controlled_quality_report.json(品質報告)、language_sanity_report.json(語言檢查)
耗時:0.3 秒
關係:依序執行
結果:完成
6. 播放時間軸回填
工具:videonote_rebase_display_timeline.py
處理:將網頁播放時間軸回填為校正字幕時間
產出:display_timeline_rebase_report.json(時間軸回填報告)
耗時:0.3 秒
關係:依序執行
結果:完成
7. 來源句整理
工具:videonote_prepare_source_entries.py
處理:整理來源句子供時間軸與頁面使用
產出:sentence_units.json(句子單元)
耗時:0.3 秒
關係:依序執行
結果:完成
8. 生成筆記摘要
工具:videonote-generate-note(生成筆記摘要)
處理:生成筆記摘要與重點
產出:note.md(筆記 Markdown)、note.html(筆記 HTML)
耗時:124.2 秒
關係:依序執行
結果:完成
9. 建立筆記頁面
工具:videonote-render-html(產生筆記 HTML)
處理:產生可閱讀的筆記 HTML 頁面
產出:note.html(筆記 HTML)
耗時:0.3 秒
關係:依序執行
結果:完成
10. 發布公開頁面
工具:videonote-publish(發布公開頁面)
處理:發布到公開網站與每日列表
產出:公開筆記網址、result.json(管線結果)
耗時:0.4 秒
關係:依序執行
結果:完成
手動來源字幕
自動來源字幕
Whisper 字幕
- 尚未產生
Qwen 字幕
- 尚未產生
Parakeet 字幕
- 尚未產生
MOSS 字幕
- 尚未產生
聲學時間證據(PyAnnote / WhisperX)
- 尚未產生
校正前後逐字稿
- 校正前 transcript.source.json 純文字 JSON
- 校正後 transcript.json 純文字 JSON
目前待解決問題與初步改善方向
- 逐字稿全文快速校讀未完成
已保留自動字幕/原始逐字稿並繼續生成筆記。;未完成的全文速讀項目可在精進模式回聽確認。;RAPID_REVIEW_STATUS=REVIEW_REQUIRED_INVALID_OUTPUT
- Qwen3-ASR 未完成,需確認 qwen_asr_lab 環境與音訊後重試精進模式。
- 音訊下載不穩定,需更新 cookies / PO token 後重試精進模式。