Communitygithub.com

TaurenRen/srt-whiteboard-workflow

將 SRT 字幕/解說稿轉換成「手握筆邊畫邊講」的白板手繪動畫成片(含配音、錄屏、關鍵截圖)。

srt-whiteboard-workflow 是什麼?

此技能專為開發者與內容創作者設計,能將結構化的 SRT 字幕或解說稿自動化地轉變為高品質的白板手繪動畫影片。它基於開源專案 `srt-whiteboard-animation`,並整合了從腳本分析、AI 主題圖片生成、像素級別標註到配音對齊、動畫渲染與字幕燒錄的完整工作流程。此技能特別之處在於其內建了大量經由實際專案驗證的「鐵律」,能有效避免渲染失敗、畫面雜亂、節奏錯位等常見問題,並支援地理路線與時間軸兩種主要的敘事構圖模式。生成的影片包含連續的「手握筆」動畫、經過去紙紋處理的乾淨線稿、以及與旁白精準同步的字幕,適用於製作科普、歷史解說、產品發展歷程等白板影片。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/TaurenRen/srt-whiteboard-workflow/tree/HEAD

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

手繪白板動畫製作全流程(SRT → 手繪動畫成片)

基於開源專案 srt-whiteboard-animation(github.com/geeklee/srt-whiteboard-animation)實測沉澱。 產出:帶配音的「手握筆邊畫邊講」白板動畫 MP4 + 關鍵截圖 + 操作錄屏。

