> ## Documentation Index
> Fetch the complete documentation index at: https://chatgpttech.mobi/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Video models

> Video generation models and API reference. Video generation is asynchronous — submit a task, then poll for the result.

## Available models

<Card title="Use Seedance in the dashboard" icon="bolt" href="https://app.chatgpttech.mobi/models">
  Open model page
</Card>

| Model        | Model ID                            | Max duration | Aspect ratios                 | Resolutions         |
| ------------ | ----------------------------------- | ------------ | ----------------------------- | ------------------- |
| Seedance 2.0 | `doubao/doubao-seedance-2-0-260128` | 60s          | 16:9 / 9:16 / 1:1 / 4:3 / 3:4 | 480p / 720p / 1080p |

***

## Billing

Video generation is billed by **pixel-tokens**, following the official Volcengine formula:

```
tokens = duration (s) × width (px) × height (px) × 24 fps ÷ 1024
cost   = tokens ÷ 1,000,000 × price_per_million
```

**Token reference table** (pure generation, 16:9):

| Resolution        | Tokens / sec | 5s video  | 10s video |
| ----------------- | ------------ | --------- | --------- |
| 480p (848×480)    | \~9,540      | \~47,700  | \~95,400  |
| 720p (1280×720)   | \~21,600     | \~108,000 | \~216,000 |
| 1080p (1920×1080) | \~48,600     | \~243,000 | \~486,000 |

Check your dashboard for the current per-million-token price for each model.

> **Note:** Only successful generations are charged. Tasks that fail due to content policy or upstream errors incur no cost. Billing is reconciled against the actual token count reported by the upstream API on task completion.

***

## How it works

Video generation is a two-step async process: **submit a task**, then **poll for the result**.

