# 视频生成 API 使用说明（凭证用户版）

本文档面向持有**视频凭证**（卡密 `XXXX-XXXX-XXXX` 或 `dbk_` 开头的 API Key）的开发者。
用 HTTP 接口提交视频生成任务、查询进度、下载成片，与网页端 `https://alliex.space/user` 走的是同一套后端。
下文所有「输入 / 输出示例」均来自**真实 API 实测**（2026-08-14，图转视频·多角色 @参考图）。

- **服务地址**：`https://alliex.space`
- **数据格式**：JSON（参考图上传用 `multipart/form-data`）
- **认证**：`Authorization: Bearer <凭证>`，卡密与 API Key 用法完全相同

> 没有凭证？找管理员（微信号：xin_1696891425开通即可。凭证即「次数」，每次成功出片扣 1 次。

---

## 1. 快速开始（curl 三步）

```bash
BASE=https://alliex.space
CRED=XXXX-XXXX-XXXX        # 换成你的卡密或 dbk_ API Key

# ① 提交文转视频任务
curl -s $BASE/api/video \
  -H "Authorization: Bearer $CRED" -H "Content-Type: application/json" \
  -d '{"prompt":"一只小猫追蝴蝶，阳光下的草地","ratio":"9:16","duration":10,"mode":"t2v"}'

# ② 用返回的 job id 查状态（等待 status=done）
curl -s $BASE/api/video/<job_id> -H "Authorization: Bearer $CRED"

# ③ 查出片（命中该会话素材），再下载
curl -s -X POST $BASE/api/video/<job_id>/query -H "Authorization: Bearer $CRED"
curl -s -o out.mp4 "$BASE/api/assets/media/job/<job_id>?item=0&filename=out.mp4" \
  -H "Authorization: Bearer $CRED"

# ④ 随时查剩余次数
curl -s $BASE/api/card/info -H "Authorization: Bearer $CRED"
```

提交后**立即**返回任务（`status=queued`），后台自动受理并生成，最终 `done` 或 `error`。
期间用 `GET /api/video/{id}` 轮询，不用重复提交。

---

## 2. 额度说明（冻结制）

额度 = 凭证还能生成几次视频；生成图片按张计费（0.02 额度 / 张），所以额度**可以有两位小数**。

| 概念 | 说明 |
|---|---|
| `quota` | 总额度里的**整数视频次数**（管理员分配；保留给旧客户端读取） |
| `quota_display` | 总额度（含小数零头，如 `8.5`）——**展示请用它** |
| `used` | 已成功出片的次数 + 已生成图片折算的额度（可能有小数） |
| `reserved` | 冻结中（已提交、尚未出片的额度） |
| `remaining` | 剩余可提交额度 = `quota_display − used − reserved` |

- 提交任务时按 `count` **先冻结**额度；任务成功出片 → 扣 1 次；失败 / 取消 → **自动退回**。
- 冻结中的额度不可再提交（避免重复占用）。
- 额度不足 → `403`，提示「剩余 X 次，本次需 N 次」。
- `remaining < 1` 时提交不了视频（不足一次），但仍可能够生成图片。

**真实输出**（实测：提交 1 个任务后查询）：

```json
{
  "ok": true,
  "code": "dbk_11cb9459",
  "quota": 5,
  "quota_display": 5,
  "used": 0,
  "reserved": 1,
  "remaining": 4,
  "status": "active",
  "created": 1786691041,
  "last_used": null,
  "kind": "api"
}
```

> `code` 是脱敏前缀（只显示前 10 位）；`reserved: 1` 表示刚提交的 1 个任务正在冻结；出片成功后会变成 `used: 1`、`reserved: 0`。
> 只有整数额度时 `quota_display` 与 `quota` 相同（都是整数）；管理员设过小数额度时两者才不同（如 `quota: 8` 而 `quota_display: 8.5`）。

