manimotion
The official Python SDK for manimotion: send a topic, get back a narrated video lecture.
from manimotion import Manimotion
client = Manimotion() # reads MANIMOTION_API_KEY
video = client.videos.generate("Explain how binary search works")
print(video.video_url) # https://…/lecture.mp4
- Python 3.9+
- Sync and
asyncclients - Fully type-hinted
- Built-in polling, progress callbacks, retries and typed errors
Contents
- Install
- Get an API key
- Quickstart
- Storage: where your video goes
- Creating videos
- Waiting and progress
- Async usage
- Errors
- Client options
- Framework examples
- API reference
- FAQ
Install
pip install manimotion
# or
uv add manimotion
poetry add manimotion
Get an API key
- Sign in at manimotion.dev.
- Open Settings → API keys and create a key. It starts with
chalk_. - Put it in an environment variable. Never hard-code it or commit it to git.
export MANIMOTION_API_KEY=chalk_xxxxxxxxxxxxxxxx
Quickstart
from manimotion import Manimotion
client = Manimotion()
video = client.videos.generate(
"Why is the sky blue? Explain Rayleigh scattering for high-school students.",
on_progress=lambda v: print(f"[{v.status}] {v.message or ''}"),
)
print("Done:", video.video_url)
generate() creates the video and waits until it's ready. Most videos take 1–5 minutes.
Storage: where your video goes
manimotion uploads the finished MP4 to your bucket, so you own the file and the URL. There are three ways to set this up:
1. Save a bucket once (easiest). Add it in Settings → Storage. Once it's saved, it's used automatically and you don't need to pass anything.
2. Use a specific saved bucket. Click Copy id next to it in Settings → Storage:
client.videos.generate(
"Intro to photosynthesis",
storage={"integration_id": "your-integration-id"},
)
3. Pass credentials for this request only:
import os
# Cloudflare R2
client.videos.generate(
"Intro to photosynthesis",
storage={
"inline": {
"provider": "r2",
"bucket": "my-videos",
"account_id": os.environ["R2_ACCOUNT_ID"],
"access_key_id": os.environ["R2_ACCESS_KEY_ID"],
"secret_access_key": os.environ["R2_SECRET_ACCESS_KEY"],
"public_url": "https://pub-xxxx.r2.dev",
}
},
)
# AWS S3 (or "minio" / "backblaze" / "custom_s3" with an "endpoint")
client.videos.generate(
"Intro to photosynthesis",
storage={
"inline": {
"provider": "s3",
"bucket": "my-videos",
"region": "us-east-1",
"access_key_id": os.environ["AWS_ACCESS_KEY_ID"],
"secret_access_key": os.environ["AWS_SECRET_ACCESS_KEY"],
}
},
)
If none of these is set up, the call raises BadRequestError saying "No storage configured". See the storage guide.
Creating videos
| Argument | Type | Default | What it does |
|---|---|---|---|
prompt |
str |
required | What to teach (10–2000 characters). |
style |
"auto", "math", "graphics" |
"auto" |
math for equations and graphs; graphics for concepts, processes and comparisons. |
duration |
int (15–180) |
automatic | Soft target length in seconds. |
model |
str |
default | Model that writes the lesson, e.g. google/gemini-2.5-flash. |
tier |
"tier1", "tier2", "tier3" |
automatic | tier1 fastest, tier2 standard, tier3 most detailed. |
storage |
dict, see Storage |
your saved bucket | Where to upload. |
Fire-and-forget (web apps, queues)
generate() blocks for minutes. In a web request, start the job and check on it later instead:
job = client.videos.create("The water cycle")
save_to_db(job.id)
# later…
video = client.videos.retrieve(job.id)
if video.status == "completed":
print(video.video_url)
elif video.status == "failed":
print(video.error)
Waiting and progress
video = client.videos.wait(
job.id,
poll_interval=5, # seconds, default 3
timeout=10 * 60, # seconds, default 20 min
on_progress=lambda v: print(v.status, v.message, v.eta_seconds),
)
Statuses, in order: queued → planning → generating_audio → generating_code → merging → uploading → completed (or failed).
If wait() times out, the video keeps rendering on the server, so you can retrieve() it later.
The Video object
Video(
id="a1b2c3…",
status="completed",
video_url="https://…/lecture.mp4",
error=None, # short reason when status == "failed"
cached=False, # True if an identical recent request was reused
style="graphics", # "math" | "graphics" | None
duration=94.2, # seconds, once completed
phase="completed",
message="Done!",
eta_seconds=0,
eta_display="~2–3 min",
tier="tier2",
)
video.done # True once completed or failed
video.raw # the untouched JSON from the API
Async usage
Every method has an async twin on AsyncManimotion:
import asyncio
from manimotion import AsyncManimotion
async def main():
async with AsyncManimotion() as client:
video = await client.videos.generate("Explain recursion with a factorial example")
print(video.video_url)
asyncio.run(main())
on_progress can be a normal function or an async def.
Errors
Every error inherits from ManimotionError. Errors from the API also have .status_code, .message and .body.
from manimotion import (
Manimotion, AuthenticationError, RateLimitError, VideoFailedError,
)
try:
video = client.videos.generate("Explain TCP handshakes")
except AuthenticationError:
... # wrong or revoked key
except RateLimitError as e:
... # too many requests, or plan quota used up (see e.message)
except VideoFailedError as e:
print("Video failed:", e, e.video.id)
| Error | When |
|---|---|
BadRequestError |
400: e.g. no storage configured, bad storage credentials. |
AuthenticationError |
401: missing, wrong or revoked API key. |
NotFoundError |
404: unknown job id (or it belongs to another key). |
ValidationError |
422: a parameter is invalid (message says which). |
RateLimitError |
429: too many requests, or daily/monthly quota reached. |
InternalServerError |
5xx: something broke on our side. |
APIConnectionError |
Couldn't reach the API (offline, DNS, …). |
APITimeoutError |
A request or wait() took too long. |
VideoFailedError |
The job ended with status == "failed". |
Retries: status checks (retrieve, wait, health) are retried automatically on network errors and 5xx. create() is never retried automatically, because a retry could start a second video and use up your quota.
Client options
client = Manimotion(
api_key="chalk_…", # default: MANIMOTION_API_KEY env var
base_url="https://api.manimotion.dev", # default: MANIMOTION_BASE_URL or this
timeout=60.0, # seconds per HTTP request
max_retries=2, # status checks only
default_headers={"x-request-source": "my-app"},
http_client=None, # bring your own httpx.Client (proxies, etc.)
)
Use with Manimotion() as client: (or call client.close()) to close connections when you're done.
Framework examples
FastAPI
from fastapi import FastAPI, HTTPException
from manimotion import AsyncManimotion, APIError
app = FastAPI()
client = AsyncManimotion()
@app.post("/lectures")
async def start_lecture(prompt: str):
try:
job = await client.videos.create(prompt)
except APIError as e:
raise HTTPException(e.status_code, e.message)
return {"id": job.id, "eta": job.eta_display}
@app.get("/lectures/{video_id}")
async def lecture_status(video_id: str):
video = await client.videos.retrieve(video_id)
return {"status": video.status, "url": video.video_url, "message": video.message}
Django / Flask
Use the sync Manimotion client. Call create() in the view, store job.id, and run wait() in a background worker (Celery, RQ, a cron job) so web requests stay fast.
Jupyter notebook
from IPython.display import Video as Player
from manimotion import Manimotion
video = Manimotion().videos.generate("Visualize the derivative of x^2")
Player(video.video_url, width=720)
API reference
| Method | Returns | Description |
|---|---|---|
videos.create(prompt, **options) |
Video |
Start a video; returns right away. |
videos.retrieve(video_id) |
Video |
Latest status of a job. |
videos.wait(video_id, **wait_options) |
Video |
Poll until completed (raises if failed). |
videos.generate(prompt, **options) |
Video |
create + wait in one call. |
health() |
Health |
API status; no key needed. |
The HTTP API behind these methods is documented at manimotion.dev/docs/api.
FAQ
How long does a video take? Usually 1–5 minutes, depending on length and tier. video.eta_display gives an estimate.
I sent the same prompt twice and got the same video instantly. Identical requests made within a few hours are served from cache (video.cached is True). Change the prompt slightly to get a fresh render.
My job id stopped working after a while. Job status is kept for a limited time. Save video_url as soon as the job completes; the file in your bucket stays there.
License
Apache-2.0
Metadata
Release files for manimotion 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| manimotion-0.1.0.tar.gz | 17.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| manimotion-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 34.2 kB
Release files / manimotion-0.1.0.tar.gz
| Download URL | manimotion-0.1.0.tar.gz |
|---|---|
| Size | 17.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6713a2ce15b553c37458828118b6db8172345a9566870bae62fee5e01804e734
|
|
BLAKE2b-256 checksum How to use checksums |
5157ecf9733839ffbd6042758882f1cc3792de2a8b019057d41e99cf6966052c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / manimotion-0.1.0-py3-none-any.whl
| Download URL | manimotion-0.1.0-py3-none-any.whl |
|---|---|
| Size | 17.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f2f3f1e5cea96e476b90aa04196b81b274df98a939cc865ffaf21aa87681e3da
|
|
BLAKE2b-256 checksum How to use checksums |
f8b402f854905ba0e624bd76903846e78b3dc2e8a871b575cd59ab0d7bef6d05
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|