<Steps>
  <Step title="Submit a generation task">
    Send a `POST` request to `/v1/generation/tasks`:

    <CodeGroup>
      ```bash cURL theme={null}
      curl https://oss.chatgpttech.mobi/v1/generation/tasks \
        -H "Authorization: Bearer sk-hub-your-key-here" \
        -H "Content-Type: application/json" \
        -d '{
          "model": "doubao/doubao-seedance-2-0-260128",
          "content": [{"type": "text", "text": "A beautiful sunset over mountains"}],
          "ratio": "16:9",
          "resolution": "720p",
          "duration": 5
        }'
      ```

      ```typescript Node.js theme={null}
      const task = await fetch('https://oss.chatgpttech.mobi/v1/generation/tasks', {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer sk-hub-your-key-here',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: 'doubao/doubao-seedance-2-0-260128',
          content: [{ type: 'text', text: 'A beautiful sunset over mountains' }],
          ratio: '16:9',
          resolution: '720p',
          duration: 5,
        }),
      }).then(r => r.json());

      console.log(task.id); // use this to poll
      ```

      ```python Python theme={null}
      import requests, time

      res = requests.post(
          'https://oss.chatgpttech.mobi/v1/generation/tasks',
          headers={'Authorization': 'Bearer sk-hub-your-key-here'},
          json={
              'model': 'doubao/doubao-seedance-2-0-260128',
              'content': [{'type': 'text', 'text': 'A beautiful sunset over mountains'}],
              'ratio': '16:9',
              'resolution': '720p',
              'duration': 5,
          }
      )
      task = res.json()
      print(task['id'])
      ```
    </CodeGroup>

    The response returns a task ID you'll use to check progress:

    ```json theme={null}
    {
      "id": "task_abc123",
      "status": "queued"
    }
    ```
  </Step>

  <Step title="Poll for the result">
    Query the task status using the returned `id`. Poll every 3–5 seconds until the status is `succeeded` or `failed`:

    <CodeGroup>
      ```bash cURL theme={null}
      curl https://oss.chatgpttech.mobi/v1/generation/tasks/task_abc123 \
        -H "Authorization: Bearer sk-hub-your-key-here"
      ```

      ```typescript Node.js theme={null}
      let result: any = { status: 'queued' };
      while (result.status === 'queued' || result.status === 'running') {
        await new Promise(r => setTimeout(r, 4000));
        result = await fetch(
          `https://oss.chatgpttech.mobi/v1/generation/tasks/${task.id}`,
          { headers: { 'Authorization': 'Bearer sk-hub-your-key-here' } }
        ).then(r => r.json());
      }

      if (result.status === 'succeeded') {
        console.log('Video URL:', result.content.video_url);
      }
      ```

      ```python Python theme={null}
      import time

      while True:
          time.sleep(4)
          result = requests.get(
              f"https://oss.chatgpttech.mobi/v1/generation/tasks/{task['id']}",
              headers={'Authorization': 'Bearer sk-hub-your-key-here'}
          ).json()
          if result['status'] not in ('queued', 'running'):
              break

      if result['status'] == 'succeeded':
          print('Video URL:', result['content']['video_url'])
      ```
    </CodeGroup>

    Completed response:

    ```json theme={null}
    {
      "id": "task_abc123",
      "status": "succeeded",
      "content": {
        "video_url": "https://cdn.example.com/output/abc123.mp4"
      },
      "usage": {
        "completion_tokens": 108000
      }
    }
    ```

    <Note>
      `content` on the response is a single object (`{ video_url }`), not the part-array format used in the *request* `content` field. Don't reuse your request-parsing code for the response.
    </Note>
  </Step>
</Steps>

### Task status reference

| Status      | Description                                           |
| ----------- | ----------------------------------------------------- |
| `queued`    | Waiting to start                                      |
| `running`   | Generation in progress                                |
| `succeeded` | Done — read the URL from `content.video_url`          |
| `failed`    | Generation failed — see the `error` field for details |

***

## Image-to-video

To animate a still image instead of generating from text alone, add an `image_url` part to the `content` array alongside your text prompt. There are three ways to supply that image, in order of preference:

<CardGroup cols={3}>
  <Card title="1. Upload the file" icon="upload">Recommended. Small, reliable request bodies.</Card>
  <Card title="2. Public image URL" icon="link">Simplest, if the image is already hosted.</Card>
  <Card title="3. Base64 data URI" icon="binary">Quick one-off tests only.</Card>
</CardGroup>

### Option 1: Upload the file (recommended)

`POST /v1/assets/upload` with a `multipart/form-data` body containing the file and the target `model`. It returns a hosted `assetUrl` you pass straight into `image_url` — your generation request body stays small no matter how large the source photo is.

<CodeGroup>
  ```bash cURL theme={null}
  # Step 1: upload the image
  curl https://oss.chatgpttech.mobi/v1/assets/upload \
    -H "Authorization: Bearer sk-hub-your-key-here" \
    -F "model=doubao/doubao-seedance-2-0-260128" \
    -F "file=@./photo.jpg"
  # → { "assetUrl": "https://..." }

  # Step 2: create the task with that URL
  curl https://oss.chatgpttech.mobi/v1/generation/tasks \
    -H "Authorization: Bearer sk-hub-your-key-here" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "doubao/doubao-seedance-2-0-260128",
      "content": [
        {"type": "text", "text": "Make the scene come alive"},
        {"type": "image_url", "image_url": {"url": "ASSET_URL_FROM_STEP_1"}}
      ],
      "ratio": "16:9",
      "resolution": "1080p",
      "duration": 5
    }'
  ```

  ```typescript Node.js theme={null}
  import { readFileSync } from 'node:fs';

  // Step 1: upload the image
  const form = new FormData();
  form.append('model', 'doubao/doubao-seedance-2-0-260128');
  form.append('file', new Blob([readFileSync('./photo.jpg')]), 'photo.jpg');

  const { assetUrl } = await fetch('https://oss.chatgpttech.mobi/v1/assets/upload', {
    method: 'POST',
    headers: { 'Authorization': 'Bearer sk-hub-your-key-here' },
    body: form,
  }).then(r => r.json());

  // Step 2: create the task with that URL
  const task = await fetch('https://oss.chatgpttech.mobi/v1/generation/tasks', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk-hub-your-key-here',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'doubao/doubao-seedance-2-0-260128',
      content: [
        { type: 'text', text: 'Make the scene come alive' },
        { type: 'image_url', image_url: { url: assetUrl } },
      ],
      ratio: '16:9',
      resolution: '1080p',
      duration: 5,
    }),
  }).then(r => r.json());

  console.log(task.id); // use this to poll, same as text-to-video above
  ```

  ```python Python theme={null}
  import requests

  # Step 1: upload the image
  upload = requests.post(
      'https://oss.chatgpttech.mobi/v1/assets/upload',
      headers={'Authorization': 'Bearer sk-hub-your-key-here'},
      data={'model': 'doubao/doubao-seedance-2-0-260128'},
      files={'file': open('./photo.jpg', 'rb')},
  )
  asset_url = upload.json()['assetUrl']

  # Step 2: create the task with that URL
  res = requests.post(
      'https://oss.chatgpttech.mobi/v1/generation/tasks',
      headers={'Authorization': 'Bearer sk-hub-your-key-here'},
      json={
          'model': 'doubao/doubao-seedance-2-0-260128',
          'content': [
              {'type': 'text', 'text': 'Make the scene come alive'},
              {'type': 'image_url', 'image_url': {'url': asset_url}},
          ],
          'ratio': '16:9',
          'resolution': '1080p',
          'duration': 5,
      }
  )
  task = res.json()
  print(task['id'])  # use this to poll, same as text-to-video above
  ```
</CodeGroup>

Accepted file types: `image/png`, `image/jpeg`, `image/webp` (max 10 MB) and `video/mp4` (max 50 MB, for video-editing tasks).

### Option 2: Use a public image URL

If the image is already hosted somewhere reachable, skip the upload step entirely and pass the URL directly:

```bash cURL theme={null}
curl https://oss.chatgpttech.mobi/v1/generation/tasks \
  -H "Authorization: Bearer sk-hub-your-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "doubao/doubao-seedance-2-0-260128",
    "content": [
      {"type": "text", "text": "Make the scene come alive"},
      {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
    ],
    "ratio": "16:9",
    "resolution": "1080p",
    "duration": 5
  }'
