From 946fdaeb53a6e174adcc67b5f1aac2bf65b64cff Mon Sep 17 00:00:00 2001 From: aabao <602018325@qq.com> Date: Wed, 8 Apr 2026 16:32:13 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kilocode/setup-script | 39 ++ docs/openapi/doubao-video-api.yaml | 623 +++++++++++++++++++++++++++++ 2 files changed, 662 insertions(+) create mode 100644 .kilocode/setup-script create mode 100644 docs/openapi/doubao-video-api.yaml diff --git a/.kilocode/setup-script b/.kilocode/setup-script new file mode 100644 index 000000000000..24b867a49da4 --- /dev/null +++ b/.kilocode/setup-script @@ -0,0 +1,39 @@ +#!/bin/bash +# Kilo Code Worktree Setup Script +# This script runs before the agent starts in a worktree (new sessions only). +# +# Available environment variables: +# WORKTREE_PATH - Absolute path to the worktree directory +# REPO_PATH - Absolute path to the main repository +# +# Example tasks: +# - Copy .env files from main repo +# - Install dependencies +# - Run database migrations +# - Set up local configuration + +set -e # Exit on error + +echo "Setting up worktree: $WORKTREE_PATH" + +# Uncomment and modify as needed: + +# Copy environment files +# if [ -f "$REPO_PATH/.env" ]; then +# cp "$REPO_PATH/.env" "$WORKTREE_PATH/.env" +# echo "Copied .env" +# fi + +# Install dependencies (Node.js) +# if [ -f "$WORKTREE_PATH/package.json" ]; then +# cd "$WORKTREE_PATH" +# npm install +# fi + +# Install dependencies (Python) +# if [ -f "$WORKTREE_PATH/requirements.txt" ]; then +# cd "$WORKTREE_PATH" +# pip install -r requirements.txt +# fi + +echo "Setup complete!" diff --git a/docs/openapi/doubao-video-api.yaml b/docs/openapi/doubao-video-api.yaml new file mode 100644 index 000000000000..1fa205ed8174 --- /dev/null +++ b/docs/openapi/doubao-video-api.yaml @@ -0,0 +1,623 @@ +openapi: 3.1.0 +info: + title: 豆包视频生成 API + description: | + 基于 new-api 网关的豆包视频生成接口文档。 + + 支持的豆包视频模型(`DoubaoVideo` 渠道类型 54): + - `doubao-seedance-1-0-pro-250528` + - `doubao-seedance-1-0-lite-t2v`(仅文生视频) + - `doubao-seedance-1-0-lite-i2v`(仅图生视频) + - `doubao-seedance-1-5-pro-251215` + - `doubao-seedance-2-0-260128` + - `doubao-seedance-2-0-fast-260128` + + **计费说明**: + - 计费单位为系统配额(Quota),由管理员为每个模型配置 `ModelRatio`(每次任务的基础价格) + - 当请求中的 `metadata.content` 包含 `video_url` 类型的内容(即图生视频/视频续拍)时,系统会自动应用视频输入折扣: + - `doubao-seedance-2-0-260128`:折扣比率 ≈ 0.6087(≈6折) + - `doubao-seedance-2-0-fast-260128`:折扣比率 ≈ 0.5946(≈6折) + - 视频生成为异步任务,提交时即预扣费,任务失败会自动退款 + + **认证**:所有接口均需在请求头中携带 API Token,格式:`Authorization: Bearer sk-xxxx` + + version: "1.0.0" + contact: + name: API Support + +servers: + - url: https://api.example.com + description: new-api 网关地址(请替换为实际地址) + +tags: + - name: 视频生成 + description: 豆包视频生成任务提交与查询 + - name: 视频内容 + description: 已完成视频的内容下载 + +paths: + /v1/video/generations: + post: + tags: + - 视频生成 + summary: 提交视频生成任务 + description: | + 向豆包视频模型提交一个视频生成任务(异步)。 + + 提交后立即返回任务 ID,视频不会立刻生成完毕。 + 请使用返回的 `id`(即 `task_id`)轮询 `GET /v1/video/generations/{task_id}` 查询结果。 + + **支持的场景**: + - 文生视频:仅提供 `prompt`,不传 `image` + - 图生视频:提供 `prompt` 和 `image`(图片 URL),或在 `metadata.content` 中传入图片 + - 视频续拍/参考:在 `metadata.content` 中传入 `video_url` 类型内容(会触发视频输入折扣) + + operationId: submitVideoGeneration + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VideoGenerationRequest' + examples: + 文生视频: + summary: 基础文生视频 + value: + model: doubao-seedance-2-0-260128 + prompt: 宇航员在月球表面慢慢行走,地球悬挂在黑色天空中 + seconds: "5" + metadata: + ratio: "16:9" + 图生视频: + summary: 图生视频(含参考图片) + value: + model: doubao-seedance-2-0-260128 + prompt: 让图片中的人物微笑并转身 + image: "https://example.com/reference.jpg" + seconds: "5" + metadata: + ratio: "16:9" + 视频续拍(含视频输入折扣): + summary: 基于已有视频片段续拍 + value: + model: doubao-seedance-2-0-260128 + prompt: 继续往前走,走向远处的山丘 + metadata: + content: + - type: video_url + video_url: + url: "https://example.com/input.mp4" + seconds: "5" + 高级参数: + summary: 完整参数示例 + value: + model: doubao-seedance-2-0-260128 + prompt: 一朵玫瑰花在阳光下缓缓绽放,微风吹拂 + seconds: "8" + metadata: + ratio: "16:9" + resolution: "720p" + seed: 42 + watermark: false + camera_fixed: false + responses: + "200": + description: 任务提交成功,返回任务信息(此时任务仍在生成中,status 为 queued) + content: + application/json: + schema: + $ref: '#/components/schemas/VideoTaskResponse' + example: + id: task_abc123def456 + task_id: task_abc123def456 + object: video + model: doubao-seedance-2-0-260128 + status: queued + progress: 0 + created_at: 1712345678 + "400": + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "401": + description: 未授权(Token 无效或缺失) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "402": + description: 账户余额不足 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "500": + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /v1/video/generations/{task_id}: + get: + tags: + - 视频生成 + summary: 查询视频生成任务状态 + description: | + 根据任务 ID 查询视频生成任务的当前状态和结果。 + + **任务状态说明**: + - `queued`:任务已提交,等待处理 + - `in_progress`:任务处理中 + - `completed`:任务已完成,可从 `metadata.url` 获取视频地址 + - `failed`:任务失败,查看 `error` 字段了解失败原因 + + **获取视频**: + - 任务 `completed` 后,`metadata.url` 字段包含视频的直接下载链接 + - 也可调用 `GET /v1/videos/{task_id}/content` 通过代理下载视频文件 + + **轮询建议**:每 5 秒查询一次,超时时间建议设置为 10 分钟。 + + operationId: getVideoGenerationTask + security: + - BearerAuth: [] + parameters: + - name: task_id + in: path + required: true + description: 提交任务时返回的任务 ID(`id` 字段) + schema: + type: string + example: task_abc123def456 + responses: + "200": + description: 查询成功(可能处于任意状态) + content: + application/json: + schema: + $ref: '#/components/schemas/VideoTaskResponse' + examples: + 处理中: + summary: 任务进行中 + value: + id: task_abc123def456 + task_id: task_abc123def456 + object: video + model: doubao-seedance-2-0-260128 + status: in_progress + progress: 50 + created_at: 1712345678 + 已完成: + summary: 任务完成,视频可下载 + value: + id: task_abc123def456 + task_id: task_abc123def456 + object: video + model: doubao-seedance-2-0-260128 + status: completed + progress: 100 + created_at: 1712345678 + completed_at: 1712345900 + metadata: + url: "https://cdn.example.com/videos/result.mp4" + 失败: + summary: 任务失败 + value: + id: task_abc123def456 + task_id: task_abc123def456 + object: video + model: doubao-seedance-2-0-260128 + status: failed + progress: 100 + created_at: 1712345678 + completed_at: 1712345700 + error: + code: content_policy_violation + message: 请求内容违反了内容安全策略 + "404": + description: 任务不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "401": + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /v1/videos: + post: + tags: + - 视频生成 + summary: 提交视频生成任务(OpenAI 兼容格式) + description: | + 与 `POST /v1/video/generations` 功能完全相同,为兼容 OpenAI Video API 格式而提供。 + 请求和响应格式与 `/v1/video/generations` 一致。 + operationId: submitVideoOpenAI + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VideoGenerationRequest' + responses: + "200": + description: 任务提交成功 + content: + application/json: + schema: + $ref: '#/components/schemas/VideoTaskResponse' + "400": + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "401": + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /v1/videos/{task_id}: + get: + tags: + - 视频生成 + summary: 查询任务状态(OpenAI 兼容格式) + description: | + 与 `GET /v1/video/generations/{task_id}` 功能相同,兼容 OpenAI Video API 格式。 + operationId: getVideoTaskOpenAI + security: + - BearerAuth: [] + parameters: + - name: task_id + in: path + required: true + description: 任务 ID + schema: + type: string + example: task_abc123def456 + responses: + "200": + description: 查询成功 + content: + application/json: + schema: + $ref: '#/components/schemas/VideoTaskResponse' + "404": + description: 任务不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /v1/videos/{task_id}/content: + get: + tags: + - 视频内容 + summary: 下载视频文件内容 + description: | + 通过代理方式下载已完成的视频文件内容(二进制流)。 + + **前置条件**:任务状态必须为 `completed`,否则返回 400 错误。 + + 此接口会: + 1. 查询数据库获取任务关联的视频 URL + 2. 通过服务器代理请求并转发视频内容给客户端 + 3. 响应头包含 `Cache-Control: public, max-age=86400`(24小时缓存) + + **支持认证方式**:Bearer Token 或 Session Cookie(Dashboard 登录用户均可访问) + + operationId: downloadVideoContent + security: + - BearerAuth: [] + - SessionAuth: [] + parameters: + - name: task_id + in: path + required: true + description: 任务 ID(`completed` 状态才可下载) + schema: + type: string + example: task_abc123def456 + responses: + "200": + description: 视频文件内容(二进制流) + content: + video/mp4: + schema: + type: string + format: binary + video/*: + schema: + type: string + format: binary + "400": + description: 任务未完成 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: + message: "Task is not completed yet, current status: in_progress" + type: invalid_request_error + "404": + description: 任务不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "502": + description: 上游视频 URL 不可用 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: "API Token,格式:Bearer sk-xxxx" + SessionAuth: + type: apiKey + in: cookie + name: session + description: Dashboard 登录后的 Session Cookie + + schemas: + VideoGenerationRequest: + type: object + required: + - model + - prompt + properties: + model: + type: string + description: | + 视频生成模型名称。支持的模型: + - `doubao-seedance-1-0-pro-250528` + - `doubao-seedance-1-0-lite-t2v`(仅文生视频) + - `doubao-seedance-1-0-lite-i2v`(仅图生视频) + - `doubao-seedance-1-5-pro-251215` + - `doubao-seedance-2-0-260128`(推荐) + - `doubao-seedance-2-0-fast-260128`(更快速) + example: doubao-seedance-2-0-260128 + enum: + - doubao-seedance-1-0-pro-250528 + - doubao-seedance-1-0-lite-t2v + - doubao-seedance-1-0-lite-i2v + - doubao-seedance-1-5-pro-251215 + - doubao-seedance-2-0-260128 + - doubao-seedance-2-0-fast-260128 + prompt: + type: string + description: 视频内容描述(文本提示词)。建议使用详细、具体的描述以获得更好的生成效果。 + example: 一朵玫瑰花在阳光下缓缓绽放,微风轻抚花瓣,背景是模糊的绿色草坪 + image: + type: string + description: | + 参考图片(图生视频时使用)。支持 HTTP/HTTPS URL 或 Base64 编码的图片数据。 + 当同时使用 `metadata.content` 中的图片时,此字段的图片会被添加到 `content` 数组前端。 + example: "https://example.com/reference-image.jpg" + seconds: + type: string + description: 视频时长(秒),以字符串形式传入。常用值为 "5" 或 "8"。 + example: "5" + metadata: + $ref: '#/components/schemas/VideoMetadata' + + VideoMetadata: + type: object + description: | + 豆包视频特定的扩展参数,通过 `metadata` 字段传入。 + 所有字段均为可选,未传入时使用模型默认值。 + properties: + ratio: + type: string + description: 视频宽高比 + example: "16:9" + enum: + - "16:9" + - "9:16" + - "1:1" + - "4:3" + - "3:4" + resolution: + type: string + description: 输出视频分辨率 + example: "720p" + enum: + - "480p" + - "720p" + - "1080p" + seed: + type: integer + description: 随机种子,固定相同种子可复现相似结果。范围:0 ~ 2147483647 + example: 42 + watermark: + type: boolean + description: 是否在视频上添加水印。默认为 false(不加水印)。 + example: false + camera_fixed: + type: boolean + description: 是否固定摄像机(镜头不移动)。默认为 false(允许镜头运动)。 + example: false + content: + type: array + description: | + 多模态内容数组,支持图片或视频输入。 + **注意**:当包含 `video_url` 类型条目时,系统会自动应用视频输入折扣计费: + - `doubao-seedance-2-0-260128`:约 6.1 折 + - `doubao-seedance-2-0-fast-260128`:约 5.9 折 + items: + $ref: '#/components/schemas/ContentItem' + return_last_frame: + type: boolean + description: 是否返回视频最后一帧图片(用于续拍场景) + example: false + generate_audio: + type: boolean + description: 是否生成配套音频 + example: false + draft: + type: boolean + description: 是否生成草稿(快速预览,质量较低) + example: false + service_tier: + type: string + description: 服务等级(影响优先级和价格) + example: default + callback_url: + type: string + description: 任务完成后的回调通知 URL(需配合豆包官方回调机制使用) + example: "https://your-server.com/webhook/video-callback" + + ContentItem: + type: object + required: + - type + properties: + type: + type: string + description: 内容类型 + enum: + - text + - image_url + - video_url + example: image_url + text: + type: string + description: 文本内容(type 为 text 时使用) + example: 背景是黄昏时分的海滩 + image_url: + type: object + description: 图片 URL(type 为 image_url 时使用) + properties: + url: + type: string + description: 图片的 HTTP/HTTPS URL 或 base64 数据 + example: "https://example.com/photo.jpg" + video_url: + type: object + description: 视频 URL(type 为 video_url 时使用,**会触发视频输入折扣**) + properties: + url: + type: string + description: 视频的 HTTP/HTTPS URL + example: "https://example.com/input-video.mp4" + + VideoTaskResponse: + type: object + description: 视频任务响应(提交后和查询时返回相同结构) + properties: + id: + type: string + description: 任务唯一标识符(由 new-api 系统生成,非豆包原始 ID) + example: task_abc123def456 + task_id: + type: string + description: 与 `id` 相同,兼容旧版接口(已废弃,请使用 `id`) + example: task_abc123def456 + deprecated: true + object: + type: string + description: 对象类型,固定为 "video" + example: video + model: + type: string + description: 使用的模型名称 + example: doubao-seedance-2-0-260128 + status: + type: string + description: | + 任务当前状态: + - `queued`:已提交,排队等待 + - `in_progress`:生成中 + - `completed`:已完成,可下载视频 + - `failed`:生成失败 + enum: + - queued + - in_progress + - completed + - failed + example: completed + progress: + type: integer + description: 任务进度(0-100 的整数) + minimum: 0 + maximum: 100 + example: 100 + created_at: + type: integer + format: int64 + description: 任务创建时间(Unix 时间戳,秒) + example: 1712345678 + completed_at: + type: integer + format: int64 + description: 任务完成时间(Unix 时间戳,秒)。仅在 status 为 completed 或 failed 时返回。 + example: 1712345900 + expires_at: + type: integer + format: int64 + description: 任务结果过期时间(Unix 时间戳) + example: 1712432300 + metadata: + type: object + description: 任务结果元数据 + properties: + url: + type: string + description: 视频文件的直接访问 URL(仅在 status 为 completed 时存在) + example: "https://cdn.example.com/videos/output-video.mp4" + error: + $ref: '#/components/schemas/VideoTaskError' + remixed_from_video_id: + type: string + description: 如果是 remix 任务,此字段为原始视频的任务 ID + example: task_origin789ghi012 + + VideoTaskError: + type: object + description: 视频任务失败时的错误详情 + properties: + code: + type: string + description: 错误码 + example: content_policy_violation + message: + type: string + description: 人类可读的错误描述 + example: 请求内容违反了内容安全策略,请修改 prompt 后重试 + + ErrorResponse: + type: object + properties: + error: + type: object + properties: + message: + type: string + description: 错误描述信息 + example: Invalid authentication token + type: + type: string + description: 错误类型 + example: invalid_request_error + code: + type: string + description: 错误码(部分场景下包含) + example: invalid_api_key From 4ab4aba89a2d1b49403068cf44074720418a25a0 Mon Sep 17 00:00:00 2001 From: aabao <602018325@qq.com> Date: Wed, 8 Apr 2026 16:52:09 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E4=BC=98=E5=8C=96sora-2-pro=20=E8=AE=A1?= =?UTF-8?q?=E8=B4=B9=EF=BC=8C=E6=94=AF=E6=8C=81=201080P?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/locales/zh-CN.yaml | 2 +- model/user.go | 2 +- relay/channel/task/sora/adaptor.go | 32 ++++++++++++++++++++++++++---- relay/common/relay_utils.go | 6 +++--- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml index 1f3b5a7b4bc5..d0fc9ea6b8b8 100644 --- a/i18n/locales/zh-CN.yaml +++ b/i18n/locales/zh-CN.yaml @@ -56,7 +56,7 @@ user.password_login_disabled: "管理员关闭了密码登录" user.register_disabled: "管理员关闭了新用户注册" user.password_register_disabled: "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册" user.username_or_password_empty: "用户名或密码为空" -user.username_or_password_error: "用户名或密码错误,或用户已被封禁" +user.username_or_password_error: "用户名或密码错误,请检查后重试" user.email_or_password_empty: "邮箱地址或密码为空!" user.exists: "用户名已存在,或已注销" user.not_exists: "用户不存在" diff --git a/model/user.go b/model/user.go index 1210b5435d04..402023dfb84b 100644 --- a/model/user.go +++ b/model/user.go @@ -604,7 +604,7 @@ func (user *User) ValidateAndFill() (err error) { DB.Where("username = ? OR email = ?", username, username).First(user) okay := common.ValidatePasswordAndHash(password, user.Password) if !okay || user.Status != common.UserStatusEnabled { - return errors.New("用户名或密码错误,或用户已被封禁") + return errors.New("用户名或密码错误,请检查后重试") } return nil } diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go index e9029aa20d46..5c71d381a4f1 100644 --- a/relay/channel/task/sora/adaptor.go +++ b/relay/channel/task/sora/adaptor.go @@ -95,6 +95,16 @@ func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycom } // EstimateBilling 根据用户请求的 seconds 和 size 计算 OtherRatios。 +// +// Sora 官方定价(按秒计费): +// +// sora-2 720p (720x1280 / 1280x720) $0.10/s +// sora-2-pro 720p (720x1280 / 1280x720) $0.30/s → size ratio = 1.0 (sora-2-pro 基准) +// sora-2-pro 1024p (1024x1792 / 1792x1024) $0.50/s → size ratio = 5/3 ≈ 1.6667 +// sora-2-pro 1080p (1080x1920 / 1920x1080) $0.70/s → size ratio = 7/3 ≈ 2.3333 +// +// 管理员应分别为 sora-2 设置 $0.10/s 的基准价,为 sora-2-pro 设置 $0.30/s 的基准价(720p)。 +// 系统会自动乘以 seconds 和 size ratio 得出最终费用。 func (a *TaskAdaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInfo) map[string]float64 { // remix 路径的 OtherRatios 已在 ResolveOriginTask 中设置 if info.Action == constant.TaskActionRemix { @@ -119,12 +129,26 @@ func (a *TaskAdaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInf size = "720x1280" } + model := info.OriginModelName + + // size ratio 相对于各模型自身的 720p 基准价 + // sora-2 只支持 720p → ratio = 1.0 + // sora-2-pro 720p → ratio = 1.0, 1024p → ratio = 5/3 ≈ 1.6667, 1080p → ratio = 7/3 ≈ 2.3333 + sizeRatio := 1.0 + if strings.HasPrefix(model, "sora-2-pro") { + switch size { + case "1920x1080", "1080x1920": + sizeRatio = 7.0 / 3.0 // $0.70 / $0.30 + case "1792x1024", "1024x1792": + sizeRatio = 5.0 / 3.0 // $0.50 / $0.30 + default: // 720x1280, 1280x720 + sizeRatio = 1.0 + } + } + ratios := map[string]float64{ "seconds": float64(seconds), - "size": 1, - } - if size == "1792x1024" || size == "1024x1792" { - ratios["size"] = 1.666667 + "size": sizeRatio, } return ratios } diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go index 18df77a645d6..1f4f3572f3b8 100644 --- a/relay/common/relay_utils.go +++ b/relay/common/relay_utils.go @@ -168,10 +168,10 @@ func ValidateMultipartDirect(c *gin.Context, info *RelayInfo) *dto.TaskError { } if model == "sora-2" && !lo.Contains([]string{"720x1280", "1280x720"}, size) { - return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true) + return createTaskError(fmt.Errorf("sora-2 only supports 720p (720x1280 or 1280x720)"), "invalid_size", http.StatusBadRequest, true) } - if model == "sora-2-pro" && !lo.Contains([]string{"720x1280", "1280x720", "1792x1024", "1024x1792"}, size) { - return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true) + if model == "sora-2-pro" && !lo.Contains([]string{"720x1280", "1280x720", "1792x1024", "1024x1792", "1920x1080", "1080x1920"}, size) { + return createTaskError(fmt.Errorf("sora-2-pro only supports 720p (720x1280/1280x720), 1024p (1024x1792/1792x1024) or 1080p (1080x1920/1920x1080)"), "invalid_size", http.StatusBadRequest, true) } // OtherRatios 已移到 Sora adaptor 的 EstimateBilling 中设置 } From 4d56fc9918abed40d2638808f7f19aed79b5d3e1 Mon Sep 17 00:00:00 2001 From: aabao <602018325@qq.com> Date: Wed, 8 Apr 2026 18:44:19 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=B1=86=E5=8C=85?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/openapi/doubao-video-api.yaml | 486 ++++++++++++++++++++--------- 1 file changed, 335 insertions(+), 151 deletions(-) diff --git a/docs/openapi/doubao-video-api.yaml b/docs/openapi/doubao-video-api.yaml index 1fa205ed8174..76ff186d53fb 100644 --- a/docs/openapi/doubao-video-api.yaml +++ b/docs/openapi/doubao-video-api.yaml @@ -2,38 +2,103 @@ openapi: 3.1.0 info: title: 豆包视频生成 API description: | - 基于 new-api 网关的豆包视频生成接口文档。 - - 支持的豆包视频模型(`DoubaoVideo` 渠道类型 54): - - `doubao-seedance-1-0-pro-250528` - - `doubao-seedance-1-0-lite-t2v`(仅文生视频) - - `doubao-seedance-1-0-lite-i2v`(仅图生视频) - - `doubao-seedance-1-5-pro-251215` - - `doubao-seedance-2-0-260128` - - `doubao-seedance-2-0-fast-260128` - - **计费说明**: - - 计费单位为系统配额(Quota),由管理员为每个模型配置 `ModelRatio`(每次任务的基础价格) - - 当请求中的 `metadata.content` 包含 `video_url` 类型的内容(即图生视频/视频续拍)时,系统会自动应用视频输入折扣: - - `doubao-seedance-2-0-260128`:折扣比率 ≈ 0.6087(≈6折) - - `doubao-seedance-2-0-fast-260128`:折扣比率 ≈ 0.5946(≈6折) - - 视频生成为异步任务,提交时即预扣费,任务失败会自动退款 - - **认证**:所有接口均需在请求头中携带 API Token,格式:`Authorization: Bearer sk-xxxx` - - version: "1.0.0" + # 豆包 Seedance 2.0 视频生成 API + + 基于 new-api 网关统一接入豆包视频模型,提供标准化的视频生成与查询接口。 + + > 📖 **官方参数文档**:[豆包视频生成 API 参数说明](https://www.volcengine.com/docs/82379/1520758?lang=zh) + + --- + + ## 支持的模型 + + 本网关当前支持豆包 Seedance 2.0 系列视频生成模型: + + | 模型名称 | 简介 | 适用场景 | + |---------|------|---------| + | `doubao-seedance-2-0-260128` | **Seedance 2.0 旗舰版**(推荐)
高质量、强动态表现、精准遵循提示词 | 商业级视频创作、高质量内容生成 | + | `doubao-seedance-2-0-fast-260128` | **Seedance 2.0 快速版**
生成速度更快,适合对延迟敏感的场景 | 快速预览、实时应用、批量生成 | + + --- + + ## 使用流程 + + 视频生成为**异步任务**,完整调用流程: + + ``` + 1. POST /v1/video/generations → 获得 task_id + 2. GET /v1/video/generations/{task_id} → 轮询状态(建议每 5 秒查询一次) + 3. 状态变为 completed → 从 metadata.url 获取视频地址 + 4. GET /v1/videos/{task_id}/content → 可选:通过代理下载视频文件 + ``` + + --- + + ## 计费说明 + + - **预扣费机制**:提交任务时预扣费,任务失败自动退款 + - **视频时长与费用相关**:时长设置越长,消耗配额越多,请谨慎设置 + - **视频输入折扣**:当请求 `metadata.content` 中包含 `video_url` 类型内容(即视频续拍场景)时,系统自动应用折扣: + - `doubao-seedance-2-0-260128`:折扣约 **60.87%**(含视频 28元/千帧 vs 不含视频 46元/千帧) + - `doubao-seedance-2-0-fast-260128`:折扣约 **59.46%**(含视频 22元/千帧 vs 不含视频 37元/千帧) + + --- + + ## 认证 + + 所有接口均需在请求头携带 API Token: + + ``` + Authorization: Bearer sk-xxxx + ``` + + version: "2.0.0" contact: name: API Support +externalDocs: + description: 豆包视频生成官方参数文档 + url: https://www.volcengine.com/docs/82379/1520758?lang=zh + servers: - - url: https://api.example.com + - url: https://your-new-api-host.com description: new-api 网关地址(请替换为实际地址) tags: + - name: 模型介绍 + description: | + ## Seedance 2.0 模型系列 + + 豆包 Seedance 2.0 是字节跳动火山引擎推出的最新一代视频生成大模型,具备以下核心能力: + + ### 核心特性 + + - **高质量输出**:支持 480p / 720p 分辨率输出 + - **多模态输入**:支持纯文本(文生视频)、图片+文本(图生视频)、视频+文本(视频续拍)三种输入方式 + - **灵活时长**:支持 4~15 秒范围内的任意整数时长,或设为 -1 由模型智能选择 + - **多尺寸支持**:支持 16:9、9:16、1:1、4:3、3:4 等多种宽高比 + - **可控生成**:支持固定镜头、随机种子、是否添加水印等控制参数 + + ### 模型对比 + + | 特性 | Seedance 2.0 | Seedance 2.0 Fast | + |------|-------------|-----------------| + | 生成质量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | + | 生成速度 | 中等 | 快速 | + | 支持分辨率 | 480p、720p | 480p、720p | + | 推荐使用场景 | 高质量商业内容 | 实时预览、批量任务 | + + ### 参数限制说明 + + - **时长(duration)**:支持 4~15 秒整数,或 -1(模型智能选择) + - **分辨率(resolution)**:仅支持 `480p` 和 `720p`,**不支持 1080p** + - **宽高比(ratio)**:`16:9`、`9:16`、`1:1`、`4:3`、`3:4` + + > 📖 更多参数细节请参考:[官方文档](https://www.volcengine.com/docs/82379/1520758?lang=zh) - name: 视频生成 - description: 豆包视频生成任务提交与查询 + description: 豆包视频生成任务提交与状态查询 - name: 视频内容 - description: 已完成视频的内容下载 + description: 已完成视频的内容代理下载 paths: /v1/video/generations: @@ -42,15 +107,20 @@ paths: - 视频生成 summary: 提交视频生成任务 description: | - 向豆包视频模型提交一个视频生成任务(异步)。 + 向豆包 Seedance 2.0 视频模型提交一个视频生成任务(**异步**)。 - 提交后立即返回任务 ID,视频不会立刻生成完毕。 - 请使用返回的 `id`(即 `task_id`)轮询 `GET /v1/video/generations/{task_id}` 查询结果。 + 提交成功后立即返回任务 ID,视频不会即时生成完毕。 + 请使用返回的 `id` 轮询 `GET /v1/video/generations/{task_id}` 查询任务状态。 - **支持的场景**: - - 文生视频:仅提供 `prompt`,不传 `image` - - 图生视频:提供 `prompt` 和 `image`(图片 URL),或在 `metadata.content` 中传入图片 - - 视频续拍/参考:在 `metadata.content` 中传入 `video_url` 类型内容(会触发视频输入折扣) + **支持的生成场景**: + + | 场景 | 所需字段 | + |------|---------| + | 文生视频 | `model` + `prompt` | + | 图生视频 | `model` + `prompt` + `image`(图片URL)| + | 视频续拍 | `model` + `prompt` + `metadata.content`(含 `video_url`)| + + > ⚠️ **计费提示**:视频时长影响费用,`duration=-1` 时由模型自动选择,实际时长可通过查询 API 的 `duration` 字段获取。 operationId: submitVideoGeneration security: @@ -62,49 +132,58 @@ paths: schema: $ref: '#/components/schemas/VideoGenerationRequest' examples: - 文生视频: - summary: 基础文生视频 + 文生视频(基础): + summary: 文生视频 - 智能选择时长 value: model: doubao-seedance-2-0-260128 - prompt: 宇航员在月球表面慢慢行走,地球悬挂在黑色天空中 - seconds: "5" + prompt: 宇航员在月球表面慢慢行走,地球悬挂在黑色天空中,画面宏大震撼 + seconds: "-1" metadata: ratio: "16:9" + resolution: "720p" + 文生视频(指定时长): + summary: 文生视频 - 指定 8 秒时长 + value: + model: doubao-seedance-2-0-260128 + prompt: 一朵玫瑰花在阳光下缓缓绽放,微风轻抚花瓣,背景是模糊的绿色草坪 + seconds: "8" + metadata: + ratio: "9:16" + resolution: "720p" + watermark: false + seed: 42 图生视频: summary: 图生视频(含参考图片) value: model: doubao-seedance-2-0-260128 - prompt: 让图片中的人物微笑并转身 - image: "https://example.com/reference.jpg" + prompt: 让图片中的人物微笑并缓缓转身,眼神温柔 + image: "https://example.com/portrait.jpg" seconds: "5" metadata: ratio: "16:9" 视频续拍(含视频输入折扣): - summary: 基于已有视频片段续拍 + summary: 视频续拍 - 基于已有视频片段续拍(约6折计费) value: model: doubao-seedance-2-0-260128 - prompt: 继续往前走,走向远处的山丘 + prompt: 继续往前走,走向远处连绵起伏的山丘,镜头缓缓推进 + seconds: "5" metadata: content: - type: video_url video_url: - url: "https://example.com/input.mp4" - seconds: "5" - 高级参数: - summary: 完整参数示例 + url: "https://example.com/input-clip.mp4" + 快速版示例: + summary: 使用快速版模型(速度更快) value: - model: doubao-seedance-2-0-260128 - prompt: 一朵玫瑰花在阳光下缓缓绽放,微风吹拂 - seconds: "8" + model: doubao-seedance-2-0-fast-260128 + prompt: 城市夜晚的延时摄影,车流如光河,霓虹闪烁 + seconds: "-1" metadata: ratio: "16:9" - resolution: "720p" - seed: 42 - watermark: false - camera_fixed: false + resolution: "480p" responses: "200": - description: 任务提交成功,返回任务信息(此时任务仍在生成中,status 为 queued) + description: 任务提交成功(此时任务仍在排队,`status` 为 `queued`) content: application/json: schema: @@ -118,11 +197,15 @@ paths: progress: 0 created_at: 1712345678 "400": - description: 请求参数错误 + description: 请求参数错误(如模型名称不支持、参数超出范围等) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + example: + error: + message: "invalid model: doubao-seedance-2-0-xxx" + type: invalid_request_error "401": description: 未授权(Token 无效或缺失) content: @@ -130,7 +213,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' "402": - description: 账户余额不足 + description: 账户余额不足,无法完成预扣费 content: application/json: schema: @@ -150,17 +233,21 @@ paths: description: | 根据任务 ID 查询视频生成任务的当前状态和结果。 - **任务状态说明**: - - `queued`:任务已提交,等待处理 - - `in_progress`:任务处理中 - - `completed`:任务已完成,可从 `metadata.url` 获取视频地址 - - `failed`:任务失败,查看 `error` 字段了解失败原因 + **任务状态流转**: + + ``` + queued → in_progress → completed ✅ 可下载视频 + ↘ failed ❌ 查看 error 字段 + ``` - **获取视频**: - - 任务 `completed` 后,`metadata.url` 字段包含视频的直接下载链接 - - 也可调用 `GET /v1/videos/{task_id}/content` 通过代理下载视频文件 + **视频获取方式**(任务 `completed` 后): + 1. 从响应体 `metadata.url` 字段获取视频直链 + 2. 调用 `GET /v1/videos/{task_id}/content` 通过代理下载视频文件 - **轮询建议**:每 5 秒查询一次,超时时间建议设置为 10 分钟。 + **轮询建议**: + - 每 **5 秒**查询一次 + - 总超时建议设置为 **10 分钟** + - Seedance 2.0 通常在 **1~3 分钟**内完成生成 operationId: getVideoGenerationTask security: @@ -169,20 +256,30 @@ paths: - name: task_id in: path required: true - description: 提交任务时返回的任务 ID(`id` 字段) + description: 提交任务时返回的任务 ID(响应体 `id` 字段) schema: type: string - example: task_abc123def456 + example: task_abc123def456 responses: "200": - description: 查询成功(可能处于任意状态) + description: 查询成功(任务可能处于任意状态) content: application/json: schema: $ref: '#/components/schemas/VideoTaskResponse' examples: - 处理中: - summary: 任务进行中 + 排队等待: + summary: 任务排队中 + value: + id: task_abc123def456 + task_id: task_abc123def456 + object: video + model: doubao-seedance-2-0-260128 + status: queued + progress: 0 + created_at: 1712345678 + 生成中: + summary: 视频生成中 value: id: task_abc123def456 task_id: task_abc123def456 @@ -192,7 +289,7 @@ paths: progress: 50 created_at: 1712345678 已完成: - summary: 任务完成,视频可下载 + summary: 生成完成,视频可下载 value: id: task_abc123def456 task_id: task_abc123def456 @@ -203,9 +300,9 @@ paths: created_at: 1712345678 completed_at: 1712345900 metadata: - url: "https://cdn.example.com/videos/result.mp4" - 失败: - summary: 任务失败 + url: "https://cdn.volcengineapi.com/videos/output-abc123.mp4" + 生成失败: + summary: 生成失败 value: id: task_abc123def456 task_id: task_abc123def456 @@ -217,15 +314,15 @@ paths: completed_at: 1712345700 error: code: content_policy_violation - message: 请求内容违反了内容安全策略 - "404": - description: 任务不存在 + message: 请求内容违反了内容安全策略,请修改 prompt 后重试 + "401": + description: 未授权 content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - "401": - description: 未授权 + "404": + description: 任务不存在(task_id 无效或不属于当前用户) content: application/json: schema: @@ -237,8 +334,8 @@ paths: - 视频生成 summary: 提交视频生成任务(OpenAI 兼容格式) description: | - 与 `POST /v1/video/generations` 功能完全相同,为兼容 OpenAI Video API 格式而提供。 - 请求和响应格式与 `/v1/video/generations` 一致。 + 与 `POST /v1/video/generations` 功能完全相同,为兼容 OpenAI Video API 格式而提供的别名路由。 + 请求和响应格式与 `/v1/video/generations` 完全一致。 operationId: submitVideoOpenAI security: - BearerAuth: [] @@ -274,7 +371,7 @@ paths: - 视频生成 summary: 查询任务状态(OpenAI 兼容格式) description: | - 与 `GET /v1/video/generations/{task_id}` 功能相同,兼容 OpenAI Video API 格式。 + 与 `GET /v1/video/generations/{task_id}` 功能相同,兼容 OpenAI Video API 格式的别名路由。 operationId: getVideoTaskOpenAI security: - BearerAuth: [] @@ -285,7 +382,7 @@ paths: description: 任务 ID schema: type: string - example: task_abc123def456 + example: task_abc123def456 responses: "200": description: 查询成功 @@ -304,18 +401,18 @@ paths: get: tags: - 视频内容 - summary: 下载视频文件内容 + summary: 代理下载视频文件 description: | - 通过代理方式下载已完成的视频文件内容(二进制流)。 + 通过 new-api 服务器代理下载已完成的视频文件(二进制流)。 **前置条件**:任务状态必须为 `completed`,否则返回 400 错误。 - 此接口会: - 1. 查询数据库获取任务关联的视频 URL - 2. 通过服务器代理请求并转发视频内容给客户端 - 3. 响应头包含 `Cache-Control: public, max-age=86400`(24小时缓存) + **工作原理**: + 1. 根据 `task_id` 查询数据库获取豆包存储的视频 URL + 2. 通过服务器代理请求并将视频内容流式转发给客户端 + 3. 响应头包含 `Cache-Control: public, max-age=86400`(24 小时缓存) - **支持认证方式**:Bearer Token 或 Session Cookie(Dashboard 登录用户均可访问) + **认证支持**:Bearer Token 或 Dashboard Session Cookie 均可访问。 operationId: downloadVideoContent security: @@ -325,24 +422,21 @@ paths: - name: task_id in: path required: true - description: 任务 ID(`completed` 状态才可下载) + description: 任务 ID(任务必须处于 `completed` 状态) schema: type: string - example: task_abc123def456 + example: task_abc123def456 responses: "200": - description: 视频文件内容(二进制流) + description: 视频文件二进制流 content: video/mp4: schema: type: string format: binary - video/*: - schema: - type: string - format: binary + example: "[视频二进制数据]" "400": - description: 任务未完成 + description: 任务未完成,无法下载 content: application/json: schema: @@ -351,6 +445,12 @@ paths: error: message: "Task is not completed yet, current status: in_progress" type: invalid_request_error + "401": + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' "404": description: 任务不存在 content: @@ -358,7 +458,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' "502": - description: 上游视频 URL 不可用 + description: 上游视频 URL 不可访问 content: application/json: schema: @@ -375,7 +475,7 @@ components: type: apiKey in: cookie name: session - description: Dashboard 登录后的 Session Cookie + description: Dashboard 登录后的 Session Cookie(仅视频下载接口支持) schemas: VideoGenerationRequest: @@ -383,51 +483,76 @@ components: required: - model - prompt + description: 视频生成请求参数 properties: model: type: string description: | - 视频生成模型名称。支持的模型: - - `doubao-seedance-1-0-pro-250528` - - `doubao-seedance-1-0-lite-t2v`(仅文生视频) - - `doubao-seedance-1-0-lite-i2v`(仅图生视频) - - `doubao-seedance-1-5-pro-251215` - - `doubao-seedance-2-0-260128`(推荐) - - `doubao-seedance-2-0-fast-260128`(更快速) + 视频生成模型名称。 + + 支持的模型: + - `doubao-seedance-2-0-260128`:旗舰版,高质量(**推荐**) + - `doubao-seedance-2-0-fast-260128`:快速版,速度优先 example: doubao-seedance-2-0-260128 enum: - - doubao-seedance-1-0-pro-250528 - - doubao-seedance-1-0-lite-t2v - - doubao-seedance-1-0-lite-i2v - - doubao-seedance-1-5-pro-251215 - doubao-seedance-2-0-260128 - doubao-seedance-2-0-fast-260128 prompt: type: string - description: 视频内容描述(文本提示词)。建议使用详细、具体的描述以获得更好的生成效果。 - example: 一朵玫瑰花在阳光下缓缓绽放,微风轻抚花瓣,背景是模糊的绿色草坪 + description: | + 视频内容的文字描述(提示词)。 + + 建议: + - 使用详细、具体的描述 + - 可包含场景、动作、风格、镜头语言等 + - 中英文均支持 + example: 宇航员在月球表面漫步,地球悬挂在黑色星空中,宏大震撼的画面 image: type: string description: | - 参考图片(图生视频时使用)。支持 HTTP/HTTPS URL 或 Base64 编码的图片数据。 - 当同时使用 `metadata.content` 中的图片时,此字段的图片会被添加到 `content` 数组前端。 + 参考图片(**图生视频**场景使用)。 + + 支持格式: + - HTTP/HTTPS 图片 URL + - Base64 编码的图片数据(格式:`data:image/jpeg;base64,...`) + + 当同时在 `metadata.content` 中也传入了图片时,此字段的图片会附加在 content 数组前端。 example: "https://example.com/reference-image.jpg" seconds: type: string - description: 视频时长(秒),以字符串形式传入。常用值为 "5" 或 "8"。 - example: "5" + description: | + 视频时长设置(字符串格式)。 + + **Seedance 2.0 & 2.0 Fast 支持两种配置方式**: + + 1. **指定具体时长**:填写 4~15 范围内的任意整数(单位:秒),如 `"5"`、`"8"`、`"15"` + 2. **智能选择**:设置为 `"-1"`,由模型在有效范围内自主选择合适的视频长度 + + > ⚠️ 视频时长与计费相关,时长越长消耗配额越多,请谨慎设置。 + > 实际生成视频的时长可通过查询任务 API 返回的 `duration` 字段获取。 + example: "-1" + default: "-1" metadata: $ref: '#/components/schemas/VideoMetadata' VideoMetadata: type: object description: | - 豆包视频特定的扩展参数,通过 `metadata` 字段传入。 + 豆包视频的扩展参数,通过 `metadata` 字段传入。 + 所有字段均为可选,未传入时使用模型默认值。 + + > 📖 详细参数说明请参考 [官方文档](https://www.volcengine.com/docs/82379/1520758?lang=zh) properties: ratio: type: string - description: 视频宽高比 + description: | + 视频宽高比(输出画面比例)。 + + 常用场景参考: + - `16:9`:横屏,适合普通视频、电影感 + - `9:16`:竖屏,适合短视频、手机端 + - `1:1`:方形,适合社交媒体 example: "16:9" enum: - "16:9" @@ -437,58 +562,91 @@ components: - "3:4" resolution: type: string - description: 输出视频分辨率 + description: | + 输出视频分辨率。 + + **Seedance 2.0 & 2.0 Fast 分辨率限制**: + - ✅ `480p`:支持 + - ✅ `720p`:支持(推荐) + - ❌ `1080p`:**不支持**(Seedance 2.0 系列均不支持 1080p) + + > 注:1080p 仅在 Seedance 1.0 lite 的参考图(i2v)场景下支持。 example: "720p" enum: - "480p" - "720p" - - "1080p" seed: type: integer - description: 随机种子,固定相同种子可复现相似结果。范围:0 ~ 2147483647 + description: | + 随机种子,用于控制生成结果的随机性。 + + - 相同的 `seed` + 相同的 `prompt` 会产生相似(但不完全相同)的视频 + - 有效范围:0 ~ 2,147,483,647 + - 不传时随机生成 example: 42 + minimum: 0 + maximum: 2147483647 watermark: type: boolean - description: 是否在视频上添加水印。默认为 false(不加水印)。 + description: | + 是否在视频上添加水印。 + + - `false`(默认):不添加水印 + - `true`:添加豆包水印 example: false + default: false camera_fixed: type: boolean - description: 是否固定摄像机(镜头不移动)。默认为 false(允许镜头运动)。 + description: | + 是否固定摄像机(禁止镜头运动)。 + + - `false`(默认):允许镜头自然运动 + - `true`:固定镜头,适合需要静态构图的场景 example: false + default: false content: type: array description: | 多模态内容数组,支持图片或视频输入。 - **注意**:当包含 `video_url` 类型条目时,系统会自动应用视频输入折扣计费: - - `doubao-seedance-2-0-260128`:约 6.1 折 - - `doubao-seedance-2-0-fast-260128`:约 5.9 折 + + **视频续拍场景**:传入 `video_url` 类型条目,系统会自动识别并应用视频输入折扣计费: + - `doubao-seedance-2-0-260128`:折扣约 **60.87%** + - `doubao-seedance-2-0-fast-260128`:折扣约 **59.46%** + + **图片输入场景**:传入 `image_url` 类型条目。 + + 数组中同时支持 `text` 类型(覆盖顶层 `prompt`)。 items: $ref: '#/components/schemas/ContentItem' return_last_frame: type: boolean - description: 是否返回视频最后一帧图片(用于续拍场景) + description: 是否返回视频最后一帧图片(可用于下一段视频续拍的参考帧) example: false generate_audio: type: boolean - description: 是否生成配套音频 + description: 是否为视频生成配套音频(实验性功能) example: false draft: type: boolean - description: 是否生成草稿(快速预览,质量较低) + description: 是否生成草稿版本(生成速度快,质量较低,适合快速预览) example: false service_tier: type: string - description: 服务等级(影响优先级和价格) + description: 服务等级(影响任务优先级) example: default callback_url: type: string - description: 任务完成后的回调通知 URL(需配合豆包官方回调机制使用) + format: uri + description: | + 任务完成/失败时的回调通知 URL(HTTP POST 方式推送)。 + 需配合豆包官方回调格式,建议通过业务服务器接收。 example: "https://your-server.com/webhook/video-callback" ContentItem: type: object required: - type + description: 多模态内容条目(用于 `metadata.content` 数组) properties: type: type: string @@ -500,54 +658,66 @@ components: example: image_url text: type: string - description: 文本内容(type 为 text 时使用) - example: 背景是黄昏时分的海滩 + description: 文本内容(`type` 为 `text` 时使用,会覆盖顶层 `prompt` 字段) + example: 宇航员站起身,回望地球 image_url: type: object - description: 图片 URL(type 为 image_url 时使用) + description: 图片引用(`type` 为 `image_url` 时使用) + required: + - url properties: url: type: string - description: 图片的 HTTP/HTTPS URL 或 base64 数据 - example: "https://example.com/photo.jpg" + description: 图片的 HTTP/HTTPS URL 或 Base64 数据 + example: "https://example.com/reference-photo.jpg" video_url: type: object - description: 视频 URL(type 为 video_url 时使用,**会触发视频输入折扣**) + description: | + 视频引用(`type` 为 `video_url` 时使用)。 + + > ⚠️ 包含此类型条目时,系统自动应用视频输入折扣计费(约6折)。 + required: + - url properties: url: type: string description: 视频的 HTTP/HTTPS URL - example: "https://example.com/input-video.mp4" + example: "https://example.com/input-video-clip.mp4" VideoTaskResponse: type: object - description: 视频任务响应(提交后和查询时返回相同结构) + description: 视频任务响应体(任务提交后和状态查询时均返回此结构) properties: id: type: string - description: 任务唯一标识符(由 new-api 系统生成,非豆包原始 ID) + description: 任务唯一标识符(由 new-api 系统生成) example: task_abc123def456 task_id: type: string - description: 与 `id` 相同,兼容旧版接口(已废弃,请使用 `id`) + description: 同 `id` 字段,保持向后兼容(**已废弃**,请使用 `id`) example: task_abc123def456 deprecated: true object: type: string - description: 对象类型,固定为 "video" + description: 对象类型,固定返回 `"video"` example: video + enum: + - video model: type: string - description: 使用的模型名称 + description: 实际使用的模型名称 example: doubao-seedance-2-0-260128 status: type: string description: | 任务当前状态: - - `queued`:已提交,排队等待 - - `in_progress`:生成中 - - `completed`:已完成,可下载视频 - - `failed`:生成失败 + + | 状态 | 说明 | + |------|------| + | `queued` | 已提交,排队等待处理 | + | `in_progress` | 视频生成中 | + | `completed` | 生成完成,可从 `metadata.url` 下载视频 | + | `failed` | 生成失败,查看 `error` 字段了解原因 | enum: - queued - in_progress @@ -556,7 +726,7 @@ components: example: completed progress: type: integer - description: 任务进度(0-100 的整数) + description: 任务进度(0~100 的整数,仅供参考) minimum: 0 maximum: 100 example: 100 @@ -568,12 +738,12 @@ components: completed_at: type: integer format: int64 - description: 任务完成时间(Unix 时间戳,秒)。仅在 status 为 completed 或 failed 时返回。 + description: 任务完成/失败时间(Unix 时间戳,秒)。仅在任务结束后返回。 example: 1712345900 expires_at: type: integer format: int64 - description: 任务结果过期时间(Unix 时间戳) + description: 任务结果链接过期时间(Unix 时间戳,秒) example: 1712432300 metadata: type: object @@ -581,13 +751,18 @@ components: properties: url: type: string - description: 视频文件的直接访问 URL(仅在 status 为 completed 时存在) - example: "https://cdn.example.com/videos/output-video.mp4" + description: | + 视频文件的直接访问 URL。 + + - 仅在 `status` 为 `completed` 时存在 + - URL 有效期有限,建议及时下载或缓存 + - 也可通过 `GET /v1/videos/{task_id}/content` 代理下载 + example: "https://cdn.volcengineapi.com/videos/output-abc123.mp4" error: $ref: '#/components/schemas/VideoTaskError' remixed_from_video_id: type: string - description: 如果是 remix 任务,此字段为原始视频的任务 ID + description: 如果是 remix(续拍)任务,此字段为原始视频的任务 ID example: task_origin789ghi012 VideoTaskError: @@ -596,7 +771,7 @@ components: properties: code: type: string - description: 错误码 + description: 错误码(由豆包上游返回) example: content_policy_violation message: type: string @@ -605,9 +780,13 @@ components: ErrorResponse: type: object + description: API 错误响应(HTTP 4xx/5xx 时返回) properties: error: type: object + required: + - message + - type properties: message: type: string @@ -617,6 +796,11 @@ components: type: string description: 错误类型 example: invalid_request_error + enum: + - invalid_request_error + - authentication_error + - permission_error + - server_error code: type: string description: 错误码(部分场景下包含) From cbe1d1d99b919d9921b5f7a601c476ea33dc1bcf Mon Sep 17 00:00:00 2001 From: aabao <602018325@qq.com> Date: Fri, 10 Apr 2026 17:05:11 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=B1=86=E5=8C=85?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middleware/doubao_adapter.go | 237 +++++++++++++++++++++++++++++++++++ router/video-router.go | 12 ++ 2 files changed, 249 insertions(+) create mode 100644 middleware/doubao_adapter.go diff --git a/middleware/doubao_adapter.go b/middleware/doubao_adapter.go new file mode 100644 index 000000000000..a3d320652151 --- /dev/null +++ b/middleware/doubao_adapter.go @@ -0,0 +1,237 @@ +package middleware + +import ( + "bytes" + "io" + "net/http" + "strconv" + + "github.com/QuantumNous/new-api/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/gin-gonic/gin" +) + +// doubaoContentItem mirrors the "content" array items from the Doubao native API. +type doubaoContentItem struct { + Type string `json:"type,omitempty"` + Text string `json:"text,omitempty"` + ImageURL *doubaoMediaURL `json:"image_url,omitempty"` + VideoURL *doubaoMediaURL `json:"video_url,omitempty"` + AudioURL *doubaoMediaURL `json:"audio_url,omitempty"` +} + +type doubaoMediaURL struct { + URL string `json:"url,omitempty"` +} + +// doubaoNativeRequest is the native Doubao video creation request body. +// POST /api/v3/contents/generations/tasks +type doubaoNativeRequest struct { + Model string `json:"model"` + Content []doubaoContentItem `json:"content,omitempty"` + Resolution string `json:"resolution,omitempty"` + Ratio string `json:"ratio,omitempty"` + // Duration and Seed can be int or object {"value": N} from upstream. + // We accept any JSON here and re-pack them into metadata. + Duration interface{} `json:"duration,omitempty"` + Seed interface{} `json:"seed,omitempty"` + CameraFixed interface{} `json:"camera_fixed,omitempty"` + Watermark interface{} `json:"watermark,omitempty"` + // Pass-through extra fields + CallbackURL string `json:"callback_url,omitempty"` + ReturnLastFrame interface{} `json:"return_last_frame,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` + ExecutionExpiresAfter interface{} `json:"execution_expires_after,omitempty"` + GenerateAudio interface{} `json:"generate_audio,omitempty"` + Draft interface{} `json:"draft,omitempty"` + Frames interface{} `json:"frames,omitempty"` + Tools interface{} `json:"tools,omitempty"` +} + +// DoubaoRequestConvert converts native Doubao video API requests into new-api's +// internal task format so they can be handled by the existing task relay pipeline. +// +// Supported paths: +// POST /api/v3/contents/generations/tasks → create task +// GET /api/v3/contents/generations/tasks/:task_id → fetch task +func DoubaoRequestConvert() func(c *gin.Context) { + return func(c *gin.Context) { + // Determine if this is a fetch request (GET with task_id) + if c.Request.Method == http.MethodGet { + taskID := c.Param("task_id") + if taskID == "" { + abortWithOpenAiMessage(c, http.StatusBadRequest, "task_id is required") + return + } + // Route to internal fetch endpoint + c.Request.URL.Path = "/v1/video/generations/" + taskID + c.Set("task_id", taskID) + c.Set("relay_mode", relayconstant.RelayModeVideoFetchByID) + c.Next() + return + } + + // POST: parse native Doubao request and convert to internal format + var nativeReq doubaoNativeRequest + if err := common.UnmarshalBodyReusable(c, &nativeReq); err != nil { + abortWithOpenAiMessage(c, http.StatusBadRequest, "invalid request body: "+err.Error()) + return + } + + // Extract prompt and images from the content array + var prompt string + var images []string + + for _, item := range nativeReq.Content { + switch item.Type { + case "text": + if item.Text != "" { + prompt = item.Text + } + case "image_url": + if item.ImageURL != nil && item.ImageURL.URL != "" { + images = append(images, item.ImageURL.URL) + } + } + } + + // Build metadata — carry all non-standard fields so the doubao adaptor + // can pick them up via taskcommon.UnmarshalMetadata. + metadata := make(map[string]interface{}) + + // Re-pack the full original content array so hasVideoInMetadata() works + // correctly (doubao adaptor checks metadata["content"]). + if len(nativeReq.Content) > 0 { + rawContent := make([]interface{}, len(nativeReq.Content)) + for i, item := range nativeReq.Content { + m := make(map[string]interface{}) + if item.Type != "" { + m["type"] = item.Type + } + if item.Text != "" { + m["text"] = item.Text + } + if item.ImageURL != nil { + m["image_url"] = map[string]interface{}{"url": item.ImageURL.URL} + } + if item.VideoURL != nil { + m["video_url"] = map[string]interface{}{"url": item.VideoURL.URL} + } + if item.AudioURL != nil { + m["audio_url"] = map[string]interface{}{"url": item.AudioURL.URL} + } + rawContent[i] = m + } + metadata["content"] = rawContent + } + + if nativeReq.Resolution != "" { + metadata["resolution"] = nativeReq.Resolution + } + if nativeReq.Ratio != "" { + metadata["ratio"] = nativeReq.Ratio + } + if nativeReq.Duration != nil { + metadata["duration"] = normalizeDuration(nativeReq.Duration) + } + if nativeReq.Seed != nil { + metadata["seed"] = nativeReq.Seed + } + if nativeReq.CameraFixed != nil { + metadata["camera_fixed"] = nativeReq.CameraFixed + } + if nativeReq.Watermark != nil { + metadata["watermark"] = nativeReq.Watermark + } + if nativeReq.CallbackURL != "" { + metadata["callback_url"] = nativeReq.CallbackURL + } + if nativeReq.ReturnLastFrame != nil { + metadata["return_last_frame"] = nativeReq.ReturnLastFrame + } + if nativeReq.ServiceTier != "" { + metadata["service_tier"] = nativeReq.ServiceTier + } + if nativeReq.ExecutionExpiresAfter != nil { + metadata["execution_expires_after"] = nativeReq.ExecutionExpiresAfter + } + if nativeReq.GenerateAudio != nil { + metadata["generate_audio"] = nativeReq.GenerateAudio + } + if nativeReq.Draft != nil { + metadata["draft"] = nativeReq.Draft + } + if nativeReq.Frames != nil { + metadata["frames"] = nativeReq.Frames + } + if nativeReq.Tools != nil { + metadata["tools"] = nativeReq.Tools + } + + // Build unified internal request + unifiedReq := map[string]interface{}{ + "model": nativeReq.Model, + "prompt": prompt, + "metadata": metadata, + } + + if len(images) == 1 { + unifiedReq["image"] = images[0] + } else if len(images) > 1 { + unifiedReq["images"] = images + } + + // Extract seconds/duration for billing estimation + if nativeReq.Duration != nil { + if secs := extractDurationSeconds(nativeReq.Duration); secs > 0 { + unifiedReq["seconds"] = strconv.Itoa(secs) + } + } + + jsonData, err := common.Marshal(unifiedReq) + if err != nil { + abortWithOpenAiMessage(c, http.StatusInternalServerError, "failed to marshal request: "+err.Error()) + return + } + + // Replace request body + c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonData)) + c.Set(common.KeyRequestBody, jsonData) + + // Redirect to the internal video generation endpoint + c.Request.URL.Path = "/v1/video/generations" + + c.Next() + } +} + +// normalizeDuration converts various duration representations to a plain int. +// Doubao native API accepts both {"value": N} and plain N. +func normalizeDuration(v interface{}) interface{} { + switch d := v.(type) { + case float64: + return int(d) + case int: + return d + case map[string]interface{}: + if val, ok := d["value"]; ok { + return normalizeDuration(val) + } + } + return v +} + +// extractDurationSeconds returns the integer second value from a duration field. +func extractDurationSeconds(v interface{}) int { + switch d := v.(type) { + case float64: + return int(d) + case int: + return d + case map[string]interface{}: + if val, ok := d["value"]; ok { + return extractDurationSeconds(val) + } + } + return 0 +} diff --git a/router/video-router.go b/router/video-router.go index 461451104520..f838443e81e6 100644 --- a/router/video-router.go +++ b/router/video-router.go @@ -49,4 +49,16 @@ func SetVideoRouter(router *gin.Engine) { // Maps to: /?Action=CVSync2AsyncSubmitTask&Version=2022-08-31 and /?Action=CVSync2AsyncGetResult&Version=2022-08-31 jimengOfficialGroup.POST("/", controller.RelayTask) } + + // Doubao video native API routes — allows downstream to call us using the + // official Doubao video API path format directly. + // POST /api/v3/contents/generations/tasks → create video task + // GET /api/v3/contents/generations/tasks/:task_id → fetch task status + doubaoVideoGroup := router.Group("/api/v3/contents/generations") + doubaoVideoGroup.Use(middleware.RouteTag("relay")) + doubaoVideoGroup.Use(middleware.DoubaoRequestConvert(), middleware.TokenAuth(), middleware.Distribute()) + { + doubaoVideoGroup.POST("/tasks", controller.RelayTask) + doubaoVideoGroup.GET("/tasks/:task_id", controller.RelayTaskFetch) + } } From 0ceb0d5e46a4030f03cad659fbcb51554ad4b944 Mon Sep 17 00:00:00 2001 From: aabao <602018325@qq.com> Date: Fri, 10 Apr 2026 17:18:44 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kilocode/setup-script | 39 --------------------------------------- model/user.go | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 .kilocode/setup-script diff --git a/.kilocode/setup-script b/.kilocode/setup-script deleted file mode 100644 index 24b867a49da4..000000000000 --- a/.kilocode/setup-script +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# Kilo Code Worktree Setup Script -# This script runs before the agent starts in a worktree (new sessions only). -# -# Available environment variables: -# WORKTREE_PATH - Absolute path to the worktree directory -# REPO_PATH - Absolute path to the main repository -# -# Example tasks: -# - Copy .env files from main repo -# - Install dependencies -# - Run database migrations -# - Set up local configuration - -set -e # Exit on error - -echo "Setting up worktree: $WORKTREE_PATH" - -# Uncomment and modify as needed: - -# Copy environment files -# if [ -f "$REPO_PATH/.env" ]; then -# cp "$REPO_PATH/.env" "$WORKTREE_PATH/.env" -# echo "Copied .env" -# fi - -# Install dependencies (Node.js) -# if [ -f "$WORKTREE_PATH/package.json" ]; then -# cd "$WORKTREE_PATH" -# npm install -# fi - -# Install dependencies (Python) -# if [ -f "$WORKTREE_PATH/requirements.txt" ]; then -# cd "$WORKTREE_PATH" -# pip install -r requirements.txt -# fi - -echo "Setup complete!" diff --git a/model/user.go b/model/user.go index 402023dfb84b..1210b5435d04 100644 --- a/model/user.go +++ b/model/user.go @@ -604,7 +604,7 @@ func (user *User) ValidateAndFill() (err error) { DB.Where("username = ? OR email = ?", username, username).First(user) okay := common.ValidatePasswordAndHash(password, user.Password) if !okay || user.Status != common.UserStatusEnabled { - return errors.New("用户名或密码错误,请检查后重试") + return errors.New("用户名或密码错误,或用户已被封禁") } return nil } From eb72bd29e093c5807aa85d1846c57aa18578737c Mon Sep 17 00:00:00 2001 From: aabao <602018325@qq.com> Date: Fri, 10 Apr 2026 17:20:10 +0800 Subject: [PATCH 6/8] Update zh-CN.yaml --- i18n/locales/zh-CN.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml index d0fc9ea6b8b8..1f3b5a7b4bc5 100644 --- a/i18n/locales/zh-CN.yaml +++ b/i18n/locales/zh-CN.yaml @@ -56,7 +56,7 @@ user.password_login_disabled: "管理员关闭了密码登录" user.register_disabled: "管理员关闭了新用户注册" user.password_register_disabled: "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册" user.username_or_password_empty: "用户名或密码为空" -user.username_or_password_error: "用户名或密码错误,请检查后重试" +user.username_or_password_error: "用户名或密码错误,或用户已被封禁" user.email_or_password_empty: "邮箱地址或密码为空!" user.exists: "用户名已存在,或已注销" user.not_exists: "用户不存在" From f84ac1846554be9677d5394befa1e4fb7afb932f Mon Sep 17 00:00:00 2001 From: aabao <602018325@qq.com> Date: Fri, 10 Apr 2026 17:20:55 +0800 Subject: [PATCH 7/8] Delete doubao-video-api.yaml --- docs/openapi/doubao-video-api.yaml | 807 ----------------------------- 1 file changed, 807 deletions(-) delete mode 100644 docs/openapi/doubao-video-api.yaml diff --git a/docs/openapi/doubao-video-api.yaml b/docs/openapi/doubao-video-api.yaml deleted file mode 100644 index 76ff186d53fb..000000000000 --- a/docs/openapi/doubao-video-api.yaml +++ /dev/null @@ -1,807 +0,0 @@ -openapi: 3.1.0 -info: - title: 豆包视频生成 API - description: | - # 豆包 Seedance 2.0 视频生成 API - - 基于 new-api 网关统一接入豆包视频模型,提供标准化的视频生成与查询接口。 - - > 📖 **官方参数文档**:[豆包视频生成 API 参数说明](https://www.volcengine.com/docs/82379/1520758?lang=zh) - - --- - - ## 支持的模型 - - 本网关当前支持豆包 Seedance 2.0 系列视频生成模型: - - | 模型名称 | 简介 | 适用场景 | - |---------|------|---------| - | `doubao-seedance-2-0-260128` | **Seedance 2.0 旗舰版**(推荐)
高质量、强动态表现、精准遵循提示词 | 商业级视频创作、高质量内容生成 | - | `doubao-seedance-2-0-fast-260128` | **Seedance 2.0 快速版**
生成速度更快,适合对延迟敏感的场景 | 快速预览、实时应用、批量生成 | - - --- - - ## 使用流程 - - 视频生成为**异步任务**,完整调用流程: - - ``` - 1. POST /v1/video/generations → 获得 task_id - 2. GET /v1/video/generations/{task_id} → 轮询状态(建议每 5 秒查询一次) - 3. 状态变为 completed → 从 metadata.url 获取视频地址 - 4. GET /v1/videos/{task_id}/content → 可选:通过代理下载视频文件 - ``` - - --- - - ## 计费说明 - - - **预扣费机制**:提交任务时预扣费,任务失败自动退款 - - **视频时长与费用相关**:时长设置越长,消耗配额越多,请谨慎设置 - - **视频输入折扣**:当请求 `metadata.content` 中包含 `video_url` 类型内容(即视频续拍场景)时,系统自动应用折扣: - - `doubao-seedance-2-0-260128`:折扣约 **60.87%**(含视频 28元/千帧 vs 不含视频 46元/千帧) - - `doubao-seedance-2-0-fast-260128`:折扣约 **59.46%**(含视频 22元/千帧 vs 不含视频 37元/千帧) - - --- - - ## 认证 - - 所有接口均需在请求头携带 API Token: - - ``` - Authorization: Bearer sk-xxxx - ``` - - version: "2.0.0" - contact: - name: API Support - -externalDocs: - description: 豆包视频生成官方参数文档 - url: https://www.volcengine.com/docs/82379/1520758?lang=zh - -servers: - - url: https://your-new-api-host.com - description: new-api 网关地址(请替换为实际地址) - -tags: - - name: 模型介绍 - description: | - ## Seedance 2.0 模型系列 - - 豆包 Seedance 2.0 是字节跳动火山引擎推出的最新一代视频生成大模型,具备以下核心能力: - - ### 核心特性 - - - **高质量输出**:支持 480p / 720p 分辨率输出 - - **多模态输入**:支持纯文本(文生视频)、图片+文本(图生视频)、视频+文本(视频续拍)三种输入方式 - - **灵活时长**:支持 4~15 秒范围内的任意整数时长,或设为 -1 由模型智能选择 - - **多尺寸支持**:支持 16:9、9:16、1:1、4:3、3:4 等多种宽高比 - - **可控生成**:支持固定镜头、随机种子、是否添加水印等控制参数 - - ### 模型对比 - - | 特性 | Seedance 2.0 | Seedance 2.0 Fast | - |------|-------------|-----------------| - | 生成质量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | - | 生成速度 | 中等 | 快速 | - | 支持分辨率 | 480p、720p | 480p、720p | - | 推荐使用场景 | 高质量商业内容 | 实时预览、批量任务 | - - ### 参数限制说明 - - - **时长(duration)**:支持 4~15 秒整数,或 -1(模型智能选择) - - **分辨率(resolution)**:仅支持 `480p` 和 `720p`,**不支持 1080p** - - **宽高比(ratio)**:`16:9`、`9:16`、`1:1`、`4:3`、`3:4` - - > 📖 更多参数细节请参考:[官方文档](https://www.volcengine.com/docs/82379/1520758?lang=zh) - - name: 视频生成 - description: 豆包视频生成任务提交与状态查询 - - name: 视频内容 - description: 已完成视频的内容代理下载 - -paths: - /v1/video/generations: - post: - tags: - - 视频生成 - summary: 提交视频生成任务 - description: | - 向豆包 Seedance 2.0 视频模型提交一个视频生成任务(**异步**)。 - - 提交成功后立即返回任务 ID,视频不会即时生成完毕。 - 请使用返回的 `id` 轮询 `GET /v1/video/generations/{task_id}` 查询任务状态。 - - **支持的生成场景**: - - | 场景 | 所需字段 | - |------|---------| - | 文生视频 | `model` + `prompt` | - | 图生视频 | `model` + `prompt` + `image`(图片URL)| - | 视频续拍 | `model` + `prompt` + `metadata.content`(含 `video_url`)| - - > ⚠️ **计费提示**:视频时长影响费用,`duration=-1` 时由模型自动选择,实际时长可通过查询 API 的 `duration` 字段获取。 - - operationId: submitVideoGeneration - security: - - BearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/VideoGenerationRequest' - examples: - 文生视频(基础): - summary: 文生视频 - 智能选择时长 - value: - model: doubao-seedance-2-0-260128 - prompt: 宇航员在月球表面慢慢行走,地球悬挂在黑色天空中,画面宏大震撼 - seconds: "-1" - metadata: - ratio: "16:9" - resolution: "720p" - 文生视频(指定时长): - summary: 文生视频 - 指定 8 秒时长 - value: - model: doubao-seedance-2-0-260128 - prompt: 一朵玫瑰花在阳光下缓缓绽放,微风轻抚花瓣,背景是模糊的绿色草坪 - seconds: "8" - metadata: - ratio: "9:16" - resolution: "720p" - watermark: false - seed: 42 - 图生视频: - summary: 图生视频(含参考图片) - value: - model: doubao-seedance-2-0-260128 - prompt: 让图片中的人物微笑并缓缓转身,眼神温柔 - image: "https://example.com/portrait.jpg" - seconds: "5" - metadata: - ratio: "16:9" - 视频续拍(含视频输入折扣): - summary: 视频续拍 - 基于已有视频片段续拍(约6折计费) - value: - model: doubao-seedance-2-0-260128 - prompt: 继续往前走,走向远处连绵起伏的山丘,镜头缓缓推进 - seconds: "5" - metadata: - content: - - type: video_url - video_url: - url: "https://example.com/input-clip.mp4" - 快速版示例: - summary: 使用快速版模型(速度更快) - value: - model: doubao-seedance-2-0-fast-260128 - prompt: 城市夜晚的延时摄影,车流如光河,霓虹闪烁 - seconds: "-1" - metadata: - ratio: "16:9" - resolution: "480p" - responses: - "200": - description: 任务提交成功(此时任务仍在排队,`status` 为 `queued`) - content: - application/json: - schema: - $ref: '#/components/schemas/VideoTaskResponse' - example: - id: task_abc123def456 - task_id: task_abc123def456 - object: video - model: doubao-seedance-2-0-260128 - status: queued - progress: 0 - created_at: 1712345678 - "400": - description: 请求参数错误(如模型名称不支持、参数超出范围等) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: - message: "invalid model: doubao-seedance-2-0-xxx" - type: invalid_request_error - "401": - description: 未授权(Token 无效或缺失) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - "402": - description: 账户余额不足,无法完成预扣费 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - "500": - description: 服务器内部错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /v1/video/generations/{task_id}: - get: - tags: - - 视频生成 - summary: 查询视频生成任务状态 - description: | - 根据任务 ID 查询视频生成任务的当前状态和结果。 - - **任务状态流转**: - - ``` - queued → in_progress → completed ✅ 可下载视频 - ↘ failed ❌ 查看 error 字段 - ``` - - **视频获取方式**(任务 `completed` 后): - 1. 从响应体 `metadata.url` 字段获取视频直链 - 2. 调用 `GET /v1/videos/{task_id}/content` 通过代理下载视频文件 - - **轮询建议**: - - 每 **5 秒**查询一次 - - 总超时建议设置为 **10 分钟** - - Seedance 2.0 通常在 **1~3 分钟**内完成生成 - - operationId: getVideoGenerationTask - security: - - BearerAuth: [] - parameters: - - name: task_id - in: path - required: true - description: 提交任务时返回的任务 ID(响应体 `id` 字段) - schema: - type: string - example: task_abc123def456 - responses: - "200": - description: 查询成功(任务可能处于任意状态) - content: - application/json: - schema: - $ref: '#/components/schemas/VideoTaskResponse' - examples: - 排队等待: - summary: 任务排队中 - value: - id: task_abc123def456 - task_id: task_abc123def456 - object: video - model: doubao-seedance-2-0-260128 - status: queued - progress: 0 - created_at: 1712345678 - 生成中: - summary: 视频生成中 - value: - id: task_abc123def456 - task_id: task_abc123def456 - object: video - model: doubao-seedance-2-0-260128 - status: in_progress - progress: 50 - created_at: 1712345678 - 已完成: - summary: 生成完成,视频可下载 - value: - id: task_abc123def456 - task_id: task_abc123def456 - object: video - model: doubao-seedance-2-0-260128 - status: completed - progress: 100 - created_at: 1712345678 - completed_at: 1712345900 - metadata: - url: "https://cdn.volcengineapi.com/videos/output-abc123.mp4" - 生成失败: - summary: 生成失败 - value: - id: task_abc123def456 - task_id: task_abc123def456 - object: video - model: doubao-seedance-2-0-260128 - status: failed - progress: 100 - created_at: 1712345678 - completed_at: 1712345700 - error: - code: content_policy_violation - message: 请求内容违反了内容安全策略,请修改 prompt 后重试 - "401": - description: 未授权 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - "404": - description: 任务不存在(task_id 无效或不属于当前用户) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /v1/videos: - post: - tags: - - 视频生成 - summary: 提交视频生成任务(OpenAI 兼容格式) - description: | - 与 `POST /v1/video/generations` 功能完全相同,为兼容 OpenAI Video API 格式而提供的别名路由。 - 请求和响应格式与 `/v1/video/generations` 完全一致。 - operationId: submitVideoOpenAI - security: - - BearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/VideoGenerationRequest' - responses: - "200": - description: 任务提交成功 - content: - application/json: - schema: - $ref: '#/components/schemas/VideoTaskResponse' - "400": - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - "401": - description: 未授权 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /v1/videos/{task_id}: - get: - tags: - - 视频生成 - summary: 查询任务状态(OpenAI 兼容格式) - description: | - 与 `GET /v1/video/generations/{task_id}` 功能相同,兼容 OpenAI Video API 格式的别名路由。 - operationId: getVideoTaskOpenAI - security: - - BearerAuth: [] - parameters: - - name: task_id - in: path - required: true - description: 任务 ID - schema: - type: string - example: task_abc123def456 - responses: - "200": - description: 查询成功 - content: - application/json: - schema: - $ref: '#/components/schemas/VideoTaskResponse' - "404": - description: 任务不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /v1/videos/{task_id}/content: - get: - tags: - - 视频内容 - summary: 代理下载视频文件 - description: | - 通过 new-api 服务器代理下载已完成的视频文件(二进制流)。 - - **前置条件**:任务状态必须为 `completed`,否则返回 400 错误。 - - **工作原理**: - 1. 根据 `task_id` 查询数据库获取豆包存储的视频 URL - 2. 通过服务器代理请求并将视频内容流式转发给客户端 - 3. 响应头包含 `Cache-Control: public, max-age=86400`(24 小时缓存) - - **认证支持**:Bearer Token 或 Dashboard Session Cookie 均可访问。 - - operationId: downloadVideoContent - security: - - BearerAuth: [] - - SessionAuth: [] - parameters: - - name: task_id - in: path - required: true - description: 任务 ID(任务必须处于 `completed` 状态) - schema: - type: string - example: task_abc123def456 - responses: - "200": - description: 视频文件二进制流 - content: - video/mp4: - schema: - type: string - format: binary - example: "[视频二进制数据]" - "400": - description: 任务未完成,无法下载 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: - message: "Task is not completed yet, current status: in_progress" - type: invalid_request_error - "401": - description: 未授权 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - "404": - description: 任务不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - "502": - description: 上游视频 URL 不可访问 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - description: "API Token,格式:Bearer sk-xxxx" - SessionAuth: - type: apiKey - in: cookie - name: session - description: Dashboard 登录后的 Session Cookie(仅视频下载接口支持) - - schemas: - VideoGenerationRequest: - type: object - required: - - model - - prompt - description: 视频生成请求参数 - properties: - model: - type: string - description: | - 视频生成模型名称。 - - 支持的模型: - - `doubao-seedance-2-0-260128`:旗舰版,高质量(**推荐**) - - `doubao-seedance-2-0-fast-260128`:快速版,速度优先 - example: doubao-seedance-2-0-260128 - enum: - - doubao-seedance-2-0-260128 - - doubao-seedance-2-0-fast-260128 - prompt: - type: string - description: | - 视频内容的文字描述(提示词)。 - - 建议: - - 使用详细、具体的描述 - - 可包含场景、动作、风格、镜头语言等 - - 中英文均支持 - example: 宇航员在月球表面漫步,地球悬挂在黑色星空中,宏大震撼的画面 - image: - type: string - description: | - 参考图片(**图生视频**场景使用)。 - - 支持格式: - - HTTP/HTTPS 图片 URL - - Base64 编码的图片数据(格式:`data:image/jpeg;base64,...`) - - 当同时在 `metadata.content` 中也传入了图片时,此字段的图片会附加在 content 数组前端。 - example: "https://example.com/reference-image.jpg" - seconds: - type: string - description: | - 视频时长设置(字符串格式)。 - - **Seedance 2.0 & 2.0 Fast 支持两种配置方式**: - - 1. **指定具体时长**:填写 4~15 范围内的任意整数(单位:秒),如 `"5"`、`"8"`、`"15"` - 2. **智能选择**:设置为 `"-1"`,由模型在有效范围内自主选择合适的视频长度 - - > ⚠️ 视频时长与计费相关,时长越长消耗配额越多,请谨慎设置。 - > 实际生成视频的时长可通过查询任务 API 返回的 `duration` 字段获取。 - example: "-1" - default: "-1" - metadata: - $ref: '#/components/schemas/VideoMetadata' - - VideoMetadata: - type: object - description: | - 豆包视频的扩展参数,通过 `metadata` 字段传入。 - - 所有字段均为可选,未传入时使用模型默认值。 - - > 📖 详细参数说明请参考 [官方文档](https://www.volcengine.com/docs/82379/1520758?lang=zh) - properties: - ratio: - type: string - description: | - 视频宽高比(输出画面比例)。 - - 常用场景参考: - - `16:9`:横屏,适合普通视频、电影感 - - `9:16`:竖屏,适合短视频、手机端 - - `1:1`:方形,适合社交媒体 - example: "16:9" - enum: - - "16:9" - - "9:16" - - "1:1" - - "4:3" - - "3:4" - resolution: - type: string - description: | - 输出视频分辨率。 - - **Seedance 2.0 & 2.0 Fast 分辨率限制**: - - ✅ `480p`:支持 - - ✅ `720p`:支持(推荐) - - ❌ `1080p`:**不支持**(Seedance 2.0 系列均不支持 1080p) - - > 注:1080p 仅在 Seedance 1.0 lite 的参考图(i2v)场景下支持。 - example: "720p" - enum: - - "480p" - - "720p" - seed: - type: integer - description: | - 随机种子,用于控制生成结果的随机性。 - - - 相同的 `seed` + 相同的 `prompt` 会产生相似(但不完全相同)的视频 - - 有效范围:0 ~ 2,147,483,647 - - 不传时随机生成 - example: 42 - minimum: 0 - maximum: 2147483647 - watermark: - type: boolean - description: | - 是否在视频上添加水印。 - - - `false`(默认):不添加水印 - - `true`:添加豆包水印 - example: false - default: false - camera_fixed: - type: boolean - description: | - 是否固定摄像机(禁止镜头运动)。 - - - `false`(默认):允许镜头自然运动 - - `true`:固定镜头,适合需要静态构图的场景 - example: false - default: false - content: - type: array - description: | - 多模态内容数组,支持图片或视频输入。 - - **视频续拍场景**:传入 `video_url` 类型条目,系统会自动识别并应用视频输入折扣计费: - - `doubao-seedance-2-0-260128`:折扣约 **60.87%** - - `doubao-seedance-2-0-fast-260128`:折扣约 **59.46%** - - **图片输入场景**:传入 `image_url` 类型条目。 - - 数组中同时支持 `text` 类型(覆盖顶层 `prompt`)。 - items: - $ref: '#/components/schemas/ContentItem' - return_last_frame: - type: boolean - description: 是否返回视频最后一帧图片(可用于下一段视频续拍的参考帧) - example: false - generate_audio: - type: boolean - description: 是否为视频生成配套音频(实验性功能) - example: false - draft: - type: boolean - description: 是否生成草稿版本(生成速度快,质量较低,适合快速预览) - example: false - service_tier: - type: string - description: 服务等级(影响任务优先级) - example: default - callback_url: - type: string - format: uri - description: | - 任务完成/失败时的回调通知 URL(HTTP POST 方式推送)。 - 需配合豆包官方回调格式,建议通过业务服务器接收。 - example: "https://your-server.com/webhook/video-callback" - - ContentItem: - type: object - required: - - type - description: 多模态内容条目(用于 `metadata.content` 数组) - properties: - type: - type: string - description: 内容类型 - enum: - - text - - image_url - - video_url - example: image_url - text: - type: string - description: 文本内容(`type` 为 `text` 时使用,会覆盖顶层 `prompt` 字段) - example: 宇航员站起身,回望地球 - image_url: - type: object - description: 图片引用(`type` 为 `image_url` 时使用) - required: - - url - properties: - url: - type: string - description: 图片的 HTTP/HTTPS URL 或 Base64 数据 - example: "https://example.com/reference-photo.jpg" - video_url: - type: object - description: | - 视频引用(`type` 为 `video_url` 时使用)。 - - > ⚠️ 包含此类型条目时,系统自动应用视频输入折扣计费(约6折)。 - required: - - url - properties: - url: - type: string - description: 视频的 HTTP/HTTPS URL - example: "https://example.com/input-video-clip.mp4" - - VideoTaskResponse: - type: object - description: 视频任务响应体(任务提交后和状态查询时均返回此结构) - properties: - id: - type: string - description: 任务唯一标识符(由 new-api 系统生成) - example: task_abc123def456 - task_id: - type: string - description: 同 `id` 字段,保持向后兼容(**已废弃**,请使用 `id`) - example: task_abc123def456 - deprecated: true - object: - type: string - description: 对象类型,固定返回 `"video"` - example: video - enum: - - video - model: - type: string - description: 实际使用的模型名称 - example: doubao-seedance-2-0-260128 - status: - type: string - description: | - 任务当前状态: - - | 状态 | 说明 | - |------|------| - | `queued` | 已提交,排队等待处理 | - | `in_progress` | 视频生成中 | - | `completed` | 生成完成,可从 `metadata.url` 下载视频 | - | `failed` | 生成失败,查看 `error` 字段了解原因 | - enum: - - queued - - in_progress - - completed - - failed - example: completed - progress: - type: integer - description: 任务进度(0~100 的整数,仅供参考) - minimum: 0 - maximum: 100 - example: 100 - created_at: - type: integer - format: int64 - description: 任务创建时间(Unix 时间戳,秒) - example: 1712345678 - completed_at: - type: integer - format: int64 - description: 任务完成/失败时间(Unix 时间戳,秒)。仅在任务结束后返回。 - example: 1712345900 - expires_at: - type: integer - format: int64 - description: 任务结果链接过期时间(Unix 时间戳,秒) - example: 1712432300 - metadata: - type: object - description: 任务结果元数据 - properties: - url: - type: string - description: | - 视频文件的直接访问 URL。 - - - 仅在 `status` 为 `completed` 时存在 - - URL 有效期有限,建议及时下载或缓存 - - 也可通过 `GET /v1/videos/{task_id}/content` 代理下载 - example: "https://cdn.volcengineapi.com/videos/output-abc123.mp4" - error: - $ref: '#/components/schemas/VideoTaskError' - remixed_from_video_id: - type: string - description: 如果是 remix(续拍)任务,此字段为原始视频的任务 ID - example: task_origin789ghi012 - - VideoTaskError: - type: object - description: 视频任务失败时的错误详情 - properties: - code: - type: string - description: 错误码(由豆包上游返回) - example: content_policy_violation - message: - type: string - description: 人类可读的错误描述 - example: 请求内容违反了内容安全策略,请修改 prompt 后重试 - - ErrorResponse: - type: object - description: API 错误响应(HTTP 4xx/5xx 时返回) - properties: - error: - type: object - required: - - message - - type - properties: - message: - type: string - description: 错误描述信息 - example: Invalid authentication token - type: - type: string - description: 错误类型 - example: invalid_request_error - enum: - - invalid_request_error - - authentication_error - - permission_error - - server_error - code: - type: string - description: 错误码(部分场景下包含) - example: invalid_api_key From fa42bcc32be621155052a601826514b41fdae5c0 Mon Sep 17 00:00:00 2001 From: aabao <602018325@qq.com> Date: Fri, 10 Apr 2026 20:36:19 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E5=85=BC=E5=AE=B9=E8=B1=86=E5=8C=85/api/v3?= =?UTF-8?q?/=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middleware/doubao_adapter.go | 18 +++++-- relay/channel/adapter.go | 8 +++ relay/channel/task/doubao/adaptor.go | 74 ++++++++++++++++++++++++++++ relay/relay_task.go | 27 ++++++++++ 4 files changed, 122 insertions(+), 5 deletions(-) diff --git a/middleware/doubao_adapter.go b/middleware/doubao_adapter.go index a3d320652151..05a000287084 100644 --- a/middleware/doubao_adapter.go +++ b/middleware/doubao_adapter.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "strconv" + "strings" "github.com/QuantumNous/new-api/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" @@ -63,10 +64,13 @@ func DoubaoRequestConvert() func(c *gin.Context) { abortWithOpenAiMessage(c, http.StatusBadRequest, "task_id is required") return } - // Route to internal fetch endpoint + // Route to internal fetch endpoint. + // Set doubao_native_route so that videoFetchByIDRespBodyBuilder + // returns Doubao-native format, which downstream ParseTaskResult can parse. c.Request.URL.Path = "/v1/video/generations/" + taskID c.Set("task_id", taskID) c.Set("relay_mode", relayconstant.RelayModeVideoFetchByID) + c.Set("doubao_native_route", true) c.Next() return } @@ -79,14 +83,14 @@ func DoubaoRequestConvert() func(c *gin.Context) { } // Extract prompt and images from the content array - var prompt string + var promptParts []string var images []string for _, item := range nativeReq.Content { switch item.Type { case "text": - if item.Text != "" { - prompt = item.Text + if t := strings.TrimSpace(item.Text); t != "" { + promptParts = append(promptParts, t) } case "image_url": if item.ImageURL != nil && item.ImageURL.URL != "" { @@ -94,6 +98,7 @@ func DoubaoRequestConvert() func(c *gin.Context) { } } } + prompt := strings.Join(promptParts, "\n") // Build metadata — carry all non-standard fields so the doubao adaptor // can pick them up via taskcommon.UnmarshalMetadata. @@ -194,9 +199,12 @@ func DoubaoRequestConvert() func(c *gin.Context) { return } - // Replace request body + // Replace request body — also clear the BodyStorage cache so that + // downstream handlers (e.g. ValidateBasicTaskRequest) read the new body + // instead of the cached original Doubao native payload. c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonData)) c.Set(common.KeyRequestBody, jsonData) + c.Set(common.KeyBodyStorage, nil) // Redirect to the internal video generation endpoint c.Request.URL.Path = "/v1/video/generations" diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go index d2f7c6bb6d5a..1158c4925f77 100644 --- a/relay/channel/adapter.go +++ b/relay/channel/adapter.go @@ -81,3 +81,11 @@ type TaskAdaptor interface { type OpenAIVideoConverter interface { ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) } + +// DoubaoNativeResponseConverter is implemented by adaptors that can convert an +// internal Task record back into the Doubao-native API response format, so that +// a downstream new-api instance using the "doubao-video" channel type can parse +// the polling response with its own ParseTaskResult method. +type DoubaoNativeResponseConverter interface { + ConvertToDoubaoNativeResponse(originTask *model.Task) ([]byte, error) +} diff --git a/relay/channel/task/doubao/adaptor.go b/relay/channel/task/doubao/adaptor.go index a6dabb5f1086..c51d2a57e073 100644 --- a/relay/channel/task/doubao/adaptor.go +++ b/relay/channel/task/doubao/adaptor.go @@ -366,3 +366,77 @@ func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, erro return common.Marshal(openAIVideo) } + +// ConvertToDoubaoNativeResponse converts an internal Task record back to the +// Doubao-native API response format. This is used when a downstream new-api +// instance (configured with the "doubao-video" channel type pointing to us) +// polls for task status via GET /api/v3/contents/generations/tasks/:task_id. +// The downstream's ParseTaskResult expects this exact format. +func (a *TaskAdaptor) ConvertToDoubaoNativeResponse(originTask *model.Task) ([]byte, error) { + // Try to reconstruct from stored upstream response first + var dResp responseTask + if len(originTask.Data) > 0 { + _ = common.Unmarshal(originTask.Data, &dResp) + } + + // If the upstream task_id is stored, use it; otherwise fall back to public ID + upstreamID := originTask.GetUpstreamTaskID() + if upstreamID == "" { + upstreamID = originTask.TaskID + } + + // Map internal status back to Doubao native status strings + doubaoStatus := internalStatusToDoubaoStatus(originTask.Status) + + // Build a Doubao-native response payload + native := map[string]interface{}{ + "id": upstreamID, + "model": originTask.Properties.OriginModelName, + "status": doubaoStatus, + } + + if doubaoStatus == "succeeded" { + videoURL := originTask.GetResultURL() + if videoURL == "" { + videoURL = dResp.Content.VideoURL + } + native["content"] = map[string]interface{}{ + "video_url": videoURL, + } + } + + if doubaoStatus == "failed" { + native["error"] = map[string]interface{}{ + "code": dResp.Error.Code, + "message": dResp.Error.Message, + } + } + + if dResp.Usage.TotalTokens > 0 { + native["usage"] = dResp.Usage + } + if originTask.CreatedAt > 0 { + native["created_at"] = originTask.CreatedAt + } + if originTask.UpdatedAt > 0 { + native["updated_at"] = originTask.UpdatedAt + } + + return common.Marshal(native) +} + +// internalStatusToDoubaoStatus maps internal task status to Doubao native status strings. +func internalStatusToDoubaoStatus(status model.TaskStatus) string { + switch status { + case model.TaskStatusQueued, model.TaskStatusSubmitted, model.TaskStatusNotStart: + return "pending" + case model.TaskStatusInProgress: + return "processing" + case model.TaskStatusSuccess: + return "succeeded" + case model.TaskStatusFailure: + return "failed" + default: + return "pending" + } +} diff --git a/relay/relay_task.go b/relay/relay_task.go index 098e23828b6c..4f422c619cbb 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -384,6 +384,33 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d return } + // Doubao native route: return Doubao-native format so downstream new-api + // instances (using the "doubao-video" channel type) can parse the polling + // response with their own ParseTaskResult method. + if c.GetBool("doubao_native_route") { + adaptor := GetTaskAdaptor(originTask.Platform) + if adaptor != nil { + if converter, ok := adaptor.(channel.DoubaoNativeResponseConverter); ok { + nativeData, convertErr := converter.ConvertToDoubaoNativeResponse(originTask) + if convertErr != nil { + taskResp = service.TaskErrorWrapper(convertErr, "convert_to_doubao_native_failed", http.StatusInternalServerError) + return + } + respBody = nativeData + return + } + // Fallback: if the platform doesn't implement DoubaoNativeResponseConverter, + // return OpenAI Video API format as the next best option. + if converter, ok := adaptor.(channel.OpenAIVideoConverter); ok { + openAIVideoData, convertErr := converter.ConvertToOpenAIVideo(originTask) + if convertErr == nil { + respBody = openAIVideoData + return + } + } + } + } + // OpenAI Video API 格式: 走各 adaptor 的 ConvertToOpenAIVideo if isOpenAIVideoAPI { adaptor := GetTaskAdaptor(originTask.Platform)