鐵律(違反即返工,全部來自實測翻車)

  1. 禁止拿示例圖當交付。項目 examples/ 裡的 scene-01-monkey-mountain-banana.png 只能驗證管線, 用戶要什麼主題就必須生成什麼主題的圖。用示例圖交付 = 交付失敗。
  2. 源圖必須淨化。AI 生成的米黃紙底帶大量紋理顆粒,渲染器 adaptiveThreshold 會把紙紋判成墨跡, 成片線條碎成點陣、背景爬滿噪點。必須跑 scripts/clean_source.py 去紙紋後再渲染。
  3. 末位元素禁止用整幅 region。allowed mask = region − 後續 region,若最後一個元素 region 是整幅畫布, 它會把前面所有元素的 allowed 扣空 → 前幾幕什麼也畫不出來、顏色全丟(實測確認)。 收尾幕要挑一塊真實的小區域(如頂部雲帶)。
  4. 手部素材要按畫布縮放。原始 drawing-hand.png 是 1069×1472,直接貼到 1280×720 會佔屏 40%+, 嚴重遮擋畫面。實測縮到 55%(587×809),手寬約佔畫布 21%,觀感正常。
  5. 渲染輸出時長會短於標註。渲染器按各區域實際可繪製像素決定幀數,sceneDurationMs 只是上限。 實測 8×8000ms=64s 的標註只渲出 45.8s。必須傳 --total-ms,並以 ffprobe 實測時長校準。
  6. 錄屏不能用 kill 結束。ffmpeg 被強殺會丟 moov atom,檔案報 moov atom not found。 用 -t <秒> 讓錄製自然結束。
  7. 必須交付錄屏與截圖。這是需求的一部分,不是可選項。截圖要覆蓋:源圖、標註校驗圖、 渲染控制台、成片關鍵幀。用「文檔記錄代替截圖」屬於未完成。
  8. 一張畫布只講一件事,而且必須是「連續推進」。 實測返工:把紅旗、橋、雪山、寶塔平鋪在一張圖裡一次性同時畫 → 用戶評價「畫得很亂」。 正確做法是讓元素沿一條主線(地理路線 / 時間軸)排開,筆跡按主線的行進方向連續移動, 觀眾看到的是「筆在走路」,而不是「在一張畫布上到處補細節」。
  9. 空間佈局必須服從真實走向。畫長征就要按真實地理:瑞金在東南 → 向西 → 北上 → 陝北。 構圖錯了,節奏再準也救不回來。畫之前先在地圖上確認每個節點的相對方位。
  10. 手繪節奏 = 口播節奏,一元素對一句。每個元素的 durationMs 必須等於它那句配音的實際時長 (用 sync_timing.py 從靜音檢測反推),絕不允許平均分配。第 N 幕的筆停在「第 N 句說完」的瞬間。
  11. 幕與旁白語義必須一一對應。實測踩過:寶塔(吳起鎮)排在總結句那一幕,結果旁白說「抵達吳起鎮」 時畫面在畫別的東西。收尾的總結句(「二萬五千里…」)不要配新元素,讓它落在「全部畫完的定格」上, 渲染器會自動補 --total-ms 的空幀做凝視。
  12. 字幕要單獨燒,不要指望 force_style。純 SRT 沒有 PlayRes,libass 用內部默認解析度換算 FontSize/MarginV,實測調不出準值。走 make_srt.py → srt2ass.py(顯式寫 PlayRes = 影片尺寸, 並用 {\an2\pos(x,y)} 錨定底邊)才可控。
  13. 字幕只允許一行。兩行會同時佔掉畫面上下兩塊,中文兩行讀起來也斷氣。 一行裝不下就拆成多條連續字幕(按字數比例分攤時長),不要壓行、不要縮字號。 實測:46px 字號 + 單行 ≤14 字,720p 下約佔屏寬 60%,手機上看剛好。
  14. 畫布要能移動,別把整張圖塞進一個固定鏡頭。觀眾反饋「沒必要一張畫布畫完」—— 正確做法是把畫布放大(2 倍)渲染,每幕只把當前元素取景到滿屏,畫完鏡頭搖到下一個, 最後拉遠到全圖收尾。一張固定全景從頭看到尾 = 每個元素只佔屏幕 10%,什麼都看不清。
  15. 筆尖必須跟著內容走。上游 contour-wipe 是「整片自下而上擦除 + 筆尖橫向亂掃」, 兩者完全脫鉤 → 用戶看到「手在動但沒東西畫出來」。改成蛇形筆刷:筆掃到哪,顏色出現在哪。
  16. 講關鍵節點時要寫字。只畫物不寫字,觀眾記不住「哪一年、什麼地方」。 把地名/年份用楷體烘進底圖(add_handwriting.py),再讓渲染器用 textwrite 模式逐列寫出來 —— 筆順著筆畫推進,字一個個出現,跟「手寫」是同一個動作。
  17. 畫布放大後必須做兩件事,否則要麼慢死要麼糊: ① 源圖先用 INTER_CUBIC 預放大到畫布尺寸(渲染器內部的 resize 是 INTER_AREA, 放大時等價最近鄰,會把線條變成方塊); ② 落墨改按包圍盒局部處理(上游每段都 new 一張整畫布掩碼,2560 畫布下慢 4 倍)。
  18. 合成音軌前的成片必須留 3–7 秒給結尾拉遠。最後一幕不要排到配音結束的最後一毫秒, 否則「展示全圖」這個收尾動作沒有時間發生。
  19. 取景框要用「真實墨跡包圍盒」,不能用標註 region。region 是手畫的框,普遍比圖形大一圈, 密集圖裡還會橫跨 2–3 個鄰居;直接拿它算 zoom,結果是「畫面正中空白、圖形縮在角落、鄰居被切一半」。 渲染器已內置:set_target(element, allowed) 裡用 ink_pixels & allowed 求 bbox 再外擴。 例外:文字幕不走 bbox——標籤字常比 region 還寬,套 bbox 會把整句裁到框外(實測「對話式 AI」出畫)。
  20. 換場要先搖鏡頭再落筆。每幕開頭空轉約 5 幀(0.22s),否則筆在鏡頭還沒到位時就開始畫, 畫到畫面外,觀眾看到「手在動卻沒內容」。
  21. --zoom-fill 按底圖疏密分別取值,稀疏圖 0.86、密集時間軸 0.70;一個參數連通吃會兩頭翻車。
  22. 豎長元素滿不了屏要接受。16:9 畫面裡,縱向元素只能佔 60–70% 高、25% 寬,這是幾何約束; 想更滿就把元素畫寬,或者出豎屏。
  23. 配音整篇一次生成,禁止逐句分段再拼接。分段拼接 = 各段語速/停頓不一致 + 時間軸必然錯位, 用戶聽到的評語就是「胡言亂語」。即夢單次可吃 1500+ 字(≈2 分鐘),整篇一次餵進去。
  24. 時間軸來自配音的轉寫時間戳,不來自靜音檢測。停頓 ≠ 句子邊界;句子數對不上就只能平均分配, 字幕必錯。用 asr_align.py(ffmpeg → faster-whisper word_timestamps → 字符級時間軸)。
  25. 換了配音就必須重派生一遍字幕和標註時長。字幕時間戳是那條音頻的屬性,不是畫面的屬性; 舊字幕配新音頻等於沒做。
  26. 交付前用 difflib 核對「文案 vs 轉寫」。相似度 <0.8、或出現整句缺失/重複 → 音頻有問題,重生成。 同音誤識(弘/紅、尊/遵、武/吳)屬正常 ASR 現象,不算朗讀錯誤。
  27. 配音生成後必須做靜音體檢。「整篇一次送 TTS」不代表平台內部不切塊: 即夢會在塊間插入 -120dB 絕對靜音(實測 4.00s / 2.60s)。靜音佔比 >20% 或存在

    1.2s 的停頓 → 先用 tighten_audio.py 壓掉,再談時間軸對齊。

  28. 字幕起點要吸附到真實發聲幀。ASR 在有長靜音時會把下一句 start 標在靜音起點, 觀眾會「看著字幕等聲音」。asr_align.py 默認 snap_to_voice 修正,別關。