```

### Option 3: Inline base64 (quick tests only)

For a one-off test without wiring up an upload step, you can inline the image directly as a base64 [data URI](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data):

<CodeGroup>
  ```typescript Node.js theme={null}
  import { readFileSync } from 'node:fs';

  const imageBuffer = readFileSync('./photo.jpg');
  const imageUrl = `data:image/jpeg;base64,${imageBuffer.toString('base64')}`;

  const task = await fetch('https://oss.chatgpttech.mobi/v1/generation/tasks', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk-hub-your-key-here',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'doubao/doubao-seedance-2-0-260128',
      content: [
        { type: 'text', text: 'Make the scene come alive' },
        { type: 'image_url', image_url: { url: imageUrl } },
      ],
      ratio: '16:9',
      resolution: '1080p',
      duration: 5,
    }),
  }).then(r => r.json());

  console.log(task.id);
  ```

  ```python Python theme={null}
  import base64, requests

  with open('./photo.jpg', 'rb') as f:
      image_url = f"data:image/jpeg;base64,{base64.b64encode(f.read()).decode()}"

  res = requests.post(
      'https://oss.chatgpttech.mobi/v1/generation/tasks',
      headers={'Authorization': 'Bearer sk-hub-your-key-here'},
      json={
          'model': 'doubao/doubao-seedance-2-0-260128',
          'content': [
              {'type': 'text', 'text': 'Make the scene come alive'},
              {'type': 'image_url', 'image_url': {'url': image_url}},
          ],
          'ratio': '16:9',
          'resolution': '1080p',
          'duration': 5,
      }
  )
  task = res.json()
  print(task['id'])
  ```
</CodeGroup>

<Warning>
  Base64 data URIs count toward the request body size. Large source photos (multi-megapixel phone camera shots) can produce multi-megabyte payloads — downscale to around 1024px on the long edge before encoding, or use Option 1 instead, if you hit a `413 Request Entity Too Large`.
</Warning>

Whichever option you use, poll `/v1/generation/tasks/{id}` exactly as shown above — the response shape and polling logic are identical regardless of how the image was supplied.

***

## Request parameters

| Parameter    | Type   | Required | Description                                                 |
| ------------ | ------ | -------- | ----------------------------------------------------------- |
| `model`      | string | ✓        | Model ID                                                    |
| `content`    | array  | ✓        | Prompt array. Each item has `type` of `text` or `image_url` |
| `ratio`      | string | —        | Aspect ratio: `16:9` (default), `9:16`, `1:1`, `4:3`, `3:4` |
| `duration`   | number | —        | Duration in seconds: `5` (default), max `60`                |
| `resolution` | string | —        | Output resolution: `480p`, `720p`, or `1080p` (default)     |

> **Note:** Video generation models are not available through `/v1/chat/completions`. Use `/v1/generation/tasks` for all video generation requests.

***

## Related

<CardGroup cols={3}>
  <Card title="Quickstart" icon="rocket" href="/api-guide/quickstart" />

  <Card title="API keys" icon="key" href="/api-guide/api-keys" />

  <Card title="Text models" icon="message" href="/api-guide/text-models" />
</CardGroup>
