manim-engine
A minimal, aesthetic CLI engine that turns a JSON content plan into a verified physics education video rendered with Manim. It is built for agents — an AI agent authors the plan (content, math, layout, expected visuals), the engine does the mechanical work (render, assemble, verify), and returns a machine-readable report with an exit code so the agent never has to eyeball frames.
$ manim-engine doctor
$ manim-engine plan examples/ring-disc/plan.json
$ manim-engine build examples/ring-disc/plan.json --draft # fast iteration
$ manim-engine build examples/ring-disc/plan.json # final 1080p60
$ manim-engine verify .engine/ring-disc.mp4 examples/ring-disc/plan.json
manim-engine is a single command installed on PATH — no absolute paths, no working-directory requirements; run it from anywhere. Exit codes: 0 = everything ok (including all frame checks), 1 = any error or failed check. Append --json for a single JSON object on stdout.
1. What this engine is (and is not)
Is: a deterministic pipeline plan → validate → per-segment manim render → concat → frame verification → report. Content correctness lives in the plan, which the agent verifies before rendering (the ledger field is the place to record every symbol, value and formula used). Rendering and checking are mechanical and repeatable.
Is not: a Manim IDE, a physics solver, or a generic video editor. It produces educational/physics-style videos: title cards, question statements, equations, animated diagrams, boxed answers. If a segment needs bespoke animation (a race, a force diagram), it is supplied as a plain Manim Scene class — see Custom scenes.
Why verification matters: agents cannot reliably "watch" a 74-second video, but they can compare numeric pixel facts against expectations they calibrated themselves: "at the end of the race scene the blue disc centroid must be within 12 px of (919, 857)", "the yellow ring must occupy ≥ 400 pixels there". build fails (exit 1) if any check fails, so an agent gets feedback it can actually act on.
Reference example: examples/ring-disc/ — JEE Advanced 2018, ring vs disc rolling down a 60° incline. The physics was SymPy-verified first, then rendered as a 74.4 s, 1920×1080@60 video with 4 frame checks, all passing.
2. Quick start
Install (one command)
Windows:
irm https://<host>/install.ps1 | iex
macOS / Linux:
curl -fsSL https://<host>/install.sh | sh
That one command downloads the engine, creates an isolated .venv, installs every Python dependency (manim, numpy, a bundled static ffmpeg via imageio-ffmpeg), and auto-installs missing system tools (ffmpeg / LaTeX) via winget / brew / apt — then runs doctor to prove it works. Prefer uv for speed if present.
Alternatively, without hosting, anyone with the repo can run the same script locally:
# from the repo root
sh install.sh # macOS / Linux
powershell -ExecutionPolicy Bypass -File .\install.ps1 # Windows
Or do it manually — all Python packages come from one command:
pip install . # or: uv pip install . | pipx install . | uv tool install .
manim-engine doctor --fix # installs ffmpeg + LaTeX via winget/brew/apt if missing
Only LaTeX (for math rendering) is a system binary that pip/uv cannot ship — doctor --fix handles it. After any system-tool install, open a new terminal so PATH updates take effect.
Use
# 1. environment check (add --fix to auto-install missing tools)
manim-engine doctor --fix
# 2. write a plan skeleton and fill it in
manim-engine new my-plan.json
# 3. validate + summarize
manim-engine plan my-plan.json
# 4. iterate fast: half resolution, 15 fps (~5x faster)
manim-engine build my-plan.json --draft
# 5. final quality
manim-engine build my-plan.json -o out/my-video.mp4
# 6. re-verify any built video against the plan
manim-engine --json verify out/my-video.mp4 my-plan.json
--json is a global flag — it goes before the subcommand (manim-engine --json build ...). Without installation, the same commands work from the repo root as python engine.py ... or python -m engine ....
3. The agent workflow (the intended loop)
- Plan the content (agent-side, off-engine): derive every formula and value, and record them in the plan's
ledger. For the ring-disc video this was done with SymPy before any rendering. - Author
plan.json: segments in order; per-segment JSON fields control timing, colors, and layout. Useexpected_durationas your pacing budget; treat it as a plan — the build reports actuals. - Calibrate checks (agent-side, once): build with
--draft, extract frames at interesting moments (engine exports a frame-extraction helper,engine.verify._extract_rgb_frame), measure the blue-centroid / yellow-count / density you expect, and encode those numbers ascheckswith segment-relative times. - Run the loop:
build --draft→ fix plan/custom scenes until exit 0 →buildat full quality →verifyto confirm. - Consume the report:
--jsongives{ok, video, duration, segments[{id,duration}], checks[{at,type,ok,measured,expected}]}.
How an agent invokes it
manim-engine is a plain console command on PATH, so a calling agent just shells out — no absolute paths, no working directory requirements:
manim-engine build path\to\plan.json --json # exit 0/1 + JSON report on stdout
manim-engine verify video.mp4 plan.json --json # re-verify after the fact
A plan directory is self-contained and portable: segment content, custom scene modules, and checks all resolve relative to the plan file (absolute paths also accepted). Share plan.json + its custom scenes and anyone can rebuild the identical video.
4. Plan schema (complete reference)
{
"id": "ring-disc", // required, non-empty; used for file naming
"title": "JEE Advanced 2018 — Ring vs Disc on an Incline",
"resolution": [1920, 1080], // optional, default [1920, 1080]
"fps": 60, // optional, default 60
"style": { // optional; named/hex colors, see §4.1
"heading": "yellow", "accent": "blue", "ink": "white",
"muted": "gray", "green": "green", "orange": "orange", "red": "red"
},
"ledger": [ // optional; documented facts (no render effect)
{ "symbol": "a_ring", "meaning": "ring acceleration",
"rendered": "(g sin theta)/2 = 5√3/2 ≈ 4.33" }
],
"checks": [ // optional; frame checks, see §6
{ "seg": "race", "at": 9.1, "type": "centroid",
"color": "blue", "expected": [919, 857], "tol": 12 },
{ "seg": "race", "at": 9.1, "type": "count",
"color": "yellow", "min_count": 400 }
],
"segments": [ /* see §5 */ ]
}
4.1 Style and colors
style keys: heading, accent, ink, muted, green, orange, red (defaults: yellow, blue, white, gray, green, orange, red). Values are named colors (yellow, blue, white, gray/grey, green, orange, red), #RRGGBB hex strings, or raw manim color objects in custom scenes. Individual segments/lines may override with their own color fields.
5. Segment kinds
5.1 title — heading + subtitle lines
{
"id": "title", "kind": "title",
"lines": [
{ "role": "heading", "text": "JEE ADVANCED 2018", "size": 64 },
{ "role": "sub", "text": "Paper 1 · Physics · Q9 (Numerical)", "size": 34 }
],
"heading_color": "yellow", // optional
"sub_color": "gray", // optional
"final_wait": 2.0 // optional, default 2.0
}
Heading is Writen over 3 s; each sub-line FadeIns with an upward shift.
5.2 text — sequential blocks (question statements, narration lines)
{
"id": "question", "kind": "text",
"blocks": [
{ "text": "A ring and a disc are initially at rest, side by side,\nat the top of an inclined plane that makes an angle 60°\nwith the horizontal.",
"run_time": 1.6, "pause": 0.8 },
{ "text": "If the time difference between their reaching the ground is",
"run_time": 1.6, "pause": 1.2,
"with": [ { "kind": "math", "latex": "\\frac{2-\\sqrt{3}}{\\sqrt{10}}", "size": 38 } ] }
],
"top_buff": 0.7, // optional
"block_buff": 0.9, // optional, spacing between blocks
"final_wait": 1.5 // optional, default 1.5
}
\n inside text makes newlines. with attaches extra mobjects (inline math, e.g. the value of a fraction) to a block; inline items may set kind, latex/text, color, size.
5.3 math — stacked, left-aligned equations
{
"id": "kinematics", "kind": "math",
"note": { "text": "Kinematics", "size": 28 }, // optional top caption
"lines": [
{ "latex": "s = \\frac{h}{\\sin\\theta}, \\qquad t = \\sqrt{\\frac{2s}{a}}",
"run_time": 1.0, "pause_after": 1.0 },
{ "latex": "t_{\\text{ring}} = \\sqrt{\\frac{16h}{3g}}",
"color": "yellow", "run_time": 1.0 },
{ "latex": "t_{\\text{disc}} = \\sqrt{\\frac{4h}{g}}", "color": "blue",
"run_time": 1.0, "pause_after": 1.5,
"with": [ { "kind": "math", "latex": "\\left(\\sin^2 60^\\circ = \\frac{3}{4}\\right)" } ] }
],
"top_buff": 1.0, // optional, default 1.0
"line_buff": 0.6, // optional, default 0.6
"final_wait": 0.0 // optional, default 2.0
}
Each line is Writen at run_time (default 1.0), then the video pauses pause_after (default 0). Use with for side annotations rendered next to a line.
5.4 answer — equation stack + boxed answer + takeaway
{
"id": "answer", "kind": "answer",
"lines": [
{ "latex": "(2-\\sqrt{3})\\sqrt{\\frac{2h}{15}} = \\frac{2-\\sqrt{3}}{\\sqrt{10}}" },
{ "latex": "\\sqrt{\\frac{2h}{15}} = \\frac{1}{\\sqrt{10}} \\;\\Rightarrow\\; h = \\frac{3}{4}" }
],
"answer": { "latex": "h = 0.75\\ \\text{m}", "color": "yellow" },
"takeaway": "The disc wins — less rotational inertia means faster translation.",
"top_buff": 1.4, // optional, default 1.4
"final_wait": 2.5 // optional, default 2.5
}
Lines are Writen (1.0 s each + 1.2 s pause); the answer is Writen and enclosed in a SurroundingRectangle; the takeaway fades in at the bottom.
5.5 custom — your own Manim scene
{
"id": "race", "kind": "custom",
"module": "race_scene.py", // path relative to the plan dir (or absolute)
"class": "RaceScene",
"expected_duration": 18.6
}
The engine loads the file, instantiates the class, and renders it like any other segment (it participates in concat and checks). See §7 for the scene contract and known Manim-0.20.1 gotchas.
6. Checks (frame verification)
Checks run against the final concatenated video at times that are seg-relative and resolved to absolute times using the actual rendered segment durations (from the build manifest; standalone verify falls back to expected_duration if no manifest exists next to the video). All checks are resolution-scaled, so the same plan works at draft (960×540) and full (1920×1080) resolution.
| type | fields | semantics |
|---|---|---|
density |
min_fraction (default 0.001) |
fraction of frame pixels darker than luminance 140 must be ≥ min_fraction. Catches "empty/black scene" regressions. |
centroid |
color (default blue), expected [x, y] at plan resolution, tol (default 10) |
centroid of all pixels of that color within tol px (both scaled to actual resolution). Verifies where something is. |
count |
color (default yellow), min_count |
number of pixels of that color (reported as full-resolution-equivalent) must be ≥ min_count. Verifies that something is present. |
Color classifiers (validated on the reference video; the same thresholds power _RULES in engine/verify.py):
| color | predicate (r, g, b) |
|---|---|
| yellow | r>190, g>190, b<120 |
| blue | b>140, r<110 |
| red | r>180, g<110, b<110 |
| orange | r>200, 90<g<180, b<80 |
| green | g>170, g>r+40, g>b+40 |
| white | r>235, g>235, b>235 |
Density sampling: every 2nd pixel on both axes (1/4 of the frame); count/centroid use the same sample grid but counts are scaled back to full-resolution equivalents.
Calibration workflow (how an agent sets expected/tol/min_count)
- Render with
--draft. - Extract the frame at the interesting moment (e.g.
engine.verify._extract_rgb_frame(video, t)returns(w, h, rgb_bytes)), or viaffmpeg -ss T -i video.mp4 -frames:v 1 out.png. - Measure the centroid/count with
_frame_stats(w, h, data)["px"]["blue"]etc. - Encode the measured value (at plan resolution) plus a sensible tolerance, and the count threshold (e.g. 400 for a fully-visible ring), into the plan.
Reference values from examples/ring-disc/plan.json (all currently passing): disc centroid at race end measured (918.5, 857.2) vs expected (919, 857), tol 12; ring yellow count 908 vs min 400.
7. Custom scenes contract
- The file is a plain Manim module.
class RaceScene(Scene)withconstruct(self). - The engine imports the class by name and renders it with
manim --disable_caching --media_dir <work>/media/<seg> --resolution W,H --fps N <generated> <ClassName>Scene. - Use the plan
stylecolors if you want consistency:from engine.style import resolve_style/resolve_color, or hardcode manim colors (the examples do). - The full frame is 1920×1080 px; Manim's default frame height is 8 scene units → 135 px per unit, width 14.22 units, origin at screen center. Screen pixel:
px_x = (scene_x + 7.11) * 135,px_y = (3.98 - scene_y) * 135.
Manim 0.20.1 gotchas (all already fixed inside the examples)
ACCENTis not exported — useBLUE.- Mobjects have no
.create— useCreate(mobj). wait(0)raises — guard every wait withif duration > 0.- Remember
Write,LEFT, etc. imports; the engine's generated files only import what the builders need.
8. Pipeline internals
build:
load plan (validated) engine/plan.py
└ per segment:
generate_segment → <work>/generated/<id>.py engine/render.py
- standard kinds: subclass of builder_for(seg), SEGMENT = raw JSON
- custom: subclass of load_custom(module, class)
render_segment → manim subprocess → media dir → latest mtime mp4
concat → ffmpeg -f concat -safe 0 -c copy (stream copy, same codec)
write <work>/<id>.manifest.json (actual per-segment durations)
checks → resolved to absolute times → per-check results
report → human lines or --json doc engine.py
Layout under the plan directory (defaults): <plan_dir>/.engine/ for generated code and media, .engine/<id>.mp4 for the video, .engine/<id>.manifest.json for durations.
Performance (measured, ring-disc example)
| mode | resolution | fps | render time (6 segments, 74.4 s video) |
|---|---|---|---|
--draft |
960×540 | 15 | ~23 s |
| full | 1920×1080 | 60 | ~77 s |
9. Known limitations / roadmap
MathTexrequires a working LaTeX (pdflatex) installation.- Frame checks sample 1/4 of pixels; fine for large marks, weak for 1–2 px details (they are still detected — the ring's 8 px stroke counts 908 px full-res).
- No audio track yet — narration/TTS, subtitles, and a segment-kind catalog are natural next steps.
expected_durationis a planning aid, not enforced; if you need hard pacing, tighten the final report comparison (engine reports both planned and actual).- LaTeX (pdflatex) is required for
MathTexand cannot be installed by pip/uv — usemanim-engine doctor --fix(or the bootstrap scripts) which install it via winget/brew/apt.
10. Repository map
manim-engine/
├── engine.py thin shim: `python engine.py ...` works without install
├── install.sh/.ps1 one-command installers (curl | sh / irm | iex)
├── bootstrap.ps1/.sh scripted venv + deps + ffmpeg + LaTeX setup
├── pyproject.toml package metadata + `manim-engine` console command
├── engine/
│ ├── cli.py CLI entry: doctor(--fix) | plan | new | build | verify, --json, exit codes
│ ├── plan.py plan loading + validation
│ ├── style.py named/hex color resolution
│ ├── tools.py ffmpeg/ffprobe resolution (PATH → imageio-ffmpeg fallback), duration/dims probing
│ ├── scenes.py TitleBuilder, TextBuilder, MathBuilder, AnswerBuilder, load_custom
│ ├── render.py generate_segment, render_segment (manim), concat (ffmpeg)
│ └── verify.py frame extraction, _frame_stats, run_check
└── examples/ring-disc/
├── plan.json the reference plan (6 segments, ledger, 4 checks)
├── race_scene.py custom scene: ring vs disc race + geometry annotations
└── accel_scene.py custom scene: accelerations + force diagram
The reference physics facts (SymPy-verified, recorded in the ledger): a_ring = (g sinθ)/2 = 5√3/2 ≈ 4.330, a_disc = (2/3) g sinθ = 10√3/3 ≈ 5.774, t_ring = √(16h/3g), t_disc = √(4h/g), time difference (2−√3)√(2h/15), answer h = 3/4 = 0.75 m.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file manim_engine-0.1.0.tar.gz.
File metadata
- Download URL: manim_engine-0.1.0.tar.gz
- Upload date:
- Size: 31.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b534e61fe775e8a8d068b65e61406273476d01015647eb8196c8f7cb57d6c77
|
|
| MD5 |
ccb8dee59ea0530f357059a99233dddf
|
|
| BLAKE2b-256 |
015ef7623567072f068e3119547f50a7c6d1424aac712c6b25573d2d344ea7fd
|
File details
Details for the file manim_engine-0.1.0-py3-none-any.whl.
File metadata
- Download URL: manim_engine-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83f509e8055a1594ed00ab4e4f10f8717f75b00035670ecb21b316bf0721ae27
|
|
| MD5 |
24f6501a2059e6297d36d88577e12953
|
|
| BLAKE2b-256 |
77236fe0c9750473957e5b48a9e7a09aa38871d1fc8b8d5df4381cf224ec1ab1
|