第 9 天把一個連結走到文章的過程拆成四次交接。到了第二次交接,我只敢把輸出叫作「文字候選」。
因為一份逐字稿就算排版整齊、讀起來通順,還是可能聽錯、漏字、重複,或在整理時失去原本的時間資訊。它可以進入下一站;要直接引用,還欠一次回查。
今天就把「擷取產物 → 文字候選」這一格放大,只回答一個問題:
一段準備被摘要或引用的整理後文字,怎麼回到產生它的字幕片段與影音時間窗?
模型比較和通用及格分數先放在一旁。說話者是誰,留到 Day 11;影片裡的說法有沒有外部證據,留到 Day 13。今天先守住最前面這段路:文字整理過後,來源座標還在不在。
人讀逐字稿,希望它越乾淨越好。程式要回查來源,需求剛好相反。它需要片段識別、開始時間、結束時間、字幕版本,以及這段文字經過哪些整理。
我公開的媒體轉文字工具,剛好可以看到這個落差。以下只看固定版本的程式碼,不把它寫成這次案例實際使用的工具。
在原始 JSON 那一層,每個片段原本有 id、start、end 和 text;使用 faster-whisper 的路徑,還可能留下字詞的起訖時間與機率值。可是輸出成方便閱讀的 transcript.md 後,只留下片段起點和文字;到了 transcript.txt,連時間也沒有了。
後面的輸出流程 還會用 OpenCC 整理全文與片段文字,再覆寫名為 whisper_raw.json 的檔案,卻沒有同步改寫 words[].word,也沒有另外記下轉換步驟。檔名雖然還有 raw,內容已經不是模型剛輸出的原始位元。
工具照樣能用。這個落差提醒我:適合閱讀的逐字稿,和適合回放的逐字稿,是兩種不同的產物。
前者要減少雜訊;後者要保留來路。內容產線如果只存前者,後面的 AI 確實比較好讀,卻很難回答某一句到底從哪裡來。
要談回放,我先把三種判定拆開:
| 判定 | 它回答的問題 | 這次能走到哪裡 |
|---|---|---|
位置/location_status |
給定時間碼,能不能落在這份字幕的片段範圍,形成播放定位? | 可以做確定性檢查,但只限字幕範圍 |
文字忠實/fidelity_status |
整理後文字是否忠於該時間窗的原聲? | unchecked,本輪沒有人工聽校 |
來源主張/truth_status |
影片作者說的內容是否符合外部事實? | out-of-scope,留到 Day 13 |
位置找得到,不等於文字聽得對。文字聽得對,也不等於影片裡的主張是真的。
這三件事一旦塞進同一個 verified: true,下游看到的只會是一盞很有自信、卻不知道驗過什麼的綠燈。
這次實驗還有兩個來源關係要另外記:caption_lineage_status: pass,表示整理後文字已沿片段序號綁回指定字幕;same_version_media_replay_status: unknown,表示缺少影音檔摘要,同一版本的原聲仍未鎖定。這兩項各自保留,不併進前面三項。
這次準備文章時,做了一個很窄的回放實驗。輸入只有三份:
執行前後都用 SHA-256 計算摘要,確認正式輸入沒有被實驗改動。這裡的摘要只做一件事:辨認「這次讀的是不是同一份位元」。
摘要相同,能說的只有位元相同。字幕由誰產生、文字有沒有聽對,以及影片作者說的是否成立,都需要別的證據。NIST FIPS 180-4 定義的是訊息摘要,不是來源簽章。SHA-256 的責任到這裡就結束了。
這份來源包還缺一樣關鍵資料:影音檔本身的摘要。平台字幕軌 ID、字幕擷取時間與擷取工具版本也沒有留下。YouTube 的字幕資料模型 會替字幕軌保存獨立 ID、語言、種類與更新資訊,這也說明只留影片 ID 還不夠。因此我能說的是「這些片段序號屬於這一份字幕」;至於是否回到完全相同版本的影音,狀態維持 unknown。
這個差別很小,卻會影響後面每一句話可以寫多滿。
第一版檢查很直白:讀出台帳裡的每一個時間碼,看它是否落在某個字幕片段的開始與結束之間。
結果很好看。這批台帳時間碼在範圍檢查裡沒有報錯。
如果在這裡停下來,很容易把結果寫成「每一筆內容都能回到原片」。實際上,這個檢查只問了:
這個秒數是否存在於字幕時間軸?
它沒有問:
台帳裡這段文字,是否真的對應到那個秒數附近的內容?
為了確認這個盲點,正式台帳沒有被改動;實驗只在系統暫存副本裡,把其中一個時間碼換成片內另一個語意無關的時間碼。
新的時間仍在字幕涵蓋範圍內,所以舊檢查照樣沒有報錯。
這是一個設計好的限制探針,不是本次案例真的找出一筆錯誤。它證明的是:時間碼格式正確、也落在字幕裡,仍然可能指向不相干的位置。
所以 location_status: pass 只能表示定位可解析,不能代替語意對應,更不能把 fidelity_status 一起改成 confirmed。
要讓整理後文字可以回放,我需要保存的不是一個孤立的時間碼,而是一條完整關係:
整理後文字
→ 整理前文字
→ 字幕片段序號
→ 同一份字幕摘要
→ 起訖時間窗
→ 公開播放位置
這條關係裡,每一段都有工作。
單獨的時間碼無法指認字幕版本。字幕片段序號也得有依附對象,因為 WebVTT 的片段識別碼可以省略;這次讀到的 VTT 就沒有明示識別碼,只能用檔案中的順序建立 cue_ordinals。
順序本身不是永久身分。字幕只要重新分段、插入一個片段,後面的序號就可能全部改變。因此 cue_ordinals 必須和 caption_artifact_sha256 一起保存。摘要一變,舊序號就過期,重新建立關係後才能使用。
WebVTT 規格 還允許字幕片段在時間上重疊。也就是說,一個時間點可能找不到候選、找到一個,或同時落在多個片段。程式若只取第一個命中,這是實作選擇,不是格式保證的唯一答案。
時間窗本身也有邊界。我採用半開區間來判定:
start <= point < end
這和 W3C Media Fragments 描述媒體時間片段的方式一致。不過時間碼仍只是導航。播放器或影音工具可能為了從可解碼位置開始,落在稍早的位置;精準度停在「縮小回聽範圍」,還沒到逐幀或逐音框對齊。
Replayable Transcript Packet v1把前面的需求收斂後,我整理出一份最小的「可回放逐字稿包」。這是本文的提案,也是這次文章實驗新建立的資料形狀;現有證據只到這裡,既有影片導讀流程尚未被證明共用這套格式。
{
"source": {
"source_id": "<公開來源識別碼>",
"canonical_locator": "<公開網址>",
"source_media_sha256": null
},
"caption_artifact": {
"sha256": "<字幕摘要>",
"track_id": null,
"language": "<字幕語言>",
"kind": "<字幕軌種類或 unknown>",
"captured_at": null,
"capture_tool_version": null
},
"segment": {
"segment_id": "<此資料包內的穩定值>",
"cue_ordinals": ["<cue-ordinal>"],
"start_ms": "<start-ms>",
"end_ms": "<end-ms>",
"raw_text": "<保留於工作產物>",
"normalized_text": "<保留於工作產物>",
"normalization_ops": [
"strip-webvtt-inline-timestamps",
"strip-webvtt-tags",
"collapse-whitespace",
"remove-rolling-caption-token-overlap"
],
"caption_lineage_status": "pass",
"location_status": "pass | fail",
"same_version_media_replay_status": "unknown",
"fidelity_status": "unchecked | confirmed | contradicted | unclear",
"truth_status": "out-of-scope"
}
}
這裡刻意同時保留 raw_text、normalized_text 和 normalization_ops。
移除 WebVTT 標記、合併空白,通常不難理解。比較麻煩的是滾動字幕:後一個片段常會重複前一個片段的尾巴。如果只把每段文字串起來,同一句話可能出現兩次;如果去重規則太積極,又可能刪掉原本真的重複說過的字。
因此整理後文字應該另存,原文繼續保留。新文字還要記下自己由哪一份原文、經過哪些操作產生。這也是 W3C PROV-DM 對內容產線很有用的地方:輸入、轉換活動與輸出要分開描述。檔案放在同一個資料夾只能證明它們一起出現,逐段衍生關係仍要另外記錄。
資料形狀寫完,還要知道它失敗時會不會真的停。
這次在暫存副本與記憶體裡做了四個負向測試,正式字幕和台帳都沒有被改動:
| 刻意破壞的地方 | 稽核器應該回報 |
|---|---|
| 改動字幕的一處位元 | caption-digest-mismatch |
讓 end_ms 小於 start_ms |
invalid-time-range |
| 指向不存在的字幕片段序號 | cue-reference-missing |
| 保留內容,但替換字幕軌的語言身分 | caption-track-mismatch |
四項都照預期失敗。這代表同一份字幕、有效時間範圍、存在的片段關係與字幕軌身分,可以被程式擋住。
但前面那個「片內時間換成另一個片內時間」的限制探針沒有被擋。因為兩個時間都合法,也都落在字幕範圍裡。要知道文字是否真的對到那段原聲,還是得回到內容本身。
所以 Day 10 不該再寫成一篇「多加幾個驗證器就安全了」。機器很適合檢查位元、欄位、範圍與關係;語意是否吻合,沒有人工聽校或可信的對齊證據時,就應該保留 unchecked。
回放資料最後要影響工作流下一步,不然它只是一份漂亮的 JSON。
下面先把控制方式整理成一份提案:
| 檢查結果 | 下一步 | 文章能寫到哪裡 |
|---|---|---|
| 字幕摘要不符、片段不存在或時間範圍無效 | reject |
不使用舊片段證據 |
| 來源影音版本無法核對 | needs-human |
只說找到公開定位,不說已回到同一版影音 |
fidelity_status 是 unchecked 或 unclear |
needs-human |
不下精確引言,先回聽 |
人工聽校結果是 contradicted |
reject 或重整文字 |
留下錯誤紀錄,不引用整理句 |
人工聽校結果是 confirmed,外部真實性未查 |
只做來源歸屬 | 寫成「影片作者表示」,不升級成外部事實 |
這份表目前停在文章提案,現有證據沒有支持它已接進所有內容工作流。
它和 AI 自動化的關係也很直接:AI 可以繼續摘要、分類、找觀點,但它拿到的每一段文字都要帶著目前能相信到哪裡。位置失效就拒收;文字還沒聽校就停在人這裡;已聽對但尚未查證,就保留來源歸屬。
自動化在這裡的價值,是讓不同的不確定走到不同的下一步。
第 9 天問的是:下一站憑什麼接手?
Day 10 的答案,是把乾淨文字和回程資料一起交出去:字幕版本、片段關係、起訖時間、整理操作,以及彼此分開的判定狀態。
這樣做的作用很務實:錯誤發生時,還有路可以回去;證據不足時,也知道要在哪裡停下來。至於逐字稿是否正確,仍由回聽結果回答。
今天先找到「這句話位於哪裡」。即使下一步真的回聽並確認內容,多人會議或訪談還會多一個問題:
這句到底是誰說的?
Day 11 再來處理說話者標記、姓名與人工確認之間,最容易被 AI 填得太滿的那一段。
-ss 與精確跳轉