# Pruna P-Video-Replace

> Fast person swap in your video from 1–3 photos. Motion, camera and sound stay as filmed. Up to 30 seconds, 720p or 1080p.

Pruna P-Video-Replace takes your video and 1–3 photos and puts the character from the photos in place of the person in the clip.

Motion, timing, camera and scene stay as in the source; the clip audio is kept. It is fast: about 3–4 seconds of generation per second of video.

The video is up to 30 seconds (longer is trimmed to 30); the result has the same length. Resolution 720p or 1080p, frame rate original, 24 or 48. The prompt is optional. You pay per second of your video.

One model, `pruna-p-video-replace` · ~2 min. The mode is the `input.mode` field.

## Modes

One ID, `pruna-p-video-replace`. `input.mode` chooses what happens to the clip. Omitted, it is motion transfer. The official ids work too: `higgsfield/genjutsu/object-swap/v1.0` and `higgsfield/genjutsu/restyle/v1.0` select the mode, so `mode` can be left out.

| `mode` | What it does | What to send |
|---|---|---|
| `motion-transfer` | Motion, camera and timing come from the video; the look comes from the photos | `video_url` + `image_urls` (1–3) |
| `object-swap` | Replaces a person, outfit or object in the clip. The rest of the shot stays | `video_url` + `image_urls` (1–3) |
| `restyle` | The same clip in a chosen style. The source motion and audio stay | `video_url` + `preset_id`. `image_urls` — optional, up to 5 |

Shared by all three: the video is at least 4 seconds, longer than 30 seconds is trimmed to 30, and the result has the same length. `prompt` may be omitted. `resolution`: `480p`, `720p` (default) or `1080p`. The mode does not change the price.

Object swap needs a source frame of at least 409,600 pixels (width × height). Ordinary HD is fine; a tiny preview is not.

### Styles for restyle

Style list: `GET https://mixmedia.tech/api/v1/genjutsu/presets`. No key. `preset_id` is the item's `id`, not its name and not the preview image.

```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "items": [
      {
        "id": "c2143317-f28d-4c3c-a0b8-39bd547e08a7",
        "name": "Cel-Shaded CG Anime",
        "preview_url": "https://…/style.webp"
      }
    ]
  }
}
```

The list changes. If a saved `preset_id` is no longer accepted, fetch the list again and use a current id.



## Price

| Resolution | Price per second of your video |
|---|---|
| 720p | $0.039 |
| 1080p | $0.078 |

- Price = per-second price × clip length, rounded up to $0.001. Charged when the task is created and fully refunded if it fails.
- You pay per second of your video (2 to 30 s). A clip longer than 30 s is trimmed to 30; the result has the same length. `prompt` may be omitted.

## Media limits