**常见误解**：提交后刷新网页 / 断开请求，任务依然在服务端继续生成，额度照常冻结，不会「白嫖」——退款只会因任务最终失败而触发。

---

## 3. 接口总览

| 接口 | 说明 |
|---|---|
| `POST /api/video` | 提交视频生成 |
| `GET /api/video/{id}` | 查任务状态（仅自己的任务） |
| `POST /api/video/{id}/query` | 手动查出片（仅自己的任务） |
| `GET /api/jobs` | 自己的任务列表 |
| `GET /api/card/info` | 查本凭证额度 |
| `POST /api/upload` / `POST /api/upload-zip` | 上传参考图（`.zip` 批量） |
| `GET /api/refs` / `DELETE /api/refs` | 参考图列表 / 清空（仅自己的图） |
| `GET /api/preview` | 参考图预览缩略图 |
| `GET /api/assets/media/job/{job_id}` | 按任务 ID 播放 / 下载素材 |
| `POST /api/assets/pack/job/{job_id}` | 按任务 ID 打包素材 ZIP |
| `POST /api/jobs/{id}/restore` | 任务「重新生成」：回档该任务的提示词与参考图 |
| `POST /api/jobs/{id}/hide` / `POST /api/jobs/hide-all` | 软删除任务卡片（从列表隐藏，任务本体保留） |
| `GET /api/archives` / `POST /api/archives` | 存档库：列出 / 保存配置（按凭证隔离） |
| `POST /api/archives/{id}/restore` | 回档存档（提示词 + 参考图原样恢复） |
| `DELETE /api/archives/{id}` / `DELETE /api/archives` | 删除单个 / 清除全部存档 |

> 所有素材下载都走「按任务 ID」代理，上游源地址不暴露给客户端。

---

## 4. POST /api/video —— 提交视频生成

### 请求体（JSON）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `prompt` | string | ✅ | 提示词。超过 5000 字会被拒绝（除非 `nolimit: true`） |
| `mode` | string | | 二选一：`t2v` 文转视频（不带参考图）/ `r2v` 图转视频（必须带 `images`）；不传或传其它值 → 按是否带图自动判断 |
| `model` | string | | 视频模型标识，当前固定 `doubao-seedance-2-5-260628`（默认）；不传即用当前模型，传其它值一律忽略。响应不返回 `model` 字段 |
| `images` | array | 图转视频 | `[{"store_uri":"…","name":"a.png","account":0}]`，由 `/api/upload` 获得 |
| `ratio` | string | | 画面比例：`16:9` / `21:9` / `9:16` / `4:3` / `1:1`，默认 `9:16` |
| `duration` | int | | 3–30 秒，默认 5 |
| `count` | int | | 一次提交 N 个独立任务（各开新会话），1–10，默认 1，按此数冻结额度 |
| `nolimit` | bool | | `true` 跳过 5000 字上限校验 |
| `two_step` | bool | | 两段式（先图后指令）**暂未开放**：传 `true` 也会被忽略，按一步式处理 |

### 示例一：文转视频（t2v）

请求体（JSON）：

```json
{
  "prompt": "夕阳下的海边，海浪拍打礁石",
  "ratio": "9:16",
  "duration": 10,
  "mode": "t2v",
  "count": 2
}
```

### 示例二：图转视频（r2v，多角色 @参考图 引用）—— 本次真实实测

先上传 5 张参考图（见第 7 节），拿到各自的 `store_uri` 后，请求体：

