Communitygithub.com

manim-video-explainer

使用 Manim Community(无 LaTeX 依赖)渲染高清 1080p60 技术讲解视频、分步系统演示和多幕架构动画,支持可选的节拍同步神经 TTS 画外音(离线 Piper、兼容 OpenAI 的 HTTP TTS 或 CLI TTS)、EBU R128 响度归一化、WebVTT 字幕以及可选的独立 HTML5 章节播放器。

manim-video-explainer 是什么?

该技能旨在帮助 AI 智能体(如 Claude Code、Cursor 等)使用 Manim Community 库创建专业级、1080p60 的技术讲解视频。它提供了一个完整的系统,用于为视频设计故事板和编写分步动画脚本,特别是系统架构、数据处理流程和故障恢复场景。其核心优势在于严格遵循“无 LaTeX”和“正交端口连接器”规则,从而确保生成清晰的架构图,避免手动绘制连接线时出现的歪斜对角线。除了渲染视频外,该技能还通过 `mux_narration.py` 脚本集成了一个强大的画外音系统。它支持各种文本转语音引擎,包括离线 Piper ONNX 模型、任何兼容 OpenAI 的 `/v1/audio/speech` API 或自定义 CLI TTS。该智能体可以创建与视频节奏精确同步的画外音,应用 EBU R128 响度归一化,并生成 WebVTT 字幕。输出结果可以进一步汇编成包含多个章节的视频集,并搭配一个完全离线的、基于 HTML5 的“影院播放器”,从而实现无需外部 CDN 或服务器的即插即用交付。

兼容平台~Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/iuriimedvedev-dev/manim-video-explainer-skill/tree/HEAD

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

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 影院播放器,具有章节选项卡、可点击的节拍标记、播放速率(0.75x..2.0x)以及可选的切换音频/字幕功能。
  • check_scene_bounds.py:预渲染布局验证器,检查 Manim 场景或 JSON 检查点是否存在 [-6.8, 6.8] x [-3.75, 3.75] 帧溢出以及同级文本标签碰撞。
  • mux_narration.py:基于 Stdlib 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.264 yuv420p + 音频流、WebVTT 头部/提示单调性以及离线 HTML5 播放器 CSP/零 CDN/零表情符号不变性。
  • references/manim-layout-and-choreography.md:在设计多节点拓扑、正交总线/弯头路由、动态计数器、双向箭头或多场景 ffmpeg 拼接时阅读。
  • references/voiceover-and-commentary.md:在创作旁白清单、自然叙述者韵律指令、音标发音词汇表、EBU R128 归一化或双语画外音轨道时阅读。

1. 故事板与多幕叙事设计

在编写 Manim 代码之前,将讲解内容结构化为离散的幕和逐节拍的故事板表格。每个“幕”保留一个 Scene 类,这样对第 2 幕的编辑就永远不需要重新渲染第 1 幕或第 3 幕:

  • 第 1 幕 —— 设置与请求准入:介绍拓扑结构、不变量和入口守卫(例如,客户端 -> API 网关 -> 令牌桶速率限制器)。
  • 第 2 幕 —— 稳态执行:跟踪主要数据路径和状态转换(例如,读穿透缓存 单次飞行查找 -> 主数据库 持久化 -> TTL 写回)。
  • 第 3 幕 —— 故障模式与恢复:演示当限制触发或节点失败时会发生什么(例如,HTTP 429 突发拒绝或 分布式锁与预写日志复制 法定人数提升)。

在编码前明确映射每一个节拍:

