# 字幕切片貼齊停頓，6 片變成 0 秒

- URL: https://justfly.idv.tw/%e5%ad%97%e5%b9%95%e5%88%87%e7%89%87%e8%b2%bc%e9%bd%8a%e5%81%9c%e9%a0%93%ef%bc%8c6-%e7%89%87%e8%ae%8a%e6%88%90-0-%e7%a7%92/
- 日期: 2026-11-09
- 分類: 技術筆記
- 標籤: python, QC, 字幕, 影片處理

![字幕切片貼齊停頓，6 片變成 0 秒]

上週剪一支直式短片，旁白是一整段話，字幕要照語音的停頓切成一張一張的卡。成片看一遍，有幾句字幕就是沒出來，像「光，」和「就在那個方向」這幾句，聲音有、字沒有。

我先去翻時間軸，不看畫面。71 張字幕卡裡有 6 張長度正好 0.00 秒，另外 5 張不到 0.35 秒。0 秒的卡當然畫不出來，那幾句就是這樣不見的。比較尷尬的是，我原本的驗收是每張卡抽中間那一幀看有沒有字，這一關全過，一張都沒抓到。

##### 為什麼會切出 0 秒的卡

原本的切法很直覺：每個原始切點各自去找離它最近的停頓，貼過去。切點密集、停頓稀疏的時候，好幾個切點最近的停頓是同一個，第 N 個邊界和第 N+1 個邊界就被貼到同一個時間，夾在中間那張卡長度是 0。

驗收沒抓到的原因跟上面無關，但更值得記下來。0 秒的卡，中點就是那個邊界時間，抽到的那一幀其實是隔壁那張卡正在顯示的畫面，上面當然有字。檢查本身沒算錯，它只是在回答另一張卡的問題。

##### 改法：邊界只能往後走，每張卡有下限

核心只有一件事：貼停頓的時候，候選範圍限制在「上一個邊界 + 最短長度」之後。這樣邊界一定嚴格遞增，每張卡至少 0.6 秒。

光這樣還不夠。候選的上界如果只設成「結尾 − 0.6」，前面的邊界還是可能把後面好幾張卡一起擠到結尾去，最後一張被壓扁。所以上界改成「結尾 − 剩下的卡數 × 0.6」，每個邊界都得替後面每一張卡留位子。

範圍裡找不到停頓的時候，就把原切點夾進 [上一邊界 + 0.6, 上界] 這段區間，然後寫一行警告。這時邊界已經離開停頓點，字幕可能切在話講到一半的地方，但每張卡的長度守得住，日誌也看得出來是哪一張。

另外在函式一開始先算總容量，(切點數 + 1) × 0.6 比音軌還長的話直接拋 ValueError。這種情況怎麼切都會被驗收擋掉，不如一開始就講清楚。

切完之後，字數少於 4 的碎片併回前一張，這次 71 張變成 70 張。最後驗收函式直接對時間軸量每張卡的長度，有一張不夠就讓建置失敗。

```
import logging

MIN_LEN = 0.6   # 每片最短秒數
MIN_CHARS = 4   # 少於這個字數的碎片併回前一片
EPS = 1e-6      # 浮點容差

log = logging.getLogger(__name__)

def snap_monotonic(cuts, pauses, end, min_len=MIN_LEN):
    """cuts: 原始切點(秒)；pauses: 停頓時間點(秒)，皆已排序；end: 音軌結尾(秒)
    回傳嚴格遞增的邊界，保證每一片(含最後一片)至少 min_len 秒；容量不夠直接拋錯"""
    n = len(cuts)
    need = (n + 1) * min_len
    if end + EPS < need:
        raise ValueError(f"not enough room: {n + 1} segments need {need:.2f}s, have {end:.2f}s")
    out, prev = [], 0.0
    for i, c in enumerate(cuts):
        lo = prev + min_len              # 下界：上一個邊界 + 最短秒數
        hi = end - (n - i) * min_len     # 上界：替後面 n-i 片(含最後一片)預留容量
        cand = [p for p in pauses if lo - EPS