目錄規範(強制)

D:/04-項目實測/<YYMMDD>-<項目簡稱>/
  00-資料/          項目資料與源碼筆記
  01-素材/          原始 AI 出圖、手部素材
  02-字幕與配音/     SRT、配音 mp3、時長表
  03-標註/          <圖名>.png + <圖名>.annotation.json(必須同名同目錄,預覽台依賴)
  04-渲染輸出/      單幕 silent.mp4、成片 mp4
  05-錄屏截圖/      錄屏 mp4 + 關鍵截圖 png
  06-文檔/          腳本、操作記錄、驗收清單

前置

git clone https://github.com/geeklee/srt-whiteboard-animation.git
cd srt-whiteboard-animation && python scripts/prepare_env.py   # 建 .venv 裝 opencv/numpy/av

後續一切渲染命令都用 srt-whiteboard-animation/.venv/Scripts/python.exe。

若報 _lay_ink() takes 6 positional arguments but 7 were given,是倉庫已知 bug: render_stream_whiteboard.py 裡 self._lay_ink(writer, ink_frames, [], set(), None, allowed) 多餘的 None 刪掉即可(scripts/check_bug_fix.py 可自動檢測修復)。

工作流(7 步:分鏡 → 出圖 → 空間板 → 標註 → 配音對齊 → 渲染 → 字幕)

階段 1 · 解說稿與分鏡(先確認再往下)

  1. 寫 6–10 句解說,每句 6–10 秒,總長 40–90 秒。
  2. 解析分鏡(可選,也可直接手排):
    <ENV_PY> srt-whiteboard-animation/scripts/parse_srt.py 字幕.srt --target-sec 30 --min-sec 25 --max-sec 35
    
  3. 輸出分鏡表(第幾幕 / 核心表達 / 畫面主體 / 字幕原文),停下等用戶確認。

階段 2 · AI 出主題手繪圖

按項目「統一出圖視覺規範」寫出圖提示詞,每條都必須含:

  • 極簡手繪線稿 / 純素描草圖 / Notion 式克制塗鴉,16:9,大量留白,元素互不重疊
  • 背景暖米黃 #F5EBD7,線條深灰 #333333,僅紅/橙/藍少量點綴
  • 絕對禁止任何文字(漢字/字母/數字/標籤/地名/標題/水印),禁止寫實/3D/漸層/高飽和

用 ImageGen 或即夢 image 出圖。每張圖對應一幕核心意思,元素要能被矩形分區切開。

出圖後必做三件事:

  1. 去水印:AI 出圖右下角常有 AI生成 WORKBUDDY,用 cv2.inpaint 抹掉。
  2. 轉 16:9:生成圖多為 3:2,左右補米黃紙底後縮放到 1280×720(不要居中裁切,會切掉主體)。
  3. 淨化:去紙紋,保留線條與彩色點綴。
    <ENV_PY> scripts/clean_source.py 原始圖.png 03-標註/主圖.png
    
    原理:paper = 大核中值濾波,線條 = gray < paper-18,彩色 = 飽和度>70 且 gray<190, 只保留 線條|彩色,其餘填紙底;四周 8px 強制紙底去邊緣殘留。

階段 2.5 · 空間敘事板(決定成片「亂不亂」,必須先畫這個)

在寫出圖提示詞之前,先用文字把主線定死。分兩類:

