# 视频 (https://hypit.ai/zh/api-reference/video/)

> 文生视频与图生视频。一个异步端点，按秒计价。

`POST /v1/videos` 提交一段视频，返回 `202 Accepted` 加一个 [Job 信封](/zh/api-reference/jobs)。
视频是这套 API 上耗时最长的东西——以分钟计而不是秒——所以异步在这里不是不便，而是唯一可行的形态。

## 提交 [#提交]

```bash
curl https://hypit.ai/v1/videos \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bytedance/seedance-2",
    "prompt": "一只纸船顺着雨水沟漂走，镜头跟拍",
    "seconds": 5,
    "resolution": "720p",
    "aspect_ratio": "16:9"
  }'
```

```json
{
  "id": "job_2f81c0a4d3b57e9016ab24cf",
  "status": "queued",
  "kind": "video",
  "model": "bytedance/seedance-2",
  "progress": 0,
  "created_at": 1787824589,
  "dispatch_deadline_at": 1787825189
}
```

### 请求体 [#请求体]

| 字段                     | 类型        | 说明                                          |
| ---------------------- | --------- | ------------------------------------------- |
| `model`                | string    | **必填**                                      |
| `prompt`               | string    | 必填，**除非**提供了参考图、首帧、参考视频/音频或 `ref_video_url` |
| `seconds`              | number    | 时长。`duration` 是同义字段                         |
| `resolution`           | string    | `480p`、`720p`、`1080p`、`4k`，以模型公布的为准         |
| `size`                 | string    | 如 `"1920x1080"`，`resolution` 的替代写法          |
| `aspect_ratio`         | string    | 如 `"16:9"`。`ratio` 是同义字段                    |
| `fps`                  | integer   | 不能为负                                        |
| `seed`                 | integer   |                                             |
| `negative_prompt`      | string    |                                             |
| `camera_fixed`         | boolean   |                                             |
| `watermark`            | boolean   |                                             |
| `input_reference`      | 参考素材      | 单张参考图，不是首帧                                  |
| `reference_image_urls` | 参考素材数组    | 参考图列表，支持的模型上生效；与首尾帧语义独立                     |
| `image_url`            | 参考素材      | 单张首帧图的兼容别名；多张参考图请用 `reference_image_urls`   |
| `first_frame`          | 参考素材      | 首帧图。`image` 是同义字段                           |
| `last_frame`           | 参考素材      | 尾帧，支持的模型上生效                                 |
| `reference_videos`     | string\[] | 参考视频 URL 数组，支持多参模型的模型上生效                    |
| `reference_audios`     | string\[] | 参考音频 URL 数组，支持多参模型的模型上生效                    |
| `generate_audio`       | boolean   | 是否生成原生音频；仅支持该能力的模型生效                        |
| `ref_video_url`        | string    | 源视频，支持视频生视频的模型上生效；`video_url` 是兼容别名         |
| `extra`                | object    | 厂商透传                                        |

`input_reference` / `reference_image_urls` 始终表示参考图；`image_url`、`first_frame` /
`image` 和 `last_frame` 始终表示帧。它们不会互相转换；
模型不支持对应语义时返回 400。`reference_videos` / `reference_audios` 只接受 URL，建议先通过
`POST /v1/files` 上传较大的素材，再把返回的 URL 放进数组。

请求体上限 **24 MiB**——因为图像可能以 base64 `data:` URL 的形式内嵌在里面。

### 参考素材 [#参考素材]

`input_reference`、`reference_image_urls`、`image_url`、`first_frame`、`image` 和 `last_frame`
各接受三种写法：

```json
{ "input_reference": "https://example.com/reference.png" }
```

```json
{ "first_frame": "data:image/png;base64,iVBORw0KGgo…" }
```

```json
{ "input_reference": { "url": "https://example.com/frame.png" } }
```

```json
{ "input_reference": { "b64": "iVBORw0KGgo…", "mime_type": "image/png" } }
```

裸 base64 字符串在解码后能被识别为图片时也接受——有几家 SDK 就是这么传原始字节的。其余情况返回
`400 invalid_input_reference`。

每个 `http(s)` URL 都走受保护的抓取器，拒绝内网与链路本地地址，并逐跳复核重定向。

表中没有列出的模型专属字段也会保留并透传（可以直接放在顶层，或放在 `extra` 对象里），例如
`cfg_scale`、`camera_control`、`output_format`。媒体 URL 仍会经过同样的安全校验；模型不支持的字段由对应适配器返回明确的 400。

## 计价形态 [#计价形态]

视频按**每秒产出**计价，再乘以分辨率比率。请从[模型卡片](/zh/api-reference/models)读
`pricing.credits.per_second` 和 `pricing.resolution_ratio`；同一模型下，一秒 1080p 常常是一秒
480p 的好几倍。部分模型还有 `flag_ratio`，用来给请求字段表达不了的维度计价——比如默认会生成一条
音轨、或者带了参考视频——这也是为什么提交后的估算可能高于「`per_second × seconds`」的直觉值。

