手写multipart上传:claude-video零依赖调用Whisper API的完整原理指南
【免费下载链接】claude-videoGive Claude the ability to watch any video. /watch downloads, extracts frames, transcribes, hands it all to Claude.项目地址: https://gitcode.com/GitHub_Trending/cl/claude-video
claude-video是一个给 Claude 装上"眼睛和耳朵"的开源技能:/watch一条命令,Claude 就能下载任意视频、按自动帧率抽帧、拉取带时间戳的字幕(无字幕时回退到Whisper API语音转写),像真的看过视频一样回答你的问题。本文面向新手,完整拆解它最巧妙的一环——不安装任何第三方 SDK,只用 Python 标准库手写 multipart/form-data,零依赖上传音频完成 Whisper API 视频转写。
先认识 claude-video:字幕优先,Whisper 兜底
它的转写策略很务实:先用 yt-dlp 拉取平台原生字幕(免费、秒回、覆盖大多数公开视频);只有当视频确实没有字幕(本地文件、TikTok、部分 YouTube 无字幕上传)时,才走 Whisper API 兜底。
按常理,调用 Whisper API 应该pip install groq或pip install openai,几行代码搞定。但 claude-video 的 whisper.py 文件头写得明明白白:"Pure stdlib — nopip install groqorpip install openaineeded."(纯标准库,无需安装任何 SDK)。
为什么要走这条"难路"?
- 零安装成本:整个技能只依赖
ffmpeg/yt-dlp两个外部命令,首次运行由 setup.py 引导安装。再多一个 Python SDK 依赖,首次体验就要多一步pip install。 - 协议足够简单:Whisper 上传就是一个标准的 multipart 请求,手写约 30 行代码,比翻 SDK 源码还透明。
- 一套实现打两家:Groq 与 OpenAI 的转写接口格式几乎一致,一个
_post_whisper()函数通吃两家(端点与模型定义见 whisper.py#L28-L32)。
音频预处理:先把体积压到最小再上传 🎚️
上传入口是 transcribe_video():先从视频抽出音频,再上传。但它抽的不是"原始音频",而是 extract_audio() 用 ffmpeg 压出的特定形态:
- 单声道(mono)
- 16 kHz 采样率
- 64 kbps MP3 编码
结果是每分钟音频仅约 480 KB——50 分钟的播客约 24 MB,刚好卡在 Whisper API 的 25 MB 上传限制之内。这也是 README 里"Whisper 最长可处理约 50 分钟"说法的由来。这不是随手写的参数,而是"为 API 限制反推数据格式"的典范。
核心拆解:手写 multipart/form-data 请求体
全文核心在 _build_multipart()。用过 Postman 或网页上传表单的读者都听过 multipart/form-data:文本字段和文件被切成一段段"分块"拼成一个字节流,段与段之间用**边界值(boundary)**分隔。它的组装就三步:
第一步:用 UUID 生成唯一 boundary
boundary = "----WatchBoundary" + uuid4().hex,每次请求随机生成 32 位十六进制边界。两个要点:
- 随机性保证边界字符串不会和视频内容撞车;
- 请求头
Content-Type: multipart/form-data; boundary=...里的边界,必须和请求体里的边界是同一个字符串——这是服务端切分段落的唯一依据。
第二步:文本字段在前,文件段在后,顺序固定
本次请求共 4 个段:3 个文本字段(model、response_format=verbose_json、temperature=0)+ 1 个音频文件,拼装后的骨架长这样:
--boundary Content-Disposition: form-data; name="model" whisper-large-v3 --boundary Content-Disposition: form-data; name="file"; filename="audio.mp3" Content-Type: audio/mpeg <mp3 二进制内容> --boundary--规则很简单:每段以--boundary\r\n开头,跟一个Content-Disposition头、一个空行、内容,再以\r\n收尾;文件段多带一个Content-Type头(用mimetypes.guess_type从扩展名推断);结尾的--boundary--比普通边界多两个横杠,表示"最后一个段"。
第三步:io.BytesIO 内存拼装,不落临时文件
整个请求体用 io.BytesIO 在内存中组装,最后以(请求体字节, boundary)元组返回,全程不产生磁盘临时文件。源码注释直接点明动机:Whisper 的 multipart 上传"小而可预测",手写它正是为了留在纯标准库里。
发送与重试:一套"聪明"的容错策略 🔁
_post_whisper() 用urllib.request发 POST 请求,其中有两处细节值得新手抄作业:
- 自定义 User-Agent:默认的
Python-urllib/3.x会被 Groq 前置的 Cloudflare WAF 规则 1010 直接拦截(403,连鉴权都不到)。代码因此诚实地署名watch-skill/1.0 (+claude-code; python-urllib),顺利过关。 - 按错误码决定重试策略(常量定义见 whisper.py#L143-L145):
- 4xx(除 429):客户端错误,重试无意义,直接报错退出并附上服务端返回的错误体;
- 429 限流:优先尊重服务端
Retry-After头,最多重试 2 次; - 网络错误(超时、连接重置等):按 2 秒基数递增退避,最多共尝试 4 次。
响应解析:统一成管道通用的字幕格式
Whisper 返回verbose_json,但 claude-video 的流水线对"原生字幕"和"Whisper 转写"走同一套下游代码。关键在 _segments_from_response():它把 API 响应转成与 VTT 字幕解析 完全相同的{start, end, text}分段格式(无分段时降级为整段文本兜底)。
于是入口 watch.py 完全不关心转写来自哪里——拿到分段列表后统一做--start/--end范围过滤、格式化成[MM:SS] 文本行,与帧路径一起写进报告交给 Claude。这个"统一数据结构"的抽象,是整个管道读起来干净利落的根本原因。
零依赖方案给新手的 3 点启发 💡
- 协议简单就别怕手写:multipart/form-data 完整规则并不复杂,用 BytesIO 自己拼比黑盒 SDK 更透明、更好调试。
- 为 API 限制设计数据:mono + 16kHz + 64kbps 不是随意选的,是精确反推自 25 MB 上传上限。
- 重试不是一句 sleep:按错误码分流策略、429 听
Retry-After、网络错误指数退避——这套三件套可直接搬进你自己的项目。
延伸阅读
| 想了解什么 | 看哪里 |
|---|---|
| 技能整体用法与帧预算设计 | SKILL.md |
| yt-dlp 下载与原生字幕优先逻辑 | download.py |
| 自动帧率抽帧逻辑 | frames.py |
| VTT 字幕解析与滚动去重 | transcribe.py |
| 入口编排(下载→抽帧→转写) | watch.py |
| 版本演进记录 | CHANGELOG.md |
想动手体验:git clone https://gitcode.com/GitHub_Trending/cl/claude-video ~/.claude/skills/watch,然后在 Claude Code 里直接/watch <视频链接> <你的问题>。原生字幕免费可用,仅无字幕视频需要配置 Groq(首选,更快更省)或 OpenAI 的 API key。
【免费下载链接】claude-videoGive Claude the ability to watch any video. /watch downloads, extracts frames, transcribes, hands it all to Claude.项目地址: https://gitcode.com/GitHub_Trending/cl/claude-video
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考