題材主線佈局原則
歷史路線 / 戰役 / 遷徙真實地理路線起點在真實方位,走「西→北→東」就老老實實這麼排;節點的 x/y 與真實方位一致
技術發展史 / 公司史一條橫向時間軸軌道貫穿全幅,節點等距,圖示上下交替掛(上→下→上),避免全部堆在一側

硬約束:

  • 只允許一條主線。不要在主線上再插畫「補充說明」式的零散元素。
  • 元素之間不重疊,且每個元素都能被一個不跨主線的矩形框住(否則 region 會互相扣減)。
  • 主線要貫穿全幅:留白均勻分佈在主線兩側,不要出現「全部擠在右下角、左上一大片空」。 實測長征東南起點在右下角,前 20 秒畫面右下堆疊、左上全空 —— 這是構圖本身的鍋,出圖時就要把 起點往中間收,讓路線把畫布用滿。

出圖提示詞裡要寫明每個元素在畫面中的位置(「右下角一面小紅旗」「中間偏左一座鐵索橋」), 不要只寫「畫一座橋」 —— 位置不指定,AI 給的構圖就沒法用。

階段 2.6 · 兩種底圖範式(實測參數,直接照抄)

成片「能不能滿屏、鏡頭推不推得動」,出圖階段就決定了。兩種範式各有一套參數,別混用:

A. 地理路線型(歷史/戰役/遷徙) —— 代表作:紅軍長征路線

項取值依據
構圖起點按真實方位,路線西→北→東貫穿全幅構圖錯,節奏再準也救不回
節點數6–8 站一站兩幕(畫物 + 寫字)→ 12–16 元素
鏡頭 --zoom-fill0.86元素之間留白多,推近才不空
密度元素可大可小(橋比旗子寬 3 倍也 OK)路線本身就是不同尺度的東西

B. 橫向時間軸型(技術史/公司史/產品迭代) —— 代表作:Agent 智能體發展史 (用戶評價「效果很不錯」的那版,參數照抄即可)

項取值依據
軌道一條 S 形/波形主軸貫穿全幅,8 個節點等距等距 → 鏡頭平移速度恆定,看著穩
圖示上下交替(上→下→上…),不要全排一行一排就只剩半幅畫面可用,取景縱向浪費一半
標籤掛在圖示同側更外側:上方節點的標籤在上面,下方節點在下面和圖示同處一屏,寫字的鏡頭不用再搖走
字號主名 36–40px 楷體,年份 0.6 倍在下方取景推近後 ≈ 螢幕 60–80px,手機可讀
鏡頭 --zoom-fill0.70節點左右挨得近,推太近會把鄰居攔腰切斷
一站兩幕配比畫圖示 0.60 / 寫「名字 + 年份」0.40寫字幕必須留夠時間,否則字還沒寫完就切走
時間軸本體畫在相鄰兩站之間,跟該站 icon 同幕處理否則軸線會一次性整條出現,節奏斷掉

B 型的三條硬注意:

  1. 文字幕不要套墨跡包圍盒(見鐵律 19 例外)。標籤字常比 region 寬,套 bbox 會把整句裁到畫外。
  2. 標籤的 region 要按「主名 + 年份」兩行留高,高度給 80–90px;只按主名一行給會切掉年份。
  3. 結尾必須拉遠到全軸。時間軸的敘事價值在「整體跨度」,最後 3–5 秒不拉遠等於白畫。

實測基線(1280×720 / 24fps):8 節點 × 2 幕 = 16 元素,成片 43.5s,渲染 33s, --zoom-fill 0.70 --zoom-max 7 --hand-height 350。

階段 3 · 像素級標註(最容易翻車,必須校驗)

先量座標,不要目視估。實測教訓:把 640×360 的預覽顯示尺寸當成 1280×720 實際座標, 導致兩輪座標全錯。正確做法是用顏色/線條檢測拿真實 bbox:

<ENV_PY> scripts/detect_marks.py 03-標註/主圖.png 標籤   # 紅/橙/藍點綴的連通域 bbox
<ENV_PY> scripts/detect_lines.py 03-標註/主圖.png 標籤   # 深灰線條簇 bbox

需人眼校準時疊加標尺:scripts/grid_ruler.py 主圖.png 帶網格.png(100px 網格,標的是真實像素)。