## 取成片 [#取成片]

状态查 `GET /v1/jobs/{job_id}`，签名 MP4 链接查 `GET /v1/jobs/{job_id}/assets`。状态接口保留一个可读别名：

```bash
# 与 GET /v1/jobs/{job_id} 是同一个 handler
curl https://hypit.ai/v1/videos/job_2f81c0a4d3b57e9016ab24cf \
  -H "Authorization: Bearer $HYPIT_API_KEY"

# 列出资产，再下载上面返回的 `items[0].url`（下载时不要带 API key）
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf/assets \
  -H "Authorization: Bearer $HYPIT_API_KEY"
```

签名 URL 有效期 15 分钟，支持 HTTP Range 请求，可以直接交给 `<video>` 元素。需要新链接就再调一次
assets 路由；产物保留 30 天。

## 完整流程 [#完整流程]

```python
import os, time, httpx

BASE = os.environ.get("HYPIT_BASE_URL", "https://hypit.ai/v1")
AUTH = {"Authorization": f"Bearer {os.environ['HYPIT_API_KEY']}"}

created = httpx.post(
    f"{BASE}/videos",
    headers={**AUTH, "Idempotency-Key": "demo-clip-0001"},
    json={
        "model": "bytedance/seedance-2",
        "prompt": "一只纸船顺着雨水沟漂走",
        "seconds": 5,
        "resolution": "720p",
    },
    timeout=60,
)
created.raise_for_status()
job_id = created.json()["id"]
print("job", job_id)

delay = 5.0
while True:
    job = httpx.get(f"{BASE}/jobs/{job_id}", headers=AUTH, timeout=30).json()
    print(job["status"], job["progress"])
    if job["status"] in {"succeeded", "failed", "queue_expired"}:
        break
    time.sleep(delay)
    delay = min(delay * 1.3, 15.0)

if job["status"] != "succeeded":
    raise RuntimeError(f"{job['status']}: {job.get('error_code')} {job.get('error')}")

asset = httpx.get(f"{BASE}/jobs/{job_id}/assets", headers=AUTH, timeout=30).json()["items"][0]
with httpx.stream("GET", asset["url"], follow_redirects=True, timeout=None) as src:
    src.raise_for_status()
    with open("clip.mp4", "wb") as f:
        for chunk in src.iter_bytes():
            f.write(chunk)
```

```js
import { writeFile } from "node:fs/promises";

const BASE = process.env.HYPIT_BASE_URL ?? "https://hypit.ai/v1";
const AUTH = { Authorization: `Bearer ${process.env.HYPIT_API_KEY}` };
const TERMINAL = new Set(["succeeded", "failed", "queue_expired"]);

const created = await fetch(`${BASE}/videos`, {
  method: "POST",
  headers: { ...AUTH, "Content-Type": "application/json", "Idempotency-Key": "demo-clip-0001" },
  body: JSON.stringify({
    model: "bytedance/seedance-2",
    prompt: "一只纸船顺着雨水沟漂走",
    seconds: 5,
    resolution: "720p",
  }),
});
if (!created.ok) throw new Error(`${created.status} ${await created.text()}`);
const { id } = await created.json();

let job, delay = 5_000;
for (;;) {
  job = await (await fetch(`${BASE}/jobs/${id}`, { headers: AUTH })).json();
  console.log(job.status, job.progress);
  if (TERMINAL.has(job.status)) break;
  await new Promise((r) => setTimeout(r, delay));
  delay = Math.min(delay * 1.3, 15_000);
}
if (job.status !== "succeeded") throw new Error(`${job.status}: ${job.error_code} ${job.error}`);

const { items } = await (await fetch(`${BASE}/jobs/${id}/assets`, { headers: AUTH })).json();
await writeFile("clip.mp4", Buffer.from(await (await fetch(items[0].url)).arrayBuffer()));
```

## 这条路由特有的错误 [#这条路由特有的错误]

| 状态    | `code`                                               | 含义                                     |
| ----- | ---------------------------------------------------- | -------------------------------------- |
| `400` | `missing_model`                                      | `model` 必填                             |
| `400` | `missing_prompt`                                     | `prompt`、参考图、首帧、`ref_video_url` 一个都没提供 |
| `400` | `invalid_seconds`                                    | `seconds` 不能为负                         |
| `400` | `invalid_fps`                                        | `fps` 不能为负                             |
| `400` | `invalid_input_reference`                            | 参考素材不是 URL、`data:` URL 或 base64 图片     |
| `400` | `invalid_json`                                       | 请求体不是合法 JSON                           |
| `400` | `invalid_media_url` / `unsupported_media_url_scheme` | URL 未通过受保护抓取策略                         |
| `413` | `body_too_large`                                     | 请求体超过 24 MiB                           |
| `404` | `model_not_found`                                    | 没有启用的上游为这个模型提供视频能力                     |