```json
{
  "prompt": "30秒音乐跳舞视频，16:9，先是个人solo，出场顺序为第一 @参考图4【D】 、第二 @参考图3【C】 、第三 @参考图1【A】 ，然后 @参考图2【B】 @参考图5【E】 同时出场。最后 @参考图1【A】 和 @参考图2【B】 分别牵着 @参考图4【D】 和 @参考图3【C】 的手一起跳起来",
  "ratio": "16:9",
  "duration": 30,
  "mode": "r2v",
  "count": 1,
  "images": [
    { "store_uri": "tos-cn-i-a9rns2rl98/053569abe5ca4db5b410121c9ad40b44.png", "name": "A.png", "account": 0 },
    { "store_uri": "tos-cn-i-a9rns2rl98/cd38141b2a0044e1a47b621cc1a7a0e1.png", "name": "B.png", "account": 0 },
    { "store_uri": "tos-cn-i-a9rns2rl98/067674a008234440a46b3b97198e8f3c.png", "name": "C.png", "account": 0 },
    { "store_uri": "tos-cn-i-a9rns2rl98/17a12e2112824eddbf02994254d2ec8c.png", "name": "D.png", "account": 0 },
    { "store_uri": "tos-cn-i-a9rns2rl98/5f775b8a6d0e4ef28ea50ae6037b9f76.png", "name": "E.png", "account": 0 }
  ]
}
```

> 提示词里的 `@参考图1【A】…@参考图5【E】` 与 `images` 数组一一对应（第 1 张 = A，第 5 张 = E）。多角色场景把每张图的「角色编号 + 文件名」写进提示词即可。

### 响应（真实实测）

```json
{
  "jobs": [
    {
      "id": "8ef298d3abab",
      "status": "queued",
      "prompt": "30秒音乐跳舞视频，16:9，先是个人solo，出场顺序为第一 @参考图4【D】 、第二 @参考图3【C】 、第三 @参考图1【A】 ，然后 @参考图2【B】 @参考图5【E】 同时出场。最后 @参考图1【A】 和 @参考图2【B】 分别牵着 @参考图4【D】 和 @参考图3【C】 的手一起跳起来",
      "ratio": "16:9",
      "duration": 30,
      "mode": "r2v",
      "image_uris": [
        "tos-cn-i-a9rns2rl98/eca87887a4e4487b9816b9213607f83f.png",
        "tos-cn-i-a9rns2rl98/4078242fe44a418ebf5504f7f4938482.png",
        "tos-cn-i-a9rns2rl98/e7d4f9ae0e2f481486dc3eff1eb8aada.png",
        "tos-cn-i-a9rns2rl98/0a4cacf8f77c4fd98bee7e2414b8f6d4.png",
        "tos-cn-i-a9rns2rl98/2c79e935999d472ea582e1c9a80dc34e.png"
      ],
      "image_names": ["A.png", "B.png", "C.png", "D.png", "E.png"],
      "image_name": null,
      "created": 1786691107,
      "done_at": null,
      "progress": "",
      "error": null,
      "detail": "任务已排队",
      "attempts": 0
    }
  ]
}
```

> **注意**：任务里的 `image_uris` 是服务端把参考图**重绑到实际出片账号**后的新 TOS URI，与第 7 节上传响应返回的 `store_uri` **不一定相同**——这是正常的，客户端不要用上传时的 `store_uri` 去比对任务里的 `image_uris`。任务里只透出白名单字段（无 `model` / `conversation_id` / `url` 等内部字段）。

### 常见错误

```json
{ "error": "prompt 不能为空" }                                    // 400
{ "error": "server_busy" }                                        // 409 服务器额度繁忙，稍后重试
{ "error": "卡密剩余额度不足（剩余 X 次，本次需 N 次）" }          // 403
{ "error": "凭证无效" }                                           // 401
```

### 错误码速查（繁忙 / 账号类失败）

服务器繁忙、账号不可用这类失败，`error` 与 `detail` 字段返回**机器可读错误码**（无中文文案），便于程序判断后处理：

| 错误码 | 含义 | 建议处理 |
|---|---|---|
| `server_busy` | 服务器额度繁忙（所有账号都在生成或今日额度用完） | 稍后重试 |
| `account_busy` | 指定账号正在生成视频 | 稍后重试 |
| `account_quota_exhausted` | 指定账号今日额度已用完 | 换账号或明日再试 |
| `group_busy` | 所选小组暂无可用账号 | 稍后重试 / 换组 |
| `no_account` | 服务器无可用账号 | 联系管理员 |