region 覆蓋率自檢(決定成片是否完整):

  • 所有 region 的 x 區間要連成不間斷的帶(允許重疊,不允許留縫)
  • 貫穿性主體(路線、時間線)必須被某幾個 region 完整覆蓋,否則成片斷線
  • 末位元素單獨挑小塊區域(見鐵律 3)

寫 03-標註/<名字>.annotation.json:

{
  "sceneId": "scene-01",
  "canvas": { "width": 1280, "height": 720 },
  "storyBasis": "本幕字幕事件摘要",
  "sceneDurationMs": 56000,
  "elements": [
    {
      "id": "ruijin", "label": "出發·瑞金", "sequence": 1,
      "narrativeRole": "長征起點", "subtitle": "對應字幕原文",
      "type": "point",
      "region": { "x": 265, "y": 515, "width": 145, "height": 100 },
      "reveal": { "direction": "left_to_right", "startMs": 0, "durationMs": 8100,
                  "maskPaddingPx": 16, "protectedRegions": [] },
      "handPath": { "start": [275, 570], "end": [400, 575], "easing": "easeInOut" }
    }
  ]
}
  • canvas 必須等於源圖真實像素;region 必須整數像素且在畫布內
  • sequence 按敘事順序,不是視覺從左到右
  • stream 畫法下 direction/handPath 只供預覽台演示,不影響成片筆跡

出校驗圖並逐格肉眼確認:

<ENV_PY> scripts/region_sheet.py 主圖.png 標註.json 05-錄屏截圖/校驗-region.png

每格應恰好框住目標元素;框住空白或別的元素 → 座標錯,回上一步。

階段 4 · 配音與時間軸對齊

① 配音:整篇一次生成,絕不逐句分段(2026-11-05 返工教訓)

"<TTS_PYTHON>  # 任意裝有 requests 的 Python 3.9+" \
  "jimeng-suite/scripts/jimeng_cli.py" audio \
  --text "整篇解說,一段話,用句號斷句" \
  --model "SeedAudio 1.0" --submode "全能配音" --voice "狒狒_講解" \
  --out "xxx-voice.mp3" --outdir "<項目>/02-字幕與配音" --timeout 900
  • Seed TTS + 文本朗讀 已於 2026-10-24 下線,不要再走
  • --text 一次餵整篇。單次能吃掉 1500+ 字(≈2 分鐘音頻),別再拆成一小堆三秒的小段: 實測把 56 秒的稿子拆成 12 段分別生成再拼接,各段語速/停頓/重音互不一致, 聽感就是「胡言亂語」,而且拼接後時間軸與字幕必然錯位 —— 用戶直接判不合格。
  • 只按敘事段落切分(一段一句話群),一段 15–40 秒,全片 1–3 段搞定;能一段就別兩段。
  • 內容審核會攔:軍事/暴力/傷害類措辭(血戰、殲滅、擊敗、敵人)返回 JimengContentRejectedError。改寫為中性敘述(「付出沉重代價」「在火力封鎖下」), 年份寫中文數字(一九三四年)更穩
  • 99 秒台詞實測 62 秒出片(1.19MB / 99.05s)

② 靜音體檢與節奏壓縮(必做,v5 返工教訓)

「整篇一次送 TTS」 不等於平台內部合成了一整段:即夢會把長文本內部切塊再拼接, 塊間插入 -120dB 的絕對靜音。長征 v4 實測 317 字 / 99.05s 裡, 31 處 ≥0.4s 停頓合計 27.2s(27.5%),其中 4.00s @14.05、2.60s @70.60 兩處 是純數字零採樣(自然停頓有底噪,約 -50~-60dB,不會到 -120dB)。 有效發聲其實只有 71.85s(4.41 字/秒,並不慢)——「節奏慢、怪怪的」全是空白造成的。

# 1) 體檢:任何 >1.2s 的靜音都要處理
ffmpeg -hide_banner -i xxx-voice.mp3 -af "silencedetect=noise=-38dB:d=0.35" -f null - 2>&1 | grep silence_start

# 2) 壓縮:分級保留自然停頓,不動任何發聲內容
<ENV_PY> scripts/tighten_audio.py 02-字幕與配音/xxx-voice.mp3 \
  02-字幕與配音/xxx-voice-tight.mp3 --report 06-文檔/_tighten.json