| Type | Limits |
|---|---|
| Images | **1–3** for motion transfer and object swap; **0–5** for restyle. JPG / PNG / WebP; public https or [files/upload](https://docs.mixmedia.tech/en/endpoints/upload) — up to **30 MB** per file |
| Video | one clip, `video_url`; **2 s** to **30 s** (longer is trimmed to 30); MP4 / MOV → MP4 on the server; upload ≤ **200 MB** |

Photos — public https or upload; video and audio — files/upload only.


## Parameters (field `input`)

| Parameter | Description | Values (default in bold) |
|---|---|---|
| `prompt` | What happens in the clip: scene, motion, camera, light, style | text, up to 30000 chars (optional, default empty) |
| `video_url` | Motion video (MP4): one clip, at least 4 s. Longer than 30 s is trimmed to 30. Upload it via files/upload first | `"https://…"` |
| `image_urls` | Character photos (https): look, face, clothes | `["https://…"]`, 1–3 |
| `resolution` | Video resolution | **`720p`** · `1080p` |
| `target_fps` | Частота кадров | **`original`** `24` `48` |
| `save_audio` | Сохранить звук ролика | `true` `false` (default `true`) |
| `ignore_audio` | Не учитывать звук при генерации | `true` `false` (default `false`) |
| `turbo` | Турбо (быстрее, чуть ниже качество) | `true` `false` (default `false`) |
| `seed` | Variant number: the same number with the same prompt gives a similar result; empty or -1 = random | -1…4294967295 |

## Examples

### Motion transfer

```bash
curl -X POST https://mixmedia.tech/api/v1/jobs/createTask \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"pruna-p-video-replace","input":{"mode":"motion-transfer","prompt":"Keep the face from the photo, take the motion from the video","video_url":"https://mixmedia.tech/files/<id>.mp4","image_urls":["https://mixmedia.tech/static/img/previews/gpt-t2i.jpg"],"resolution":"720p"}}'
```

```python
import os, requests

r = requests.post(
    "https://mixmedia.tech/api/v1/jobs/createTask",
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
    json={
      "model": "pruna-p-video-replace",
      "input": {
        "mode": "motion-transfer",
        "prompt": "Keep the face from the photo, take the motion from the video",
        "video_url": "https://mixmedia.tech/files/<id>.mp4",
        "image_urls": [
          "https://mixmedia.tech/static/img/previews/gpt-t2i.jpg"
        ],
        "resolution": "720p"
      }
    },
)
print(r.json())  # {"code": 200, "data": {"taskId": "..."}}
```

```javascript
const r = await fetch("https://mixmedia.tech/api/v1/jobs/createTask", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "model": "pruna-p-video-replace",
    "input": {
      "mode": "motion-transfer",
      "prompt": "Keep the face from the photo, take the motion from the video",
      "video_url": "https://mixmedia.tech/files/<id>.mp4",
      "image_urls": [
        "https://mixmedia.tech/static/img/previews/gpt-t2i.jpg"
      ],
      "resolution": "720p"
    }
  }),
});
console.log(await r.json()); // { code: 200, data: { taskId: "..." } }
```

### Object swap

```bash
curl -X POST https://mixmedia.tech/api/v1/jobs/createTask \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"pruna-p-video-replace","input":{"mode":"object-swap","prompt":"Replace the person with the character in the photo","video_url":"https://mixmedia.tech/files/<id>.mp4","image_urls":["https://mixmedia.tech/static/img/previews/gpt-t2i.jpg"],"resolution":"720p"}}'
```

```python
import os, requests

r = requests.post(
    "https://mixmedia.tech/api/v1/jobs/createTask",
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
    json={
      "model": "pruna-p-video-replace",
      "input": {
        "mode": "object-swap",
        "prompt": "Replace the person with the character in the photo",
        "video_url": "https://mixmedia.tech/files/<id>.mp4",
        "image_urls": [
          "https://mixmedia.tech/static/img/previews/gpt-t2i.jpg"
        ],
        "resolution": "720p"
      }
    },
)
print(r.json())  # {"code": 200, "data": {"taskId": "..."}}
```

```javascript
const r = await fetch("https://mixmedia.tech/api/v1/jobs/createTask", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "model": "pruna-p-video-replace",
    "input": {
      "mode": "object-swap",
      "prompt": "Replace the person with the character in the photo",
      "video_url": "https://mixmedia.tech/files/<id>.mp4",
      "image_urls": [
        "https://mixmedia.tech/static/img/previews/gpt-t2i.jpg"
      ],
      "resolution": "720p"
    }
  }),
});
console.log(await r.json()); // { code: 200, data: { taskId: "..." } }
```

### Restyle

```bash
curl -X POST https://mixmedia.tech/api/v1/jobs/createTask \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"pruna-p-video-replace","input":{"mode":"restyle","video_url":"https://mixmedia.tech/files/<id>.mp4","preset_id":"<preset_id>","resolution":"720p"}}'
```

```python
import os, requests

r = requests.post(
    "https://mixmedia.tech/api/v1/jobs/createTask",
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
    json={
      "model": "pruna-p-video-replace",
      "input": {
        "mode": "restyle",
        "video_url": "https://mixmedia.tech/files/<id>.mp4",
        "preset_id": "<preset_id>",
        "resolution": "720p"
      }
    },
)
print(r.json())  # {"code": 200, "data": {"taskId": "..."}}
```

```javascript
const r = await fetch("https://mixmedia.tech/api/v1/jobs/createTask", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "model": "pruna-p-video-replace",
    "input": {
      "mode": "restyle",
      "video_url": "https://mixmedia.tech/files/<id>.mp4",
      "preset_id": "<preset_id>",
      "resolution": "720p"
    }
  }),
});
console.log(await r.json()); // { code: 200, data: { taskId: "..." } }
```

## Create and result

1. `POST /api/v1/jobs/createTask` with `model` and `input`.
2. Poll `GET /api/v1/jobs/recordInfo?taskId=…` every 3–5 s (or a [callback](https://docs.mixmedia.tech/en/callbacks)).

### Successful createTask (HTTP 200)

```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "task_...",
    "task_id": "task_...",
    "status": "submitted"
  }
}
```

### Completed task (recordInfo)

```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "task_...",
    "task_id": "task_...",
    "state": "success",
    "status": "completed",
    "costUsd": "0.384",
    "refunded": false,
    "resultJson": {
      "videos": [
        {
          "url": "https://…/out.mp4"
        }
      ],
      "duration": 8
    }
  }
}
```

Aliases: `task_id` = `taskId`; `status`: `pending` / `processing` / `completed` / `failed` alongside `state`. Clip URL is in `resultJson`. `size` is an alias of `aspect_ratio` in `input`.


Photos can be any public https links or uploads via [files/upload](https://docs.mixmedia.tech/en/endpoints/upload). Videos and audio — via files/upload only.

## Error handling

API responses are JSON `{"code", "msg", "data"}`; errors also have a string `error` field (type). **On `/api/v1/*` the HTTP status matches body `code`.**

### Example response bodies

**400**

```json
{
  "code": 400,
  "msg": "input.duration: must be between 4 and 30, or \"auto\" / -1",
  "data": null,
  "error": "invalid_request_error"
}
```

**401**

```json
{
  "code": 401,
  "msg": "Authentication failed. Please check your API key",
  "data": null,
  "error": "authentication_error"
}
```

**402**

```json
{
  "code": 402,
  "msg": "Insufficient balance. Please top up and try again. Available: $0.01, required: $0.015. Top up at https://mixmedia.tech/cabinet/billing",
  "data": {
    "balance": "0.0100",
    "required": "0.0150",
    "currency": "USD",
    "topUpUrl": "https://mixmedia.tech/cabinet/billing"
  },
  "error": "payment_required"
}
```

**429**

```json
{
  "code": 429,
  "msg": "Rate limit exceeded: 10 createTask/upload requests per 10 s per key. Retry after 8s",
  "data": null,
  "error": "rate_limit_error"
}
```

**500**

```json
{
  "code": 500,
  "msg": "Internal server error",
  "data": null,
  "error": "server_error"
}
```

### Codes on createTask / upload

| code | error | Meaning | What to do |
|---|---|---|---|
| 400 | `invalid_request_error` | Invalid parameter or body | See `msg` |
| 400 | `unsupported_model` | Unknown model | [Model list](https://docs.mixmedia.tech/en/models) |
| 400 | `nsfw_content_detected` | Moderation / content policy — `msg` / `failMsg` is the upstream text verbatim | Read the refusal text, change the prompt or files |
| 401 | `authentication_error` | Missing or invalid key | Check the key |
| 402 | `payment_required` | Not enough balance | Top up |
| 403 | `permission_error` | No access | Contact support |
| 429 | `rate_limit_error` | Too many requests | Wait for `Retry-After` |
| 505 | `model_disabled` | Model temporarily disabled | Pick another |
| 500 | `server_error` | Our failure | Retry later |

Example `msg` values: `Authentication failed. Please check your API key` (401); `Insufficient balance. Please top up and try again…` (402).

### Task errors (recordInfo)

If the task was created and then failed: `state: "fail"` (alias `status: "failed"`), `failCode` / `failMsg`, and `error: { code, message, type }`. Money is refunded (`refunded: true`).

| failCode | error.type | Meaning |
|---|---|---|
| 400 | `nsfw_content_detected` / `invalid_request_error` | Moderation — `failMsg` is upstream text verbatim |
| 422 | `invalid_request_error` | The model rejected the parameters (length, mode, resolution, file) |
| 501 | `server_error` | The model could not generate (`failMsg`) |
| 504 | `timeout_error` | Timed out — retry |
| 500 / 503 | `server_error` | Service temporarily unavailable |

### Rate limits

| What | Limit |
|---|---|
| createTask and upload | 20 per 10 s per key |
| recordInfo / list / balance | 100 per 10 s per key |

Over the limit — HTTP 429 and `Retry-After`.