其余失败（额度不足、版权、参数错误、超时等）`error` / `detail` 仍为中文说明。

---

## 5. GET /api/video/{id} —— 查任务状态

```bash
curl -s $BASE/api/video/<job_id> -H "Authorization: Bearer $CRED"
```

任务状态机：

```
queued → accepting → generating → done | error
```

| 状态 | 含义 |
|---|---|
| `queued` | 已受理，排队中 |
| `accepting` | 正在提交到生成服务 |
| `generating` | 生成中（通常需要几分钟） |
| `done` | 已出片 |
| `error` | 失败（见 `error` 字段说明，额度已退回） |

### 真实输出一：生成中（generating）

```json
{
  "id": "8ef298d3abab",
  "status": "generating",
  "prompt": "30秒音乐跳舞视频，16:9，先是个人solo，出场顺序为第一 @参考图4【D】 、第二 @参考图3【C】 、第三 @参考图1【A】 ，然后 @参考图2【B】 @参考图5【E】 同时出场。最后 @参考图1【A】 和 @参考图2【B】 分别牵着 @参考图4【D】 和 @参考图3【C】 的手一起跳起来",
  "ratio": "16:9",
  "duration": 30,
  "mode": "r2v",
  "image_uris": [ "…（同上，5 个）…" ],
  "image_names": ["A.png", "B.png", "C.png", "D.png", "E.png"],
  "image_name": null,
  "created": 1786691107,
  "done_at": null,
  "progress": "视频正在生成中，耐心等待",
  "error": null,
  "detail": "已受理，正在生成视频",
  "attempts": 1
}
```

### 真实输出二：已出片（done，本次实测用时 7 分 35 秒）

```json
{
  "id": "8ef298d3abab",
  "status": "done",
  "prompt": "30秒音乐跳舞视频，16:9，先是个人solo，出场顺序为第一 @参考图4【D】 、第二 @参考图3【C】 、第三 @参考图1【A】 ，然后 @参考图2【B】 @参考图5【E】 同时出场。最后 @参考图1【A】 和 @参考图2【B】 分别牵着 @参考图4【D】 和 @参考图3【C】 的手一起跳起来",
  "ratio": "16:9",
  "duration": 30,
  "mode": "r2v",
  "image_uris": [ "…（同上，5 个）…" ],
  "image_names": ["A.png", "B.png", "C.png", "D.png", "E.png"],
  "image_name": null,
  "created": 1786691107,
  "done_at": 1786691562,
  "progress": "视频成功生成，本次用时：7分钟35秒，点击右下角查询按钮获取视频",
  "error": null,
  "detail": "视频已生成",
  "attempts": 1
}
```

> `detail` 为当前阶段状态说明（如生成中显示「已受理，正在生成视频」），仅作展示；**判断任务请以 `status` 为准**，不要解析 `detail` 文案。
> 出片后任务列表 `GET /api/jobs` 返回同样的字段结构（`jobs` 数组包裹）。

---

## 6. POST /api/video/{id}/query —— 查出片 + 下载

任务进入 `generating` 一段时间后即可主动查出片，命中后自动置 `done`：

```bash
curl -s -X POST $BASE/api/video/<job_id>/query -H "Authorization: Bearer $CRED"
```

未出片时：

```json
{ "ok": false, "error": "暂未查询到该任务视频，请耐心等待或者重新提交" }
```

命中后（**真实实测**：本次任务出片 1 个 1280×720 视频）：

```json
{
  "ok": true,
  "video_items": [
    {
      "type": "video",
      "name": "video.mp4",
      "node_id": "52603089981680386",
      "key": "v0269cg10004d9vburi7dldfo4tpdu40",
      "message_id": "52601107523635458",
      "conversation_id": "38437745109779458",
      "duration": 30.042,
      "create_time": 1786691502,
      "ai_skill_status": 0,
      "width": 1280,
      "height": 720,
      "account_index": 32,
      "has_src": true,
      "has_cover": true,
      "ext": ".mp4"
    }
  ],
  "total": 1,
  "items": [ "…（与 video_items 同结构）…" ]
}
```

