Metadata-Version: 2.5
Name: manimotion
Version: 0.1.0
Summary: Official Python SDK for the manimotion API — turn a topic into a narrated video lecture.
Project-URL: Homepage, https://manimotion.dev
Project-URL: Documentation, https://manimotion.dev/docs/sdk
Project-URL: Source, https://github.com/Proxyy587/manimotion/tree/main/sdks/python
Project-URL: Issues, https://github.com/Proxyy587/manimotion/issues
Author: manimotion
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: ai,api,education,lecture,manimotion,sdk,text-to-video,video
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Video
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.23
Requires-Dist: typing-extensions>=4.5; python_version < '3.11'
Description-Content-Type: text/markdown

# manimotion

The official Python SDK for [manimotion](https://manimotion.dev): send a topic, get back a narrated video lecture.

```python
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 `async` clients
- Fully type-hinted
- Built-in polling, progress callbacks, retries and typed errors

---

## Contents

- [Install](#install)
- [Get an API key](#get-an-api-key)
- [Quickstart](#quickstart)
- [Storage: where your video goes](#storage-where-your-video-goes)
- [Creating videos](#creating-videos)
- [Waiting and progress](#waiting-and-progress)
- [Async usage](#async-usage)
- [Errors](#errors)
- [Client options](#client-options)
- [Framework examples](#framework-examples)
- [API reference](#api-reference)
- [FAQ](#faq)

---

## Install

```bash
pip install manimotion
# or
uv add manimotion
poetry add manimotion
```

## Get an API key

1. Sign in at [manimotion.dev](https://manimotion.dev).
2. Open **Settings → API keys** and create a key. It starts with `chalk_`.
3. Put it in an environment variable. Never hard-code it or commit it to git.

```bash
export MANIMOTION_API_KEY=chalk_xxxxxxxxxxxxxxxx
```

## Quickstart

```python
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:

```python
client.videos.generate(
    "Intro to photosynthesis",
    storage={"integration_id": "your-integration-id"},
)
```

**3. Pass credentials for this request only:**

```python
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](https://manimotion.dev/docs/storage).

## 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](#storage-where-your-video-goes) | 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:

```python
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

```python
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

```python
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`:

```python
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`.

```python
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

```python
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

```python
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

```python
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](https://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