tighten_audio.py:50ms RMS 視窗、<-40dB 判靜音,≥1.0s 的停頓留 0.55s、 0.35–1.0s 的留 0.32s、片頭留 0.12s,按樣本重新拼接。 v4→v5 實測 99.05s → 83.15s,靜音佔比 27.8% → 14.0%,無任何 >0.7s 停頓。

壓縮會改變音頻長度,之後 ASR 轉寫、timing、SRT、ASS、標註時長、渲染 total-ms 必須全部基於壓縮後的音頻重跑(鐵律 25)。--reuse-asr 只用於調字幕斷行, 音頻一變就絕不能複用舊 dump。

③ 時間軸:用配音自己的轉寫時間戳反推,不要用靜音檢測猜

<ENV_PY> scripts/asr_align.py 02-字幕與配音/xxx-voice.mp3 06-文檔/文案.txt \
  --model small \
  --out-timing 03-標註/xxx.annotation.timing.json \
  --out-srt 02-字幕與配音/xxx.srt --max-chars 18 \
  --dump-asr 06-文檔/_asr.json

asr_align.py 幹的事:ffmpeg 轉 16k 單聲道 → faster-whisper(word_timestamps=True) → 鋪成逐字符時間軸 → 把文案的每一句在轉寫全文裡定位(精確找 → 首 8 字錨點 → 兜底) → 輸出 timing.json(兼容 segments[i/start/end/text])與單行 SRT。

為什麼不用 silencedetect:靜音只說明「這裡有停頓」,換氣、強調都會停; 句子數和停頓數一旦對不上,就只能按字數平均分配,字幕必然錯位。

asr_align.py 默認還會做句邊界吸附(snap_to_voice):faster-whisper 在長靜音處 習慣把下一句 start 標在靜音剛開始的位置,導致字幕提前亮出、觀眾盯著字等聲音 (v4 提前了整整 4 秒)。腳本按 20ms 網格找真實發聲起點並修正;--no-snap 可關閉。

④ 必須核對音頻內容(判「胡言亂語」的唯一硬標準):

<ENV_PY> -c "..."   # difflib 比對 文案 vs _asr.json 的 full

相似度應 >0.8;small 模型對中文專有名詞只會同音誤識(弘/紅、尊/遵、武/吳、鎖/索), 結構(句序、句長)一致就說明朗讀正確。若出現整句缺失、重複、跳躍 → 音頻本身有問題,重新生成。

⑤ 字幕斷行三條規則(都在 asr_align.py::split_subs 裡):

  • 先按標點切小句,只有當前塊 < limit×0.55 才合併下一個小句 —— 否則會出現 「踏上了舉世聞名的長征之路那一年」這種跨小句粘連
  • 合併後不得超過 limit(不要留 +3 容差,20 字被均分會切出「…要走多遠只」這種單字甩行)
  • 單塊仍超長才均分;尾巴 ≤4 字並進前一條

⑥ 結尾留白(鐵律 18 的落地手法):給最後一個 station 加 "tail": 0.30, 該段最後 30% 不排任何元素,渲染器就會用這段時間做拉遠+定格。 實測 99 秒片子留 4 秒,正好夠看全圖。

⑦ 分鏡時長分配:一元素對一句(或一句裡的一個動作),durationMs 直接取該句時長, 段內按 ratio 再切「畫物 / 寫字」。見 scripts/make_annotation_v3.py。

階段 5 · 渲染、合成、校驗

<ENV_PY> srt-whiteboard-animation/scripts/render_stream_whiteboard.py \
  03-標註/主圖.png 03-標註/主圖.annotation.json 04-渲染輸出/xxx-silent.mp4 \
  01-素材/hand-55pct.png \
  --cap-long-edge 1280 --fps 24 --total-ms <配音毫秒>
  • --cap-long-edge 等於源圖長邊時不再縮放;預覽可調小加速
  • --total-ms 傳配音時長(毫秒),保證畫面不早於配音結束
  • 末行輸出 OUTPUT=<路徑>

合成音軌:

ffmpeg -y -i xxx-silent.mp4 -i 配音.mp3 -c:v copy -c:a aac -b:a 192k -shortest 成片.mp4

必查三點(各抽一幀):

  1. 開場乾淨米黃底,無提前露線
  2. 中段:線條連續、顏色正確(紅/橙/藍都在)
  3. 末尾:整幅完整,無斷線、無空白大塊

時長偏差 >1s 就調 --total-ms 或微調各 durationMs 重渲。

