Metadata-Version: 2.4
Name: runmux
Version: 0.1.0
Summary: Official RunMux Python SDK — video generation (Seedance 2.0) and the face asset library, with submit-and-wait helpers so you never hand-roll polling.
Project-URL: Homepage, https://runmux.com
Author: RunMux
License: MIT
Keywords: ai-video,runmux,sdk,seedance,video-generation
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Description-Content-Type: text/markdown

# runmux

Official Python SDK for **RunMux** — generate video with Seedance 2.0 and manage the
face asset library, without hand-rolling polling loops or the multi-step face upload.

Synchronous and Pythonic: construct one client, call resource methods with
keyword arguments, get plain dicts back.

## Install

```bash
pip install runmux
```

Requires Python 3.9+ (built on [httpx](https://www.python-httpx.org/)).

## Quickstart — text to video (one call)

```python
import os
from runmux import RunmuxClient

client = RunmuxClient(api_key=os.environ["RUNMUX_API_KEY"])

# run() submits the job AND waits for the result — no polling code on your side.
video = client.videos.run(
    model="seedance-2-0-mini",
    prompt="a diamond necklace rotating on black velvet, soft highlights, product ad",
    resolution="480p",
    duration=5,
)

print(video["url"])  # downloadable result (expires ~1h unless you pass ttl)
```

The API key falls back to the `RUNMUX_API_KEY` environment variable, so
`RunmuxClient()` works when that variable is set.

Prefer to manage polling yourself? Use `create()` then `wait()`:

```python
job = client.videos.create(model="seedance-2-0-mini", prompt="...")
done = client.videos.wait(job["id"])
```

## Putting a person in the video (faces)

Raw human faces cannot be sent to the model directly. `faces.enroll()` runs the
whole flow — register, wait until active, return the ready-to-use `asset://`
reference:

```python
# Use an ordinary (non-celebrity) face photo.
asset_uri = client.faces.enroll(url="https://your-cdn.com/model.jpg")

video = client.videos.run(
    model="seedance-2-0-mini",
    prompt="Image 1 wearing the necklace, smiling at the camera",
    image_url=asset_uri,  # or reference_images=[asset_uri]
)
```

Even simpler — let RunMux enroll the face for you in one shot:

```python
video = client.videos.run(
    model="seedance-2-0-mini",
    prompt="Image 1 wearing the necklace",
    image_url="https://your-cdn.com/model.jpg",
    auto_enroll_faces=True,
)
```

If a face is rejected (e.g. a celebrity / copyrighted likeness), the call raises
a `RunmuxError` whose message explains why — switch to an ordinary face photo.

## Image-to-video, batches, and inputs

```python
# Product image as the first frame:
client.videos.run(
    model="seedance-2-0-mini",
    prompt="the ring rotates",
    frame_images=["https://cdn/ring.jpg"],
)

# Several variations at once (number_results 1–4) returns a list:
variations = client.videos.run(
    model="seedance-2-0-mini", prompt="...", number_results=3
)

# Upload a local file, then reference it:
with open("ring.png", "rb") as f:
    file_url = client.files.upload(f.read(), "image/png")
```

`frame_images` entries may be plain URL/`asset://` strings, or dicts with an
explicit frame, e.g. `{"image": "https://cdn/ring.jpg", "frame": "first"}`.

## Webhooks

Submit with `webhook_url` to get the result POSTed to you, then verify the
signature against the raw request body:

```python
ok = client.webhooks.verify(
    payload=raw_request_body,                 # the raw string body, not re-serialized
    signature=request.headers.get("x-runmux-signature"),
    secret=os.environ["RUNMUX_WEBHOOK_SECRET"],
)
```

The signature scheme is `sha256=<hex>` where the hex is
`HMAC-SHA256(raw_body, secret)`; comparison is constant-time.

## Errors

Every non-2xx response (and any failed job/asset) raises a `RunmuxError` with
`.code`, `.status`, and `.request_id` for precise branching and support.

```python
from runmux import RunmuxError

try:
    client.videos.run(model="seedance-2-0-mini", prompt="...")
except RunmuxError as exc:
    print(exc.code, exc.status, exc.request_id)
```

## API surface

- `client.videos` — `create`, `run`, `wait`, `get`
- `client.assets` — `create`, `get`, `list`, `delete`, `wait_active`
- `client.faces` — `enroll`, `enroll_asset`
- `client.files` — `create_upload`, `upload`
- `client.webhooks` — `verify`

## Wire field names

You pass clean Python snake_case keyword arguments; the SDK maps them to the
exact field names the API expects. For example `image_url`, `reference_images`,
`auto_enroll_faces`, and `number_results` are sent as-is, while `output_type`,
`output_format`, `output_quality`, and `upload_endpoint` are sent as their
camelCase wire equivalents. Pass `idempotency_key` to dedup retries — it is sent
as the `Idempotency-Key` header, not a body field.