> `items` 数组下标与后续素材代理的 `item` 参数一一对应。`has_src` 标记该素材可下载，`ext` 是扩展名。`node_id`/`key`/`message_id`/`conversation_id`/`create_time`/`ai_skill_status`/`account_index` 为服务端内部标识，客户端**不需要使用**，下载 / 打包只认 `items` 下标。

### 下载素材

```bash
# 下载第 0 个素材（带文件名 → 浏览器/工具会存成文件）
curl -s -o out.mp4 "$BASE/api/assets/media/job/<job_id>?item=0&filename=out.mp4" \
  -H "Authorization: Bearer $CRED"

# 取封面（kind=cover；无封面回退素材本体）
curl -s -o cover.jpg "$BASE/api/assets/media/job/<job_id>?item=0&kind=cover&filename=cover.jpg" \
  -H "Authorization: Bearer $CRED"

# 打包素材为一个 ZIP（items 可加多项，本次实测只打 1 个视频）
curl -s -X POST $BASE/api/assets/pack/job/<job_id> \
  -H "Authorization: Bearer $CRED" -H "Content-Type: application/json" \
  -d '{"items":[{"index":0,"type":"video"}]}' -o all.zip
```

**真实输出**（本次实测，返回 200 + ZIP 流）：

```
HTTP 200；ZIP 内文件：8ef298d3abab_video_1.mp4（36513771 字节，视频按 STORED 存储）
下载单素材 out.mp4：36513771 字节，合法 MP4（30 秒 1280×720）
```

---

## 7. 图转视频：先传参考图

```bash
# 单张
curl -s -F "file=@ref.png" $BASE/api/upload -H "Authorization: Bearer $CRED"

# 批量 .zip（压缩包内多张图片）
curl -s -F "file=@refs.zip" $BASE/api/upload-zip -H "Authorization: Bearer $CRED"
```

返回（**响应不含 `ok` 字段**，成功以 `store_uri` 存在为准）。**真实实测**（A.png，1596779 字节）：

```json
{
  "store_uri": "tos-cn-i-a9rns2rl98/053569abe5ca4db5b410121c9ad40b44.png",
  "name": "A.png",
  "size": 1596779,
  "account_index": 0
}
```

把 `store_uri`、`account_index` 填进 `/api/video` 的 `images`：

```bash
curl -s $BASE/api/video \
  -H "Authorization: Bearer $CRED" -H "Content-Type: application/json" \
  -d '{"prompt":"让画面中的人物跳起舞来","mode":"r2v","images":[{"store_uri":"tos-cn-i-a9rns2rl98/053569abe5ca4db5b410121c9ad40b44.png","name":"a.png","account":0}]}'
```

- 单张 ≤30MB；请求体整体 ≤64MB；**最多 30 张**。
- 参考图按凭证隔离，只自己可见；`GET /api/refs` 可查、`DELETE /api/refs` 可清空。

---

## 8. Python 客户端

仓库内 `api_client.py` 同时提供命令行与库两种用法。

### 命令行（提交并自动等待出片，快速测试）

```bash
python api_client.py --base https://alliex.space --api-key XXXX-XXXX-XXXX \
  --prompt "一只小猫追蝴蝶" --ratio 9:16 --duration 10 --mode t2v
```

> 命令行提交后会自动等待出片。注意凭证用户拿不到直链，**下载成片请用下面的库模式**（`query()` + `download_asset()`）。

### 库模式（推荐，可逐步控制）