beat_idvisual_actionmin_visual_seccommentary_script (如果配有旁白)
act1_ingress渲染 4 张 Nord 拓扑卡片 + 标题;将请求包动画化为进入网关3.2入站写入在接触有状态存储之前要经过边缘网关。
act1_bucket将令牌计数器从 100 递减到 99;闪烁绿色 ADMITTED 徽章3.8原子令牌桶检查在吸收流量突发的同时限制了下游并发。
act1_reject当令牌达到 0 时触发 animate_blocked_pulse(429 RATE LIMITED)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 与正交示意图规则

  1. 零 TeX 子进程:永远不要使用 Tex、MathTex、Title 或 DecimalNumber(它们会调用 pdflatex/dvisvgm)。请使用 Text、MarkupText(<b>、<i>、<sub>、<sup>、<span foreground="...">)、Code、RoundedRectangle、Arrow、DashedVMobject 和 MoveAlongPath。
  2. 黄金示意图规则(端口锚定正交线):在构建架构框图时,永远不要直接在两个 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) 验证连线几何形状。
  3. 安全框坐标网格: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 区域为空。
  4. 导入 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(
            "Act 1 / Request Admission",
            "API Gateway & Token-Bucket Rate Limiter",
        )
        gw = make_node_card("API Gateway", "TLS + Route Match", width=2.5, accent=ACCENT_CYAN).move_to([-4.5, 0.2, 0])
        limiter = make_node_card("Rate Limiter", "Bucket: 100 tok", width=2.5, accent=ACCENT_BLUE).move_to([-1.5, 0.2, 0])
        cache = make_node_card("Read Cache", "Single-Flight TTL", width=2.5, accent=ACCENT_CYAN).move_to([1.5, 0.2, 0])
        db = make_node_card("Primary DB", "WAL Quorum", 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 RATE LIMITED", 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 Gateway -> Rate Limiter"。请编写解释性过程评论,说明为什么每个转换会发生、什么不变量保护系统以及什么原因导致拒绝或恢复。

先节拍后声音的两遍工作流

  1. 编写 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": "write-ahead log",
    "TTL": "T T L",
    "HTTP 429": "H T T P four twenty-nine"
  },
  "scenes": [
    {
      "id": "act1",
      "beats": [
        {
          "id": "act1_ingress",
          "min_visual_s": 3.2,
          "text": "Inbound writes terminate at the gateway before touching stateful storage."
        },
        {
          "id": "act1_bucket",
          "min_visual_s": 3.8,
          "text": "An atomic token-bucket check absorbs burst traffic while bounding database concurrency."
        },
        {
          "id": "act1_reject",
          "min_visual_s": 3.5,
          "text": "When burst tokens drain to zero, excess callers fail fast with HTTP 429."
        }
      ]
    }
  ]
}
  1. 第 1 遍 —— 合成逐节拍 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 "Calm, clear technical documentary narrator; brief pauses at clauses." \
  --emit-timings tmp/beat_timings.json

(任何自定义 CLI 合成器也可以通过 --tts-cmd "my-tts --out {wav} --text {text}" 使用,或者预渲染的 .wav 文件可以直接放置在 --wav-dir 中。)

  1. 渲染由 tmp/beat_timings.json 驱动的 1080p60 Manim 视频:
./venv/bin/manim -qh --fps 60 scenes.py Act1RateLimiter
  1. 第 2 遍 —— 采样级精确主轨道组装、响度归一化、尾部保护与 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 放置在 round(start_s * sample_rate) 处的 16 位 PCM 主轨道上,每当最终语音超过视频时长时,通过 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 atoms):

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 影院播放器

在交付多章节演示或双语讲解包时:

  1. 将 assets/video_player_template.html 复制到 output/index.html。
  2. 自定义 PLAYER_CONFIG(chapters、sources 和匹配 beat_timings.json 中节拍时间戳的 markers)。如果只交付单一语言,隐藏或省略 #langGroup 中的次要语言按钮。
  3. 保持所有样式、单色 SVG 图标和脚本 100% 内联,以便该包无需外部 CDN 即可离线工作。
  4. HTTP 206 Partial Content 服务说明:浏览器需要 HTTP 字节范围(206)响应才能正常搜索 <video> 时间线并加载 <track> .vtt 文件(file:// 会通过 CORS 阻止 WebVTT 轨道,而 Python 的基本 http.server 在没有额外配置的情况下也不支持字节范围请求)。可以将播放器部署到任何支持静态文件的 Web 服务器,或使用 npm 包 http-server 或 serve 等工具进行测试。

相关技能