read-video —— 按需取字节地「讀」影片
這條技能解決什麼問題
用戶丟來一個影片連結,想理解內容。樸素做法是把影片下載下來再處理 —— 慢、佔儲存、有版權風險,而且往往只需要其中幾秒。本技能的做法是:只取目標時刻附近的那一小段位元組。音訊軌在本地離線轉成文字(回答「講了什麼」的主力),視訊軌解出關鍵幀交給視覺模型補畫面。
實測基準(249 MB / 9 分 56 秒的 MP4,非 faststart):
| 動作 | 上游取位元組 | 佔整片 |
|---|---|---|
| 取第 30 秒一幀 | 2.0 MB | 0.839% |
| 取第 8 分鐘一幀 | 1.7 MB | 0.707% |
| 取 480s 起 20 秒音訊 | 6.6 MB | 2.784% |
何時用 / 何時不用
用:用戶給了影片直鏈,或給了含影片的網頁連結(bilibili 等平台由 resolve.py 自動解析,不必手寫 API 呼叫)。
不用:本地檔案(直接讀)、需要逐秒完整理解的長影片(成本仍高,先問用戶要時間點)。
硬原則
- 不下載整片。 任何一步都不要
wget/curl -O整個檔案。 - 先探測再動手。 不探測就 seek,會在不支援 Range 的伺服器上白等一場。
-ss必須放在-i之前。 放後面是輸出側 seek,等於先順序解到目標點再丟,退化為拉全片。放在前面才是輸入側 seek,解復用器直接跳。- 計量開著。
--measure預設開:它用本地流式代理數上游位元組,是「真的按需取了」的唯一硬證據,也是向用戶證明沒偷偷下整片的依據。 - 網頁連結交給腳本解析。 頁面 URL ≠ 直鏈,且直鏈帶簽名會過期。用
resolve.py,或直接把連結傳給grab.py/transcribe.py(它們會自動解析並補上請求頭)。別在會話裡現寫站方 API 呼叫 —— 那種程式碼每次都長得不一樣,也沒人 review 過。
工作流
第 0 步:分清輸入 —— 網頁連結還是直鏈
- 網頁連結(bilibili、YouTube、抖音、微博、Vimeo…共 1700+ 站點)→ 先解析成直鏈。也可以直接把連結交給
grab.py/transcribe.py,它們認得出並自動解析、自動帶上所需請求頭。 - 直鏈(
.mp4/.m3u8等媒體副檔名)→ 可跳過解析。
判定規則在 resolvers.is_page_url():帶媒體副檔名的一律當直鏈,其餘 http 連結都送去解析層。
第一步:拿到可取的流
python scripts/resolve.py "<網頁連結>"
輸出 video_url(取幀用)、audio_url(轉寫用,與前者不是同一條)、headers(CDN 要求的請求頭),外加標題、時長,以及一組建議抽幀時間點。
平台 那行會標出用的是哪個後端:native(手寫適配器,目前是 bilibili)還是 yt-dlp(通用,1700+ 站點)。要強制指定用 --backend native|yt-dlp|auto。
一條命令走完全程(解析與取數自動銜接,推薦):
python scripts/transcribe.py --url "<網頁連結>" --seconds 90
python scripts/grab.py "<網頁連結>" --times 14 29 44 58 73 --prefix shot --outdir ./out
第二步:探測(直鏈建議先做,網頁連結可跳過)
python scripts/probe.py <直鏈> --json
若直鏈來自 CDN,要帶來源頭:--header 'Referer: https://www.bilibili.com'。
看三個欄位:
seekable——false就停,伺服器忽略 Range,定點取幀會退化為順序拉流。改走「邊下邊解」(-i <url>不加-ss,取流最前面的幀)或直接告訴用戶做不到。container.moov_offset/faststart—— 索引在頭部(faststart)時固定開銷幾十 KB;在尾部時要先取檔案尾一段(實測約 441 KB)。這是「取一幀」的固定成本,省不掉。content_length—— 後面算「佔整片百分比」的分母。
第三步:按用戶意圖取數
用戶給了具體時間點 → 定點取證:
python scripts/grab.py <url> --time 183.5 --out shot.jpg
用戶沒給時間點 → 先抽幾幀了解大概:
python scripts/grab.py <url> --times 5 60 180 420 --prefix shot --outdir /tmp/rv
選點技巧:按內容密度取 4~6 幀,別均勻鋪滿全片(那等於把整片解了一遍)。拿不準就先取 3 幀,看完了再決定要不要細看某一段。
要理解講了什麼 → 直接轉成文字(推薦先做這一步):
python scripts/transcribe.py --url <url> --segments 0:90 320:90 630:97
一條命令完成「截音訊 → 本地識別」,輸出帶時間戳的中文文字,不需要外部 API、不需要連線推理。首次執行自動下載 SenseVoice 模型(約 240 MB,從 hf-mirror 拉,僅一次)。
長影片取 3~4 段各 90 秒即可覆蓋主線,不必整片轉 —— 這比逐幀看畫面快得多,也更適合回答「講了什麼」這類問題。先轉文字,再用幀補充視覺資訊,是這個順序。
轉寫完先看產率。 正常口播約 46 字/秒;腳本輸出裡帶 10 幀)做畫面分析 —— 聲音裡沒資訊,答案只在畫面裡。chars_per_second,低於 1 字/秒就會主動提示。遇到這種(純音樂、遊戲實況、無聲演示)不要拿那點文字當內容,直接改走更密的抽幀(8
只需要音訊檔案本身時(比如送去別處轉寫):
python scripts/grab.py <url> --start 0 --seconds 60 --audio clip.wav
有內嵌字幕軌時優先讀字幕(ffmpeg -i <url> 看有沒有 subtitle 流),比 ASR 更準更快。
第四步:把幀交給視覺模型
grab.py 只負責把像素拿到本地,不理解內容。把輸出的 jpg 路徑交給視覺模型提問(「這一幀裡有什麼」「這段在講什麼」)。多幀時按時間順序一起給,讓模型串成敘述。
輸出怎麼讀
grab.py 輸出 JSON,每個動作帶一份位元組帳:
{
"ok": true,
"actions": [{
"kind": "frame", "time": 30,
"out_bytes": 48211,
"upstream_bytes": 2097152,
"requests": 4,
"wall_seconds": 2.317
}],
"total_upstream_bytes": 2097152
}
upstream_bytes / content_length就是「佔整片多少」。超過 5% 就要停下來複查:多半是-ss位置寫錯了,或者伺服器不認 Range。requests多是正常的:ffmpeg 要分幾次取(moov、關鍵幀到目標幀之間、音訊頭)。ok: false時看err_tail,常見是超時和 5XX —— 腳本已內置重試,仍失敗就換時間點再試。
transcribe.py 的輸出:
{
"ok": true,
"text": "這是一片被大河與群山環抱的土地……",
"segments": [{"start": 0, "end": 25, "text": "…"}],
"audio_seconds": 276.7,
"model": "SenseVoiceSmall-int8"
}
text 是所有分段拼成的整段;segments 帶時間戳,需要「第 X 秒說了什麼」時查它。
環境
- 解譯器:本機託管 venv 的 python。Windows 上路徑形如
~/.workbuddy/binaries/python/envs/default/Scripts/python.exe。注意是Scripts/,不是 Unix 風格的bin/(照文件寫bin/python會報檔案不存在)。 - 依賴:
httpx、imageio-ffmpeg(取數)、sherpa-onnx、numpy(轉寫)、yt-dlp(通用平台解析),都已裝在同一個 venv 裡;缺哪個腳本會明確報錯。沒裝 yt-dlp 時 bilibili 仍可用(走手寫適配器),但其它站點會解析失敗。 - ffmpeg:由
imageio-ffmpeg自帶靜態構建,無需系統安裝。要指定別的用環境變數RV_FFMPEG或--ffmpeg。 - ASR 模型快取:
~/.workbuddy/cache/read-video/models(約 240 MB,首次自動下載)。檔案存在且位元組數匹配就跳過下載。
已知邊界
- CDN 常校驗來源:bilibili 等站點的影片 CDN 要求
Referer,不帶就 403。用resolve.py或--page會自動帶上;手動傳直鏈時記得--header 'Referer: https://www.bilibili.com'(三個腳本都支援,可重複)。 - 直鏈會過期:站方直鏈帶簽名與 deadline,同一批操作內用完;一旦報 403 就重新跑
resolve.py。 - 平台覆蓋面 = yt-dlp 的覆蓋面(1700+ 站點),不是自己維護的。解析不了的站點先用
--backend yt-dlp看原始報錯;需要登入 / Cookie 的站點會失敗,這類只能放棄或手工提供 Cookie。yt-dlp 本身要定期升級(站點改版後舊版會失效)——pip install -U yt-dlp。 - 「解析通」≠「取得到」:解析層只負責給 URL。取數失敗時先分清是解析失敗還是網路不可達 —— 實測本機到 YouTube / Vimeo 不通(代理 502),而抖音 / 微博 / 西瓜影片可達。
- HLS / m3u8:不是單檔案,
probe.py的 box 解剖會失敗(有error欄位)。此時grab.py仍可用,ffmpeg 自己會拉需要的分片;content_length為空,算不出佔比。 - DASH 音視訊分離:不少站點(bilibili 等)視訊流與音訊流是兩條獨立 URL。取幀用視訊流,轉寫用音訊流 —— 用錯了會靜默拿到空結果。
-ss精度受 GOP 限制:要取某一秒的幀,必須從最近的關鍵幀解起,所以最小取數單位是「關鍵幀到目標幀」,不是「一個位元組區間」。- ASR 同音字:SenseVoiceSmall 對中文效果不錯,但人名、地名、古詞常見同音錯字(實測「阪泉之野」→「版圈之也」、「涿鹿」→「涿陸」)。引用專有名詞前要按上下文校正,別把轉寫文字原樣當成事實。
- 依賴:
httpx+imageio-ffmpeg(取數)+sherpa-onnx+numpy(轉寫)。缺少時腳本會明確報錯並提示裝哪個。
更完整的原理、踩坑記錄與實測資料見 references/methodology.md。
本技能與 dsh 插件 dsh-readvideo 的關係見 references/dsh-dual-form.md。