階段 5.5 · 鏡頭與寫字(v3 核心,決定「像不像在畫」)

上游渲染器是一張固定畫布畫到底。想讓畫面像鏡頭語言,加一個 render_camera_whiteboard.py 子類, 不改倉庫代碼(scripts/render_camera_whiteboard.py 已可複用):

<ENV_PY> scripts/render_camera_whiteboard.py 03-標註/主圖.png 03-標註/主圖.annotation.json \
  04-渲染輸出/xxx-silent.mp4 01-素材/hand.png \
  --canvas-long-edge 2560 --frame 1280x720 --fps 24 --grid-edge 20 \
  --zoom-fill 0.86 --zoom-max 7 --hand-height 350 --total-ms 56064

它做了六件事:

機制實現為什麼
鏡頭取景畫布 2560×1440 渲染,每幀按 viewport 裁剪縮放成 1280×720元素能「滿屏畫」,畫完搖走
墨跡包圍盒取景目標框 = 「該幕 allowed 掩碼內的實體墨跡 bbox」,不是 regionregion 往往比圖形大一圈甚至橫跨鄰居,直接用它取景 → 畫面正中空白、圖形被頂到邊角(v3 第一版就翻在這)
鏡頭跟隨相鄰幕指數平滑 tau 0.20/0.30,寫字幕 pad 0.34、畫物 pad 0.05天然產生搖鏡,不用做轉場幀
換場緩衝每幕開頭空轉 0.22s(約 5 幀)再落筆鏡頭從上一幕搖過來要 0.3–0.6s,這期間落筆會「筆在畫面外畫」,是「手在動沒內容」的主因之一
結尾全圖最後一幕結束後 target 設為全畫布,補幀到 --total-ms「最後展示一張全圖」
手部固定螢幕尺寸手不畫進畫布,改貼到輸出幀上(--hand-height 350)否則一推近手就佔滿屏;430 實測仍偏大、會蓋住正在寫的字

--zoom-fill 要按底圖疏密分別定(同一套參數不能通吃):

底圖類型zoom-fill理由
稀疏(路線圖/流程圖,元素間有大片留白)0.86可以放心推近,畫面才不空
密集(橫向時間軸,元素左右緊鄰)0.70推太近會把鄰居切一半,顯得雜亂

16:9 的硬約束:豎長元素(棋子盤、手機)受畫面高度限制,滿屏只能做到縱向 60–70%, 橫向 25% 左右,這是幾何決定的,不是 bug。要更滿就得把元素畫寬,或改成豎屏輸出。

寫字(把地名/年份寫出來):

# 1) 先把標籤烘進底圖(座標用 1280×720 標註座標,腳本會自動按字號排兩行)
<ENV_PY> scripts/add_handwriting.py 底圖2x.png 03-標註/主圖-v3.png 02-字幕與配音/標籤.json
# 2) 標註裡給這一幕寫 "inkPath": "textwrite",region 就是文字框

textwrite 按列從右推進,筆尖在揭示前沿,保證文字一定寫完(骨架追蹤會漏細筆畫,別用)。 前 30% 幀持筆等鏡頭到位,再開始寫 —— 否則字會寫到畫框外。 標籤排版:主名 34–40px 楷體(C:/Windows/Fonts/simkai.ttf)+ 年份 0.6 倍字號在下方, 顏色 (58,56,52) 與線稿同色系但略淺。

幕的拆分:一站 = 兩幕。先畫物(0.6 時長),緊接著寫字(0.4 時長), 寫字幕的取景框 = 自己的文字框,這樣鏡頭會輕微推過去,像「筆移到旁邊寫字」。

階段 6 · 字幕燒錄(SRT → ASS → 燒進畫面)

字幕時間軸不要另標,直接用階段 4 產出的 .timing.json —— 它已經和口播逐句對齊, 字幕換句的時刻就是筆停下的時刻,這才是「節奏對齊」。

# 1) timing.json -> SRT(**單行**,超長自動拆成多條連續字幕)
<ENV_PY> scripts/make_srt.py 03-標註/主圖.annotation.timing.json 02-字幕與配音/x.srt \
  --max-chars 14

# 2) SRT -> ASS(顯式 PlayRes + \pos 錨定底邊,字號/位置是真實像素)
<ENV_PY> scripts/srt2ass.py 0

相關技能