```python
import asyncio
from api_client import VideoGenAPI

async def main():
    api = VideoGenAPI(base_url="https://alliex.space", api_key="XXXX-XXXX-XXXX")
    # 提交文转视频，拿到任务
    jobs = await api.submit("一只小猫追蝴蝶", ratio="9:16", duration=10, mode="t2v")
    job_id = jobs[0]["id"]
    # 轮询直到出片 / 失败（默认最多等 35 分钟）
    job = await api.wait_for_done(job_id)
    if job.get("status") != "done":
        print("任务失败：", job.get("error"))
        return
    # 查出片并下载第 0 个素材
    await api.query(job_id)
    await api.download_asset(job_id, item=0, out_path="out.mp4")

asyncio.run(main())
```

### 网络环境 TLS 被重置时：用 curl_cffi 模拟浏览器指纹

个别网络链路（海外访问 / 运营商 / 企业防火墙）会对「非浏览器 TLS 指纹」的连接做重置，普通 `curl` / `httpx` 可能报 `Recv failure: Connection was reset` / `SSL: UNEXPECTED_EOF_WHILE_READING`。**服务端接受标准 TLS 1.2/1.3、不挑客户端**（curl、openssl、浏览器均验证通过），上述现象是链路中间设备的指纹干扰，可用 `curl_cffi` 模拟 Chrome 指纹绕过：

```python
from curl_cffi import requests
from curl_cffi import CurlMime

s = requests.Session(impersonate="chrome")   # 模拟 Chrome TLS 指纹
BASE = "https://alliex.space"
H = {"Authorization": "Bearer XXXX-XXXX-XXXX"}

# 提交文转视频（json= 用法同 requests）
r = s.post(f"{BASE}/api/video", headers=H,
           json={"prompt": "一只小猫追蝴蝶", "ratio": "9:16", "duration": 10, "mode": "t2v"})

# 上传参考图：curl_cffi 的 multipart 必须传 CurlMime() 对象，不能用普通 dict / files=
mime = CurlMime(s)
mime.addpart(name="file", filename="ref.png", local_path="ref.png", content_type="image/png")
r = s.post(f"{BASE}/api/upload", headers=H, multipart=mime)
data = r.json()
store_uri = data.get("store_uri")   # 成功以 store_uri 存在为准（响应无 ok 字段）
```

> 注意：`/api/upload`、`/api/upload-zip` 的响应**不含 `ok` 字段**，请以返回的 `store_uri` 是否存在判断成功。

---

## 9. 常见问题

| 问题 | 说明 |
|---|---|
| 提交后额度被冻结，任务失败了会退回吗 | 会。失败 / 取消 / 服务中断都会自动退回冻结额度 |
| 一直显示 `generating` | 生成通常需几分钟；超 35 分钟会自动判失败并退回额度，刷新（Ctrl+F5）看最新状态 |
| 怎么拿成片 | `POST /api/video/{id}/query` 查出片后，用素材代理下载（第 6 节） |
| 任务列表在哪 | `GET /api/jobs`（只返回自己凭证创建的任务） |
| 想重新用某次的提示词 / 参考图 | `POST /api/jobs/{id}/restore` 回档到表单，或存档库 `POST /api/archives` 手动存一份 |
| 凭证被禁用 / 吊销 | 新提交返回 401；服务端会取消该凭证所有在途任务并退回冻结额度 |
| `count` 能多大 | 1–10（网页端按钮组最多 4，API 可到 10，按实际次数冻结额度） |
| 为什么任务里 `image_uris` 跟上传时返回的 `store_uri` 不一样 | 服务端把参考图重绑到实际出片账号，URI 会变；只用于展示，不影响出片 |
| curl / httpx 连不上，报 `SSL 被重置` / `UNEXPECTED_EOF_WHILE_READING` | 服务端不限制客户端类型；多为网络链路对非浏览器 TLS 指纹的干扰。用 `curl_cffi` 的 `impersonate="chrome"` 即可（完整示例见第 8 节） |
| 上传接口没有 `ok` 字段 | 正常——上传成功以返回的 `store_uri` 是否存在判断，不要用 `data.get("ok")` 判断 |
