Manim 1080p60 技術影片解說與同步旁白系統
概述與捆綁資源
使用純 Python 搭配 Manim Community(無需 pdflatex 或 MacTeX),創建清晰且確定的 1920x1080 @ 60fps 技術解說影片與架構導覽。可選擇性搭配樣本精確的神經網路旁白(離線 Piper ONNX、任何相容 OpenAI 的 /v1/audio/speech HTTP 端點或自訂 CLI TTS)、EBU R128 響度正規化、同步的 .vtt WebVTT 字幕,以及一個零 CDN 的離線 HTML5 章節播放器。
assets/nord_manim_theme.py:Nord Polar/Frost/Aurora 色板代碼、安全框佈局常數、BeatTimer同步輔助工具、無需 LaTeX 的基本元件(make_header_banner、make_node_card、make_badge、animate_blocked_pulse、safe_fit_text),以及嚴格的連接埠錨定正交示意圖連線器(connect_h、connect_v、connect_elbow、animate_packet_along、assert_all_orthogonal)。assets/video_player_template.html:獨立、零 CDN 的 Nord 深色/淺色 HTML5 Cinema 播放器,包含章節標籤、可點擊的節拍標記、播放速率(0.75x..2.0x),以及可選的雙語音訊/字幕切換。check_scene_bounds.py:預渲染佈局驗證器,檢查 Manim 場景或 JSON 檢查點是否存在[-6.8, 6.8] x [-3.75, 3.75]框架溢出以及同級文字標籤碰撞。mux_narration.py:基於 Python 標準函式庫的 CLI,可透過 Piper、相容 OpenAI 的 HTTP/v1/audio/speech端點或自訂 TTS 命令合成逐個節拍的 WAV 檔案,輸出beat_timings.json,組裝樣本精確的主 WAV 音軌,複製填補影片尾端幀(tpad=stop_mode=clone)以避免語音被截斷,可選響度正規化(--loudnorm-lufs -18.0),混入 H.264 + AAC(+faststart),並寫入.vtt字幕。lint.py:輸出套件驗證器,檢查ffprobe的 H.264yuv420p與音訊串流、WebVTT 標頭/提示的單調性,以及離線 HTML5 播放器的 CSP/零 CDN/零表情符號不變量。references/manim-layout-and-choreography.md:在設計多節點拓撲、正交匯流排/肘節路由、動態計數器、雙向箭頭或多場景ffmpeg串接時閱讀。references/voiceover-and-commentary.md:在編寫旁白清單、自然敘述者語調指示、音標發音詞彙表、EBU R128 正規化或雙語旁白音軌時閱讀。
1. 分鏡與多幕敘事設計
在編寫 Manim 程式碼之前,將解釋結構化為離散的幕與逐個節拍的分鏡表。每個「幕」保留一個 Scene 類別,這樣編輯第二幕永遠不需要重新渲染第一幕或第三幕:
- 第一幕 — 設定與請求准入:介紹拓撲、不變量與入口守衛(例如
客戶端 -> API 閘道 -> 令牌桶速率限制器)。 - 第二幕 — 穩定狀態執行:追蹤主要資料路徑與狀態轉換(例如
讀取快取單次飛行查找 ->主資料庫填充 -> TTL 寫回)。 - 第三幕 — 故障模式與復原:展示當限制觸發或節點故障時會發生什麼(例如
HTTP 429突發拒絕或分散式鎖定與預寫式日誌複製法定人數提升)。
在編碼前明確映射每個節拍:
beat_id | visual_action | min_visual_sec | commentary_script (若需旁白) |
|---|---|---|---|
act1_ingress | 渲染 4 個 Nord 拓撲卡片 + 標題;將請求封包動畫化進入閘道 | 3.2 | 入站寫入在接觸有狀態儲存之前,會先通過邊緣閘道。 |
act1_bucket | 令牌計數器遞減 100 -> 99;閃爍綠色 已准入 徽章 | 3.8 | 一個原子化的令牌桶檢查可吸收流量突發,同時限制下游並發量。 |
act1_reject | 當令牌歸 0 時,觸發 animate_blocked_pulse(429 已限流) | 3.5 | 一旦突發容量耗盡,多餘的呼叫者會快速失敗並回傳 HTTP 429,而非無限排隊。 |
2. 無需 LaTeX 的 Manim Community 創作
隔離環境設定
在隔離的虛擬環境中安裝 Manim(以及可選的 piper-tts);確認 ffmpeg 和 ffprobe 已在 PATH 中:
uv venv venv
uv pip install --python venv/bin/python manim piper-tts
嚴格的無 LaTeX 與正交示意圖規則
- 零 TeX 子程序:永遠不要使用
Tex、MathTex、Title或DecimalNumber(這些會呼叫pdflatex/dvisvgm)。請使用Text、MarkupText(<b>、<i>、<sub>、<sup>、<span foreground="...">)、Code、RoundedRectangle、Arrow、DashedVMobject和MoveAlongPath。 - 黃金示意圖規則(連接埠錨定的正交導線):在建構架構方塊圖時,絕對不要直接在兩個
Mobject上呼叫Arrow(card_a, card_b)—Manim 會對齊邊界框中心射線,並在卡片寬度或 Y 位置不同時產生歪斜的對角線箭頭。請透過connect_h(card_from, card_to, y=...)、connect_v(card_from, card_to, x=...)或connect_elbow(points)路由每條邊,使用animate_packet_along(self, edge, ...)動畫化封包,並使用assert_all_orthogonal(connectors, cards)驗證導線幾何形狀。 - 安全框座標網格:Manim 的 16:9 畫布範圍為
[-7.11, 7.11] x [-4.0, 4.0]。將所有創作的內容保持在[-6.6, 6.6] x [-3.5, 3.5]內,並在顯示 WebVTT 字幕時,保持y < -3.05為空白。 - 匯入
assets/nord_manim_theme.py:設定self.camera.background_color = BG_DARK,並一致地建構卡片、正交連接器、封包動畫與節拍保持:
from pathlib import Path
import sys
SKILL_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(SKILL_DIR))
sys.path.insert(0, str(SKILL_DIR / "assets"))
from manim import Create, FadeIn, Scene, VGroup
from check_scene_bounds import dump_checkpoints
from nord_manim_theme import (
ACCENT_BLUE, ACCENT_CYAN, BG_DARK, STATUS_ERR, STATUS_OK,
BeatTimer, animate_blocked_pulse, animate_packet_along,
assert_all_orthogonal, connect_h, make_header_banner, make_node_card,
)
class Act1RateLimiter(Scene):
def construct(self) -> None:
self.camera.background_color = BG_DARK
timer = BeatTimer("tmp/beat_timings.json")
checkpoints: list[dict] = []
header = make_header_banner(
"第一幕 / 請求准入",
"API 閘道與令牌桶速率限制器",
)
gw = make_node_card("API 閘道", "TLS + 路由匹配", width=2.5, accent=ACCENT_CYAN).move_to([-4.5, 0.2, 0])
limiter = make_node_card("速率限制器", "桶: 100 tok", width=2.5, accent=ACCENT_BLUE).move_to([-1.5, 0.2, 0])
cache = make_node_card("讀取快取", "單次飛行 TTL", width=2.5, accent=ACCENT_CYAN).move_to([1.5, 0.2, 0])
db = make_node_card("主資料庫", "WAL 法定人數", width=2.5, accent=STATUS_OK).move_to([4.5, 0.2, 0])
e_gw_lim = connect_h(gw, limiter)
e_lim_cache = connect_h(limiter, cache)
e_cache_db = connect_h(cache, db, dashed=True)
assert_all_orthogonal([e_gw_lim, e_lim_cache, e_cache_db], [gw, limiter, cache, db])
edges = VGroup(e_gw_lim, e_lim_cache, e_cache_db)
self.play(FadeIn(header), FadeIn(gw), FadeIn(limiter), FadeIn(cache), FadeIn(db), Create(edges), run_time=1.1)
spent = animate_packet_along(self, e_gw_lim, label="POST /orders", run_time=0.7)
timer.hold(self, "act1_ingress", consumed_anim_s=1.1 + spent, fallback_s=3.2, checkpoints=checkpoints)
spent = animate_packet_along(self, e_lim_cache, label="token=99", color=STATUS_OK, run_time=0.7)
timer.hold(self, "act1_bucket", consumed_anim_s=spent, fallback_s=3.8, checkpoints=checkpoints)
spent = animate_blocked_pulse(self, limiter, reason_label="429 已限流", color=STATUS_ERR, run_time=0.95)
timer.hold(self, "act1_reject", consumed_anim_s=spent, fallback_s=3.5, checkpoints=checkpoints)
dump_checkpoints(checkpoints, "tmp/act1_bounds.json")
在 1080p60 渲染前驗證邊界與零碰撞
在啟動完整 1080p60 渲染之前,務必在幾秒內驗證佈局幾何形狀:
# 選項 A:在每個 Scene.wait() 呼叫上進行即時空跑鉤子(< 3 秒,零 MP4 編碼)
./venv/bin/python .agents/skills/manim-video-explainer/check_scene_bounds.py \
--module scenes.py --scene Act1RateLimiter
# 選項 B:在快速 -ql 渲染後檢查輸出的檢查點 JSON
python3 .agents/skills/manim-video-explainer/check_scene_bounds.py \
--json tmp/act1_bounds.json
請閱讀 references/manim-layout-and-choreography.md 以了解多層座標模板、動態 ValueTracker 計數器與防碰撞清理規則。
3. 可選的節拍同步旁白註解(mux_narration.py)
黃金旁白規則
絕對不要逐字朗讀螢幕上的方塊標題、箭頭或徽章文字。
觀看者已經可以看到 "API 閘道 -> 速率限制器"。請編寫解釋性的流程註解,說明為什麼每個轉換會發生、哪個不變量保護了系統,以及什麼原因導致了拒絕或復原。
節拍優先的兩遍工作流程
- 編寫
narration_manifest.json,將乾淨的字幕text與音標覆寫(tts_phonetic_text或phonetic_glossary)分開,這樣技術縮寫(WAL、gRPC、TTL、p99、HTTP 429)就能以自然的方式發音,而不會污染.vtt字幕:
{
"lead_in_s": 0.20,
"inter_beat_gap_s": 0.25,
"phonetic_glossary": {
"WAL": "預寫式日誌",
"TTL": "T T L",
"HTTP 429": "H T T P 四二九"
},
"scenes": [
{
"id": "act1",
"beats": [
{
"id": "act1_ingress",
"min_visual_s": 3.2,
"text": "入站寫入在接觸有狀態儲存之前,會先終止於閘道。"
},
{
"id": "act1_bucket",
"min_visual_s": 3.8,
"text": "一個原子化的令牌桶檢查可吸收突發流量,同時限制資料庫並發量。"
},
{
"id": "act1_reject",
"min_visual_s": 3.5,
"text": "當突發令牌耗盡至零時,多餘的呼叫者會快速失敗並回傳 HTTP 429。"
}
]
}
]
}
- 第一遍 — 合成逐個節拍的 WAV 並輸出精確時間碼:
# 選項 A:離線 Piper ONNX 神經網路 TTS(零網路呼叫)
python3 .agents/skills/manim-video-explainer/mux_narration.py \
--manifest tmp/narration_manifest.json \
--scene-id act1 \
--wav-dir cache/beats_en \
--piper-bin ./venv/bin/piper \
--model ~/.cache/piper-voices/en_US-lessac-medium.onnx \
--emit-timings tmp/beat_timings.json
# 選項 B:相容 OpenAI 的 HTTP 神經網路 TTS(/v1/audio/speech,stdlib urllib)
python3 .agents/skills/manim-video-explainer/mux_narration.py \
--manifest tmp/narration_manifest.json \
--scene-id act1 \
--wav-dir cache/beats_en \
--http-tts-url https://api.openai.com/v1/audio/speech \
--http-tts-key-env OPENAI_API_KEY \
--http-tts-model gpt-4o-mini-tts \
--http-tts-voice cedar \
--http-tts-instructions "冷靜、清晰的技術紀錄片式敘述者;在子句處短暫停頓。" \
--emit-timings tmp/beat_timings.json
(也可以透過 --tts-cmd "my-tts --out {wav} --text {text}" 使用任何自訂 CLI 合成器,或直接將預渲染的 .wav 檔案放入 --wav-dir。)
- 根據
tmp/beat_timings.json驅動的 1080p60 Manim 影片渲染:
./venv/bin/manim -qh --fps 60 scenes.py Act1RateLimiter
- 第二遍 — 樣本精確的主音軌組裝、響度正規化、尾部保護與 AAC 混流:
python3 .agents/skills/manim-video-explainer/mux_narration.py \
--manifest tmp/narration_manifest.json \
--scene-id act1 \
--wav-dir cache/beats_en \
--video media/videos/scenes/1080p60/Act1RateLimiter.mp4 \
--loudnorm-lufs -18.0 \
--output output/act1_en.mp4 \
--vtt output/act1_en.vtt
mux_narration.py 會將每個節拍的 WAV 放置在 16 位元 PCM 主音軌的 round(start_s * sample_rate) 位置,每當最終語音超過影片持續時間時,便透過 tpad=stop_mode=clone 擴展最後一幀影片(零截詞),可選應用 EBU R128 響度正規化(--loudnorm-lufs -18.0)以使音量在各章節間保持一致,編碼 -c:a aac -b:a 160k -movflags +faststart,並寫入 output/act1_en.vtt。
請閱讀 references/voiceover-and-commentary.md 以了解好壞旁白轉換、非英語音標覆寫以及雙語音訊工作流程。
4. 網路編碼、多場景串接與離線 HTML5 播放器
網路相容編碼與主捲帶串接
對於無聲導覽(不使用 mux_narration.py),確保通用的瀏覽器/QuickTime 相容性(yuv420p + 移動的 moov 原子):
ffmpeg -y -i media/videos/scenes/1080p60/Act1RateLimiter.mp4 \
-c:v libx264 -pix_fmt yuv420p -crf 23 -movflags +faststart \
output/act1_silent.mp4
若要將多個帶旁白的幕 MP4 組裝成連續的主捲帶:
printf "file 'act1_en.mp4'\nfile 'act2_en.mp4'\nfile 'act3_en.mp4'\n" > output/concat.txt
ffmpeg -y -f concat -safe 0 -i output/concat.txt \
-c:v libx264 -pix_fmt yuv420p -crf 21 -c:a aac -b:a 160k -movflags +faststart \
output/master_en.mp4
可選的獨立 HTML5 Cinema 播放器
在交付多章節導覽或雙語解說套件時:
- 將
assets/video_player_template.html複製到output/index.html。 - 自訂
PLAYER_CONFIG(chapters、sources和markers,使其與beat_timings.json中的節拍時間戳記匹配)。如果只交付單一語言,請隱藏或省略#langGroup中的次要語言按鈕。 - 將所有樣式、單色 SVG 圖示和腳本 100% 內聯,以便套件無需外部 CDN 即可離線運作。
- HTTP
206 Partial Content提供注意事項:瀏覽器需要 HTTP 位元組範圍(206)回應才能擦洗<video>時間軸並乾淨地載入<track>.vtt檔案(file://會透過 CORS 封鎖 WebVTT 軌道,而 Python 的基本http.server可能無法正確提供範圍請求)。
輸出套件驗證
交付前驗證所有成品:
python3 .agents/skills/manim-video-explainer/lint.py --bundle output/
lint.py 確認每個 MP4 串流符合 H.264 yuv420p 規範並包含預期的音訊串流、WebVTT 標頭與提示順序單調,以及 HTML5 播放器 index.html 零 CDN、零表情符號且符合 CSP 規範。
5. 常見問題與專案結構
- 每個場景的最大持續時間?
mux_narration.py對影片長度沒有限制。每秒 60 幀的 5 分鐘場景在磁碟上約為 1.5–2 GB(取決於複雜度)。對於較長的敘述,請透過多個Scene子類別拆分成多個幕,並在最後使用ffmpeg concat串接。 - Piper 模型從哪裡獲得? 從 rhasspy/piper-voices 下載您的語言和語音的
.onnx和.json檔案,放入~/.cache/piper-voices/。 - 可以對同一影片使用多個語音嗎? 可以。對同一個場景以不同語音執行兩次
mux_narration.py,並混合--wav-dir目錄。 - 如何建立雙語字幕? 為每種語言運行
mux_narration.py以產生第二個.vtt,然後手動編輯assets/video_player_template.html以包含<track kind="subtitles" src="..." srclang="...">。
建議的專案結構
project/
├── venv/ # 隔離的 Python 虛擬環境
├── scenes.py # 多個 Act 類別(Act1, Act2, ...)
├── tmp/
│ ├── narration_manifest.json # 旁白清單
│ ├── beat_timings.json # 從第一遍輸出
│ └── act1_bounds.json # 檢查點佈局轉儲
├── cache/
│ └── beats_en/ # 第一遍 WAV 檔案
├── media/videos/scenes/1080p60/ # Manim 渲染輸出
├── output/
│ ├── act1_en.mp4 # 第二遍混合 MP4
│ ├── act1_en.vtt # WebVTT 字幕
│ ├── master_en.mp4 # 串接的主捲帶
│ └── index.html # 自訂播放器
└── .agents/skills/manim-video-explainer/ # 此技能(符號連結或複製)