手繪白板動畫製作全流程(SRT → 手繪動畫成片)
基於開源專案 srt-whiteboard-animation(github.com/geeklee/srt-whiteboard-animation)實測沉澱。 產出:帶配音的「手握筆邊畫邊講」白板動畫 MP4 + 關鍵截圖 + 操作錄屏。
鐵律(違反即返工,全部來自實測翻車)
- 禁止拿示例圖當交付。項目
examples/裡的scene-01-monkey-mountain-banana.png只能驗證管線, 用戶要什麼主題就必須生成什麼主題的圖。用示例圖交付 = 交付失敗。 - 源圖必須淨化。AI 生成的米黃紙底帶大量紋理顆粒,渲染器
adaptiveThreshold會把紙紋判成墨跡, 成片線條碎成點陣、背景爬滿噪點。必須跑scripts/clean_source.py去紙紋後再渲染。 - 末位元素禁止用整幅 region。
allowed mask = region − 後續 region,若最後一個元素 region 是整幅畫布, 它會把前面所有元素的 allowed 扣空 → 前幾幕什麼也畫不出來、顏色全丟(實測確認)。 收尾幕要挑一塊真實的小區域(如頂部雲帶)。 - 手部素材要按畫布縮放。原始
drawing-hand.png是 1069×1472,直接貼到 1280×720 會佔屏 40%+, 嚴重遮擋畫面。實測縮到 55%(587×809),手寬約佔畫布 21%,觀感正常。 - 渲染輸出時長會短於標註。渲染器按各區域實際可繪製像素決定幀數,
sceneDurationMs只是上限。 實測 8×8000ms=64s 的標註只渲出 45.8s。必須傳--total-ms,並以 ffprobe 實測時長校準。 - 錄屏不能用 kill 結束。ffmpeg 被強殺會丟 moov atom,檔案報
moov atom not found。 用-t <秒>讓錄製自然結束。 - 必須交付錄屏與截圖。這是需求的一部分,不是可選項。截圖要覆蓋:源圖、標註校驗圖、 渲染控制台、成片關鍵幀。用「文檔記錄代替截圖」屬於未完成。
- 一張畫布只講一件事,而且必須是「連續推進」。 實測返工:把紅旗、橋、雪山、寶塔平鋪在一張圖裡一次性同時畫 → 用戶評價「畫得很亂」。 正確做法是讓元素沿一條主線(地理路線 / 時間軸)排開,筆跡按主線的行進方向連續移動, 觀眾看到的是「筆在走路」,而不是「在一張畫布上到處補細節」。
- 空間佈局必須服從真實走向。畫長征就要按真實地理:瑞金在東南 → 向西 → 北上 → 陝北。 構圖錯了,節奏再準也救不回來。畫之前先在地圖上確認每個節點的相對方位。
- 手繪節奏 = 口播節奏,一元素對一句。每個元素的
durationMs必須等於它那句配音的實際時長 (用sync_timing.py從靜音檢測反推),絕不允許平均分配。第 N 幕的筆停在「第 N 句說完」的瞬間。 - 幕與旁白語義必須一一對應。實測踩過:寶塔(吳起鎮)排在總結句那一幕,結果旁白說「抵達吳起鎮」
時畫面在畫別的東西。收尾的總結句(「二萬五千里…」)不要配新元素,讓它落在「全部畫完的定格」上,
渲染器會自動補
--total-ms的空幀做凝視。 - 字幕要單獨燒,不要指望
force_style。純 SRT 沒有 PlayRes,libass 用內部默認解析度換算FontSize/MarginV,實測調不出準值。走make_srt.py→srt2ass.py(顯式寫 PlayRes = 影片尺寸, 並用{\an2\pos(x,y)}錨定底邊)才可控。 - 字幕只允許一行。兩行會同時佔掉畫面上下兩塊,中文兩行讀起來也斷氣。 一行裝不下就拆成多條連續字幕(按字數比例分攤時長),不要壓行、不要縮字號。 實測:46px 字號 + 單行 ≤14 字,720p 下約佔屏寬 60%,手機上看剛好。
- 畫布要能移動,別把整張圖塞進一個固定鏡頭。觀眾反饋「沒必要一張畫布畫完」—— 正確做法是把畫布放大(2 倍)渲染,每幕只把當前元素取景到滿屏,畫完鏡頭搖到下一個, 最後拉遠到全圖收尾。一張固定全景從頭看到尾 = 每個元素只佔屏幕 10%,什麼都看不清。
- 筆尖必須跟著內容走。上游
contour-wipe是「整片自下而上擦除 + 筆尖橫向亂掃」, 兩者完全脫鉤 → 用戶看到「手在動但沒東西畫出來」。改成蛇形筆刷:筆掃到哪,顏色出現在哪。 - 講關鍵節點時要寫字。只畫物不寫字,觀眾記不住「哪一年、什麼地方」。
把地名/年份用楷體烘進底圖(
add_handwriting.py),再讓渲染器用textwrite模式逐列寫出來 —— 筆順著筆畫推進,字一個個出現,跟「手寫」是同一個動作。 - 畫布放大後必須做兩件事,否則要麼慢死要麼糊:
① 源圖先用
INTER_CUBIC預放大到畫布尺寸(渲染器內部的resize是INTER_AREA, 放大時等價最近鄰,會把線條變成方塊); ② 落墨改按包圍盒局部處理(上游每段都 new 一張整畫布掩碼,2560 畫布下慢 4 倍)。 - 合成音軌前的成片必須留 3–7 秒給結尾拉遠。最後一幕不要排到配音結束的最後一毫秒, 否則「展示全圖」這個收尾動作沒有時間發生。
- 取景框要用「真實墨跡包圍盒」,不能用標註 region。region 是手畫的框,普遍比圖形大一圈,
密集圖裡還會橫跨 2–3 個鄰居;直接拿它算 zoom,結果是「畫面正中空白、圖形縮在角落、鄰居被切一半」。
渲染器已內置:
set_target(element, allowed)裡用ink_pixels & allowed求 bbox 再外擴。 例外:文字幕不走 bbox——標籤字常比 region 還寬,套 bbox 會把整句裁到框外(實測「對話式 AI」出畫)。 - 換場要先搖鏡頭再落筆。每幕開頭空轉約 5 幀(0.22s),否則筆在鏡頭還沒到位時就開始畫, 畫到畫面外,觀眾看到「手在動卻沒內容」。
--zoom-fill按底圖疏密分別取值,稀疏圖 0.86、密集時間軸 0.70;一個參數連通吃會兩頭翻車。- 豎長元素滿不了屏要接受。16:9 畫面裡,縱向元素只能佔 60–70% 高、25% 寬,這是幾何約束; 想更滿就把元素畫寬,或者出豎屏。
- 配音整篇一次生成,禁止逐句分段再拼接。分段拼接 = 各段語速/停頓不一致 + 時間軸必然錯位, 用戶聽到的評語就是「胡言亂語」。即夢單次可吃 1500+ 字(≈2 分鐘),整篇一次餵進去。
- 時間軸來自配音的轉寫時間戳,不來自靜音檢測。停頓 ≠ 句子邊界;句子數對不上就只能平均分配,
字幕必錯。用
asr_align.py(ffmpeg → faster-whisper word_timestamps → 字符級時間軸)。 - 換了配音就必須重派生一遍字幕和標註時長。字幕時間戳是那條音頻的屬性,不是畫面的屬性; 舊字幕配新音頻等於沒做。
- 交付前用 difflib 核對「文案 vs 轉寫」。相似度 <0.8、或出現整句缺失/重複 → 音頻有問題,重生成。 同音誤識(弘/紅、尊/遵、武/吳)屬正常 ASR 現象,不算朗讀錯誤。
- 配音生成後必須做靜音體檢。「整篇一次送 TTS」不代表平台內部不切塊:
即夢會在塊間插入 -120dB 絕對靜音(實測 4.00s / 2.60s)。靜音佔比 >20% 或存在
1.2s 的停頓 → 先用
tighten_audio.py壓掉,再談時間軸對齊。 - 字幕起點要吸附到真實發聲幀。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 · 解說稿與分鏡(先確認再往下)
- 寫 6–10 句解說,每句 6–10 秒,總長 40–90 秒。
- 解析分鏡(可選,也可直接手排):
<ENV_PY> srt-whiteboard-animation/scripts/parse_srt.py 字幕.srt --target-sec 30 --min-sec 25 --max-sec 35 - 輸出分鏡表(第幾幕 / 核心表達 / 畫面主體 / 字幕原文),停下等用戶確認。
階段 2 · AI 出主題手繪圖
按項目「統一出圖視覺規範」寫出圖提示詞,每條都必須含:
- 極簡手繪線稿 / 純素描草圖 / Notion 式克制塗鴉,16:9,大量留白,元素互不重疊
- 背景暖米黃
#F5EBD7,線條深灰#333333,僅紅/橙/藍少量點綴 - 絕對禁止任何文字(漢字/字母/數字/標籤/地名/標題/水印),禁止寫實/3D/漸層/高飽和
用 ImageGen 或即夢 image 出圖。每張圖對應一幕核心意思,元素要能被矩形分區切開。
出圖後必做三件事:
- 去水印:AI 出圖右下角常有
AI生成 WORKBUDDY,用cv2.inpaint抹掉。 - 轉 16:9:生成圖多為 3:2,左右補米黃紙底後縮放到 1280×720(不要居中裁切,會切掉主體)。
- 淨化:去紙紋,保留線條與彩色點綴。
原理:<ENV_PY> scripts/clean_source.py 原始圖.png 03-標註/主圖.pngpaper = 大核中值濾波,線條 = gray < paper-18,彩色 = 飽和度>70 且 gray<190, 只保留線條|彩色,其餘填紙底;四周 8px 強制紙底去邊緣殘留。
階段 2.5 · 空間敘事板(決定成片「亂不亂」,必須先畫這個)
在寫出圖提示詞之前,先用文字把主線定死。分兩類:
| 題材 | 主線 | 佈局原則 |
|---|---|---|
| 歷史路線 / 戰役 / 遷徙 | 真實地理路線 | 起點在真實方位,走「西→北→東」就老老實實這麼排;節點的 x/y 與真實方位一致 |
| 技術發展史 / 公司史 | 一條橫向時間軸 | 軌道貫穿全幅,節點等距,圖示上下交替掛(上→下→上),避免全部堆在一側 |
硬約束:
- 只允許一條主線。不要在主線上再插畫「補充說明」式的零散元素。
- 元素之間不重疊,且每個元素都能被一個不跨主線的矩形框住(否則 region 會互相扣減)。
- 主線要貫穿全幅:留白均勻分佈在主線兩側,不要出現「全部擠在右下角、左上一大片空」。 實測長征東南起點在右下角,前 20 秒畫面右下堆疊、左上全空 —— 這是構圖本身的鍋,出圖時就要把 起點往中間收,讓路線把畫布用滿。
出圖提示詞裡要寫明每個元素在畫面中的位置(「右下角一面小紅旗」「中間偏左一座鐵索橋」), 不要只寫「畫一座橋」 —— 位置不指定,AI 給的構圖就沒法用。
階段 2.6 · 兩種底圖範式(實測參數,直接照抄)
成片「能不能滿屏、鏡頭推不推得動」,出圖階段就決定了。兩種範式各有一套參數,別混用:
A. 地理路線型(歷史/戰役/遷徙) —— 代表作:紅軍長征路線
| 項 | 取值 | 依據 |
|---|---|---|
| 構圖 | 起點按真實方位,路線西→北→東貫穿全幅 | 構圖錯,節奏再準也救不回 |
| 節點數 | 6–8 站 | 一站兩幕(畫物 + 寫字)→ 12–16 元素 |
鏡頭 --zoom-fill | 0.86 | 元素之間留白多,推近才不空 |
| 密度 | 元素可大可小(橋比旗子寬 3 倍也 OK) | 路線本身就是不同尺度的東西 |
B. 橫向時間軸型(技術史/公司史/產品迭代) —— 代表作:Agent 智能體發展史 (用戶評價「效果很不錯」的那版,參數照抄即可)
| 項 | 取值 | 依據 |
|---|---|---|
| 軌道 | 一條 S 形/波形主軸貫穿全幅,8 個節點等距 | 等距 → 鏡頭平移速度恆定,看著穩 |
| 圖示 | 上下交替(上→下→上…),不要全排一行 | 一排就只剩半幅畫面可用,取景縱向浪費一半 |
| 標籤 | 掛在圖示同側更外側:上方節點的標籤在上面,下方節點在下面 | 和圖示同處一屏,寫字的鏡頭不用再搖走 |
| 字號 | 主名 36–40px 楷體,年份 0.6 倍在下方 | 取景推近後 ≈ 螢幕 60–80px,手機可讀 |
鏡頭 --zoom-fill | 0.70 | 節點左右挨得近,推太近會把鄰居攔腰切斷 |
| 一站兩幕配比 | 畫圖示 0.60 / 寫「名字 + 年份」0.40 | 寫字幕必須留夠時間,否則字還沒寫完就切走 |
| 時間軸本體 | 畫在相鄰兩站之間,跟該站 icon 同幕處理 | 否則軸線會一次性整條出現,節奏斷掉 |
B 型的三條硬注意:
- 文字幕不要套墨跡包圍盒(見鐵律 19 例外)。標籤字常比 region 寬,套 bbox 會把整句裁到畫外。
- 標籤的 region 要按「主名 + 年份」兩行留高,高度給 80–90px;只按主名一行給會切掉年份。
- 結尾必須拉遠到全軸。時間軸的敘事價值在「整體跨度」,最後 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
必查三點(各抽一幀):
- 開場乾淨米黃底,無提前露線
- 中段:線條連續、顏色正確(紅/橙/藍都在)
- 末尾:整幅完整,無斷線、無空白大塊
時長偏差 >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」,不是 region | region 往往比圖形大一圈甚至橫跨鄰居,直接用它取景 → 畫面正中空白、圖形被頂到邊角(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