From 2281c64ee8ae00970a87e3df4a396885cac4b692 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Tue, 14 Apr 2026 14:10:39 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20=E9=9F=B3=E9=A2=91=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E4=BD=9C=E4=B8=BA=E5=94=AF=E4=B8=80=E5=8F=82=E8=80=83=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=20=E2=80=94=20=E5=89=8D=E7=AB=AF=E6=A0=A1=E9=AA=8C=20?= =?UTF-8?q?+=20toast=20=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seedance API 不支持"纯音频"和"文本+音频"输入,必须搭配图片或视频。 - canSubmit() 校验同时检查 references 和 assetMentions - Toolbar 点击禁用按钮时弹出 toast 提示原因 Co-Authored-By: Claude Opus 4.6 (1M context) --- web/src/components/Toolbar.tsx | 10 +++++++++- web/src/store/inputBar.ts | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/web/src/components/Toolbar.tsx b/web/src/components/Toolbar.tsx index 5bda6e9..2f431a5 100644 --- a/web/src/components/Toolbar.tsx +++ b/web/src/components/Toolbar.tsx @@ -3,6 +3,7 @@ import { useInputBarStore } from '../store/inputBar'; import { useGenerationStore } from '../store/generation'; import { useAuthStore } from '../store/auth'; import { Dropdown } from './Dropdown'; +import { showToast } from './Toast'; import type { CreationMode, AspectRatio, Duration, GenerationType, ModelOption } from '../types'; import styles from './Toolbar.module.css'; @@ -145,7 +146,14 @@ export function Toolbar() { }, [estimatedTokens, model, references, team]); const handleSend = useCallback(() => { - if (!isSubmittable) return; + if (!isSubmittable) { + const s = useInputBarStore.getState(); + if (s.mode === 'universal' && s.references.some((r) => r.type === 'audio') + && !s.references.some((r) => r.type === 'image' || r.type === 'video')) { + showToast('音频不能作为唯一的参考素材,请同时添加图片或视频'); + } + return; + } addTask(); }, [isSubmittable, addTask]); diff --git a/web/src/store/inputBar.ts b/web/src/store/inputBar.ts index dc3900d..b434f51 100644 --- a/web/src/store/inputBar.ts +++ b/web/src/store/inputBar.ts @@ -285,10 +285,19 @@ export const useInputBarStore = create((set, get) => ({ ? state.references.length > 0 : state.firstFrame !== null || state.lastFrame !== null; if (!hasText && !hasFiles) return false; - // Audio cannot be sent alone — must have image or video - if (state.mode === 'universal' && state.references.length > 0) { - const hasImageOrVideo = state.references.some((r) => r.type === 'image' || r.type === 'video'); - if (!hasImageOrVideo && !hasText) return false; + // Audio cannot be the only reference — Seedance API requires image or video alongside + if (state.mode === 'universal') { + const hasAudioRef = state.references.some((r) => r.type === 'audio'); + const hasAudioAsset = (state.assetMentions || []).some((m: Record) => + (m.assetType || '').toLowerCase() === 'audio'); + if (hasAudioRef || hasAudioAsset) { + const hasImageOrVideoRef = state.references.some((r) => r.type === 'image' || r.type === 'video'); + const hasImageOrVideoAsset = (state.assetMentions || []).some((m: Record) => { + const t = (m.assetType || '').toLowerCase(); + return t === 'image' || t === 'video'; + }); + if (!hasImageOrVideoRef && !hasImageOrVideoAsset) return false; + } } // Block submit if any reference is still uploading or failed if (state.references.some((r) => r.uploading || r.uploadError)) return false; From dafdc8983f2ddc05feed6aa154893c90a3b14352 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 17 Apr 2026 18:03:36 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20v0.18.3=20=E7=89=88=E6=9D=83?= =?UTF-8?q?=E6=8A=A5=E9=94=99=E5=8F=8B=E5=A5=BD=E6=8F=90=E7=A4=BA=20+=20?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=88=A0=E9=99=A4=E5=8D=B3=E6=A2=A6=E5=BC=8F?= =?UTF-8?q?=E8=BF=9E=E7=BB=AD=E9=87=8D=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: 版权限制错误友好提示 - ERROR_MESSAGES 加 OutputVideoSensitiveContentDetected.PolicyViolation 映射 - 漫威等知名 IP 触发的版权拦截不再显示英文 raw error Bug 2: 图片删除后同类型引用连续重命名(即梦逻辑) - inputBar.ts::removeReference 重写:删除后同类型剩余引用按顺序 1/2/3 连续编号 - 用 DOMParser 同步更新 editorHtml 里对应 data-ref-id 的 @mention span textContent - 缩略图区和提示词栏同步刷新,避免"两个图片2"命名冲突 验证 - 11 个 Vitest 单元测试覆盖图片/视频/音频删除、空 editorHtml、无 @mention、 连续快速删除等边界场景 - 3 个 Playwright E2E 真实浏览器验证:上传 3 张图 → 删中间 → 再上传 → 编号不冲突 Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/utils/airdrama_client.py | 1 + docs/archive/celery-polling-fix-20260404.md | 134 ++ docs/archive/design-review.md | 134 ++ ...ce 2.0 & 2.0 fast API文档(邀测用户版).md | 692 +++++++ ...限填客户名称】Assets API 参考文档(邀测用户版).md | 1201 ++++++++++++ docs/archive/prd.md | 1611 +++++++++++++++++ docs/archive/test-report.md | 175 ++ web/src/store/inputBar.ts | 40 +- web/test/e2e/bug2-rename.spec.ts | 133 ++ .../unit/removeReferenceRelabeling.test.ts | 234 +++ 10 files changed, 4352 insertions(+), 3 deletions(-) create mode 100644 docs/archive/celery-polling-fix-20260404.md create mode 100644 docs/archive/design-review.md create mode 100644 docs/archive/old-Seedance 2.0 & 2.0 fast API文档(邀测用户版).md create mode 100644 docs/archive/old-「保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版).md create mode 100644 docs/archive/prd.md create mode 100644 docs/archive/test-report.md create mode 100644 web/test/e2e/bug2-rename.spec.ts create mode 100644 web/test/unit/removeReferenceRelabeling.test.ts diff --git a/backend/utils/airdrama_client.py b/backend/utils/airdrama_client.py index 2796085..a3b548d 100644 --- a/backend/utils/airdrama_client.py +++ b/backend/utils/airdrama_client.py @@ -15,6 +15,7 @@ ERROR_MESSAGES = { 'InputAudioSensitiveContentDetected': '参考音频包含敏感内容,请更换音频后重试', # Output content moderation 'OutputVideoSensitiveContentDetected': '生成的视频包含敏感内容,已被系统拦截,请修改提示词后重试', + 'OutputVideoSensitiveContentDetected.PolicyViolation': '生成的视频涉及版权限制内容(如知名IP、名人肖像等),已被系统拦截,请修改提示词后重试', 'OutputImageSensitiveContentDetected': '生成的图片包含敏感内容,已被系统拦截', # Parameter errors 'InvalidParameter': '请求参数无效,请检查输入内容', diff --git a/docs/archive/celery-polling-fix-20260404.md b/docs/archive/celery-polling-fix-20260404.md new file mode 100644 index 0000000..6f6013a --- /dev/null +++ b/docs/archive/celery-polling-fix-20260404.md @@ -0,0 +1,134 @@ +# Celery 轮询机制修复报告 + +> 日期:2026-04-04 +> 版本:v0.16.0 +> 影响范围:backend/apps/generation/tasks.py, backend/config/settings.py + +--- + +## 一、问题现象 + +2026/4/1 下午,大量用户反馈视频生成任务长时间卡在"生成中",前端显示耗时 60~65 分钟。 +火山引擎侧确认视频实际生成仅需约 10 分钟,结果已就绪但未被平台及时同步。 + +**截图数据**(4/1 下午完成的任务): + +| 提交时间 | 显示耗时 | +|---------|---------| +| 2026/4/1 16:57:28 | 63 分 33 秒 | +| 2026/4/1 16:58:41 | 62 分 37 秒 | +| 2026/4/1 16:59:16 | 62 分 7 秒 | +| 2026/4/1 17:00:36 | 64 分 24 秒 | +| 2026/4/1 17:04:53 | 64 分 2 秒 | + +## 二、根因分析 + +### 2.1 状态同步链路 + +``` +用户提交任务 + → 后端调 create_task(火山 API) + → 获得 ark_task_id + → 派发 Celery 任务 poll_video_task + → Celery worker 每 5 秒查一次火山 API + → 火山返回完成 → 写 DB + 上传 TOS + 结算 + → 前端轮询 DB → 展示结果 +``` + +前端只读 DB 状态,**不直接调火山 API**。整个链路完全依赖 Celery worker 轮询。 + +### 2.2 旧实现缺陷 + +`poll_video_task` 使用 `while True` + `time.sleep(5)` 长驻循环: + +```python +# 旧代码 +while True: + time.sleep(POLL_INTERVAL) # 5 秒 + ark_resp = query_task(...) # 查一次 + if terminal: + break +``` + +**三个致命问题:** + +| 问题 | 影响 | +|------|------| +| 每个任务占死一个 worker 进程 | `concurrency=4` 最多同时轮询 4 个任务,第 5 个排队 | +| worker 重启后循环直接丢失 | 内存中的 `while True` 不可持久化,OOM/重启 = 任务丢失 | +| `time.sleep` 浪费进程资源 | worker 99% 时间在 sleep,实际有用工作不到 1% | + +### 2.3 OOM 重启链 + +``` +4 个任务同时轮询 + → 某些任务完成,触发 TOS 上传(下载视频 + 上传对象存储) + → 内存飙升超过 512Mi 限制 + → K8s OOM Kill → worker 重启(共重启 15 次) + → 4 个进程中的 while True 循环全部丢失 + → 等 recover_stuck_tasks(每 10 分钟)重新派发 + → 重新派发后 worker 又被占满 → 又 OOM → 循环 + → 实际恢复耗时 ≈ 50~60 分钟 +``` + +## 三、修复方案 + +### 3.1 核心改动:self.retry 替代 while True + +```python +# 新代码 +@shared_task(bind=True, max_retries=None, ignore_result=True) +def poll_video_task(self, record_id): + record = GenerationRecord.objects.get(pk=record_id) + + ark_resp = query_task(record.ark_task_id) + new_status = map_status(ark_resp.get('status', '')) + + if new_status in ('queued', 'processing'): + record.save(update_fields=['status', 'updated_at']) + raise self.retry(countdown=5) # 5 秒后重新入队 + + # 到达终态 → 处理结果 + ... +``` + +**原理对比:** + +| | 旧方式(while True) | 新方式(self.retry) | +|---|---|---| +| 任务生命周期 | 在 worker 进程内存中 | 在 Redis 队列中 | +| worker 占用 | 持续占用直到完成(分钟级) | 每次查询仅占用毫秒级 | +| worker 重启 | 任务丢失 | Redis 中的任务自动恢复 | +| 并发能力 | 最多 4 个(= concurrency) | 数百个(受 API RPM 限制) | + +### 3.2 recover_stuck_tasks 间隔缩短 + +| | 旧值 | 新值 | +|---|---|---| +| Beat 调度间隔 | 600 秒(10 分钟) | 180 秒(3 分钟) | +| stuck 判定门槛 | 10 分钟 | 3 分钟 | +| 最坏恢复时间 | ~20 分钟 | ~6 分钟 | + +### 3.3 变更文件 + +| 文件 | 改动 | +|------|------| +| `backend/apps/generation/tasks.py` | `poll_video_task`: while True → self.retry;`recover_stuck_tasks`: 门槛 10 → 3 分钟 | +| `backend/config/settings.py` | Beat schedule: 600 → 180 秒 | + +## 四、效果预估 + +| 指标 | 修复前 | 修复后 | +|------|--------|--------| +| 同时轮询任务数上限 | 4 | 数百 | +| worker 重启后任务恢复 | 丢失,等 10 分钟兜底 | 自动恢复,无需兜底 | +| 最坏同步延迟 | 60+ 分钟 | ~15 秒(= 查询间隔 + 网络延迟) | +| 内存占用 | 持续占满(sleep 期间不释放) | 脉冲式占用(查完释放) | +| OOM 风险 | 高(4 进程常驻 + TOS 上传峰值) | 低(进程闲置时内存极小) | + +## 五、部署注意 + +1. **无需数据库迁移** — 仅修改 Python 代码 +2. **部署后旧的 while True 任务会自然消亡** — 不需要手动干预 +3. **Redis 中可能有旧格式的任务** — 兼容无问题,新旧 `poll_video_task` 签名一致(`record_id` 参数不变) +4. **建议同步部署**:先部署代码,再重启 Celery worker(`kubectl rollout restart deployment celery-worker`) diff --git a/docs/archive/design-review.md b/docs/archive/design-review.md new file mode 100644 index 0000000..259e0b0 --- /dev/null +++ b/docs/archive/design-review.md @@ -0,0 +1,134 @@ +# 设计评审报告 + +## 评审结论: APPROVED + +**评审版本**: PRD v3.0 — Phase 3 (计量单位变更 + 管理后台重做 + 用户个人中心) +**评审日期**: 2026-03-12 +**评审范围**: Phase 3 新增/修改的原型页面(6 个新文件 + 1 个更新) + +--- + +## 功能覆盖检查 + +### Phase 3 P0 核心功能 + +| PRD 功能点 | 是否实现 | 备注 | +|-----------|---------|------| +| 管理后台布局 — 左侧 Sidebar 240px + 4 导航项 | ✅ | 所有 4 个管理页面共享一致的 Sidebar,active 高亮正确 | +| 管理后台路由 — dashboard/users/records/settings | ✅ | 4 个独立页面,导航链接互通 | +| 用户个人中心页面 `/profile` | ✅ | 消费概览 + 消费趋势 + 消费记录 + 配额警告 | +| 消费概览卡片 — 环形进度条 + 日/月配额 | ✅ | SVG 环形图 345s/600s,日额度 82.0%,月额度 39.1% | + +**P0 覆盖率: 4/4 (100%)** + +### Phase 3 P1 重要功能 + +| PRD 功能点 | 是否实现 | 备注 | +|-----------|---------|------| +| 仪表盘 — 核心指标卡片 (4 个) | ✅ | 总用户数 1,234 / 今日新增 +23 / 今日消费 4,560s / 本月消费 89,010s,含环比变化箭头 | +| 仪表盘 — 消费趋势折线图 | ✅ | SVG 折线图 + 面积填充,30 天数据,tooltip 显示 "3/28 188s" | +| 仪表盘 — 用户消费排行柱状图 | ✅ | Top 10 水平柱状图,数据递减 2,340s → 350s,前 3 名高亮 | +| 仪表盘 — 时间范围选择器 | ✅ | 今日/近7天/近30天/自定义,按钮可交互切换 | +| 仪表盘 — 图表 Mock 数据 | ✅ | 30 天数据有自然波动和上升趋势,排行榜数据合理 | +| 用户管理 — 用户列表表格 (分页) | ✅ | 9 列数据 + 7 条记录,分页 "共 56 条,第 1/3 页" | +| 用户管理 — 搜索和筛选 | ✅ | 关键字搜索 + 状态下拉 (全部/已启用/已禁用) + 刷新按钮 | +| 用户管理 — 配额编辑模态框 | ✅ | 点击"编辑配额"弹出 Modal,含日/月限额输入框 + 取消/保存按钮 | +| 用户管理 — 用户状态管理 | ✅ | 启用用户显示"禁用"(红色),禁用用户显示"启用"(绿色) | +| 用户管理 — 用户详情抽屉 | ✅ | 点击用户名打开 420px 右侧抽屉,含完整用户信息 + 近期 3 条消费记录 | +| 消费记录 — 消费明细表格 | ✅ | 6 列 (时间/用户名/消费秒数/视频描述/生成模式/状态),10 条记录,分页 "共 1,234 条,第 1/62 页" | +| 消费记录 — 时间范围筛选 | ✅ | 日期选择器 2026-03-01 ~ 2026-03-12 + 查询按钮 | +| 消费记录 — 用户筛选 | ✅ | "搜索用户名..." 搜索框 | +| 消费记录 — 导出 CSV | ✅ | 顶栏"导出 CSV"按钮 + 下载图标,主题色边框 | +| 系统设置 — 全局默认配额 | ✅ | 日限额 600s / 月限额 6000s 数字输入框 + 提示文字 + 保存按钮 | +| 系统设置 — 系统公告管理 | ✅ | 开关切换 (ON 状态) + 文本域含示例公告 + 保存按钮 + Toast 反馈 | +| 个人中心 — 消费记录列表 | ✅ | 6 条记录 (时间/秒数/prompt/模式/状态) + "加载更多"按钮 | +| 个人中心 — 消费趋势迷你图 | ✅ | Sparkline 折线图 7 天数据 (3/6-3/12),近7天/近30天切换按钮 | +| 个人中心 — 配额提示 | ✅ | 黄色警告横幅 "今日额度已消费 82%,请合理安排使用" + 日额度卡片 warning 样式 | + +**P1 覆盖率: 19/19 (100%)** + +### Phase 3 P2 锦上添花 + +| PRD 功能点 | 是否实现 | 备注 | +|-----------|---------|------| +| 页面切换过渡动画 | ✅ | fadeUp 入场动画 + 延迟序列 | +| 数据加载骨架屏 | ❌ | 未实现 (P2,不影响评审) | +| Sidebar 折叠模式 | ❌ | 未实现 (P2,不影响评审) | + +**P2 覆盖率: 1/3 (33%)** + +### 导航页更新 + +| PRD 功能点 | 是否实现 | 备注 | +|-----------|---------|------| +| index.html 更新 — Phase 3 导航卡片 | ✅ | 3 个分区:Phase 1 核心功能 + Phase 3 管理后台 (4 卡片) + Phase 3 用户端 (个人中心) | + +--- + +## 后台管理系统专项检查 + +| 检查项 | 结果 | 说明 | +|--------|------|------| +| 按功能模块拆分多个页面 + 侧边导航 | ✅ | 4 个独立页面 + 240px Sidebar 导航,active 状态正确 | +| 仪表盘图表有真实 Mock 数据 | ✅ | 折线图 30 天波动数据 + 排行榜 10 用户递减数据 + tooltip | +| 独立的用户管理页面 | ✅ | 完整表格 + 搜索/筛选 + 编辑配额模态框 + 用户详情抽屉 | +| 数据表格有搜索、分页、操作列 | ✅ | 搜索框 + 状态筛选 + 分页控件 + 编辑/禁用操作按钮 | + +**专项检查: 4/4 全部通过** + +--- + +## 素材使用质量 + +| 检查项 | 结果 | 问题描述 | +|--------|------|---------| +| 图片方向 | N/A | 原型为纯 HTML/CSS/SVG,无外部图片素材 | +| 精灵图集裁切 | N/A | 不涉及精灵图 | +| 尺寸比例 | ✅ | SVG 图表自适应宽度,卡片网格响应式 | +| 真实素材引用 | ✅ | 使用 SVG 内联图标(非 emoji 占位符) | +| 素材加载 | ✅ | 所有页面资源正常加载,无 broken image | +| 视觉层次 | ✅ | Sidebar → Content → Modal/Drawer 层叠正确 | + +--- + +## 设计质量 + +- **视觉一致性**: 5/5 — 所有 6 个 Phase 3 页面共享统一的深色主题 (Linear/Vercel 风格),CSS 变量一致 (#0a0a0f, #111118, #16161e, #2a2a38, #00b8e6),字体统一 (Noto Sans SC + Space Grotesk + JetBrains Mono) +- **交互合理性**: 5/5 — Drawer/Modal/Toggle/Toast 交互完整,时间选择器可切换,分页/搜索/筛选 UI 齐全 +- **响应式设计**: 4/5 — 仪表盘卡片使用 grid-cols-4 max-lg:grid-cols-2 响应式,个人中心 max-width 900px 居中。Sidebar 无折叠模式 (P2) +- **素材使用正确性**: 5/5 — 纯 SVG 图表和图标,无外部依赖,渲染准确 + +**综合评分: 4.8/5** + +--- + +## Playwright 验证截图 + +| 页面 | 截图文件 | 验证结果 | +|------|---------|---------| +| 仪表盘 | prototype-admin-dashboard.png | ✅ 4 统计卡片 + 折线图 + 排行榜 + 时间选择器 | +| 用户管理 | prototype-admin-users.png | ✅ 搜索/筛选栏 + 7 行用户表格 + 分页 | +| 用户详情抽屉 | prototype-admin-users-drawer.png | ✅ 右侧 420px 抽屉 + 用户信息 + 近期消费 | +| 消费记录 | prototype-admin-records.png | ✅ 筛选栏 + 10 行记录表格 + 导出 CSV + 分页 | +| 系统设置 | prototype-admin-settings.png | ✅ 配额表单 + 公告管理 + Toggle 开关 | +| 个人中心 | prototype-user-profile.png | ✅ 环形图 + 日/月额度 + Sparkline + 消费记录 + 配额警告 | +| 导航页 | prototype-index.png | ✅ Phase 1 + Phase 3 管理后台 + Phase 3 用户端分区 | + +--- + +## 评审总结 + +Phase 3 原型质量**极高**,完整覆盖了 PRD 中所有 P0 (4/4) 和 P1 (19/19) 功能需求。 + +**亮点**: +1. 管理后台 4 个子页面布局一致,Sidebar 导航交互正确 +2. 仪表盘图表使用真实感 Mock 数据(折线图有自然波动,排行榜递减合理) +3. 用户管理交互完整:表格 + 搜索 + 筛选 + 编辑模态框 + 详情抽屉 +4. 个人中心环形进度条 + Sparkline 趋势图 + 配额警告横幅,信息层次清晰 +5. 整体深色主题统一,Linear/Vercel 风格专业感强 +6. 所有页面计量单位正确使用「秒数」(非「次数」),与 Phase 3 需求一致 + +**可改进项** (不影响通过): +- P2: Sidebar 折叠模式未实现 +- P2: 骨架屏加载态未实现 +- 用户表格缺少头像列(可在开发阶段补充) diff --git a/docs/archive/old-Seedance 2.0 & 2.0 fast API文档(邀测用户版).md b/docs/archive/old-Seedance 2.0 & 2.0 fast API文档(邀测用户版).md new file mode 100644 index 0000000..969cf38 --- /dev/null +++ b/docs/archive/old-Seedance 2.0 & 2.0 fast API文档(邀测用户版).md @@ -0,0 +1,692 @@ +# 【申请权限填客户名称】Seedance 2.0 & 2.0 fast API文档(邀测用户版) + +该文档目前仅限开白客户使用,发送前请和销管确认客户是否在开白名单内 + +***【❗️❗️❗️】该文档限制客户申请权限,只有返回了服务协议的客户方可申请*** + +本文介绍 Seedance 2.0 & 2.0 fast 模型相较于存量模型 **新增/配置有区别 **的 API 参数介绍,存量 API 参数的完整介绍参见 [视频生成 API](https://www.volcengine.com/docs/82379/1520758?lang=zh)。 + +> 本文档仅限预览及邀测用户使用: +> +> * 不承诺正式API上线100%一致。 +> +> * 仅限邀测用户阅读,请勿截图/分享给其他人员。 +> +> * 您上传的内容请确保由您原创或已取得授权。 + +# 模型能力 + +> **Seedance 2.0 和 Seedance 2.0 fast 提供的模型能力一致,**追求最高生成品质,推荐使用 **Seedance 2.0**;更注重成本与生成速度,不要求极限品质,推荐使用 **Seedance 2.0 fast**。 + +**Seedance 2.0 & 2.0 fast (有声视频/无声视频)** + +* **多模态参考生视频**:输入参考图片(0\~9)+参考视频(0\~3)+ 参考音频(0\~3)+ 文本提示词(可选)生成 1 个目标视频。支持生成全新视频、编辑视频、延长视频。 + +> **注意:不可单独输入音频,应至少包含 1 个参考视频或图片。** + +* **图生视频-首尾帧**:输入首帧图片+尾帧图片+文本提示词(可选)生成 1 个目标视频。 + +* **图生视频-首帧**:输入首帧图片+文本提示词(可选)生成 1 个目标视频。 + +* **文生视频**:输入文本提示词生成 1 个目标视频。 + + + +**模型能力对比表:** + +| 模型名称 | | [Seedance 2.0](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-2-0) | [Seedance 2.0 fast](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-2-0-fast\&projectName=default) | [Seedance 1.5 pro](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-5-pro\&projectName=default) | [Seedance 1.0 pro ](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-0-pro\&projectName=default) | [Seedance 1.0 pro fast ](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-0-pro-fast\&projectName=default) | [Seedance 1.0 lite i2v](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-0-lite-i2v\&projectName=default) | [Seedance-1.0 lite t2v ](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-0-lite-t2v) | +| ------------ | -------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Model ID | | doubao-seedance-2-0-260128 | doubao-seedance-2-0-fast-260128 | doubao-seedance-1-5-pro-251215 | doubao-seedance-1-0-pro-250528 | doubao-seedance-1-0-pro-fast-251015 | doubao-seedance-1-0-lite-i2v-250428 | doubao-seedance-1-0-lite-t2v-250428 | +| 文生视频 | | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | +| 图生视频-首帧 | | ✅ | | ✅ | ✅ | ✅ | ✅ | ❌ | +| 图生视频-首尾帧 | | ✅ | | ✅ | ✅ | ❌ | ✅ | ❌ | +| 多模态参考【New】 | 图片参考 | ✅ | | ❌ | ❌ | ❌ | ✅ | ❌ | +| | 视频参考 | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | +| | 组合参考 | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | +| 编辑视频【New】 | | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | +| 延长视频【New】 | | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | +| 生成有声视频 | | ✅ | | ✅ | ❌ | ❌ | ❌ | ❌ | +| 联网搜索增强【New】 | | ✅ | | ❌ | [❌](https://p9-arcosite.byteimg.com/obj/tos-cn-i-goo7wpa0wc/f359753773c94d97885008ca1223c9bc) | ❌ | ❌ | ❌ | +| 样片模式 | | ❌ | | ✅ | ❌ | ❌ | ❌ | ❌ | +| 返回视频尾帧 | | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | +| 输出视频规格 | 输出分辨率 | 480p, 720p | | 480p, 720p, 1080p | 480p, 720p, 1080p | 480p, 720p, 1080p | 480p, 720p, 1080p | 480p, 720p, 1080p | +| | 输出宽高比 | 21:9, 16:9, 4:3, 1:1, 3:4, 9:16 | | | | | | | +| | 输出时长 | 4\~15 秒 | | 4\~12 秒 | 2\~12 秒 | 2\~12 秒 | 2\~12 秒 | 2\~12 秒 | +| | 输出视频格式 | mp4 | | mp4 | mp4 | mp4 | mp4 | mp4 | +| 离线推理 | | [❌](https://p9-arcosite.byteimg.com/obj/tos-cn-i-goo7wpa0wc/f359753773c94d97885008ca1223c9bc) | | ✅ | ✅ | ✅ | ✅ | ✅ | +| 在线推理限流 | RPM | 600 | | 600 | 600 | 600 | 300 | 300 | +| | 并发数 | 10 | | 10 | 10 | 10 | 5 | 5 | +| 离线推理限流 | TPD | - | | 5000亿 | 5000亿 | 5000亿 | 2500亿 | 2500亿 | + + + + + +# Creat-创建视频生成任务 + +> POST https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks + +## 请求参数 + + + +#### **content** `object[]` `必选` + +输入给模型,生成视频的信息,支持文本、图片、音频、视频、样片任务 ID。支持以下几种组合: + +* **文本** + +* **文本(可选)+ 图片** + +* **文本(可选)+ 视频** + +* **文本(可选)+ 图片 + 音频** + +* **文本(可选)+ 图片 + 视频** + +* **文本(可选)+ 视频 + 音频** + +* **文本(可选)+ 图片 + 视频 + 音频** + +*** + +**信息类型:** + +* **文本信息**`object` + +输入给模型的提示词信息。 + +*** + +content.**type **`string` `必选` + +输入内容的类型,此处应为 **text**。 + +*** + +content.**text **`string` `必选` + +输入给模型的文本提示词,描述期望生成的视频。 + +支持中英文。建议中文不超过500字,英文不超过1000词。字数过多信息容易分散,模型可能因此忽略细节,只关注重点,造成视频缺失部分元素。提示词的更多使用技巧请参见 [Seedance 提示词指南](https://www.volcengine.com/docs/82379/1587797)。 + + + + + +* **图片信息** `object` + +输入给模型的图片信息。 + +*** + +content.**type **`string` `必选` + +输入内容的类型,此处应为 **image\_url**。 + +*** + +content.**image\_url **`object` `必选` + +输入给模型的图片对象。 + +*** + +content.image\_url.**url **`string` `必选` + +图片 URL 、图片 Base64 编码、素材 ID。 + +* 图片 URL:填入图片的公网 URL。 + +* Base64 编码:将本地文件转换为 Base64 编码字符串,然后提交给大模型。遵循格式:data:image/<图片格式>;base64,\,注意 <图片格式> 需小写,如 data:image/png;base64,{base64\_image}。 + +* 素材 ID:用于视频生成的预置素材及虚拟人像的 ID,遵循格式:asset://\,可从 [素材&虚拟人像库](https://console.volcengine.com/ark-stg/region:ark-stg+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128) 获取,详细使用请参见[文档](https://www.volcengine.com/docs/82379/2223965?lang=zh)。 + +> **传入单张图片要求** +> +> * 格式:jpeg、png、webp、bmp、tiff、gif +> +> * 宽高比(宽/高): (0.4, 2.5) +> +> * 宽高长度(px):(300, 6000) +> +> * 大小:单张图片小于 30 MB。请求体大小不超过 64 MB。大文件请勿使用Base64编码。 +> +> * 图片数量: +> +> * 图生视频-首帧:1 张 +> +> * 图生视频-首尾帧:2 张 +> +> * Seedance 2.0 & 2.0 fast 多模态参考生视频:1\~9 张 + +*** + +content.**role **`string` `条件必填` + +图片的位置或用途。 + +> **注意** +> +> * **图生视频-首帧**、**图生视频-首尾帧**、**多模态参考生视频**(包括参考图、视频、音频)为 3 种互斥场景,**不可混用**。 +> +> * **多模态参考生视频**可通过提示词指定参考图片作为首帧/尾帧,间接实现“首尾帧+多模态参考”效果。若需严格保障首尾帧和指定图片一致,**优先使用图生视频-首尾帧**(配置 role 为 **first\_frame / last\_frame**)。 + +*** + +**图生视频-首帧** + +> 需要传入1个 image\_url 对象 + +* **字段role取值:** + + * **first\_frame 或不填** + +*** + +**图生视频-首尾帧** + +> 需要传入2个 image\_url 对象 + +* **字段role取值:** + + * 首帧图片对应的字段 role 为:**first\_frame**,必填 + + * 尾帧图片对应的字段 role 为:**last\_frame**,必填 + +*** + +**图生视频-参考图 ** + +> 可传入 1\~9 个 image\_url 对象 + +* **字段role取值**: + + * 每张参考图对应的字段 role 均为:**reference\_image**,必填 + + + + + +* **视频信息** `object` + +输入给模型的视频信息。仅 Seedance 2.0 & 2.0 fast 支持输入视频。 + +*** + +content.**type **`string` `必选` + +输入内容的类型,此处应为 **video\_url**。 + +*** + +content.**video\_url **`object` `必选` + +输入给模型的视频对象。 + +*** + +content.video\_url.**url **`string` `必选` + +视频URL、素材 ID。 + +* 视频 URL:填入视频的公网 URL。 + +* 素材 ID:用于视频生成的预置素材及虚拟人像视频的 ID,遵循格式:asset://\。可从[素材&虚拟人像库](https://console.volcengine.com/ark-stg/region:ark-stg+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128)获取。 + +> **传入单个视频要求** +> +> * 视频格式:mp4、mov。 +> +> * 分辨率:480p、720p +> +> * 时长:单个视频时长 \[2, 15] s,最多传入 3 个参考视频,所有视频总时长不超过 15s。 +> +> * 尺寸: +> +> * 宽高比(宽/高):\[0.4, 2.5] +> +> * 宽高长度(px):\[300, 6000] +> +> * 画面像素(宽 × 高):\[409600, 927408] ,示例: +> +> * 画面尺寸 640×640=409600 满足最小值 ; +> +> * 画面尺寸 834×1112=927408 满足最大值。 +> +> * 大小:单个视频不超过 50 MB。 +> +> * 帧率 (FPS):\[24, 60] + +*** + +content.**role **`string` `条件必填` + +视频的位置或用途。当前仅支持 **reference\_video**。 + + + + + +* **音频信息 **`object` + +输入给模型的音频信息。仅 Seedance 2.0 & 2.0 fast 支持输入音频。注意不可单独输入音频,应至少包含 1 个参考视频或图片。 + +*** + +content.**type **`string` `必选` + +输入内容的类型,此处应为 **audio\_url**。 + +*** + +content.**audio\_url **`object` `必选` + +输入给模型的音频对象。 + +*** + +content.audio\_url.**url **`string` `必选` + +音频 URL 、音频 Base64 编码、素材 ID。 + +* 音频 URL:填入音频的公网 URL。 + +* Base64 编码:将本地文件转换为 Base64 编码字符串,然后提交给大模型。遵循格式:data:audio/<音频格式>;base64,\,注意 <音频格式> 需小写,如 data:audio/wav;base64,{base64\_audio}。 + +* 素材 ID:用于视频生成的虚拟人的音频素材 ID,遵循格式:asset://\。可从[素材&虚拟人像库](https://console.volcengine.com/ark-stg/region:ark-stg+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128)获取。 + +> **传入单个音频要求** +> +> * 格式:wav、mp3 +> +> * 时长:单个音频时长 \[2, 15] s,最多传入 3 段参考音频,所有音频总时长不超过 15 s。 +> +> * 大小:单个音频不超过 15 MB,请求体大小不超过 64 MB。大文件请勿使用Base64编码。 + +*** + +content.**role **`string` `条件必填` + +音频的位置或用途。当前仅支持 **reference\_audio** 。 + + + +#### **service\_tier** `string` + + Seedance 2.0 & 2.0 fast 暂不支持 + + + +#### **generate\_audio **`boolean` + +> Seedance 2.0 & 2.0 fast 默认值: true + +控制生成的视频是否包含与画面同步的声音。 + +* true:模型输出的视频包含同步音频。模型会基于文本提示词与视觉内容,自动生成与之匹配的人声、音效及背景音乐。建议将对话部分置于双引号内,以优化音频生成效果。例如:男人叫住女人说:“你记住,以后不可以用手指指月亮。” + +* false:模型输出的视频为无声视频。 + +> **说明** +> +> 生成的有声视频均为单声道,和传入的音频声道数无关。 + +#### + +#### **draft **`boolean` + + Seedance 2.0 & 2.0 fast 暂不支持 + + + +#### **tools **`object[]` + +> 仅 Seedance 2.0 & 2.0 fast 支持 + +配置模型要调用的工具。 + +*** + +tools.**type **`string` + +指定使用的工具类型。 + +* web\_search:联网搜索工具。 + +> **说明** +> +> * 开启联网搜索后,模型会根据用户的提示词自主判断是否搜索互联网内容(如商品、天气等)。可提升生成视频的时效性,但也会增加一定的时延。 +> +> * 实际搜索次数可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 usage.tool\_usage.**web\_search** 字段获取,如果为 0 表示未搜索。 + + + +#### **resolution ** `string` + +> Seedance 2.0 & 2.0 fast 默认值:720p + +视频分辨率,取值范围: + +* 480p + +* 720p + + + +#### **ratio **`string` + +> Seedance 2.0 & 2.0 fast 默认值: adaptive + +生成视频的宽高比例。不同宽高比对应的宽高像素值见下方表格。 + +* 16:9 + +* 4:3 + +* 1:1 + +* 3:4 + +* 9:16 + +* 21:9 + +* adaptive:根据输入自动选择最合适的宽高比 + +> **adaptive 适配规则** +> +> 当配置 **ratio** 为 adaptive 时,模型会根据生成场景自动适配宽高比;实际生成的视频宽高比可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 **ratio** 字段获取。 +> +> * 文生视频:根据输入的提示词,智能选择最合适的宽高比。 +> +> * 首帧 / 首尾帧生视频:根据上传的首帧图片比例,自动选择最接近的宽高比。 +> +> * 多模态参考生视频:根据用户提示词意图判断,如果是首帧生视频/编辑视频/延长视频,以该图片/视频为准选择最接近的宽高比;否则,以传入的第一个媒体文件为准(优先级:视频>图片)选择最接近的宽高比。 + +*** + +**不同宽高比对应的宽高像素值:** + +| 分辨率 | 宽高比 | 宽高像素值 | +| ---- | ---- | -------- | +| 480p | 16:9 | 864×496 | +| | 4:3 | 752×560 | +| | 1:1 | 640×640 | +| | 3:4 | 560×752 | +| | 9:16 | 496×864 | +| | 21:9 | 992×432 | +| 720p | 16:9 | 1280×720 | +| | 4:3 | 1112×834 | +| | 1:1 | 960×960 | +| | 3:4 | 834×1112 | +| | 9:16 | 720×1280 | +| | 21:9 | 1470×630 | + + + +#### **duration** `integer` + +> Seedance 2.0 & 2.0 fast 默认值:5 + +生成视频时长,仅支持整数,单位:秒。 + +取值范围: + +* \[4,15] 或设置为-1 + +> **配置方法** +> +> * 指定具体时长:支持有效范围内的任一整数。 +> +> * 智能指定:设置为 -1,表示由模型在有效范围内自主选择合适的视频长度(整数秒)。实际生成视频的时长可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 **duration** 字段获取。注意视频时长与计费相关,请谨慎设置。 + + + +#### **frames** `integer` + +Seedance 2.0 & 2.0 fast 暂不支持 + + + +#### **camera\_fixed** `boolean` + + Seedance 2.0 & 2.0 fast 暂不支持 + + + +# Get/List-查询视频生成任务/列表 + +> 查询视频生成任务:GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{id} +> +> 查询视频生成任务列表:GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks?page\_num={page\_num}\&page\_size={page\_size}\&filter.status={filter.status}\&filter.task\_ids={filter.task\_ids}\&filter.model={filter.model} + +## 响应参数 + +#### **tools **`object[]` + +> 仅 Seedance 2.0 & 2.0 fast 支持 + +配置模型要调用的工具。 + +*** + +tools.**type **`string` + +指定使用的工具类型。 + +* web\_search:联网搜索工具。 + + + +#### **usage** `object` + +本次请求的 token 用量。 + +*** + +usage.**completion\_tokens** `integer` + +模型输出视频花费的 token 数量。 + +*** + +usage.**total\_tokens** `integer` + +本次请求消耗的总 token 数量。 + +*** + +usage.**tool\_usage **`object` + +> 仅 Seedance 2.0 & 2.0 fast 支持 + +使用工具的用量信息。 + +*** + +usage.tool\_usage.**web\_search **`integer` + +实际调用联网搜索工具的次数,仅开启联网搜索时返回。 + + + +# 调用简介及示例 + +## 流程简介 + +任务接口是异步接口,视频生成任务流程 + +1. 创建视频生成任务接口创建视频生成任务 + +2. 定时使用查询接口查询视频生成任务状态 + + 1. 任务 running,过段时间再查询任务状态 + + 2. 任务完成,返回视频链接,在24小时内下载生成的视频文件 + +## 1. 创建视频生成任务 + +> 以下示例仅展示 Seedance 2.0 & 2.0 fast 新增能力,更多视频生成示例详见 [创建视频生成任务 API](https://www.volcengine.com/docs/82379/1520757)。 + +### 多模态参考 + +### 编辑视频 + +### 延长视频 + +### 使用联网搜索 + +仅支持文本生视频 + +## 2. 查询视频生成任务 + +# 最佳实践-使用公共虚拟人像生成视频 + +平台提供公共虚拟人像素材库,目前您可以使用其中的图像素材来创建一个统一、完备的视频主角。帮助您更好地控制主角,并确保其形象在多段视频中保持一致,避免因为真人人脸限制导致角色无法统一的问题。 + +素材模态目前包含图片,并提供人物背景描述。每个素材对应一个独立素材 ID (asset ID),在体验中心的视频生成任务中,指定角色人脸生成视频。 + +1. 在浏览器中打开[体验中心](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128\&tab=GenVideo),点击输入框下方的 **虚拟人像库** 页签。 + +2. 检索需要使用的人像,支持使用自然语言检索及筛选框组合筛选。 + +| 输入:文本 | 输入:虚拟人像、图片 | 输出 | +| ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -- | +| **图片1**中美妆博主用中文进行介绍,妆容改为明艳大气,去掉脸部反光,笑容甜美,近景镜头,手持**图片2**的面霜面向镜头展示,清新简约背景,元气甜美风格。博主台词:挖到本命面霜了!质地像云朵一样软糯,一抹就吸收,熬夜急救、补水保湿全搞定,素颜都自带柔光感。 | ![Image Token: HTf6bPRukoWaW4xnCSlcvKtUn7c](images/HTf6bPRukoWaW4xnCSlcvKtUn7c.png)![Image Token: YfCDbzJlqo4yzZxCmdscWdsInCf](images/YfCDbzJlqo4yzZxCmdscWdsInCf.jpeg) | | + + + +在 [Video Generation API](https://www.volcengine.com/docs/82379/1520758) 的 **content.<模态>\_url.url** 字段中使用 素材 URI 生成视频。 + +> 输入的参考内容,包括人像素材,需符合视频生成限制,具体信息请查看使用限制。 +> +> **注意**: +> +> * 首次在 API 中使用虚拟人像素材 Asset URI 前,需先在[方舟体验中心](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128\&tab=GenVideo)提交一次视频生成任务,阅读并同意弹出的 **虚拟人像库使用协议**。 +> +> * 体验中心支持体验视频生成能力。默认单次生成 4 段视频,为节约成本,建议设置为每次生成 1 条,具体方式可参考[虚拟人像库](https://www.volcengine.com/docs/82379/2223965?lang=zh)。 + +同意协议的操作方式如下: + +![Image Token: LK8ybUN9Ko2KkQxq2FdclVQtnkh](images/LK8ybUN9Ko2KkQxq2FdclVQtnkh.gif) + +示例代码: + +# 使用自有虚拟人像素材生成视频(线下提交) + +方舟提供私域人像素材库,您可在视频生成中使用自有虚拟人物或真人(仅限素人)素材,生成短剧等更定制化的视频内容。平台将对您提供的素材进行审核,规避可能产生的法律风险。 + +* 自有素材需入库后使用,您可将虚拟人像或真人素材发送给销售代表,同时完成合规承诺函及其他证明材料的准备。 + +* 入库后,您可使用素材的 Asset ID,在视频生成 API 中使用自有素材。 + +> **重要**: +> +> * 对虚拟人像素材,您需签署虚拟人像素材合规承诺函,并提供签署承诺函所需的材料。 +> +> * 对真实人物素材,除承诺函外,您还需额外提供真人授权材料。 +> +> * 具体流程及所需材料,请和您的销售代表确认。 + +提交自有人像素材时,需按人物将素材分组: + +* 每个人物为一个素材组。 + +* 每组可包含多个素材文件,素材文件对应唯一 ID (asset ID)。 + +## 入库流程 + +提交自有虚拟人像素材方式大致如下,请联系您的销售代表了解详情。 + +1. 准备素材文件,完成承诺函签署,并准备其他证明材料。 + +2. 准备素材文件,完成承诺函签署,并准备其他证明材料。 + + * 每个人物素材需至少提供一张正面图片文件。此外,您可按需提供该人物的其他图片、视频素材。 + + * 需确保每个人物组中的素材与该正面图片为同一人物。 + + * 每个人物创建一个文件夹(命名:“*虚拟人像 1-<人像名>*”) + + 提交素材文件夹示例: + + ![Image Token: XMQ9bz6vhof7vxxsac8cqIZmneB](images/XMQ9bz6vhof7vxxsac8cqIZmneB.png) + + > **注意**: + > + > * 以上示例仅供参考,您可根据视频创作需求,提交虚拟人物素材。 + > + > * 您仅需上传视频生成任务中需要使用的素材。 + + * 素材文件需满足视频生成 API 对输入文件的要求: + + > **传入单张图片要求** + > + > * 格式:jpeg、png、webp、bmp、tiff、gif + > + > * 宽高比(宽/高): (0.4, 2.5) + > + > * 宽高长度(px):(300, 6000) + > + > * 大小:单张图片小于 30 MB。请求体大小不超过 64 MB。大文件请勿使用Base64编码。 + + + + > **传入单个视频要求** + > + > * 视频格式:mp4、mov。 + > + > * 分辨率:480p、720p + > + > * 时长:单个视频时长 \[2, 15] s,最多传入 3 个参考视频,所有视频总时长不超过 15s。 + > + > * 尺寸: + > + > * 宽高比(宽/高):\[0.4, 2.5] + > + > * 宽高长度(px):\[300, 6000] + > + > * 画面像素(宽 × 高):\[409600, 927408] ,示例: + > + > * 画面尺寸 640×640=409600 满足最小值 ; + > + > * 画面尺寸 834×1112=927408 满足最大值。 + > + > * 大小:单个视频不超过 50 MB。 + > + > * 帧率 (FPS):\[24, 60] + + + + > **注意**: + > + > 有关提交流程、承诺函签署所需材料的具体信息,请联系您的销售代表了解详情。 + +3. 方舟将对您提供的素材进行审核,通过审核的素材将被上传至虚拟人像库。 + +4. 入库后,每个人物组素材将通过以下示例中的形式返回,您可解压后查看: + + ![Image Token: PKu6b3391oUbVKxxEGjchxBVnbg](images/PKu6b3391oUbVKxxEGjchxBVnbg.png) + +示例中: + +* Andy 为您提交的人物名称 + +* group-20260310035119-9mzqn 为该人物组的 ID + +* 解压后,可查看每张素材的 Asset ID,如: + +![Image Token: VV0ybrxNfouEhZxTjqCcX1epnzb](images/VV0ybrxNfouEhZxTjqCcX1epnzb.png) + +* 您可按 `asset: //` 规则拼接 URI,在 API 中使用对应素材生成视频: + +具体调用方式请参考 [最佳实践-使用虚拟人像生成视频](https://bytedance.larkoffice.com/wiki/SANpwJ9bgiKgrykLaMTcAB0InWc#share-YurKdrLfAocLErxsTWDcKidPnGd)。 + +## **注意事项** + +1. 首次在 API 中使用虚拟人像素材 Asset URI 前,需先在[方舟体验中心](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128\&tab=GenVideo)提交一次视频生成任务,阅读并同意弹出的 **虚拟人像库使用协议**,操作方式如下: + +![Image Token: IFfPbDgceoFXZCxdriIcnwkPnUc](images/IFfPbDgceoFXZCxdriIcnwkPnUc.gif) + +* 仅支持使用已入库素材生成视频。 diff --git a/docs/archive/old-「保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版).md b/docs/archive/old-「保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版).md new file mode 100644 index 0000000..75cb4f3 --- /dev/null +++ b/docs/archive/old-「保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版).md @@ -0,0 +1,1201 @@ +# 「⚠️保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版) + +本文介绍素材资产(Assets)API 接口的参数。您可以使用以下 Assets API 接口创建、管理个人人像素材资产。 + +> 本文档仅限预览及邀测用户使用: +> +> * 不承诺正式 API 上线100%一致。 +> +> * 仅限邀测用户阅读,请勿截图/分享给其他人员。 +> +> * 您需确保上传的虚拟人像符合以下条件: +> +> * 您合法拥有该素材,并享有完整的使用及处分权限。素材不包含未获授权的第三方商标、标识类内容。 +> +> * 素材不得与任何自然人肖像或形象雷同,素材不存在抄袭、盗用情形,不会侵害任何第三方的人格权、知识产权等合法权益。 +> +> * 素材不包含违反法规、违背公序良俗、危害国家安全的内容。 + + + +# 素材资产(Assets)API 接口功能 + +> **素材资产的概念说明:** +> +> * **Asset(素材资产)**:一个素材文件(图片),是方舟 Seedance 2.0 系列模型可直接用于推理的可信资产。 +> +> * 仅需入库推理需使用的素材资产,不需使用的素材资产请勿入库。 +> +> * 仅可使用已入库素材资产的 Id (Asset ID) 进行视频生成,同一形象未入库素材无法使用。 +> +> * **Asset Group(素材资产组合)**:单个素材文件为一个 Asset,每个 Asset 属于一个 Asset Group。 +> +> * 可以使用素材组自由管理素材,例如可将同一人物、同一工作室或项目组的素材放入同一素材组合进行管理。 + +**Asset (Group) 创建接口:** + +1. CreateAssetGroup:创建素材资产组合。**首次创建素材资产组合时需在控制台签署授权函,详情参考 [ 私域虚拟人像素材库 (WIP)](https://bytedance.larkoffice.com/wiki/RtHgwpJgviwFXLkQ9hLcRooEnVe)** + +2. CreatAsset:创建素材资产。该接口可用于上传个人素材资产,创建素材资产后可利用返回字段中的素材 **Id (需处于 `active` 状态)**用于 Seedance 2.0 系列模型生成视频。 + + + +**Asset (Group) 管理接口:** + +* ListAssetGroups:查询素材资产组合列表。 + +* ListAssets:查询素材资产列表。 + +* GetAsset:查询素材资产信息。 + +* GetAssetGroup:查询素材资产组合信息。 + +* UpdateAssetGroup:更新素材资产组合信息。 + +* UpdateAsset:更新素材资产信息。 + + + +# 鉴权方式 + +调用素材资产(Assets)API 接口需使用 Access Key 鉴权,详情参考 [获取 API 访问密钥(AK/SK)](https://www.volcengine.com/docs/6257/64983?lang=zh)。 + + + +# 限流要求 + +* 并发数限制:账号下同一时刻在处理中的任务数量上限,超过此限制的任务将进入队列等待处理。**同时进行处理的asset创建任务不超过30。** + +* QPS 限流: API接口每秒查询请求的总数上限。超过此限制的查询请求会报错。 + +| 接口名 | 账号维度的 QPS 限流 | +| ---------------- | ------------ | +| CreateAssetGroup | 30 | +| CreateAsset | 30 | +| ListAssetGroups | 30 | +| ListAssets | 30 | +| GetAsset | 100 | +| GetAssetGroup | 100 | +| UpdateAsset | 30 | +| UpdateAssetGroup | 30 | + +# CreateAssetGroup + +> **POST **/open/CreateAssetGroup + +创建 Asset Group(素材资产组合)组合,用作素材资产管理。 + +> **首次创建 Asset Group(素材资产组合)需在控制台签署授权函,详情参考 [ 私域虚拟人像素材库 (WIP)](https://bytedance.larkoffice.com/wiki/RtHgwpJgviwFXLkQ9hLcRooEnVe)** + +### **Name** `string` `必填` + +Asset Group(素材资产组合)的名称,上限为 64 字符。 + +*** + +### **Description** `string` + +Asset Group(素材资产组合)的描述,上限为 300 字符。 + +*** + +### **GroupType **`string` + +Asset Group(素材资产组合)的类型。可选值: + +* AIGC:虚拟人像。 + +*** + +### **ProjectName **`string`** ** + +资源所属的项目名称,默认值为`default`。 + +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +*** + + + +## 返回参数 + +### **Id** `string` + +Asset Group(素材资产组合)的 Id。 + +*** + +返回示例: + +```bash +{ + "Id": "group-2026**********-*****" +} +``` + + + +# CreateAsset + +> **POST **/open/CreateAsset + +向指定的Asset Group(素材资产组合)内创建Asset(素材资产)。 + + + +## 请求参数 + +### **GroupId** `string` `必填` + +Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 + +*** + +### **URL** `string` `必填` + +传入的Asset(素材资产)的公共可访问地址。 + +*** + +### **Name** `string` + +Asset(素材资产)的名称,上限为64个字符。 + +*** + +### **AssetType `string` `必填`** + +Asset(素材资产)的类型,当前仅支持传入图像。可选值: + +* Image:Asset(素材资产)的类型为图像。 + +> **传入图像的要求说明** +> +> * 格式:jpeg、png、webp、bmp、tiff、gif、heic/heif +> +> * 宽高比(宽/高): (0.4, 2.5) +> +> * 宽高长度(px):(300, 6000) +> +> * 大小:单张图片小于 30 MB。 + +*** + +### **ProjectName** `string` + +资源所属的项目名称,默认值为`default`。 + +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +> 需要和待传入的 Asset Group(素材资产组合)的 **ProjectName **保持一致。 + +*** + + + +## 返回参数 + +### **Id **`string` + +Asset(素材资产)的 Id。 + +*** + +返回示例: + +```bash +{ + "Id": "Asset-2026**********-*****" +} +``` + + + +# ListAssets + +> **POST **/open/ListAssets + +查询符合筛选条件的Assets(素材资产)列表。 + +## 请求参数 + +### **Filter** `object` `必填` + +搜索的过滤条件。 + +*** + +Filter.**GroupIds** `array` + +Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 + +*** + +Filter.**GroupType** `string` `必填` + +Asset Group(素材资产组合)的类型。可选值: + +* AIGC:虚拟人像。 + +*** + +Filter.**Statuses** `array`** ** + +任务状态。 + +* Active:素材资产(Asset)已处理完毕,可以使用。 + +* Processing:素材资产(Asset)正在预处理,无法使用。 + +* Failed:素材资产(Asset)处理失败。 + +*** + +Filter.**Name** `string`** ** + +Asset(素材资产)的名称,上限为64个字符。 + +*** + +### **PageNumber** `int (i64)` `必填` + +搜索页码,可用于列表分页功能,从 1 开始。例如:"page\_number": 1,即返回第一页的搜索结果。 + +*** + +### **PageSize** `int (i64)` `必填` + +每页搜索结果的数量,上限为100。 + +*** + +### **SortBy** `string` + +用于排序的字段名称,默认值 `createTime`。支持以下类型: + +* CreateTime:根据创建时间排序。 + +* UpdateTime:根据更新时间排序。 + +* GroupId:根据资产素材组的 Id 排序。 + +*** + +### **SortOrder** `string` + +排序顺序,默认值 `Desc`。可选值: + +* Desc:降序 + +* Asc:升序 + +*** + +### **ProjectName** `string` + +资源所属的项目名称,默认值为`default`。 + +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +*** + + + +## 返回参数 + +### **Items** `array[]` + +符合筛选条件的Asset(素材资产)数组。 + +*** + +Items.**Id** `string` + +Asset(素材资产)的 Id。 + +*** + +Items.**name** `string` + +Asset(素材资产)的名称,上限为64个字符。 + +*** + +Items.**URL** `string` + +Asset(素材资产)的公共可访问地址。 + +*** + +Items.**GroupId** `string` + +Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 + +*** + +Items.**AssetType** `string` + +Asset(素材资产)的类型。 + +* Image:Asset(素材资产)的类型为图像。 + +*** + +Items.**Status** `string` + +任务状态。 + +* Active:素材资产(Asset)已处理完毕,可以使用。 + +* Processing:素材资产(Asset)正在预处理,无法使用。 + +* Failed:素材资产(Asset)处理失败。 + +*** + +Items.**Error** `object` + +错误信息。 + +*** + +Items.Error.**Code** `string` + +错误码。 + +*** + +Items.Error.**Message** `string` + +错误信息。 + +*** + +Items.**ProjectName** `string` + +资源所属的项目名称。 + +*** + +Items.**CreateTime **`string` + +创建时间。 + +*** + +Items.**UpdateTime **`string` + +更新时间。 + +*** + +### **TotalCount **`int (i64)` + +返回总数。 + +*** + +### **PageNumber **`int (i64)` + +返回的页数。 + +*** + +### **PageSize **`int (i64)` + +每页搜索结果的数量,上限为100。 + + + +# ListAssetGroups + +> **POST **/open/ListAssetGroups + +查询符合筛选条件的Asset Groups(素材资产组合)列表。 + +## 请求参数 + +### **Filter** `object` `必填` + +搜索的过滤条件。 + +*** + +Filter.**name** `string` + +Asset Group(素材资产组合)的名称,上限为64个字符。 + +*** + +Filter.**GroupIds** `array` + +Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 + +*** + +Filter.**GroupType** `string`** **`必填` + +Asset Group(素材资产组合)的类型。可选值: + +* AIGC:虚拟人像。 + +*** + +### **PageNumber** `int (i64)` + +搜索页码,可用于列表分页功能,从 1 开始。例如:"page\_number": 1,即返回第一页的搜索结果。 + +*** + +### **PageSize** `int (i64)` + +每页搜索结果的数量,上限为100。 + +*** + +### **SortBy** `string` + +用于排序的字段名称,默认值 `createTime`。支持以下类型: + +* CreateTime:根据创建时间排序。 + +* UpdateTime:根据更新时间排序。 + +*** + +### **SortOrder** `string` + +排序顺序,默认值 `Desc`。可选值: + +* Desc:降序 + +* Asc:升序 + +*** + +### **ProjectName** `string` + +资源所属的项目名称,默认值为`default`。 + +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +*** + + + +## 返回参数 + +### **TotalCount **`int (i64)` + +返回的 Asset Group(素材资产组合)的总数。 + +*** + +### **Items** `array[]` + +符合筛选条件的 Asset Group(素材资产组合)数组。 + +*** + +Items.**Id** `string` + +Asset Group(素材资产组合)的 Id。 + +*** + +Items.**Name** `string` + +Asset Group(素材资产组合)的名称,上限为64个字符。 + +*** + +Items.**Title** `string` + +Asset Group(素材资产组合)的标题。 + +> 即将下线,请直接使用参数 Name + +*** + +Items.**Description** `string` + +Asset Group(素材资产组合)的描述,上限为 300 字符。 + +*** + +Items.**GroupType** `string` + +Asset Group(素材资产组合)的类型。 + +* AIGC:虚拟人像。 + +*** + +Items.**ProjectName** `string` + +资源所属的项目名称。 + +*** + +Items.**CreateTime** `string` + +创建时间。 + +*** + +Items.**UpdateTime** `string` + +更新时间。 + +*** + +### **PageNumber **`int (i64)` + +返回的页数。 + +*** + +### **PageSize **`int (i64)` + +每页搜索结果的数量,上限为100。 + +*** + + + +# GetAssetGroup + +> **POST **/open/GetAssetGroup + +获取单个Asset Group(素材资产组合)信息。 + +## 请求参数 + + + +### **Id **`string` `必填` + +Asset Group(素材资产组合)的 Id + +*** + +### **ProjectName **`string` + +需要查询的 Asset Group(素材资产组合)所属的项目名称,默认值为`default`。 + +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +*** + + + +## 返回参数 + +### **Id** `string` + +Asset Group(素材资产组合)的 Id。 + +*** + +### **Name** `string` + +Asset Group(素材资产组合)的名称,上限为64个字符。 + +*** + +### **Title** `string` + +Asset Group(素材资产组合)的标题。 + +> 即将下线,请直接使用参数 Name + +*** + +### **Description** `string` + +Asset Group(素材资产组合)的描述,上限为 300 字符。 + +*** + +### **GroupType **`string` + +Asset Group(素材资产组合)的类型。 + +* AIGC:虚拟人像 + +*** + +### **ProjectName **`string`** ** + +资源所属的项目名称。 + +*** + +### **CreateTime **`string` + +创建时间。 + +*** + +### **UpdateTime **`string` + +更新时间。 + +*** + +## + +# GetAsset + +> **POST **/open/GetAsset + +获取单个Asset(素材资产)信息。 + +## 请求参数 + + + +### **Id **`string` `必填` + +Asset(素材资产)的 Id。 + +*** + +### **ProjectName **`string` + +需要查询的 Asset(素材资产)所属的项目名称,默认值为`default`。 + +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +*** + + + +## 返回参数 + +### **Id **`string` + +Asset(素材资产)的 Id。 + +*** + +### **Name** `string` + +Asset(素材资产)的名称,上限为64个字符。 + +*** + +### **URL **`string` + +Asset(素材资产)的访问地址。 + +*** + +### **AssetType **`string` + +Asset(素材资产)的类型。 + +* Image:Asset(素材资产)的类型为图像。 + +*** + +### **GroupId** `string` + +Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 + +*** + +### **Status** `string` + +任务状态。 + +* Active:素材资产(Asset)已处理完毕,可以使用。 + +* Processing:素材资产(Asset)正在预处理,无法使用。 + +* Failed:素材资产(Asset)处理失败。 + +*** + +### **Error** `object` + +错误信息。 + +*** + +Error.**Code** `string` + +错误码。 + +*** + +Error.**Message** `string` + +错误信息。 + +*** + +### **CreateTime **`string` + +创建时间。 + +*** + +### **UpdateTime ** `string` + +更新时间。 + +*** + +### **ProjectName** `string` + +资源所属的项目名称。 + +*** + +## + +# **UpdateAssetGroup** + +> **POST **/open/UpdateAssetGroup + +更新单个 Asset Group(素材资产组合)信息。当前仅支持更新 Asset Group(素材资产组合)的 Name 和 Description。 + +## 请求参数 + +### **Id **`string` `必填` + +需要更新的 Asset Group(素材资产组合)的 Id + +*** + +### **Name **`string` + +需要更新的 Asset Group(素材资产组合)的新名称,上限为64个字符。 + +*** + +### **Description** `string` + +需要更新的 Asset Group(素材资产组合)的新描述,上限为 300 字符。 + +*** + +### **ProjectName** `string` + +需要更新的 Asset Group(素材资产组合)所属的项目名称,默认值为`default`。 + +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +*** + + + +## 返回参数 + +### **Id** `string` + +Asset Group(素材资产组合)的 Id。 + +*** + + + + + +# **UpdateAsset** + +> **POST **/open/UpdateAsset + +更新单个Asset(素材资产)信息。当前仅支持更新Asset(素材资产)的 Name。 + +## 请求参数 + +### **Id **`string` `必填` + +需要更新的 Asset(素材资产)的 Id + +*** + +### **Name **`string` + +需要更新的 Asset(素材资产)的新名称,上限为64个字符。 + +*** + +### **ProjectName** `string` + +需要更新的 Asset(素材资产)所属的项目名称,默认值为`default`。 + +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +*** + + + +## 返回参数 + +### **Id** `string` + +Asset(素材资产)的 Id。 + +*** + + + +# 代码示例: + +> **以下示例为使用 Asset API 创建素材资产并用于视频生成的使用链路:** +> +> 1. **创建素材资产组合:**调用 **CreateAssetGroup** 接口创建一个素材资产组合(Asset Group),用于对同一项目或人物的素材进行统一管理。首次创建时需在控制台签署授权函。 +> +> 2. **上传素材资产并等待预处理完成:**调用 **CreateAsset** 接口上传图片素材,传入图片的公共访问URL及所属的Group ID,获得素材资产ID(Asset ID)。 +> 由于上传的素材资产需经过预处理后才能使用,可轮询调用 **GetAsset** 接口查询素材状态,直至状态变为 `Active`。若状态为 `Failed` 则表示处理失败。 +> +> 3. **在视频生成 API 中使用素材:**当素材资产状态为 `Active` 后,将素材ID按 `Asset://` 的格式拼接成URL,在视频生成API(如Seedance 2.0系列模型)的请求中,将该URL作为参考图像的 `image_url` 传入,即可使用该素材资产生成视频。 +> +> **API 鉴权方式区别说明** +> +> * **Asset API:**Access Key 鉴权,详情参考 [获取 API 访问密钥(AK/SK)](https://www.volcengine.com/docs/6257/64983?lang=zh)。 +> +> * **视频生成 API:**API Key 鉴权,详情参考 [管理 API Key](https://www.volcengine.com/docs/82379/1361424?lang=zh)。 +> +> **素材库[项目](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)(Project)隔离说明** +> +> * 向指定的 Asset Group(素材资产组合)内创建或查询 Asset(素材资产)时,需保证两者的 **ProjectName **一致 +> +> * Asset(素材资产)所属的 **ProjectName** 需与调用视频生成 API 接口时使用的 API key 所属的 **ProjectName** 一致 + +## 1. 创建素材资产组合 + +```go +package main + +import ( + "fmt" + + "github.com/bytedance/sonic" + "github.com/volcengine/volcengine-go-sdk/volcengine" + "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" + "github.com/volcengine/volcengine-go-sdk/volcengine/session" + "github.com/volcengine/volcengine-go-sdk/volcengine/universal" +) + +func main() { + config := volcengine.NewConfig().WithCredentials(credentials.NewStaticCredentials("", "", "")).WithRegion("cn-beijing") + sess, _ := session.NewSession(config) + resp, err := universal.New(sess).DoCall( + universal.RequestUniversal{ + ServiceName: "ark", + Action: "CreateAssetGroup", + Version: "2024-01-01", + HttpMethod: universal.POST, + ContentType: universal.ApplicationJSON, + }, + //根据实际情况填写 + &map[string]any{ + "Name": "test", + "Description": "test", + "GroupType": "AIGC", + }, + ) + if err != nil { + return + } + if resp == nil { + return + } + respData, err := sonic.Marshal(resp) + fmt.Println(string(respData)) +} +``` + +返回示例 + +```json +{"ResponseMetadata":{"RequestId":"20260318155041036F7CB6362358FB40FC","Action":"CreateAssetGroup","Version":"2024-01-01","Service":"ark","Region":"cn-beijing"},"Result":{"Id":"group-2026**********-*****"}} +``` + + + +## 2. 上传素材资产并获取素材资产信息 + +```go +package main + +import ( + "errors" + "fmt" + "time" + + "github.com/bytedance/sonic" + "github.com/volcengine/volcengine-go-sdk/volcengine" + "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" + "github.com/volcengine/volcengine-go-sdk/volcengine/session" + "github.com/volcengine/volcengine-go-sdk/volcengine/universal" +) + +const ( + region = "cn-beijing" + serviceName = "ark" + version = "2024-01-01" + + // 轮询配置 + pollInterval = 3 * time.Second + pollTimeout = 2 * time.Minute +) + +func main() { + // TODO: 替换为你的 AK / SK + ak := "" + sk := "" + + // TODO: 替换为你的实际参数 + groupID := "" + assetURL := "" + assetType := "Image" + projectName := "Default" + + config := volcengine.NewConfig(). + WithCredentials(credentials.NewStaticCredentials(ak, sk, "")). + WithRegion(region) + + sess, err := session.NewSession(config) + if err != nil { + fmt.Printf("create session failed: %v\n", err) + return + } + + client := universal.New(sess) + + // 1. 创建素材资产 + assetID, err := createAsset(client, groupID, assetURL, assetType, projectName) + if err != nil { + fmt.Printf("create asset failed: %v\n", err) + return + } + + fmt.Printf("asset created, AssetId = %s\n", assetID) + + // 2. 查询素材资产状态 + finalURL, err := waitForAssetActive(client, assetID, pollInterval, pollTimeout) + if err != nil { + fmt.Printf("poll asset failed: %v\n", err) + return + } + + fmt.Printf("asset is active, URL = %s\n", finalURL) +} + +// createAsset 调用 CreateAsset 并返回 AssetId +func createAsset(client *universal.Universal, groupID, url, assetType, projectName string) (string, error) { + resp, err := client.DoCall( + universal.RequestUniversal{ + ServiceName: serviceName, + Action: "CreateAsset", + Version: version, + HttpMethod: universal.POST, + ContentType: universal.ApplicationJSON, + }, + &map[string]any{ + "GroupId": groupID, + "URL": url, + "AssetType": assetType, + "ProjectName": projectName, + }, + ) + if err != nil { + return "", err + } + if resp == nil { + return "", errors.New("create asset response is nil") + } + + // 打印原始返回,便于排查 + respData, _ := sonic.Marshal(resp) + fmt.Printf("CreateAsset response: %s\n", string(respData)) + + assetID := extractString(resp, "Result", "Id") + if assetID == "" { + assetID = extractString(resp, "Result", "AssetId") + } + if assetID == "" { + assetID = extractString(resp, "Id") + } + if assetID == "" { + assetID = extractString(resp, "AssetId") + } + + if assetID == "" { + return "", fmt.Errorf("cannot find AssetId in response: %s", string(respData)) + } + + return assetID, nil +} + +// waitForAssetActive 查询 GetAsset,直到 Active / Failed / 超时 +func waitForAssetActive(client *universal.Universal, assetID string, interval, timeout time.Duration) (string, error) { + deadline := time.Now().Add(timeout) + + for { + if time.Now().After(deadline) { + return "", fmt.Errorf("polling timeout after %v, assetID=%s", timeout, assetID) + } + + status, url, errMsg, err := getAssetStatus(client, assetID) + if err != nil { + return "", err + } + + fmt.Printf("asset status: %s\n", status) + + switch status { + case "Processing": + time.Sleep(interval) + continue + case "Active": + if url == "" { + return "", fmt.Errorf("asset is Active but URL is empty, assetID=%s", assetID) + } + return url, nil + case "Failed": + if errMsg == "" { + errMsg = "unknown asset processing error" + } + return "", fmt.Errorf("asset processing failed: %s", errMsg) + default: + // 若返回其他状态,保守处理为继续查询 + fmt.Printf("unexpected status %q, continue polling...\n", status) + time.Sleep(interval) + } + } +} + +// getAssetStatus 调用 GetAsset,返回 Status / URL / Error +func getAssetStatus(client *universal.Universal, assetID string) (status, url, errMsg string, err error) { + resp, err := client.DoCall( + universal.RequestUniversal{ + ServiceName: serviceName, + Action: "GetAsset", + Version: version, + HttpMethod: universal.POST, + ContentType: universal.ApplicationJSON, + }, + &map[string]any{ + "Id": assetID, + }, + ) + if err != nil { + return "", "", "", err + } + if resp == nil { + return "", "", "", errors.New("get asset response is nil") + } + + // 打印原始返回,便于排查 + respData, _ := sonic.Marshal(resp) + fmt.Printf("GetAsset response: %s\n", string(respData)) + + // 兼容不同层级的字段位置 + status = extractString(resp, "Result", "Status") + if status == "" { + status = extractString(resp, "Status") + } + + url = extractString(resp, "Result", "URL") + if url == "" { + url = extractString(resp, "URL") + } + + errMsg = extractString(resp, "Result", "Error") + if errMsg == "" { + errMsg = extractString(resp, "Error") + } + + return status, url, errMsg, nil +} + +// extractString 从响应中按层级安全提取字符串 +func extractString(data any, keys ...string) string { + current := data + + for _, key := range keys { + switch v := current.(type) { + case map[string]any: + next, ok := v[key] + if !ok { + return "" + } + current = next + + case *map[string]any: + if v == nil { + return "" + } + next, ok := (*v)[key] + if !ok { + return "" + } + current = next + + default: + return "" + } + } + + switch v := current.(type) { + case string: + return v + case fmt.Stringer: + return v.String() + case nil: + return "" + default: + return fmt.Sprintf("%v", v) + } +} +``` + +返回示例 + +```json +CreateAsset response: {"ResponseMetadata":{"RequestId":"202603181520431F067112A17FC078A6DF","Action":"CreateAsset","Version":"2024-01-01","Service":"ark","Region":"cn-beijing"},"Result":{"Id":"Asset-2026**********-*****"}} +asset created, AssetId = asset-20260318072044-n8bcl +GetAsset response: {"ResponseMetadata":{"Service":"ark","Region":"cn-beijing","RequestId":"202603181520448A995106924553F77D0E","Action":"GetAsset","Version":"2024-01-01"},"Result":{"Name":"","GroupId":"group-2026**********-*****","CreateTime":"2026-03-18T07:20:44Z","ProjectName":"default","Id":"Asset-2026**********-*****","URL":"","AssetType":"Image","Status":"Processing","UpdateTime":"2026-03-18T07:20:44Z"}} +asset status: Processing +GetAsset response: {"Result":{"UpdateTime":"2026-03-18T07:20:47Z","ProjectName":"default","Id":"Asset-2026**********-*****","CreateTime":"2026-03-18T07:20:44Z","Name":"","URL":"","AssetType":"Image","GroupId":"group-2026**********-*****","Status":"Processing"},"ResponseMetadata":{"Version":"2024-01-01","Service":"ark","Region":"cn-beijing","RequestId":"2026031815204766FE2BA543E6FF666F66","Action":"GetAsset"}} +asset status: Processing +GetAsset response: {"ResponseMetadata":{"Version":"2024-01-01","Service":"ark","Region":"cn-beijing","RequestId":"202603181520511F067112A17FC078A75A","Action":"GetAsset"},"Result":{"Name":"","URL":"https://ark-media-asset-stg.tos-cn-beijing.volces.com/xxxx","AssetType":"Image","Status":"Active","Id":"Asset-2026**********-*****","GroupId":"group-2026**********-*****","CreateTime":"2026-03-18T07:20:44Z","UpdateTime":"2026-03-18T07:20:47Z","ProjectName":"default"}} +asset status: Active +asset is active, URL = https://ark-media-asset-stg.tos-cn-beijing.volces.com/xxxx +``` + + + +更多语言的示例代码详见: + +> 注意替换 Demo 中的 AK与SK,若需调用其他接口如 ListAssets,需替换 ACTION 与对应请求参数。 + +| **Python** | 创建素材资产组合: 上传素材资产并获取素材资产信息: | +| ---------- | --------------------------- | +| **Java** | 创建素材资产组合: 上传素材资产并获取素材资产信息: | +| **PHP** | 创建素材资产组合: 上传素材资产: | + + + +## 3. 素材资产用于视频生成 + +当上传的素材资产状态为 `Active` 时,可将素材 Id 按 `Asset: //` 的规则拼接 URL,以在 **视频生成 API **中使用对应的素材资产生成视频: + +```json + { + "type": "image_url", + "image_url": { + "url": "Asset://Asset-2026**********-*****" + }, + "role": "reference_image" + }, +``` + +使用素材资产生成视频的具体调用方式请参考[ 【申请权限填客户名称】Seedance 2.0 & 2.0 fast API文档(邀测用户版)](https://bytedance.larkoffice.com/wiki/SANpwJ9bgiKgrykLaMTcAB0InWc#share-ONSwd51ezoXCJqxkAm2cIC61nMX)。 + diff --git a/docs/archive/prd.md b/docs/archive/prd.md new file mode 100644 index 0000000..a598312 --- /dev/null +++ b/docs/archive/prd.md @@ -0,0 +1,1611 @@ +# 产品需求文档 (PRD) + +## 1. 项目概述 + +- **项目名称**: Jimeng Clone — AI 视频生成平台 +- **一句话描述**: 1:1 还原即梦平台 (jimeng.jianying.com) 的 AI 视频生成页面,包含完整的用户认证体系、视频生成输入界面、后台管理系统和用户个人中心 +- **目标用户**: 前端开发学习者、AI 视频产品原型验证团队、平台管理员 +- **项目范围**: 视频生成输入栏(InputBar)及其交互逻辑 + Django 后端 API + 用户登录注册 + 后台管理(生成秒数统计与限制) + 用户个人中心(消费概览与记录) + +### 开发阶段划分 + +| 阶段 | 范围 | 状态 | +|------|------|------| +| **Phase 1** | 纯前端视频生成输入界面(InputBar、工具栏、上传、模式切换) | ✅ 已完成 | +| **Phase 2** | Django 后端 + 用户认证 + 前端路由 + 管理后台(基于调用次数) | ✅ 已完成 | +| **Phase 3** | 计量单位变更(次数→秒数)+ 管理后台重做(多页面 Sidebar)+ 用户个人中心 | 🔲 待开发 | + +> **当前迭代范围**: Phase 3。Phase 1 和 Phase 2 功能已完成。Phase 3 在 Phase 2 基础上进行重大重构和新增功能。文档中 `[Phase 3]` 标记用于区分本次迭代的新增/修改内容。 + +### Phase 3 核心变更摘要 + +1. **计量单位变更**: 所有「调用次数」改为「生成秒数」,用户每次生成视频消耗的是秒数(= 视频时长),不是次数 +2. **后台管理系统重做**: 从单页面改为多页面 + 左侧 Sidebar 导航(仪表盘/用户管理/消费记录/系统设置) +3. **新增用户个人中心** `/profile`: 消费概览 + 消费记录 + 趋势迷你图 +4. **图表库引入**: 使用 ECharts(echarts + echarts-for-react)展示趋势图、排行榜、环形进度条 +5. **设计升级**: 管理后台深色主题(Linear/Vercel 风格)、骨架屏加载、页面过渡动画 + +## 2. 功能需求 + +### 2.1 核心功能(P0) + +#### 已有前端功能 + +- [x] **深色主题全屏页面** — 背景色 `#0a0a0f`,页面无滚动,输入栏固定在底部居中,最大宽度 ~900px +- [x] **InputBar 容器** — 背景 `#16161e`,边框 `1px solid #2a2a38`,圆角 `20px`,内部分为上半区(上传+输入)和下半区(工具栏) +- [x] **提示词多行文本输入框** — 自适应高度(min 1行,max ~6行),支持换行输入,placeholder 根据当前模式动态切换 +- [x] **全能参考模式(默认模式)** — 左侧 [+ 参考内容] 上传按钮,支持上传 1-5 张图片/视频文件;上传后显示缩略图网格,每张标注「图片1」「图片2」等序号,每个缩略图右上角有 × 关闭按钮;上传区与文本输入框左右排列 +- [x] **首尾帧模式** — 上传区变为 [首帧缩略图] ↔ [+ 尾帧] 双框布局,中间双向箭头图标连接;首帧和尾帧各上传一张图片 +- [x] **模式切换下拉** — 工具栏中「全能参考/首尾帧」下拉菜单,点击切换模式,切换时联动:上传区 UI、比例选项、时长默认值 +- [x] **工具栏按钮行** — 一行横排按钮,透明背景,灰色文字 `#8a8a9a`,hover 时微亮背景 +- [x] **发送按钮** — 圆形按钮,内含上箭头图标;无内容时灰色不可点击,有内容(文字或上传文件)时变为蓝色 `#00b8e6` 可点击 + +#### [Phase 2] 用户认证系统 + +- [x] **用户注册页面** — 前端注册页 `/register`,包含用户名、邮箱、密码、确认密码字段,提交后调用后端注册 API +- [x] **用户登录页面** — 前端登录页 `/login`,包含用户名/邮箱、密码字段,登录成功后获取 JWT Token 并存储到 localStorage +- [x] **JWT 认证机制** — 后端签发 Access Token(有效期 2 小时)+ Refresh Token(有效期 7 天),前端请求自动附加 Authorization Bearer Token +- [x] **登录状态保持** — 前端全局 Auth 状态管理(Zustand),未登录用户自动跳转到登录页,已登录用户显示用户信息和退出按钮 +- [x] **Token 自动刷新** — Access Token 过期时自动使用 Refresh Token 刷新,刷新失败则跳转到登录页 + +#### [Phase 2] Django 后端服务 + +- [x] **Django 项目初始化** — 在 `backend/` 子目录创建 Django 项目,连接 MySQL 云数据库 +- [x] **RESTful API** — 使用 Django REST Framework (DRF) 提供用户认证、视频生成记录等 API +- [x] **CORS 配置** — 允许前端开发服务器(localhost:5173)跨域访问后端 API + +#### [Phase 3] 计量单位变更(次数 → 秒数) + +- [ ] **用户配额字段变更** — User 模型的 `daily_limit` / `monthly_limit`(次数)改为 `daily_seconds_limit` / `monthly_seconds_limit`(秒数),默认值分别为 600秒/日、6000秒/月 +- [ ] **消费记录增加秒数字段** — GenerationRecord 模型新增 `seconds_consumed` 字段(FloatField),记录每次生成消耗的秒数(= 视频时长 duration) +- [ ] **配额检查逻辑变更** — 后端配额检查从「今日调用次数 < daily_limit」改为「今日消费秒数 < daily_seconds_limit」 +- [ ] **前端展示变更** — 所有显示「剩余次数」的地方改为「剩余秒数」,UserInfoBar 组件配额显示改为秒数 + +#### [Phase 3] 后台管理系统重做 + +- [ ] **管理后台布局** — 采用左侧 Sidebar + 右侧内容区的经典管理后台布局,Sidebar 固定宽度 240px,支持折叠 +- [ ] **Sidebar 导航菜单** — 包含 4 个导航项:仪表盘、用户管理、消费记录、系统设置,当前项高亮,使用 react-router 嵌套路由 +- [ ] **管理后台路由** — `/admin` 为管理后台根路由(重定向到 `/admin/dashboard`),子路由:`/admin/dashboard`、`/admin/users`、`/admin/records`、`/admin/settings` + +#### [Phase 3] 用户个人中心 + +- [ ] **个人中心页面** — 新增 `/profile` 路由,已登录用户可访问,展示个人消费概览和历史记录 +- [ ] **消费概览卡片** — 显示已用秒数/总额度(环形进度条,使用 ECharts gauge)、今日已用/日限额、本月已用/月限额 + +### 2.2 重要功能(P1) + +#### 已有前端功能 + +- [x] **视频生成下拉按钮** — 蓝色文字 `#00b8e6` + 视频图标 + 下拉箭头,点击展开下拉菜单(菜单项仅 UI 展示,如"视频生成"/"图片生成"等) +- [x] **模型选择按钮** — 显示「Seedance 2.0」+ 钻石图标,点击展开模型选择下拉(仅 UI 展示,如 Seedance 2.0 / Seedance 2.0 Fast) +- [x] **比例选择按钮** — 全能参考模式下显示屏幕图标 + 当前比例值,点击弹出选项:`16:9` / `9:16` / `1:1` / `21:9` / `4:3` / `3:4`,默认 `21:9`;首尾帧模式下显示「自动匹配」不可选择 +- [x] **时长选择按钮** — 时钟图标 + 当前时长值,点击弹出选项:`5s` / `10s` / `15s`;全能参考模式默认 `15s`,首尾帧模式默认 `5s` +- [x] **@ 按钮** — 仅在全能参考模式下显示,点击插入 `@` 符号到文本输入框光标位置 +- [x] **文件上传交互** — 点击上传区触发文件选择器,accept 为 `image/*,video/*`;上传后生成本地预览缩略图;支持拖拽上传 +- [x] **上传数量限制** — 全能参考模式最多 5 张,超出时 toast 提示;首尾帧模式首帧/尾帧各 1 张 + +#### [Phase 2] 后台管理与生成接入 + +- [x] **Django Admin 集成** — 启用 Django Admin 面板(`/admin/`),管理员可查看和管理所有用户、视频生成记录 +- [x] **发送按钮接入后端** — 点击发送按钮时,调用后端 `/api/v1/video/generate` 接口(需登录),后端记录调用并检查配额 + +#### [Phase 3] 仪表盘页面(`/admin/dashboard`) + +- [ ] **核心指标卡片** — 4 个统计卡片:总用户数、今日新增用户、今日消费秒数、本月消费秒数。每个卡片显示数值 + 环比变化百分比 + 趋势箭头(↑绿色/↓红色) +- [ ] **消费趋势折线图** — 使用 ECharts 折线图展示近 30 天每日消费秒数趋势,支持 tooltip 悬浮显示具体数值,X轴为日期,Y轴为秒数 +- [ ] **用户消费排行柱状图** — 使用 ECharts 水平柱状图展示消费 Top 10 用户(按本月消费秒数降序),柱状图标签显示用户名和秒数 +- [ ] **时间范围选择器** — 支持「今日 / 近7天 / 近30天 / 自定义时间范围」切换,图表数据联动更新 +- [ ] **图表 Mock 数据** — 开发阶段使用 30 天的真实结构 Mock 数据(随机波动、周末低谷),确保图表有真实感 + +#### [Phase 3] 用户管理页面(`/admin/users`) + +- [ ] **用户列表表格** — 分页展示所有用户(每页 20 条),列:头像、用户名、邮箱、注册时间、状态(启用/禁用)、日限额(秒)、月限额(秒)、今日消费(秒)、本月消费(秒)、操作 +- [ ] **搜索和筛选** — 支持按用户名/邮箱关键字搜索 + 按状态筛选(全部/启用/禁用) +- [ ] **配额编辑** — 每个用户行的操作列有「编辑配额」按钮,点击弹出模态框,可修改日限额秒数和月限额秒数 +- [ ] **用户状态管理** — 操作列有「启用/禁用」开关按钮,点击后调用 API 切换用户 `is_active` 状态 +- [ ] **用户详情抽屉** — 点击用户名展开右侧抽屉面板,显示用户详情 + 该用户近期消费记录列表 + +#### [Phase 3] 消费记录页面(`/admin/records`) + +- [ ] **消费明细表格** — 分页展示所有用户的消费记录,列:时间、用户名、消费秒数、视频描述(prompt 截断)、生成模式、状态 +- [ ] **时间范围筛选** — 日期选择器,支持选择起止日期筛选记录 +- [ ] **用户筛选** — 支持按用户名搜索筛选特定用户的消费记录 +- [ ] **导出功能** — 「导出 CSV」按钮,将当前筛选条件下的消费记录导出为 CSV 文件 + +#### [Phase 3] 系统设置页面(`/admin/settings`) + +- [ ] **全局默认配额设置** — 表单修改全局默认日限额秒数和月限额秒数(新注册用户自动获得此配额) +- [ ] **系统公告管理** — 公告文本编辑框 + 启用/禁用开关,启用后公告内容展示在用户端页面顶部 + +#### [Phase 3] 用户个人中心详细功能 + +- [ ] **消费记录列表** — 分页展示当前用户的消费记录,每条记录显示:时间、消费秒数、生成的视频描述(prompt)、生成模式、状态 +- [ ] **消费趋势迷你图** — 使用 ECharts Sparkline 样式展示近 7 天 / 近 30 天每日消费秒数趋势,可切换时间范围 +- [ ] **配额提示** — 当日额度消费超过 80% 时显示黄色警告提示,超过 100% 时显示红色禁用提示 + +### 2.3 锦上添花(P2) + +- [x] **下拉菜单动画** — 下拉展开/收起有 fade + slide 动画过渡 +- [x] **文本输入框自动聚焦** — 页面加载后自动 focus 到文本输入框 +- [x] **键盘快捷键** — `Ctrl/Cmd + Enter` 触发发送(等同点击发送按钮) +- [ ] **上传进度条** — 文件上传时缩略图上显示加载进度 +- [ ] **拖拽排序** — 全能参考模式下已上传的缩略图支持拖拽调整顺序 +- [x] **响应式适配** — 移动端窄屏下工具栏按钮文字隐藏只显示图标,输入栏宽度自适应 +- [ ] **Tooltip 提示** — 工具栏按钮 hover 显示功能说明 tooltip +- [ ] **[Phase 2] 忘记密码** — 邮箱验证码找回密码流程 +- [ ] **[Phase 2] 用户个人资料编辑** — 查看和修改个人信息(头像、昵称) +- [ ] **[Phase 3] 页面切换过渡动画** — 路由切换时使用 fade/slide 过渡动画,提升体验流畅度 +- [ ] **[Phase 3] 数据加载骨架屏** — 管理后台和个人中心的数据加载使用骨架屏(Skeleton)替代 loading spinner +- [ ] **[Phase 3] Sidebar 折叠模式** — 管理后台 Sidebar 支持折叠为图标模式,增大内容区宽度 + +## 3. 技术栈建议 + +### 3.1 现有技术栈(保留) + +| 层级 | 技术选型 | 说明 | +|------|---------|------| +| 前端框架 | React 18 + TypeScript | 函数组件 + Hooks | +| 构建工具 | Vite 5 | 极速 HMR,原生 ESM | +| UI 组件库 | @arco-design/web-react | 字节跳动设计系统,即梦同款 | +| 状态管理 | Zustand | 轻量、TypeScript 友好 | +| 样式方案 | CSS Modules + Arco Design Token | 深色主题定制 | +| 图标 | @arco-design/web-react/icon + 自定义 SVG | 工具栏图标 | +| 文件处理 | 浏览器原生 File API | 本地预览、缩略图生成 | + +### 3.2 [Phase 2] 已有前端依赖(保留) + +| 依赖 | 说明 | +|------|------| +| react-router-dom v7 | 前端路由(登录页、注册页、管理页) | +| axios | HTTP 请求库,支持拦截器实现 Token 自动附加和刷新 | + +### 3.3 [Phase 3] 新增前端依赖 + +| 依赖 | 说明 | +|------|------| +| echarts | 图表库核心,用于折线图、柱状图、环形图等数据可视化 | +| echarts-for-react | ECharts 的 React 封装组件,声明式使用图表 | + +> **组件优先级**: 如果 Arco Design 内置了对应组件(如 Table、Modal、Skeleton、DatePicker),优先使用 Arco 组件,ECharts 仅用于复杂图表。 + +### 3.4 [Phase 2] 后端技术栈(保留) + +| 层级 | 技术选型 | 说明 | +|------|---------|------| +| 后端框架 | Django 4.2+ (LTS) | Python Web 框架 | +| API 框架 | Django REST Framework (DRF) | RESTful API 开发 | +| 认证方案 | djangorestframework-simplejwt | JWT Token 签发与验证 | +| 数据库 | MySQL 8.0(阿里云 RDS) | 云数据库,已提供连接信息 | +| 数据库驱动 | mysqlclient | Django 官方推荐的 MySQL 驱动 | +| CORS | django-cors-headers | 跨域请求支持 | +| 后端管理 | Django Admin | 内置管理后台 | +| 部署 | Gunicorn + Nginx | 生产环境部署方案 | + +> **后端代码目录**: 所有后端代码放在项目根目录下的 `backend/` 子目录中。 + +### 3.5 数据库连接配置(不变) + +```python +# backend/config/settings.py +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'video_auto', + 'USER': 'ai_video', + 'PASSWORD': 'JogNQdtrd3WY8CBCAiYfYEGx', + 'HOST': 'rm-7xv1uaw910558p1788o.mysql.rds.aliyuncs.com', + 'PORT': '3306', + 'OPTIONS': { + 'charset': 'utf8mb4', + 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", + }, + } +} +``` + +## 4. 页面列表 + +### 4.1 [已有] 视频生成页 (`/`) + +**整体布局**: +``` +┌─────────────────────────────────────────────┐ +│ [Phase 3 修改] 顶部用户信息: │ +│ 用户名 | 剩余: 345s/600s(日) | [个人中心] [退出]│ +│ │ +│ 深色背景空白区域 │ +│ #0a0a0f │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ InputBar (底部固定) │ │ +│ │ max-width: 900px, 居中 │ │ +│ │ 背景: #16161e │ │ +│ │ 边框: #2a2a38, 圆角: 20px │ │ +│ │ │ │ +│ │ ┌──────┐ ┌──────────────────────┐ │ │ +│ │ │上传区 │ │ 提示词文本输入框 │ │ │ +│ │ │ │ │ │ │ │ +│ │ └──────┘ └──────────────────────┘ │ │ +│ │ │ │ +│ │ ─────────── 工具栏按钮行 ────────── │ │ +│ │ [视频生成▼][模型][模式▼][比例][时长] │ │ +│ │ [@] flex空白 [发送⬆] │ │ +│ └─────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +- [Phase 3 修改] 顶部用户信息区的配额显示从「剩余 N 次」改为「剩余 N 秒」,增加「个人中心」链接 +- [Phase 2] 需要登录才能访问,未登录重定向到 `/login` + +**全能参考模式 — 上传区细节**: +``` +┌──────────┐ +│ + │ ← 初始状态: [+ 参考内容] 按钮 +│ 参考内容 │ 虚线边框,点击触发上传 +└──────────┘ + +┌───┐┌───┐┌──────┐ +│图1││图2││ + │ ← 已上传状态: 缩略图网格 + 添加按钮 +│ × ││ × ││ │ 每张有序号标签和关闭按钮 +└───┘└───┘└──────┘ +``` + +**首尾帧模式 — 上传区细节**: +``` +┌──────┐ ┌──────┐ +│ 首帧 │ ↔ │+ 尾帧 │ ← 两个独立上传框 +│ │ │ │ 中间双向箭头 +└──────┘ └──────┘ +``` + +**工具栏按钮排布**: +``` +全能参考模式: +[🎬 视频生成 ▼] [💎 Seedance 2.0] [✨ 全能参考 ▼] [🖥 21:9] [🕐 15s] [@] ——flex空白—— [⬆ 发送] + +首尾帧模式: +[🎬 视频生成 ▼] [💎 Seedance 2.0] [🔀 首尾帧 ▼] [自动匹配] [🕐 5s] ——flex空白—— [⬆ 发送] +``` + +### 4.2 [已有] 登录页 (`/login`) + +``` +┌─────────────────────────────────────────────┐ +│ │ +│ 深色背景 #0a0a0f │ +│ │ +│ ┌──────────────────────┐ │ +│ │ Jimeng Clone │ │ +│ │ │ │ +│ │ 用户名/邮箱: [____] │ │ +│ │ 密码: [____] │ │ +│ │ │ │ +│ │ [ 登录 ] │ │ +│ │ │ │ +│ │ 没有账号?去注册 → │ │ +│ └──────────────────────┘ │ +│ │ +└─────────────────────────────────────────────┘ +``` + +- 表单验证:用户名/邮箱必填,密码最少 6 位 +- 登录成功后跳转到 `/`(视频生成页) +- 风格与主应用一致:深色主题,卡片式表单 + +### 4.3 [已有] 注册页 (`/register`) + +``` +┌─────────────────────────────────────────────┐ +│ │ +│ 深色背景 #0a0a0f │ +│ │ +│ ┌──────────────────────┐ │ +│ │ 创建账号 │ │ +│ │ │ │ +│ │ 用户名: [____] │ │ +│ │ 邮箱: [____] │ │ +│ │ 密码: [____] │ │ +│ │ 确认密码: [____] │ │ +│ │ │ │ +│ │ [ 注册 ] │ │ +│ │ │ │ +│ │ 已有账号?去登录 → │ │ +│ └──────────────────────┘ │ +│ │ +└─────────────────────────────────────────────┘ +``` + +- 表单验证:用户名 3-20 位、邮箱格式校验、密码最少 6 位、两次密码一致 +- 注册成功后自动登录并跳转到 `/` + +### 4.4 [Phase 3] 管理后台布局(`/admin/*`) + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Jimeng Admin [管理员名] [退出] │ +├────────────┬─────────────────────────────────────────────────┤ +│ │ │ +│ SIDEBAR │ CONTENT AREA │ +│ 240px │ (根据子路由渲染不同页面) │ +│ │ │ +│ ┌──────┐ │ │ +│ │ 📊 │ │ │ +│ │仪表盘 │ │ │ +│ ├──────┤ │ │ +│ │ 👥 │ │ │ +│ │用户 │ │ │ +│ │管理 │ │ │ +│ ├──────┤ │ │ +│ │ 📋 │ │ │ +│ │消费 │ │ │ +│ │记录 │ │ │ +│ ├──────┤ │ │ +│ │ ⚙️ │ │ │ +│ │系统 │ │ │ +│ │设置 │ │ │ +│ └──────┘ │ │ +│ │ │ +│ ─────── │ │ +│ [返回首页] │ │ +│ │ │ +├────────────┴─────────────────────────────────────────────────┤ +│ Jimeng Clone Admin v3.0 │ +└──────────────────────────────────────────────────────────────┘ +``` + +**设计规范(管理后台专用)**: +- 整体风格参考 Linear / Vercel Dashboard,深色主题 +- 背景色 `#0a0a0f`,Sidebar 背景 `#111118`,内容区背景 `#0a0a0f` +- Sidebar 当前项背景 `rgba(255, 255, 255, 0.08)`,文字 `#ffffff` +- 非当前项文字 `#8a8a9a`,hover 背景 `rgba(255, 255, 255, 0.04)` +- 卡片背景 `#16161e`,边框 `1px solid #2a2a38`,圆角 `12px` +- 数据加载时显示 Arco Skeleton 骨架屏 + +### 4.5 [Phase 3] 仪表盘页面(`/admin/dashboard`) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ 仪表盘 [今日] [近7天] [近30天] [自定义]│ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 总用户数 │ │今日新增 │ │今日消费 │ │本月消费 │ │ +│ │ 1,234 │ │ +23 │ │ 4,560s │ │ 89,010s │ │ +│ │ ↑12% │ │ ↑15% │ │ ↓5% │ │ ↑8% │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 消费趋势 (ECharts 折线图) │ │ +│ │ │ │ +│ │ Y轴: 秒数 ___/\___ │ │ +│ │ / \___/\ │ │ +│ │ ___/\___/ \___ │ │ +│ │ │ │ +│ │ X轴: 日期 (3/1 3/5 3/10 3/15 3/20 3/25 3/30) │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 用户消费排行 Top 10 (ECharts 水平柱状图) │ │ +│ │ │ │ +│ │ user_a ████████████████████████ 2,340s │ │ +│ │ user_b ██████████████████ 1,890s │ │ +│ │ user_c ████████████████ 1,560s │ │ +│ │ ... │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- 仅 `is_staff=True` 的用户可访问 +- 统计卡片使用 Arco Card 组件 + 自定义样式 +- 折线图使用 ECharts `line` 类型,开启 `tooltip`、`dataZoom` 交互 +- 柱状图使用 ECharts `bar` 类型,水平方向,标签显示用户名和秒数 +- 时间范围选择器使用 Arco DatePicker.RangePicker +- 数据来源:`GET /api/v1/admin/stats` + +### 4.6 [Phase 3] 用户管理页面(`/admin/users`) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ 用户管理 │ +│ │ +│ [🔍 搜索用户名/邮箱___________] [状态: 全部 ▼] [刷新] │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 用户名 │ 邮箱 │ 注册时间 │ 状态 │ 日限额│ │ +│ │ │ │ │ │ (秒) │ │ +│ │ 月限额 │ 今日消费(秒) │ 本月消费(秒)│ 操作 │ │ +│ ├──────────┼──────────────┼──────────┼──────┼──────┤ │ +│ │ user_a │ a@test.com │ 3/1 │ ✅ │ 600 │ │ +│ │ 6000 │ 123 │ 2345 │[编辑][禁用] │ │ +│ ├──────────┼──────────────┼──────────┼──────┼──────┤ │ +│ │ user_b │ b@test.com │ 3/5 │ ❌ │ 300 │ │ +│ │ 3000 │ 0 │ 0 │[编辑][启用] │ │ +│ └──────────┴──────────────┴──────────┴──────┴──────┘ │ +│ │ +│ 共 56 条 [< 1 2 3 >] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- 使用 Arco Table 组件(分页、排序) +- 搜索使用 Arco Input.Search +- 状态筛选使用 Arco Select +- 编辑配额使用 Arco Modal 弹窗 +- 用户详情使用 Arco Drawer 右侧抽屉 +- 数据来源:`GET /api/v1/admin/users` + +### 4.7 [Phase 3] 消费记录页面(`/admin/records`) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ 消费记录 [导出 CSV] │ +│ │ +│ [用户名搜索____] [时间: 2026-03-01 ~ 2026-03-12] [查询] │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 时间 │ 用户名 │ 消费秒数│ 视频描述 │ │ +│ │ │ │ │ (prompt截断) │ │ +│ │ 模式 │ 状态 │ │ +│ ├───────────────────┼────────┼────────┼─────────────┤ │ +│ │ 3/12 14:30:00 │ user_a │ 15s │ 一只猫在... │ │ +│ │ 全能参考 │ 已完成 │ │ +│ ├───────────────────┼────────┼────────┼─────────────┤ │ +│ │ 3/12 14:25:00 │ user_b │ 5s │ 日落海边... │ │ +│ │ 首尾帧 │ 生成中 │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ 共 1,234 条 [< 1 2 3 ... 62 >] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- 使用 Arco Table 组件(分页) +- 时间范围筛选使用 Arco DatePicker.RangePicker +- 导出 CSV:前端调用 API 获取全部数据并生成 CSV 文件下载 +- 数据来源:`GET /api/v1/admin/records` + +### 4.8 [Phase 3] 系统设置页面(`/admin/settings`) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ 系统设置 │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 全局默认配额 │ │ +│ │ │ │ +│ │ 默认每日限额 (秒): [____600____] │ │ +│ │ 默认每月限额 (秒): [____6000___] │ │ +│ │ │ │ +│ │ [保存配额设置] │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 系统公告 [启用公告: ON] │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────┐ │ │ +│ │ │ │ │ │ +│ │ │ 公告内容 (支持纯文本) │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ [保存公告] │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- 使用 Arco Form、InputNumber、Switch、Input.TextArea 组件 +- 保存后 showToast 提示「设置已保存」 +- 数据来源:`GET/PUT /api/v1/admin/settings` + +### 4.9 [Phase 3] 用户个人中心(`/profile`) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ [← 返回首页] 个人中心 [用户名] [退出] │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 消费概览 │ │ +│ │ │ │ +│ │ ┌────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ (环形图)│ │ 今日额度 │ │ 本月额度 │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ 已用 │ │ 已用: 123s │ │ 已用: 2345s │ │ │ +│ │ │ 345s │ │ 限额: 600s │ │ 限额: 6000s │ │ │ +│ │ │ /600s │ │ ████████░░░ │ │ ████░░░░░░░ │ │ │ +│ │ │ │ │ 20.5% │ │ 39.1% │ │ │ +│ │ └────────┘ └──────────────┘ └──────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 消费趋势 [近7天] [近30天] │ │ +│ │ ___/\___/\___ (Sparkline 迷你折线图) │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ 消费记录 │ │ +│ │ │ │ +│ │ 3/12 14:30 │ 15s │ 一只猫在花园... │ 全能参考 │ 完成│ │ +│ │ 3/12 14:25 │ 5s │ 日落海边散步... │ 首尾帧 │ 完成│ │ +│ │ 3/12 13:00 │ 10s │ 城市夜景延时... │ 全能参考 │ 失败│ │ +│ │ ... │ │ +│ │ │ │ +│ │ [加载更多] │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +- 环形进度条使用 ECharts `gauge` 类型(半环或全环) +- 进度条使用 Arco Progress 组件 +- 消费记录列表使用 Arco List 或 Table 组件,支持「加载更多」分页 +- Sparkline 迷你图使用 ECharts `line` 类型(无坐标轴,仅曲线) +- 数据来源:`GET /api/v1/profile/overview` + `GET /api/v1/profile/records` +- 深色主题风格与主应用一致 + +## 5. API 设计 + +### 5.1 [已有] 视频生成(Phase 3 修改配额返回字段) + +``` +POST /api/v1/video/generate +Content-Type: multipart/form-data +Authorization: Bearer + +Request: +{ + "prompt": string, + "mode": "universal" | "keyframe", + "model": "seedance_2.0" | "seedance_2.0_fast", + "aspect_ratio": "16:9" | "9:16" | "1:1" | "21:9" | "4:3" | "3:4" | "auto", + "duration": 5 | 10 | 15, + "references": File[], + "first_frame": File | null, + "last_frame": File | null +} + +Response: 202 Accepted +{ + "task_id": "uuid", + "status": "queued", + "estimated_time": 120, + "seconds_consumed": 15, ← [Phase 3] 本次消耗秒数 + "remaining_seconds_today": 345 ← [Phase 3] 今日剩余秒数 +} + +Error Response: 429 Too Many Requests +{ + "error": "quota_exceeded", + "message": "您今日的生成额度已用完", ← [Phase 3] 改为秒数描述 + "daily_seconds_limit": 600, + "daily_seconds_used": 600, + "reset_at": "2026-03-13T00:00:00+08:00" +} +``` + +### 5.2 [已有] 文件上传(不变) + +``` +POST /api/v1/upload +Content-Type: multipart/form-data +Authorization: Bearer + +Request: +{ + "file": File, + "type": "image" | "video" +} + +Response: 200 OK +{ + "file_id": "uuid", + "url": "https://cdn.example.com/...", + "thumbnail_url": "https://cdn.example.com/.../thumb.jpg", + "width": 1920, + "height": 1080, + "duration": 10.5 +} +``` + +### 5.3 [已有] 用户注册(不变) + +``` +POST /api/v1/auth/register +Content-Type: application/json + +Request: +{ + "username": "string (3-20字符)", + "email": "string (合法邮箱)", + "password": "string (最少6位)" +} + +Response: 201 Created +{ + "user": { + "id": 1, + "username": "johndoe", + "email": "john@example.com" + }, + "tokens": { + "access": "eyJ...", + "refresh": "eyJ..." + } +} + +Error Response: 400 Bad Request +{ + "username": ["该用户名已被注册"], + "email": ["该邮箱已被注册"] +} +``` + +### 5.4 [已有] 用户登录(不变) + +``` +POST /api/v1/auth/login +Content-Type: application/json + +Request: +{ + "username": "string (用户名或邮箱)", + "password": "string" +} + +Response: 200 OK +{ + "user": { + "id": 1, + "username": "johndoe", + "email": "john@example.com", + "is_staff": false + }, + "tokens": { + "access": "eyJ...", + "refresh": "eyJ..." + } +} + +Error Response: 401 Unauthorized +{ + "error": "invalid_credentials", + "message": "用户名或密码错误" +} +``` + +### 5.5 [已有] Token 刷新(不变) + +``` +POST /api/v1/auth/token/refresh +Content-Type: application/json + +Request: +{ + "refresh": "eyJ..." +} + +Response: 200 OK +{ + "access": "eyJ..." +} +``` + +### 5.6 [已有] 获取当前用户信息(Phase 3 修改配额字段) + +``` +GET /api/v1/auth/me +Authorization: Bearer + +Response: 200 OK +{ + "id": 1, + "username": "johndoe", + "email": "john@example.com", + "is_staff": false, + "quota": { + "daily_seconds_limit": 600, ← [Phase 3] 改为秒数 + "daily_seconds_used": 123, + "monthly_seconds_limit": 6000, + "monthly_seconds_used": 2345 + } +} +``` + +### 5.7 [Phase 3] 管理后台 — 仪表盘统计 + +``` +GET /api/v1/admin/stats?period=30d +Authorization: Bearer +(requires is_staff=True) + +Query Parameters: + period: "today" | "7d" | "30d" | "custom" + start_date: "2026-03-01" (当 period=custom 时必填) + end_date: "2026-03-12" (当 period=custom 时必填) + +Response: 200 OK +{ + "total_users": 1234, + "new_users_today": 23, + "seconds_consumed_today": 4560, + "seconds_consumed_this_month": 89010, + "today_change_percent": -5.0, + "month_change_percent": 8.0, + "daily_trend": [ + {"date": "2026-02-11", "seconds": 3200}, + {"date": "2026-02-12", "seconds": 4100}, + ... + ], + "top_users": [ + { + "user_id": 1, + "username": "user_a", + "seconds_consumed": 2340 + }, + ... + ] +} +``` + +### 5.8 [Phase 3] 管理后台 — 用户列表 + +``` +GET /api/v1/admin/users?page=1&page_size=20&search=&status=&sort_by=created_at&order=desc +Authorization: Bearer +(requires is_staff=True) + +Query Parameters: + page: int (默认 1) + page_size: int (默认 20,最大 100) + search: string (按用户名或邮箱模糊搜索) + status: "active" | "disabled" | "" (空表示全部) + sort_by: "created_at" | "seconds_today" | "seconds_month" (排序字段) + order: "asc" | "desc" + +Response: 200 OK +{ + "total": 56, + "page": 1, + "page_size": 20, + "results": [ + { + "id": 1, + "username": "user_a", + "email": "a@test.com", + "is_active": true, + "date_joined": "2026-03-01T10:00:00+08:00", + "daily_seconds_limit": 600, + "monthly_seconds_limit": 6000, + "seconds_today": 123, + "seconds_this_month": 2345 + }, + ... + ] +} +``` + +### 5.9 [Phase 3] 管理后台 — 用户详情 + 消费记录 + +``` +GET /api/v1/admin/users/:id +Authorization: Bearer +(requires is_staff=True) + +Response: 200 OK +{ + "id": 1, + "username": "user_a", + "email": "a@test.com", + "is_active": true, + "is_staff": false, + "date_joined": "2026-03-01T10:00:00+08:00", + "daily_seconds_limit": 600, + "monthly_seconds_limit": 6000, + "seconds_today": 123, + "seconds_this_month": 2345, + "seconds_total": 5678, + "recent_records": [ + { + "id": 101, + "created_at": "2026-03-12T14:30:00+08:00", + "seconds_consumed": 15, + "prompt": "一只猫在花园里追蝴蝶", + "mode": "universal", + "model": "seedance_2.0", + "status": "completed" + }, + ... + ] +} +``` + +### 5.10 [Phase 3] 管理后台 — 修改用户配额 + +``` +PUT /api/v1/admin/users/:id/quota +Authorization: Bearer +(requires is_staff=True) +Content-Type: application/json + +Request: +{ + "daily_seconds_limit": 900, + "monthly_seconds_limit": 9000 +} + +Response: 200 OK +{ + "user_id": 1, + "username": "user_a", + "daily_seconds_limit": 900, + "monthly_seconds_limit": 9000, + "updated_at": "2026-03-12T14:30:00+08:00" +} +``` + +### 5.11 [Phase 3] 管理后台 — 启用/禁用用户 + +``` +PATCH /api/v1/admin/users/:id/status +Authorization: Bearer +(requires is_staff=True) +Content-Type: application/json + +Request: +{ + "is_active": false +} + +Response: 200 OK +{ + "user_id": 1, + "username": "user_a", + "is_active": false, + "updated_at": "2026-03-12T14:30:00+08:00" +} +``` + +### 5.12 [Phase 3] 管理后台 — 消费记录列表 + +``` +GET /api/v1/admin/records?page=1&page_size=20&search=&start_date=&end_date= +Authorization: Bearer +(requires is_staff=True) + +Query Parameters: + page: int (默认 1) + page_size: int (默认 20,最大 100) + search: string (按用户名搜索) + start_date: "2026-03-01" (起始日期) + end_date: "2026-03-12" (结束日期) + +Response: 200 OK +{ + "total": 1234, + "page": 1, + "page_size": 20, + "results": [ + { + "id": 101, + "created_at": "2026-03-12T14:30:00+08:00", + "user_id": 1, + "username": "user_a", + "seconds_consumed": 15, + "prompt": "一只猫在花园里追蝴蝶", + "mode": "universal", + "model": "seedance_2.0", + "aspect_ratio": "16:9", + "status": "completed" + }, + ... + ] +} +``` + +### 5.13 [Phase 3] 管理后台 — 系统设置 + +``` +GET /api/v1/admin/settings +Authorization: Bearer +(requires is_staff=True) + +Response: 200 OK +{ + "default_daily_seconds_limit": 600, + "default_monthly_seconds_limit": 6000, + "announcement": "系统将于今晚 22:00 进行维护", + "announcement_enabled": true +} + +PUT /api/v1/admin/settings +Authorization: Bearer +(requires is_staff=True) +Content-Type: application/json + +Request: +{ + "default_daily_seconds_limit": 600, + "default_monthly_seconds_limit": 6000, + "announcement": "系统将于今晚 22:00 进行维护", + "announcement_enabled": true +} + +Response: 200 OK +{ + "default_daily_seconds_limit": 600, + "default_monthly_seconds_limit": 6000, + "announcement": "系统将于今晚 22:00 进行维护", + "announcement_enabled": true, + "updated_at": "2026-03-12T14:30:00+08:00" +} +``` + +### 5.14 [Phase 3] 用户个人中心 — 消费概览 + +``` +GET /api/v1/profile/overview?period=7d +Authorization: Bearer + +Query Parameters: + period: "7d" | "30d" (趋势数据的时间范围) + +Response: 200 OK +{ + "daily_seconds_limit": 600, + "daily_seconds_used": 123, + "monthly_seconds_limit": 6000, + "monthly_seconds_used": 2345, + "total_seconds_used": 5678, + "daily_trend": [ + {"date": "2026-03-06", "seconds": 45}, + {"date": "2026-03-07", "seconds": 120}, + {"date": "2026-03-08", "seconds": 0}, + {"date": "2026-03-09", "seconds": 88}, + {"date": "2026-03-10", "seconds": 200}, + {"date": "2026-03-11", "seconds": 156}, + {"date": "2026-03-12", "seconds": 123} + ] +} +``` + +### 5.15 [Phase 3] 用户个人中心 — 消费记录 + +``` +GET /api/v1/profile/records?page=1&page_size=20 +Authorization: Bearer + +Query Parameters: + page: int (默认 1) + page_size: int (默认 20) + +Response: 200 OK +{ + "total": 45, + "page": 1, + "page_size": 20, + "results": [ + { + "id": 101, + "created_at": "2026-03-12T14:30:00+08:00", + "seconds_consumed": 15, + "prompt": "一只猫在花园里追蝴蝶", + "mode": "universal", + "model": "seedance_2.0", + "aspect_ratio": "16:9", + "status": "completed" + }, + ... + ] +} +``` + +## 6. 数据模型 + +### 6.1 已有前端 Store 类型(保留) + +```typescript +// 创作模式 +type CreationMode = 'universal' | 'keyframe'; + +// 模型选项 +type ModelOption = 'seedance_2.0' | 'seedance_2.0_fast'; + +// 宽高比 +type AspectRatio = '16:9' | '9:16' | '1:1' | '21:9' | '4:3' | '3:4'; + +// 时长 +type Duration = 5 | 10 | 15; + +// 上传文件 +interface UploadedFile { + id: string; + file: File; + type: 'image' | 'video'; + previewUrl: string; + label: string; +} + +// 输入栏状态 Store +interface InputBarStore { + mode: CreationMode; + setMode: (mode: CreationMode) => void; + model: ModelOption; + setModel: (model: ModelOption) => void; + aspectRatio: AspectRatio; + setAspectRatio: (ratio: AspectRatio) => void; + duration: Duration; + setDuration: (duration: Duration) => void; + prompt: string; + setPrompt: (prompt: string) => void; + references: UploadedFile[]; + addReference: (file: File) => void; + removeReference: (id: string) => void; + clearReferences: () => void; + firstFrame: UploadedFile | null; + lastFrame: UploadedFile | null; + setFirstFrame: (file: File | null) => void; + setLastFrame: (file: File | null) => void; + canSubmit: () => boolean; + switchMode: (mode: CreationMode) => void; + submit: () => void; + reset: () => void; +} +``` + +### 6.2 已有下拉菜单配置(保留) + +```typescript +interface DropdownOption { + label: string; + value: string; + icon?: string; + disabled?: boolean; +} + +const generationTypes: DropdownOption[] = [ + { label: '视频生成', value: 'video', icon: 'video' }, + { label: '图片生成', value: 'image', icon: 'image' }, +]; + +const modelOptions: DropdownOption[] = [ + { label: 'Seedance 2.0', value: 'seedance_2.0', icon: 'diamond' }, + { label: 'Seedance 2.0 Fast', value: 'seedance_2.0_fast', icon: 'diamond' }, +]; + +const modeOptions: DropdownOption[] = [ + { label: '全能参考', value: 'universal', icon: 'sparkle' }, + { label: '首尾帧', value: 'keyframe', icon: 'swap' }, +]; + +const aspectRatioOptions: DropdownOption[] = [ + { label: '16:9', value: '16:9' }, + { label: '9:16', value: '9:16' }, + { label: '1:1', value: '1:1' }, + { label: '21:9', value: '21:9' }, + { label: '4:3', value: '4:3' }, + { label: '3:4', value: '3:4' }, +]; + +const durationOptions: DropdownOption[] = [ + { label: '5s', value: '5' }, + { label: '10s', value: '10' }, + { label: '15s', value: '15' }, +]; +``` + +### 6.3 [Phase 2] 前端 Auth Store 类型(Phase 3 修改配额字段) + +```typescript +interface User { + id: number; + username: string; + email: string; + is_staff: boolean; +} + +// [Phase 3] 配额字段改为秒数 +interface Quota { + daily_seconds_limit: number; // 原 daily_limit + daily_seconds_used: number; // 原 daily_used + monthly_seconds_limit: number; // 原 monthly_limit + monthly_seconds_used: number; // 原 monthly_used +} + +interface AuthStore { + // 状态 + user: User | null; + accessToken: string | null; + refreshToken: string | null; + isAuthenticated: boolean; + isLoading: boolean; + + // 操作 + login: (username: string, password: string) => Promise; + register: (username: string, email: string, password: string) => Promise; + logout: () => void; + refreshAccessToken: () => Promise; + fetchUserInfo: () => Promise; + + // 配额 + quota: Quota | null; + fetchQuota: () => Promise; +} +``` + +### 6.4 [Phase 3] 前端新增类型 + +```typescript +// 管理后台统计数据 +interface AdminStats { + total_users: number; + new_users_today: number; + seconds_consumed_today: number; // 原 calls_today + seconds_consumed_this_month: number; // 原 calls_this_month + today_change_percent: number; + month_change_percent: number; + daily_trend: { date: string; seconds: number }[]; + top_users: { user_id: number; username: string; seconds_consumed: number }[]; +} + +// 管理后台用户列表项 +interface AdminUser { + id: number; + username: string; + email: string; + is_active: boolean; + date_joined: string; + daily_seconds_limit: number; + monthly_seconds_limit: number; + seconds_today: number; + seconds_this_month: number; +} + +// 管理后台消费记录 +interface AdminRecord { + id: number; + created_at: string; + user_id: number; + username: string; + seconds_consumed: number; + prompt: string; + mode: CreationMode; + model: ModelOption; + aspect_ratio: string; + status: 'queued' | 'processing' | 'completed' | 'failed'; +} + +// 系统设置 +interface SystemSettings { + default_daily_seconds_limit: number; + default_monthly_seconds_limit: number; + announcement: string; + announcement_enabled: boolean; +} + +// 用户个人中心概览 +interface ProfileOverview { + daily_seconds_limit: number; + daily_seconds_used: number; + monthly_seconds_limit: number; + monthly_seconds_used: number; + total_seconds_used: number; + daily_trend: { date: string; seconds: number }[]; +} + +// 用户消费记录 +interface ProfileRecord { + id: number; + created_at: string; + seconds_consumed: number; + prompt: string; + mode: CreationMode; + model: ModelOption; + aspect_ratio: string; + status: 'queued' | 'processing' | 'completed' | 'failed'; +} + +// 分页响应 +interface PaginatedResponse { + total: number; + page: number; + page_size: number; + results: T[]; +} +``` + +### 6.5 后端数据模型(Django Models) + +#### [Phase 2 → Phase 3 修改] 用户模型 + +```python +# backend/apps/accounts/models.py + +from django.contrib.auth.models import AbstractUser +from django.db import models + +class User(AbstractUser): + """扩展用户模型 — Phase 3: 配额单位改为秒数""" + email = models.EmailField(unique=True, verbose_name='邮箱') + # [Phase 3] 改为秒数限制 + daily_seconds_limit = models.IntegerField(default=600, verbose_name='每日秒数上限') + monthly_seconds_limit = models.IntegerField(default=6000, verbose_name='每月秒数上限') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + verbose_name = '用户' + verbose_name_plural = '用户' +``` + +> **迁移说明**: 需要创建 Django migration 将 `daily_limit` → `daily_seconds_limit`,`monthly_limit` → `monthly_seconds_limit`,并将现有数据按「原次数 × 15」换算为秒数(假设平均每次生成 15 秒)。 + +#### [Phase 2 → Phase 3 修改] 生成记录模型 + +```python +# backend/apps/generation/models.py + +import uuid +from django.db import models +from django.conf import settings + +class GenerationRecord(models.Model): + """视频生成记录 — Phase 3: 新增消费秒数字段""" + MODE_CHOICES = [ + ('universal', '全能参考'), + ('keyframe', '首尾帧'), + ] + MODEL_CHOICES = [ + ('seedance_2.0', 'Seedance 2.0'), + ('seedance_2.0_fast', 'Seedance 2.0 Fast'), + ] + STATUS_CHOICES = [ + ('queued', '排队中'), + ('processing', '生成中'), + ('completed', '已完成'), + ('failed', '失败'), + ] + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='generation_records', + verbose_name='用户' + ) + task_id = models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='任务ID') + prompt = models.TextField(blank=True, verbose_name='提示词') + mode = models.CharField(max_length=20, choices=MODE_CHOICES, verbose_name='创作模式') + model = models.CharField(max_length=30, choices=MODEL_CHOICES, verbose_name='模型') + aspect_ratio = models.CharField(max_length=10, verbose_name='宽高比') + duration = models.IntegerField(verbose_name='视频时长(秒)') + seconds_consumed = models.FloatField(default=0, verbose_name='消费秒数') # [Phase 3] 新增 + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='queued', verbose_name='状态') + created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间') + + class Meta: + verbose_name = '生成记录' + verbose_name_plural = '生成记录' + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['user', 'created_at']), + ] +``` + +> **消费秒数逻辑**: `seconds_consumed` = 视频 `duration`(5/10/15秒),在调用生成 API 时自动设置。 + +#### [Phase 2 → Phase 3 修改] 配额配置模型 + +```python +# backend/apps/generation/models.py (续) + +class QuotaConfig(models.Model): + """全局配额配置 — Phase 3: 改为秒数""" + default_daily_seconds_limit = models.IntegerField(default=600, verbose_name='默认每日秒数上限') + default_monthly_seconds_limit = models.IntegerField(default=6000, verbose_name='默认每月秒数上限') + announcement = models.TextField(blank=True, default='', verbose_name='系统公告') # [Phase 3] 新增 + announcement_enabled = models.BooleanField(default=False, verbose_name='启用公告') # [Phase 3] 新增 + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = '系统配置' + verbose_name_plural = '系统配置' + + def save(self, *args, **kwargs): + # 单例模式,确保只有一条记录 + self.pk = 1 + super().save(*args, **kwargs) +``` + +## 7. 非功能需求 + +### 性能要求 +- 首屏加载 < 2s(Vite 构建,代码分割) +- 文件上传后缩略图预览 < 200ms(使用 URL.createObjectURL 本地生成) +- 下拉菜单展开/收起动画 < 150ms +- 文本输入无感知延迟(非受控组件或 debounce) +- [Phase 2] 后端 API 响应时间 < 500ms(除文件上传外) +- [Phase 2] JWT Token 验证 < 50ms +- [Phase 3] ECharts 图表渲染 < 300ms(含数据加载) +- [Phase 3] 管理后台页面切换 < 200ms(路由级代码分割) +- [Phase 3] 分页列表请求 < 300ms + +### 安全要求 +- 文件上传仅限 image/* 和 video/* MIME 类型 +- 文件大小限制:图片 < 20MB,视频 < 100MB +- 使用 URL.createObjectURL 生成预览,组件卸载时调用 URL.revokeObjectURL 释放内存 +- [Phase 2] 密码使用 Django 默认的 PBKDF2 算法加密存储 +- [Phase 2] JWT Access Token 有效期 2 小时,Refresh Token 有效期 7 天 +- [Phase 2] 数据库密码等敏感配置通过环境变量管理(生产环境),开发环境可硬编码在 settings 中 +- [Phase 2] API 接口对未认证请求返回 401,对权限不足返回 403 +- [Phase 2] 管理接口仅 `is_staff=True` 用户可访问 +- [Phase 3] 管理后台操作(修改配额、禁用用户)需记录操作日志 +- [Phase 3] CSV 导出需要防止 CSV 注入攻击(prompt 字段可能包含特殊字符) + +### 响应式设计要求 +- 桌面端(≥1024px):InputBar 最大宽度 900px 居中;管理后台 Sidebar 240px + 内容区自适应 +- 平板端(768px-1023px):InputBar 宽度 90% 居中;管理后台 Sidebar 折叠为图标模式 +- 移动端(<768px):InputBar 宽度 95%,工具栏按钮文字隐藏仅显示图标;管理后台 Sidebar 隐藏,使用汉堡菜单 + +### 浏览器兼容 +- Chrome 90+、Firefox 90+、Safari 15+、Edge 90+ + +## 8. 验收标准 + +> **重要**: Phase 1 和 Phase 2 功能已完成。当前验收范围为 **Phase 3 全部功能**。Phase 1/2 功能保持不变,不重复验收。 + +### Phase 1 验收标准(已通过) + +#### P0(已全部通过) +1. 页面打开后显示深色全屏背景 `#0a0a0f`,底部居中显示 InputBar +2. InputBar 样式与参考截图视觉一致:背景 `#16161e`、边框 `#2a2a38`、圆角 `20px` +3. 默认处于「全能参考」模式,显示 [+ 参考内容] 上传按钮 + 提示词输入框 +4. 点击 [+ 参考内容] 可选择图片/视频文件,上传后显示带序号的缩略图 +5. 上传 1-5 张文件后缩略图正确显示,每个有 × 关闭按钮可删除 +6. 切换到「首尾帧」模式后,上传区变为首帧 ↔ 尾帧双框布局 +7. 工具栏所有按钮正确显示,布局与参考截图一致 +8. 发送按钮状态正确:无内容时灰色,有内容时蓝色 `#00b8e6` + +#### P1(已全部通过) +9. 「视频生成」下拉、「模型选择」下拉、「模式切换」下拉均可正常展开和选择 +10. 比例选择按钮点击弹出 6 个选项,选中后按钮文字更新 +11. 时长选择按钮点击弹出 3 个选项,选中后按钮文字更新 +12. 切换模式时联动正确:全能参考→首尾帧时比例变为「自动匹配」、时长变为 5s、隐藏 @ 按钮 +13. 文件上传支持拖拽 + +#### P2(已全部通过) +14. 下拉菜单有动画过渡效果 +15. `Ctrl/Cmd + Enter` 可触发发送 +16. 页面加载后文本输入框自动聚焦 +17. 移动端下工具栏按钮自适应 + +### Phase 2 验收标准(已通过) + +#### P0(已全部通过) +18. 未登录用户访问 `/` 自动跳转到 `/login` +19. 注册页表单验证正确,注册成功后自动登录跳转到首页 +20. 登录页输入正确凭据后成功登录,获取 JWT Token +21. 后端 Django 服务正常启动,能连接 MySQL 数据库 +22. 发送按钮点击后调用后端 API,后端记录调用并返回剩余配额 + +#### P1(已全部通过) +23. 管理员登录后可访问管理后台,普通用户无法访问 +24. 管理后台正确显示用户总数、调用统计等指标 +25. 管理员可修改用户的每日/每月调用限额 +26. 超出限额时前端显示友好提示,后端返回 429 状态码 +27. Django Admin 后台(`/admin/`)可管理用户和生成记录 + +### Phase 3 验收标准(当前迭代) + +#### P0(必须全部通过) +28. 所有「调用次数」展示改为「生成秒数」—— UserInfoBar 显示「剩余 Ns/Ns(日)」而非「剩余 N 次」 +29. 后端 User 模型字段 `daily_limit`/`monthly_limit` 迁移为 `daily_seconds_limit`/`monthly_seconds_limit` +30. GenerationRecord 模型新增 `seconds_consumed` 字段,生成 API 返回 `seconds_consumed` 和 `remaining_seconds_today` +31. 管理后台使用左侧 Sidebar + 右侧内容区布局,Sidebar 包含 4 个导航项(仪表盘/用户管理/消费记录/系统设置) +32. `/admin/dashboard` 仪表盘页面显示 4 个统计卡片(总用户、今日新增、今日消费秒数、本月消费秒数)+ ECharts 消费趋势折线图 + 用户消费排行柱状图 +33. `/admin/users` 用户管理页面支持分页列表、搜索筛选、编辑配额、启用/禁用用户 +34. `/profile` 用户个人中心显示消费概览(环形进度条)+ 消费记录列表 + +#### P1 +35. `/admin/records` 消费记录页面显示所有用户消费明细,支持时间范围筛选和导出 CSV +36. `/admin/settings` 系统设置页面支持修改全局默认配额和管理系统公告 +37. 用户个人中心显示消费趋势 Sparkline 迷你图(近 7天/30天 切换) +38. 仪表盘统计卡片显示环比变化百分比 + 趋势箭头 +39. 用户管理页面点击用户名展开详情抽屉,显示用户详情 + 近期消费记录 +40. 管理后台深色主题与 Linear/Vercel Dashboard 风格一致 +41. 数据加载使用骨架屏(Skeleton) + +#### P2 +42. 管理后台 Sidebar 支持折叠为图标模式 +43. 页面切换有 fade/slide 过渡动画 +44. 当日额度消费超过 80% 时用户端显示黄色警告提示 + +## 9. 模式切换联动逻辑 + +| 切换动作 | 上传区 | 比例 | 时长 | @ 按钮 | Placeholder | +|---------|--------|------|------|--------|-------------| +| → 全能参考 | [+ 参考内容] 多文件上传 | 恢复用户之前选择(默认 21:9) | 恢复用户之前选择(默认 15s) | 显示 | "上传1-5张参考图或视频,输入文字,自由组合图、文、音、视频多元素,定义精彩互动。" | +| → 首尾帧 | [首帧] ↔ [+ 尾帧] | 自动匹配(灰色不可选) | 5s(可切换) | 隐藏 | "输入描述,定义首帧到尾帧的运动过程" | + +> **注意**: 切换模式时不清空已输入的提示词文本,但上传的文件会被清空(因为两种模式的上传逻辑不同)。 + +## 10. 设计规范(Design Token) + +### 颜色 + +| Token | 值 | 用途 | +|-------|-----|------| +| `--color-bg-page` | `#0a0a0f` | 页面背景 | +| `--color-bg-input-bar` | `#16161e` | InputBar 背景 | +| `--color-border-input-bar` | `#2a2a38` | InputBar 边框 | +| `--color-primary` | `#00b8e6` | 主强调色(发送按钮、视频生成文字) | +| `--color-text-primary` | `#ffffff` | 主文字 | +| `--color-text-secondary` | `#8a8a9a` | 次文字(工具栏按钮、placeholder) | +| `--color-text-disabled` | `#4a4a5a` | 禁用文字 | +| `--color-bg-hover` | `rgba(255, 255, 255, 0.06)` | 按钮 hover 背景 | +| `--color-bg-dropdown` | `#1e1e2a` | 下拉菜单背景 | +| `--color-bg-upload` | `rgba(255, 255, 255, 0.04)` | 上传区背景 | +| `--color-border-upload` | `#2a2a38` | 上传区虚线边框 | +| `--color-btn-send-disabled` | `#3a3a4a` | 发送按钮禁用状态 | +| `--color-btn-send-active` | `#00b8e6` | 发送按钮激活状态 | + +### [Phase 3] 管理后台专用颜色 + +| Token | 值 | 用途 | +|-------|-----|------| +| `--color-bg-sidebar` | `#111118` | Sidebar 背景 | +| `--color-sidebar-active` | `rgba(255, 255, 255, 0.08)` | Sidebar 当前项背景 | +| `--color-sidebar-hover` | `rgba(255, 255, 255, 0.04)` | Sidebar hover 背景 | +| `--color-bg-card` | `#16161e` | 卡片/面板背景 | +| `--color-border-card` | `#2a2a38` | 卡片边框 | +| `--color-success` | `#00b894` | 正向指标(↑绿色) | +| `--color-danger` | `#e74c3c` | 负向指标(↓红色) / 禁用状态 | +| `--color-warning` | `#f39c12` | 警告提示(额度 80%+) | + +### 圆角 + +| Token | 值 | 用途 | +|-------|-----|------| +| `--radius-input-bar` | `20px` | InputBar 容器 | +| `--radius-btn` | `8px` | 工具栏按钮 | +| `--radius-send-btn` | `50%` | 发送按钮(圆形) | +| `--radius-thumbnail` | `8px` | 缩略图 | +| `--radius-dropdown` | `12px` | 下拉菜单 | +| `--radius-card` | `12px` | [Phase 3] 管理后台卡片 | + +### 尺寸 + +| Token | 值 | 用途 | +|-------|-----|------| +| `--input-bar-max-width` | `900px` | InputBar 最大宽度 | +| `--send-btn-size` | `36px` | 发送按钮直径 | +| `--thumbnail-size` | `80px` | 上传缩略图尺寸 | +| `--toolbar-height` | `44px` | 工具栏行高 | +| `--toolbar-btn-height` | `32px` | 工具栏按钮高度 | +| `--sidebar-width` | `240px` | [Phase 3] Sidebar 宽度 | +| `--sidebar-collapsed-width` | `64px` | [Phase 3] Sidebar 折叠宽度 | + +## 11. 组件树结构 + +``` +App +├── AuthProvider // [Phase 2] 认证上下文 +├── Router // [Phase 2] 路由 +│ ├── /login → LoginPage // [Phase 2] 登录页 +│ ├── /register → RegisterPage // [Phase 2] 注册页 +│ ├── / → ProtectedRoute // [Phase 2] 需要登录 +│ │ └── VideoGenerationPage +│ │ ├── UserInfoBar // [Phase 2] 顶部用户信息 + [Phase 3] 秒数配额 + 个人中心链接 +│ │ ├── PageBackground +│ │ └── InputBar +│ │ ├── InputArea +│ │ │ ├── UploadSection +│ │ │ │ ├── UniversalUpload +│ │ │ │ │ ├── UploadButton +│ │ │ │ │ └── ThumbnailGrid +│ │ │ │ │ └── ThumbnailItem +│ │ │ │ └── KeyframeUpload +│ │ │ │ ├── FrameUpload +│ │ │ │ ├── ArrowIcon +│ │ │ │ └── FrameUpload +│ │ │ └── PromptInput +│ │ └── Toolbar +│ │ ├── GenerationTypeDropdown +│ │ ├── ModelSelector +│ │ ├── ModeDropdown +│ │ ├── AspectRatioSelector +│ │ ├── DurationSelector +│ │ ├── AtButton +│ │ ├── FlexSpacer +│ │ └── SendButton +│ ├── /profile → ProtectedRoute // [Phase 3] 用户个人中心 +│ │ └── ProfilePage +│ │ ├── ProfileHeader // 返回首页 + 用户信息 +│ │ ├── ConsumptionOverview // 消费概览卡片 +│ │ │ ├── EChartsGauge // 环形进度条 +│ │ │ ├── DailyQuotaCard // 今日额度进度条 +│ │ │ └── MonthlyQuotaCard // 本月额度进度条 +│ │ ├── ConsumptionTrend // Sparkline 迷你趋势图 +│ │ └── ConsumptionRecordList // 消费记录列表 +│ ├── /admin → ProtectedRoute (requireAdmin) // [Phase 3] 管理后台 +│ │ └── AdminLayout // Sidebar + Content 布局 +│ │ ├── AdminSidebar // 左侧导航栏 +│ │ │ ├── SidebarLogo // Logo +│ │ │ ├── SidebarNav // 导航菜单 +│ │ │ │ ├── NavItem (仪表盘) +│ │ │ │ ├── NavItem (用户管理) +│ │ │ │ ├── NavItem (消费记录) +│ │ │ │ └── NavItem (系统设置) +│ │ │ └── SidebarFooter // 返回首页链接 +│ │ └── AdminContent // 右侧内容区 (Outlet) +│ │ ├── /admin/dashboard → DashboardPage +│ │ │ ├── StatsCards // 4 个指标卡片 +│ │ │ ├── TrendLineChart // ECharts 折线图 +│ │ │ └── TopUsersBarChart // ECharts 柱状图 +│ │ ├── /admin/users → UsersPage +│ │ │ ├── UserSearchBar // 搜索 + 筛选 +│ │ │ ├── UserTable // Arco Table +│ │ │ ├── QuotaEditModal // 配额编辑弹窗 +│ │ │ └── UserDetailDrawer // 用户详情抽屉 +│ │ ├── /admin/records → RecordsPage +│ │ │ ├── RecordFilters // 筛选条件 +│ │ │ ├── RecordTable // Arco Table +│ │ │ └── ExportButton // 导出 CSV +│ │ └── /admin/settings → SettingsPage +│ │ ├── QuotaSettingsCard // 全局配额表单 +│ │ └── AnnouncementCard // 公告管理 +│ └── * → Navigate to / +``` + +## 12. 后端项目结构 + +``` +backend/ +├── manage.py +├── requirements.txt +├── config/ # Django 项目配置 +│ ├── __init__.py +│ ├── settings.py +│ ├── urls.py +│ ├── wsgi.py +│ └── asgi.py +├── apps/ +│ ├── accounts/ # 用户认证模块 +│ │ ├── __init__.py +│ │ ├── models.py # User 扩展模型 (Phase 3: 秒数字段) +│ │ ├── serializers.py # 注册/登录序列化器 +│ │ ├── views.py # 注册/登录/Token刷新 API +│ │ ├── urls.py +│ │ └── admin.py # 用户管理 Admin +│ └── generation/ # 视频生成模块 +│ ├── __init__.py +│ ├── models.py # GenerationRecord (Phase 3: +seconds_consumed) + QuotaConfig (Phase 3: +announcement) +│ ├── serializers.py # [Phase 3] 新增管理后台 + 个人中心序列化器 +│ ├── views.py # [Phase 3] 新增管理后台 API (stats/users/records/settings) + 个人中心 API (overview/records) +│ ├── urls.py # [Phase 3] 新增路由 +│ ├── admin.py # 生成记录 Admin +│ └── middleware.py # 配额检查中间件 (Phase 3: 改为秒数检查) +``` + +## 13. 参考截图说明 + +参考截图存放于 `/Users/maidong/Desktop/zyc/研究openclaw/视频生成平台/` 目录: + +| 文件 | 内容描述 | +|------|---------| +| `20260311-154443.jpeg` | **空状态全貌** — 完整展示了 InputBar 在无输入时的样式 | +| `20260311-154432.jpeg` | **已上传状态** — 展示了上传 1 张图片后的 InputBar | +| `20260311-154407.jpeg` | **有内容状态** — 展示了输入完整提示词 + 多张图片引用后的 InputBar | + +## 14. 修订历史 + +| 日期 | 版本 | 变更内容 | +|------|------|---------| +| 2026-03-11 | v1.0 | 初始版本 — 纯前端视频生成输入界面 | +| 2026-03-12 | v2.0 | 增量迭代 — 新增 Django 后端、用户认证系统、后台管理系统 | +| 2026-03-12 | v2.1 | **需求修订(BUG-002)** — 引入开发阶段划分(Phase 1 / Phase 2),验收标准按阶段分组 | +| 2026-03-12 | v2.2 | **需求修订(BUG-002 后续)** — Phase 2 功能已完成开发,更新阶段状态和验收范围 | +| 2026-03-12 | v3.0 | **重大迭代 — Phase 3** — 计量单位从「调用次数」改为「生成秒数」;管理后台从单页面重做为多页面 Sidebar 布局(仪表盘/用户管理/消费记录/系统设置);新增用户个人中心(/profile)含消费概览、环形进度条、消费趋势 Sparkline、消费记录列表;引入 ECharts 图表库;管理后台深色主题升级(参考 Linear/Vercel 风格);新增骨架屏加载和页面过渡动画;新增 8 个 API 端点、修改 3 个已有 API 端点 | diff --git a/docs/archive/test-report.md b/docs/archive/test-report.md new file mode 100644 index 0000000..cab323d --- /dev/null +++ b/docs/archive/test-report.md @@ -0,0 +1,175 @@ +# 测试报告 + +## 测试结论: ALL_PASSED + +## Bug 路由摘要 +- CODE_BUG: 0 个(路由到开发 Agent) +- DESIGN_BUG: 0 个(路由到设计 Agent) +- REQUIREMENT_BUG: 0 个(路由到产品 Agent) + +## 测试概要 +- 单元测试: 224 通过 / 0 失败(9 个测试文件) +- E2E 测试: 49 通过 / 0 失败(3 个测试文件) +- 视觉质量检查: 7 通过 / 0 失败 +- 后端 API 验证: 12 端点全部通过 +- Django 系统检查: 0 问题 +- 数据库迁移: 完整 + +## 通过的测试 + +### 单元测试(224 个) + +#### 既有测试文件(已更新适配 Phase 3) +- ✅ test/unit/apiClient.test.ts — API 客户端测试(authApi、videoApi、adminApi、profileApi) +- ✅ test/unit/phase2Components.test.tsx — 组件类型测试(已更新为秒数配额格式) +- ✅ test/unit/auth.test.ts — 认证流程测试 +- ✅ test/unit/videoGeneration.test.ts — 视频生成测试 +- ✅ test/unit/adminPanel.test.ts — 管理面板测试 +- ✅ test/unit/uiComponents.test.tsx — UI 组件测试 +- ✅ test/unit/routerSetup.test.tsx — 路由配置测试 +- ✅ test/unit/phase2Features.test.ts — Phase 2 功能测试 + +#### Phase 3 新增测试(62 个,test/unit/phase3Features.test.ts) +- ✅ 秒数配额系统验证(7 个)— Quota 类型字段、默认值 600s/6000s、使用量追踪 +- ✅ 管理后台多页面布局验证(7 个)— AdminLayout 侧边栏、4 个导航链接、折叠功能 +- ✅ 仪表盘页面验证(8 个)— ECharts 图表、统计卡片、趋势数据、骨架屏加载 +- ✅ 用户管理页面验证(6 个)— 用户表格、搜索过滤、配额编辑、状态切换 +- ✅ 消费记录页面验证(5 个)— 记录表格、日期范围过滤、CSV 导出、分页 +- ✅ 系统设置页面验证(4 个)— 全局配额默认值、公告管理、开关切换 +- ✅ 个人中心页面验证(8 个)— 仪表盘图表、进度条、趋势图、消费记录列表 +- ✅ 后端 API 路由验证(4 个)— profile 和 admin URL 配置完整 +- ✅ QuotaConfig 模型验证(3 个)— 单例模式、默认值 +- ✅ ProtectedRoute 管理员守卫(1 个)— requireAdmin 属性 +- ✅ UserInfoBar 导航(2 个)— 秒数显示、个人中心链接 +- ✅ ECharts 集成(3 个)— echarts-for-react 依赖、图表组件配置 +- ✅ 路由配置(4 个)— 嵌套路由、重定向、通配符 + +### E2E 测试(49 个) + +#### 既有 E2E 测试(26 个) +- ✅ test/e2e/video-generation.spec.ts — 视频生成页 P0/P1/P2 验收 + Sidebar(14 个) +- ✅ test/e2e/auth-flow.spec.ts — 认证流程(注册、登录、登出、路由保护)(12 个) + +#### Phase 3 新增 E2E 测试(23 个,test/e2e/phase3-admin-profile.spec.ts) + +**个人中心页面(7 个)** +- ✅ 已认证用户可访问个人中心 +- ✅ 显示消费概览(今日额度、本月额度) +- ✅ 显示消费趋势(近 7 天 / 近 30 天切换) +- ✅ 显示消费记录(新用户显示"暂无记录") +- ✅ 返回首页导航 +- ✅ 退出按钮可见 +- ✅ 未认证用户重定向到登录页 + +**UserInfoBar 配额与导航(2 个)** +- ✅ 显示秒数格式配额(剩余: Xs/Xs(日)) +- ✅ 个人中心链接导航到 /profile + +**管理后台权限控制(6 个)** +- ✅ 非管理员用户无法访问 /admin/dashboard +- ✅ 非管理员用户无法访问 /admin/users +- ✅ 非管理员用户无法访问 /admin/records +- ✅ 非管理员用户无法访问 /admin/settings +- ✅ /admin 重定向到 /admin/dashboard +- ✅ 未认证访问 /admin 重定向到 /login + +**后端 API 集成(7 个)** +- ✅ GET /api/v1/auth/me 返回秒数配额(daily_seconds_limit=600, monthly_seconds_limit=6000) +- ✅ GET /api/v1/profile/overview 返回消费数据(7 天趋势) +- ✅ GET /api/v1/profile/overview 支持 30 天周期 +- ✅ GET /api/v1/profile/records 返回分页记录 +- ✅ POST /api/v1/video/generate 消耗秒数(seconds_consumed=10, remaining=590) +- ✅ 管理端点对非管理员返回 403 +- ✅ 未认证请求返回 401 + +**趋势切换(1 个)** +- ✅ 点击 30 天标签更新趋势周期 + +### 后端 API 验证(curl 直接测试) +- ✅ POST /api/v1/auth/register — 201 Created +- ✅ POST /api/v1/auth/login — 200 OK +- ✅ GET /api/v1/auth/me — 返回秒数配额 +- ✅ GET /api/v1/profile/overview?period=7d — 7 天趋势 +- ✅ GET /api/v1/profile/overview?period=30d — 30 天趋势 +- ✅ GET /api/v1/profile/records — 分页记录 +- ✅ GET /api/v1/admin/stats — 管理统计数据 +- ✅ GET /api/v1/admin/users — 用户列表(分页、搜索) +- ✅ GET /api/v1/admin/records — 消费记录(日期过滤) +- ✅ GET /api/v1/admin/settings — 系统设置 +- ✅ POST /api/v1/video/generate — 202 Accepted,seconds_consumed=10 +- ✅ Django system check: 0 issues, migrations up-to-date + +## 失败的测试(Bug 列表) + +无 + +## 视觉质量检查 + +| 检查项 | 状态 | 说明 | +|-------|------|------| +| 登录页面 | ✅ | 暗色主题,表单完整,注册链接可见 | +| 主页面(已登录) | ✅ | 显示"剩余: 600s/600s(日)"秒数配额,个人中心和管理后台入口可见 | +| 管理后台 — 仪表盘 | ✅ | 侧边栏 4 个导航项,统计卡片(用户数、今日消费、本月消费、活跃用户),ECharts 折线图和柱状图 | +| 管理后台 — 用户管理 | ✅ | 用户表格含秒数配额列,搜索过滤,分页功能 | +| 管理后台 — 消费记录 | ✅ | 记录表格含时间/用户/秒数/描述/模式/状态列,日期范围过滤,导出 CSV 按钮 | +| 管理后台 — 系统设置 | ✅ | 全局默认配额(600s/6000s),系统公告开关和文本输入 | +| 个人中心 | ✅ | 仪表盘图表(0s/600s),今日/本月额度卡片含进度条,消费趋势(近7天/近30天),消费记录(暂无记录) | + +### 视觉截图文件 +- screenshot-login-phase3.png — 登录页 +- screenshot-main-page-phase3.png — 主页面 +- screenshot-admin-dashboard-phase3.png — 管理仪表盘 +- screenshot-admin-users-phase3.png — 用户管理 +- screenshot-admin-records-phase3.png — 消费记录 +- screenshot-admin-settings-phase3.png — 系统设置 +- screenshot-profile-phase3.png — 个人中心 + +## 上一轮 Bug 修复验证 — 全部 ✅ + +| 原 Bug | 状态 | 验证方式 | +|--------|------|---------| +| API 拦截器在登录端点 401 重定向 | ✅ 已修复 | `api.ts:24-25` 排除 `/auth/login`、`/auth/register`、`/auth/token/refresh` 端点 | +| 音频类型死代码(types/store/components) | ✅ 已修复 | 源码审查确认 `UploadedFile.type` 为 `'image' \| 'video'`,音频分支已全部移除 | +| GenerationCard 硬编码模型名 | ✅ 已修复 | `GenerationCard.tsx:54` 使用 `task.model` 动态渲染 | +| 无文件大小验证 | ✅ 已修复 | UniversalUpload、InputBar、KeyframeUpload 均有 20MB/100MB 限制 | + +## Phase 3 功能覆盖总结 + +| 功能模块 | 前端 | 后端 API | 单元测试 | E2E 测试 | 视觉检查 | +|---------|------|---------|---------|---------|---------| +| 秒数配额系统 | ✅ | ✅ | ✅ | ✅ | ✅ | +| 管理后台侧边栏 | ✅ | — | ✅ | ✅ | ✅ | +| 仪表盘(ECharts) | ✅ | ✅ | ✅ | — | ✅ | +| 用户管理 | ✅ | ✅ | ✅ | — | ✅ | +| 消费记录 | ✅ | ✅ | ✅ | — | ✅ | +| 系统设置 | ✅ | ✅ | ✅ | — | ✅ | +| 个人中心 | ✅ | ✅ | ✅ | ✅ | ✅ | +| 权限控制 | ✅ | ✅ | ✅ | ✅ | — | +| CSV 导出 | ✅ | — | ✅ | — | ✅ | + +## 测试环境 +- 前端: Vite 5 + React 18 + TypeScript +- 后端: Django 4.2 + DRF + SimpleJWT +- 单元测试: Vitest 3.x (jsdom) +- E2E 测试: Playwright 1.50 +- 数据库: SQLite (开发环境) +- Node.js: v22+ +- Python: 3.12+ + +## 测试文件清单 + +| 文件 | 类型 | 测试数 | +|------|------|--------| +| `test/unit/inputBarStore.test.ts` | Store 单元测试 | 31 | +| `test/unit/generationStore.test.ts` | Store 单元测试 | 15 | +| `test/unit/authStore.test.ts` | Store 单元测试 | 15 | +| `test/unit/apiClient.test.ts` | API 客户端测试 | 20 | +| `test/unit/phase2Components.test.tsx` | Phase 2 组件测试 | 17 | +| `test/unit/designTokens.test.ts` | CSS/设计规范测试 | 30 | +| `test/unit/components.test.tsx` | 组件渲染测试 | 16 | +| `test/unit/bugfixVerification.test.ts` | Bug 修复验证 | 15 | +| `test/unit/phase3Features.test.ts` | Phase 3 功能测试 | 62 | +| `test/e2e/video-generation.spec.ts` | E2E 视频生成 | 14 | +| `test/e2e/auth-flow.spec.ts` | E2E 认证流程 | 12 | +| `test/e2e/phase3-admin-profile.spec.ts` | E2E Phase 3 | 23 | +| **总计** | | **270** | diff --git a/web/src/store/inputBar.ts b/web/src/store/inputBar.ts index b434f51..7eb18ce 100644 --- a/web/src/store/inputBar.ts +++ b/web/src/store/inputBar.ts @@ -218,9 +218,43 @@ export const useInputBarStore = create((set, get) => ({ }, removeReference: (id) => { const state = get(); - const ref = state.references.find((r) => r.id === id); - if (ref) URL.revokeObjectURL(ref.previewUrl); - set({ references: state.references.filter((r) => r.id !== id) }); + const removedRef = state.references.find((r) => r.id === id); + if (!removedRef) return; + if (removedRef.previewUrl) URL.revokeObjectURL(removedRef.previewUrl); + + // 删除后同类型的剩余引用按顺序重新编号(即梦逻辑:保持 1/2/3 连续) + const remaining = state.references.filter((r) => r.id !== id); + const labelPrefix = removedRef.type === 'image' ? '图片' : removedRef.type === 'video' ? '视频' : '音频'; + const labelUpdates = new Map(); // refId -> newLabel + let idx = 1; + const relabeled = remaining.map((r) => { + if (r.type !== removedRef.type) return r; + const newLabel = `${labelPrefix}${idx++}`; + if (r.label !== newLabel) labelUpdates.set(r.id, newLabel); + return r.label === newLabel ? r : { ...r, label: newLabel }; + }); + + // 同步更新 editorHtml 里对应 refId 的 @mention span 文本 + let newEditorHtml = state.editorHtml; + if (labelUpdates.size > 0 && newEditorHtml) { + const doc = new DOMParser().parseFromString(`
${newEditorHtml}
`, 'text/html'); + const container = doc.body.firstChild as HTMLElement | null; + if (container) { + container.querySelectorAll('[data-ref-id]').forEach((span) => { + const el = span as HTMLElement; + const refId = el.dataset.refId; + if (refId && labelUpdates.has(refId)) { + const newLabel = labelUpdates.get(refId)!; + // span 结构:[icon/img] + atHidden(@) + textNode(label) + const labelNode = [...el.childNodes].reverse().find((n) => n.nodeType === Node.TEXT_NODE); + if (labelNode) labelNode.textContent = newLabel; + } + }); + newEditorHtml = container.innerHTML; + } + } + + set({ references: relabeled, editorHtml: newEditorHtml }); }, clearReferences: () => { const state = get(); diff --git a/web/test/e2e/bug2-rename.spec.ts b/web/test/e2e/bug2-rename.spec.ts new file mode 100644 index 0000000..093ef3f --- /dev/null +++ b/web/test/e2e/bug2-rename.spec.ts @@ -0,0 +1,133 @@ +/** + * Bug 2 E2E 验证:图片删除后即梦式连续重命名 + * 针对本地开发环境(localhost:5173 + 127.0.0.1:8000) + */ +import { test, expect, Page } from '@playwright/test'; + +const BASE_URL = 'http://localhost:5173'; +const API_URL = 'http://127.0.0.1:8000'; +const USERNAME = 'admin'; +const PASSWORD = 'admin123'; + +const TEST_IMAGES_DIR = 'C:/Users/Air-work/AppData/Local/Temp/bug2test'; +const IMG_RED = `${TEST_IMAGES_DIR}/test_red.png`; +const IMG_GREEN = `${TEST_IMAGES_DIR}/test_green.png`; +const IMG_BLUE = `${TEST_IMAGES_DIR}/test_blue.png`; + +async function login(page: Page) { + const resp = await page.request.post(`${API_URL}/api/v1/auth/login`, { + data: { username: USERNAME, password: PASSWORD }, + }); + if (!resp.ok()) { + const errText = await resp.text(); + console.log('LOGIN FAILED:', resp.status(), errText); + } + expect(resp.ok()).toBeTruthy(); + const body = await resp.json(); + + await page.goto(BASE_URL); + await page.evaluate(({ access, refresh }) => { + localStorage.setItem('access_token', access); + localStorage.setItem('refresh_token', refresh); + }, { access: body.tokens.access, refresh: body.tokens.refresh }); + await page.goto(`${BASE_URL}/app`); + await page.waitForTimeout(1500); + + // 关闭初次登录可能出现的公告弹窗 + const knowBtn = page.getByRole('button', { name: /我知道了|知道了|关闭/ }).first(); + if (await knowBtn.isVisible().catch(() => false)) { + await knowBtn.click(); + await page.waitForTimeout(300); + } +} + +test.describe.serial('Bug 2: 图片删除后即梦式连续重命名', () => { + test('上传 3 张图 → 删除图片2 → 图片3 变为图片2', async ({ page }) => { + await login(page); + + // 上传 3 张图 + const fileInput = page.locator('input[type="file"]').first(); + await fileInput.setInputFiles([IMG_RED, IMG_GREEN, IMG_BLUE]); + await page.waitForTimeout(2000); // 等图片校验和 ref 添加完成 + + // 展开缩略图堆栈(hover 触发) + const thumbRow = page.locator('[class*="thumbRow"]').first(); + await thumbRow.hover(); + await page.waitForTimeout(500); + + // 验证上传后初始状态:图片1/图片2/图片3 + const labelsInitial = await page.locator('[class*="thumbLabel"]').allTextContents(); + console.log('初始标签:', labelsInitial); + expect(labelsInitial).toEqual(['图片1', '图片2', '图片3']); + + // 点第 2 张图的删除按钮 + const secondThumb = page.locator('[class*="thumbItem"]').nth(1); + await secondThumb.hover(); + await secondThumb.locator('[class*="thumbClose"]').click({ force: true }); + await page.waitForTimeout(500); + + // 验证重命名后:图片1/图片2(原图片3) + await thumbRow.hover(); + await page.waitForTimeout(300); + const labelsAfterDelete = await page.locator('[class*="thumbLabel"]').allTextContents(); + console.log('删除图片2后:', labelsAfterDelete); + expect(labelsAfterDelete).toEqual(['图片1', '图片2']); + expect(labelsAfterDelete.length).toBe(2); + }); + + test('删除图片2 后再上传 1 张 → 新图是图片3(不和现有冲突)', async ({ page }) => { + await login(page); + + // 上传 3 张 + const fileInput = page.locator('input[type="file"]').first(); + await fileInput.setInputFiles([IMG_RED, IMG_GREEN, IMG_BLUE]); + await page.waitForTimeout(2000); + + const thumbRow = page.locator('[class*="thumbRow"]').first(); + await thumbRow.hover(); + await page.waitForTimeout(500); + + // 删除第 2 张 + const secondThumb = page.locator('[class*="thumbItem"]').nth(1); + await secondThumb.hover(); + await secondThumb.locator('[class*="thumbClose"]').click({ force: true }); + await page.waitForTimeout(500); + + // 再上传 1 张 + await fileInput.setInputFiles([IMG_RED]); + await page.waitForTimeout(2000); + + // 验证:原图片1、原图片3(已改名图片2)、新图片3 + await thumbRow.hover(); + await page.waitForTimeout(300); + const finalLabels = await page.locator('[class*="thumbLabel"]').allTextContents(); + console.log('删除后再上传:', finalLabels); + expect(finalLabels).toEqual(['图片1', '图片2', '图片3']); + // 无重复编号 + expect(new Set(finalLabels).size).toBe(finalLabels.length); + }); + + test('删除第 1 张 → 剩余图片全部前移', async ({ page }) => { + await login(page); + + const fileInput = page.locator('input[type="file"]').first(); + await fileInput.setInputFiles([IMG_RED, IMG_GREEN, IMG_BLUE]); + await page.waitForTimeout(2000); + + const thumbRow = page.locator('[class*="thumbRow"]').first(); + await thumbRow.hover(); + await page.waitForTimeout(500); + + // 删除第 1 张 + const firstThumb = page.locator('[class*="thumbItem"]').nth(0); + await firstThumb.hover(); + await firstThumb.locator('[class*="thumbClose"]').click({ force: true }); + await page.waitForTimeout(500); + + await thumbRow.hover(); + await page.waitForTimeout(300); + const labels = await page.locator('[class*="thumbLabel"]').allTextContents(); + console.log('删除图片1后:', labels); + expect(labels).toEqual(['图片1', '图片2']); + }); +}); diff --git a/web/test/unit/removeReferenceRelabeling.test.ts b/web/test/unit/removeReferenceRelabeling.test.ts new file mode 100644 index 0000000..2681f51 --- /dev/null +++ b/web/test/unit/removeReferenceRelabeling.test.ts @@ -0,0 +1,234 @@ +/** + * Bug 2 fix verification: 删除引用后,同类型剩余引用连续重命名(即梦逻辑) + * 同时同步更新 editorHtml 中 @mention span 的文本。 + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useInputBarStore } from '../../src/store/inputBar'; + +function mockFile(name: string, type = 'image/jpeg'): File { + return new File(['mock'], name, { type }); +} + +function mockRef(id: string, type: 'image' | 'video' | 'audio', label: string) { + return { + id, + file: mockFile(`${id}.${type === 'image' ? 'jpg' : type === 'video' ? 'mp4' : 'mp3'}`), + type, + previewUrl: `blob:${id}`, + label, + }; +} + +function mentionSpan(refId: string, refType: string, label: string): string { + return `@${label}`; +} + +describe('removeReference — 即梦式连续重命名', () => { + beforeEach(() => { + useInputBarStore.getState().reset(); + }); + + describe('图片重命名', () => { + it('删除图片2 后,图片3 重命名为图片2(references + editorHtml 同步)', () => { + const refs = [ + mockRef('ref_1', 'image', '图片1'), + mockRef('ref_2', 'image', '图片2'), + mockRef('ref_3', 'image', '图片3'), + ]; + const editorHtml = + `开场 ${mentionSpan('ref_1', 'image', '图片1')} 和 ${mentionSpan('ref_2', 'image', '图片2')} 在和 ${mentionSpan('ref_3', 'image', '图片3')} 讲话`; + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('ref_2'); + + const state = useInputBarStore.getState(); + expect(state.references).toHaveLength(2); + expect(state.references[0].id).toBe('ref_1'); + expect(state.references[0].label).toBe('图片1'); + expect(state.references[1].id).toBe('ref_3'); + expect(state.references[1].label).toBe('图片2'); // 原图片3 → 图片2 + + // editorHtml 里 ref_3 的 textNode 应该变成 "图片2" + expect(state.editorHtml).toContain('data-ref-id="ref_3"'); + expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?图片2<\/span>/); + // ref_1 保持 "图片1" + expect(state.editorHtml).toMatch(/data-ref-id="ref_1"[^>]*>[\s\S]*?图片1<\/span>/); + }); + + it('删除图片1 后,图片2、图片3 重命名为图片1、图片2', () => { + const refs = [ + mockRef('ref_1', 'image', '图片1'), + mockRef('ref_2', 'image', '图片2'), + mockRef('ref_3', 'image', '图片3'), + ]; + const editorHtml = `${mentionSpan('ref_2', 'image', '图片2')} ${mentionSpan('ref_3', 'image', '图片3')}`; + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('ref_1'); + + const state = useInputBarStore.getState(); + expect(state.references[0].label).toBe('图片1'); // ref_2 + expect(state.references[1].label).toBe('图片2'); // ref_3 + expect(state.editorHtml).toMatch(/data-ref-id="ref_2"[^>]*>[\s\S]*?图片1<\/span>/); + expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?图片2<\/span>/); + }); + + it('删除最后一张图片(唯一图片)— references 清空,editorHtml 不变', () => { + const refs = [mockRef('ref_1', 'image', '图片1')]; + const editorHtml = `内容 ${mentionSpan('ref_1', 'image', '图片1')} 尾部`; + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('ref_1'); + + const state = useInputBarStore.getState(); + expect(state.references).toHaveLength(0); + // remaining 为空 → labelUpdates 为空 → 跳过 DOM 操作 → editorHtml 原样保留 + expect(state.editorHtml).toBe(editorHtml); + }); + }); + + describe('视频/音频独立编号', () => { + it('图片和视频混合时,删图片只重命名图片,视频不动', () => { + const refs = [ + mockRef('ref_1', 'image', '图片1'), + mockRef('ref_2', 'image', '图片2'), + mockRef('ref_3', 'video', '视频1'), + ]; + const editorHtml = + `${mentionSpan('ref_1', 'image', '图片1')} ${mentionSpan('ref_2', 'image', '图片2')} ${mentionSpan('ref_3', 'video', '视频1')}`; + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('ref_1'); + + const state = useInputBarStore.getState(); + expect(state.references).toHaveLength(2); + expect(state.references[0].label).toBe('图片1'); // 原图片2 + expect(state.references[1].label).toBe('视频1'); // 视频不变 + expect(state.editorHtml).toMatch(/data-ref-id="ref_2"[^>]*>[\s\S]*?图片1<\/span>/); + expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?视频1<\/span>/); + }); + + it('删除视频2,视频3 重命名为视频2', () => { + const refs = [ + mockRef('ref_1', 'video', '视频1'), + mockRef('ref_2', 'video', '视频2'), + mockRef('ref_3', 'video', '视频3'), + ]; + const editorHtml = `${mentionSpan('ref_3', 'video', '视频3')}`; + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('ref_2'); + + const state = useInputBarStore.getState(); + expect(state.references[0].label).toBe('视频1'); + expect(state.references[1].label).toBe('视频2'); // 原视频3 + expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?视频2<\/span>/); + }); + + it('删除音频1,音频2 重命名为音频1', () => { + const refs = [ + mockRef('ref_1', 'audio', '音频1'), + mockRef('ref_2', 'audio', '音频2'), + ]; + const editorHtml = `${mentionSpan('ref_1', 'audio', '音频1')} ${mentionSpan('ref_2', 'audio', '音频2')}`; + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('ref_1'); + + const state = useInputBarStore.getState(); + expect(state.references).toHaveLength(1); + expect(state.references[0].id).toBe('ref_2'); + expect(state.references[0].label).toBe('音频1'); // 原音频2 + expect(state.editorHtml).toMatch(/data-ref-id="ref_2"[^>]*>[\s\S]*?音频1<\/span>/); + }); + }); + + describe('边界场景', () => { + it('editorHtml 为空 — 不报错,只重命名 references', () => { + const refs = [ + mockRef('ref_1', 'image', '图片1'), + mockRef('ref_2', 'image', '图片2'), + ]; + useInputBarStore.setState({ references: refs, editorHtml: '' }); + useInputBarStore.getState().removeReference('ref_1'); + + const state = useInputBarStore.getState(); + expect(state.references).toHaveLength(1); + expect(state.references[0].label).toBe('图片1'); + expect(state.editorHtml).toBe(''); + }); + + it('editorHtml 中没有对应的 @mention span — 只改 references', () => { + const refs = [ + mockRef('ref_1', 'image', '图片1'), + mockRef('ref_2', 'image', '图片2'), + ]; + const editorHtml = '纯文本,没有 mention span'; + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('ref_1'); + + const state = useInputBarStore.getState(); + expect(state.references).toHaveLength(1); + expect(state.references[0].label).toBe('图片1'); // ref_2 重命名 + // editorHtml 不含对应 span,无法更新,但不报错 + }); + + it('传入不存在的 id — 静默返回,状态不变', () => { + const refs = [mockRef('ref_1', 'image', '图片1')]; + const editorHtml = mentionSpan('ref_1', 'image', '图片1'); + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('nonexistent_id'); + + const state = useInputBarStore.getState(); + expect(state.references).toHaveLength(1); + expect(state.references[0].label).toBe('图片1'); + expect(state.editorHtml).toBe(editorHtml); + }); + + it('删除的图片没被 @ 到 editor,其他图片仍被重命名', () => { + const refs = [ + mockRef('ref_1', 'image', '图片1'), + mockRef('ref_2', 'image', '图片2'), + mockRef('ref_3', 'image', '图片3'), + ]; + // editorHtml 只 @ 了图片3,没 @图片1/2 + const editorHtml = `${mentionSpan('ref_3', 'image', '图片3')}`; + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('ref_1'); + + const state = useInputBarStore.getState(); + expect(state.references[0].label).toBe('图片1'); // 原图片2 + expect(state.references[1].label).toBe('图片2'); // 原图片3 + // editor 里只有 ref_3 的 span,应该更新成"图片2" + expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?图片2<\/span>/); + }); + }); + + describe('连续删除(并发)', () => { + it('连续删除两张图片,剩余图片正确重编号', () => { + const refs = [ + mockRef('ref_1', 'image', '图片1'), + mockRef('ref_2', 'image', '图片2'), + mockRef('ref_3', 'image', '图片3'), + mockRef('ref_4', 'image', '图片4'), + ]; + const editorHtml = `${mentionSpan('ref_1', 'image', '图片1')} ${mentionSpan('ref_2', 'image', '图片2')} ${mentionSpan('ref_3', 'image', '图片3')} ${mentionSpan('ref_4', 'image', '图片4')}`; + + useInputBarStore.setState({ references: refs, editorHtml }); + useInputBarStore.getState().removeReference('ref_2'); + useInputBarStore.getState().removeReference('ref_1'); + + const state = useInputBarStore.getState(); + expect(state.references).toHaveLength(2); + expect(state.references[0].id).toBe('ref_3'); + expect(state.references[0].label).toBe('图片1'); + expect(state.references[1].id).toBe('ref_4'); + expect(state.references[1].label).toBe('图片2'); + expect(state.editorHtml).toMatch(/data-ref-id="ref_3"[^>]*>[\s\S]*?图片1<\/span>/); + expect(state.editorHtml).toMatch(/data-ref-id="ref_4"[^>]*>[\s\S]*?图片2<\/span>/); + }); + }); +}); From 624e12ae46f04acf591871df7486648e655f8073 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 17 Apr 2026 18:03:49 +0800 Subject: [PATCH 3/6] =?UTF-8?q?docs:=20v0.18.3=20=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=95=B4=E7=90=86=20+=20=E6=96=B0=E7=81=AB=E5=B1=B1=20API=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=20+=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 4 个火山官方 API 文档(Seedance 2.0 1080P / seedance 模型价格 / seedance 2.0 系列教程 / 创建视频生成任务API) - 归档 6 个过期文档到 docs/archive/(旧 Seedance API 邀测版 / 旧 Assets API 邀测版 / celery 轮询修复 / design-review / prd / test-report) - 新增 docs/todo/ 目录(提示词 AI 优化功能待办) - changelog.md 补 v0.18.3 条目 Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/API文档/Seedance 2.0 1080P.md | 85 + ...ance 2.0 & 2.0 fast API文档(邀测用户版).md | 692 ----- ...权限填客户名称】Assets API 参考文档(邀测用户版).md | 1201 --------- docs/API文档/seedance 2.0 系列教程.md | 2233 +++++++++++++++++ docs/API文档/seedance模型价格.md | 481 ++++ docs/API文档/创建视频生成任务API.md | 648 +++++ docs/celery-polling-fix-20260404.md | 134 - docs/changelog.md | 65 + docs/design-review.md | 134 - docs/prd.md | 1611 ------------ docs/todo/提示词AI优化功能.md | 65 + test-report.md | 175 -- 12 files changed, 3577 insertions(+), 3947 deletions(-) create mode 100644 docs/API文档/Seedance 2.0 1080P.md delete mode 100644 docs/API文档/old-Seedance 2.0 & 2.0 fast API文档(邀测用户版).md delete mode 100644 docs/API文档/old-「保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版).md create mode 100644 docs/API文档/seedance 2.0 系列教程.md create mode 100644 docs/API文档/seedance模型价格.md create mode 100644 docs/API文档/创建视频生成任务API.md delete mode 100644 docs/celery-polling-fix-20260404.md delete mode 100644 docs/design-review.md delete mode 100644 docs/prd.md create mode 100644 docs/todo/提示词AI优化功能.md delete mode 100644 test-report.md diff --git a/docs/API文档/Seedance 2.0 1080P.md b/docs/API文档/Seedance 2.0 1080P.md new file mode 100644 index 0000000..a713313 --- /dev/null +++ b/docs/API文档/Seedance 2.0 1080P.md @@ -0,0 +1,85 @@ +# [请填写客户名称]Seedance 2.0 1080P + +> 本文档仅供方舟保底客户查阅,请勿发给没有签约保底的客户 + +### 功能说明 + +⚠️❗️❗️:目前seedance2.0产出的1080p暂时无法支持产物受信功能,即seedance2.0产出的含有人脸的1080p视频将接受安全审查,如果您需要参考含有人脸的1080p视频,请您将该视频上传至虚拟素材库 + +#### **功能1 输出视频分辨率 支持 1080P** + +* **上线时间**:预计国内外4月16日22:00完成上线 + +* **用户范围**: + + * **“抢先体验计划”:功能上线后 72 小时内,部分用户可抢先体验**,官网文档暂不更新 + + * 72 小时后,面向全部用户开放,官网文档同步公开 + +* **支持模型**:仅限Seedance 2.0(Seedance 2.0 fast 不支持) + +* **使用方式:**在请求参数`resolution`中传入`1080p` + +```c++ +curl https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ARK_API_KEY" \ + -d '{ + "model": "doubao-seedance-2-0-260128", + "content": [ + { + "type": "text", + "text": "全程使用视频1的第一视角构图,全程使用音频1作为背景音乐。第一人称视角果茶宣传广告,seedance牌「苹苹安安」苹果果茶限定款;首帧为图片1,你的手摘下一颗带晨露的阿克苏红苹果,轻脆的苹果碰撞声;2-4 秒:快速切镜,你的手将苹果块投入雪克杯,加入冰块与茶底,用力摇晃,冰块碰撞声与摇晃声卡点轻快鼓点,背景音:「鲜切现摇」;4-6 秒:第一人称成品特写,分层果茶倒入透明杯,你的手轻挤奶盖在顶部铺展,在杯身贴上粉红包标,镜头拉近看奶盖与果茶的分层纹理;6-8 秒:第一人称手持举杯,你将图片2中的果茶举到镜头前(模拟递到观众面前的视角),杯身标签清晰可见,背景音「来一口鲜爽」,尾帧定格为图片2。背景声音统一为女生音色。" + }, + { + "type": "image_url", + "image_url": { + "url": "https://ark-project.tos-cn-beijing.volces.com/doc_image/r2v_tea_pic1.jpg" + }, + "role": "reference_image" + }, + { + "type": "image_url", + "image_url": { + "url": "https://ark-project.tos-cn-beijing.volces.com/doc_image/r2v_tea_pic2.jpg" + }, + "role": "reference_image" + }, + { + "type": "video_url", + "video_url": { + "url": "https://ark-project.tos-cn-beijing.volces.com/doc_video/r2v_tea_video1.mp4" + }, + "role": "reference_video" + }, + { + "type": "audio_url", + "audio_url": { + "url": "https://ark-project.tos-cn-beijing.volces.com/doc_audio/r2v_tea_audio1.mp3" + }, + "role": "reference_audio" + } + ], + "resolution": "1080p", + "generate_audio":true, + "ratio": "16:9", + "duration": 11, + "watermark": false +}' +``` + +#### **功能2 输入视频分辨率 支持 1080P** + +* **功能说明**:对输入视频的总像素限制扩大至 2086876(2206x946),支持传入1080P视频作为参考 + +* **上线时间**:预计国内外4月16日 22:00 完成上线 + +* **用户范围**:全部用户可用,官网文档同步公开 + +* **支持模型**:Seedance 2.0、Seedance 2.0 fast 均支持 + +### 费用说明 + +1080P 和 720P/480P 视频区分定价 + +价格详见:https://www.volcengine.com/docs/82379/1544106?lang=zh#02affcb8 diff --git a/docs/API文档/old-Seedance 2.0 & 2.0 fast API文档(邀测用户版).md b/docs/API文档/old-Seedance 2.0 & 2.0 fast API文档(邀测用户版).md deleted file mode 100644 index 969cf38..0000000 --- a/docs/API文档/old-Seedance 2.0 & 2.0 fast API文档(邀测用户版).md +++ /dev/null @@ -1,692 +0,0 @@ -# 【申请权限填客户名称】Seedance 2.0 & 2.0 fast API文档(邀测用户版) - -该文档目前仅限开白客户使用,发送前请和销管确认客户是否在开白名单内 - -***【❗️❗️❗️】该文档限制客户申请权限,只有返回了服务协议的客户方可申请*** - -本文介绍 Seedance 2.0 & 2.0 fast 模型相较于存量模型 **新增/配置有区别 **的 API 参数介绍,存量 API 参数的完整介绍参见 [视频生成 API](https://www.volcengine.com/docs/82379/1520758?lang=zh)。 - -> 本文档仅限预览及邀测用户使用: -> -> * 不承诺正式API上线100%一致。 -> -> * 仅限邀测用户阅读,请勿截图/分享给其他人员。 -> -> * 您上传的内容请确保由您原创或已取得授权。 - -# 模型能力 - -> **Seedance 2.0 和 Seedance 2.0 fast 提供的模型能力一致,**追求最高生成品质,推荐使用 **Seedance 2.0**;更注重成本与生成速度,不要求极限品质,推荐使用 **Seedance 2.0 fast**。 - -**Seedance 2.0 & 2.0 fast (有声视频/无声视频)** - -* **多模态参考生视频**:输入参考图片(0\~9)+参考视频(0\~3)+ 参考音频(0\~3)+ 文本提示词(可选)生成 1 个目标视频。支持生成全新视频、编辑视频、延长视频。 - -> **注意:不可单独输入音频,应至少包含 1 个参考视频或图片。** - -* **图生视频-首尾帧**:输入首帧图片+尾帧图片+文本提示词(可选)生成 1 个目标视频。 - -* **图生视频-首帧**:输入首帧图片+文本提示词(可选)生成 1 个目标视频。 - -* **文生视频**:输入文本提示词生成 1 个目标视频。 - - - -**模型能力对比表:** - -| 模型名称 | | [Seedance 2.0](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-2-0) | [Seedance 2.0 fast](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-2-0-fast\&projectName=default) | [Seedance 1.5 pro](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-5-pro\&projectName=default) | [Seedance 1.0 pro ](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-0-pro\&projectName=default) | [Seedance 1.0 pro fast ](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-0-pro-fast\&projectName=default) | [Seedance 1.0 lite i2v](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-0-lite-i2v\&projectName=default) | [Seedance-1.0 lite t2v ](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-1-0-lite-t2v) | -| ------------ | -------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| Model ID | | doubao-seedance-2-0-260128 | doubao-seedance-2-0-fast-260128 | doubao-seedance-1-5-pro-251215 | doubao-seedance-1-0-pro-250528 | doubao-seedance-1-0-pro-fast-251015 | doubao-seedance-1-0-lite-i2v-250428 | doubao-seedance-1-0-lite-t2v-250428 | -| 文生视频 | | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | -| 图生视频-首帧 | | ✅ | | ✅ | ✅ | ✅ | ✅ | ❌ | -| 图生视频-首尾帧 | | ✅ | | ✅ | ✅ | ❌ | ✅ | ❌ | -| 多模态参考【New】 | 图片参考 | ✅ | | ❌ | ❌ | ❌ | ✅ | ❌ | -| | 视频参考 | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | -| | 组合参考 | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | -| 编辑视频【New】 | | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | -| 延长视频【New】 | | ✅ | | ❌ | ❌ | ❌ | ❌ | ❌ | -| 生成有声视频 | | ✅ | | ✅ | ❌ | ❌ | ❌ | ❌ | -| 联网搜索增强【New】 | | ✅ | | ❌ | [❌](https://p9-arcosite.byteimg.com/obj/tos-cn-i-goo7wpa0wc/f359753773c94d97885008ca1223c9bc) | ❌ | ❌ | ❌ | -| 样片模式 | | ❌ | | ✅ | ❌ | ❌ | ❌ | ❌ | -| 返回视频尾帧 | | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | -| 输出视频规格 | 输出分辨率 | 480p, 720p | | 480p, 720p, 1080p | 480p, 720p, 1080p | 480p, 720p, 1080p | 480p, 720p, 1080p | 480p, 720p, 1080p | -| | 输出宽高比 | 21:9, 16:9, 4:3, 1:1, 3:4, 9:16 | | | | | | | -| | 输出时长 | 4\~15 秒 | | 4\~12 秒 | 2\~12 秒 | 2\~12 秒 | 2\~12 秒 | 2\~12 秒 | -| | 输出视频格式 | mp4 | | mp4 | mp4 | mp4 | mp4 | mp4 | -| 离线推理 | | [❌](https://p9-arcosite.byteimg.com/obj/tos-cn-i-goo7wpa0wc/f359753773c94d97885008ca1223c9bc) | | ✅ | ✅ | ✅ | ✅ | ✅ | -| 在线推理限流 | RPM | 600 | | 600 | 600 | 600 | 300 | 300 | -| | 并发数 | 10 | | 10 | 10 | 10 | 5 | 5 | -| 离线推理限流 | TPD | - | | 5000亿 | 5000亿 | 5000亿 | 2500亿 | 2500亿 | - - - - - -# Creat-创建视频生成任务 - -> POST https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks - -## 请求参数 - - - -#### **content** `object[]` `必选` - -输入给模型,生成视频的信息,支持文本、图片、音频、视频、样片任务 ID。支持以下几种组合: - -* **文本** - -* **文本(可选)+ 图片** - -* **文本(可选)+ 视频** - -* **文本(可选)+ 图片 + 音频** - -* **文本(可选)+ 图片 + 视频** - -* **文本(可选)+ 视频 + 音频** - -* **文本(可选)+ 图片 + 视频 + 音频** - -*** - -**信息类型:** - -* **文本信息**`object` - -输入给模型的提示词信息。 - -*** - -content.**type **`string` `必选` - -输入内容的类型,此处应为 **text**。 - -*** - -content.**text **`string` `必选` - -输入给模型的文本提示词,描述期望生成的视频。 - -支持中英文。建议中文不超过500字,英文不超过1000词。字数过多信息容易分散,模型可能因此忽略细节,只关注重点,造成视频缺失部分元素。提示词的更多使用技巧请参见 [Seedance 提示词指南](https://www.volcengine.com/docs/82379/1587797)。 - - - - - -* **图片信息** `object` - -输入给模型的图片信息。 - -*** - -content.**type **`string` `必选` - -输入内容的类型,此处应为 **image\_url**。 - -*** - -content.**image\_url **`object` `必选` - -输入给模型的图片对象。 - -*** - -content.image\_url.**url **`string` `必选` - -图片 URL 、图片 Base64 编码、素材 ID。 - -* 图片 URL:填入图片的公网 URL。 - -* Base64 编码:将本地文件转换为 Base64 编码字符串,然后提交给大模型。遵循格式:data:image/<图片格式>;base64,\,注意 <图片格式> 需小写,如 data:image/png;base64,{base64\_image}。 - -* 素材 ID:用于视频生成的预置素材及虚拟人像的 ID,遵循格式:asset://\,可从 [素材&虚拟人像库](https://console.volcengine.com/ark-stg/region:ark-stg+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128) 获取,详细使用请参见[文档](https://www.volcengine.com/docs/82379/2223965?lang=zh)。 - -> **传入单张图片要求** -> -> * 格式:jpeg、png、webp、bmp、tiff、gif -> -> * 宽高比(宽/高): (0.4, 2.5) -> -> * 宽高长度(px):(300, 6000) -> -> * 大小:单张图片小于 30 MB。请求体大小不超过 64 MB。大文件请勿使用Base64编码。 -> -> * 图片数量: -> -> * 图生视频-首帧:1 张 -> -> * 图生视频-首尾帧:2 张 -> -> * Seedance 2.0 & 2.0 fast 多模态参考生视频:1\~9 张 - -*** - -content.**role **`string` `条件必填` - -图片的位置或用途。 - -> **注意** -> -> * **图生视频-首帧**、**图生视频-首尾帧**、**多模态参考生视频**(包括参考图、视频、音频)为 3 种互斥场景,**不可混用**。 -> -> * **多模态参考生视频**可通过提示词指定参考图片作为首帧/尾帧,间接实现“首尾帧+多模态参考”效果。若需严格保障首尾帧和指定图片一致,**优先使用图生视频-首尾帧**(配置 role 为 **first\_frame / last\_frame**)。 - -*** - -**图生视频-首帧** - -> 需要传入1个 image\_url 对象 - -* **字段role取值:** - - * **first\_frame 或不填** - -*** - -**图生视频-首尾帧** - -> 需要传入2个 image\_url 对象 - -* **字段role取值:** - - * 首帧图片对应的字段 role 为:**first\_frame**,必填 - - * 尾帧图片对应的字段 role 为:**last\_frame**,必填 - -*** - -**图生视频-参考图 ** - -> 可传入 1\~9 个 image\_url 对象 - -* **字段role取值**: - - * 每张参考图对应的字段 role 均为:**reference\_image**,必填 - - - - - -* **视频信息** `object` - -输入给模型的视频信息。仅 Seedance 2.0 & 2.0 fast 支持输入视频。 - -*** - -content.**type **`string` `必选` - -输入内容的类型,此处应为 **video\_url**。 - -*** - -content.**video\_url **`object` `必选` - -输入给模型的视频对象。 - -*** - -content.video\_url.**url **`string` `必选` - -视频URL、素材 ID。 - -* 视频 URL:填入视频的公网 URL。 - -* 素材 ID:用于视频生成的预置素材及虚拟人像视频的 ID,遵循格式:asset://\。可从[素材&虚拟人像库](https://console.volcengine.com/ark-stg/region:ark-stg+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128)获取。 - -> **传入单个视频要求** -> -> * 视频格式:mp4、mov。 -> -> * 分辨率:480p、720p -> -> * 时长:单个视频时长 \[2, 15] s,最多传入 3 个参考视频,所有视频总时长不超过 15s。 -> -> * 尺寸: -> -> * 宽高比(宽/高):\[0.4, 2.5] -> -> * 宽高长度(px):\[300, 6000] -> -> * 画面像素(宽 × 高):\[409600, 927408] ,示例: -> -> * 画面尺寸 640×640=409600 满足最小值 ; -> -> * 画面尺寸 834×1112=927408 满足最大值。 -> -> * 大小:单个视频不超过 50 MB。 -> -> * 帧率 (FPS):\[24, 60] - -*** - -content.**role **`string` `条件必填` - -视频的位置或用途。当前仅支持 **reference\_video**。 - - - - - -* **音频信息 **`object` - -输入给模型的音频信息。仅 Seedance 2.0 & 2.0 fast 支持输入音频。注意不可单独输入音频,应至少包含 1 个参考视频或图片。 - -*** - -content.**type **`string` `必选` - -输入内容的类型,此处应为 **audio\_url**。 - -*** - -content.**audio\_url **`object` `必选` - -输入给模型的音频对象。 - -*** - -content.audio\_url.**url **`string` `必选` - -音频 URL 、音频 Base64 编码、素材 ID。 - -* 音频 URL:填入音频的公网 URL。 - -* Base64 编码:将本地文件转换为 Base64 编码字符串,然后提交给大模型。遵循格式:data:audio/<音频格式>;base64,\,注意 <音频格式> 需小写,如 data:audio/wav;base64,{base64\_audio}。 - -* 素材 ID:用于视频生成的虚拟人的音频素材 ID,遵循格式:asset://\。可从[素材&虚拟人像库](https://console.volcengine.com/ark-stg/region:ark-stg+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128)获取。 - -> **传入单个音频要求** -> -> * 格式:wav、mp3 -> -> * 时长:单个音频时长 \[2, 15] s,最多传入 3 段参考音频,所有音频总时长不超过 15 s。 -> -> * 大小:单个音频不超过 15 MB,请求体大小不超过 64 MB。大文件请勿使用Base64编码。 - -*** - -content.**role **`string` `条件必填` - -音频的位置或用途。当前仅支持 **reference\_audio** 。 - - - -#### **service\_tier** `string` - - Seedance 2.0 & 2.0 fast 暂不支持 - - - -#### **generate\_audio **`boolean` - -> Seedance 2.0 & 2.0 fast 默认值: true - -控制生成的视频是否包含与画面同步的声音。 - -* true:模型输出的视频包含同步音频。模型会基于文本提示词与视觉内容,自动生成与之匹配的人声、音效及背景音乐。建议将对话部分置于双引号内,以优化音频生成效果。例如:男人叫住女人说:“你记住,以后不可以用手指指月亮。” - -* false:模型输出的视频为无声视频。 - -> **说明** -> -> 生成的有声视频均为单声道,和传入的音频声道数无关。 - -#### - -#### **draft **`boolean` - - Seedance 2.0 & 2.0 fast 暂不支持 - - - -#### **tools **`object[]` - -> 仅 Seedance 2.0 & 2.0 fast 支持 - -配置模型要调用的工具。 - -*** - -tools.**type **`string` - -指定使用的工具类型。 - -* web\_search:联网搜索工具。 - -> **说明** -> -> * 开启联网搜索后,模型会根据用户的提示词自主判断是否搜索互联网内容(如商品、天气等)。可提升生成视频的时效性,但也会增加一定的时延。 -> -> * 实际搜索次数可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 usage.tool\_usage.**web\_search** 字段获取,如果为 0 表示未搜索。 - - - -#### **resolution ** `string` - -> Seedance 2.0 & 2.0 fast 默认值:720p - -视频分辨率,取值范围: - -* 480p - -* 720p - - - -#### **ratio **`string` - -> Seedance 2.0 & 2.0 fast 默认值: adaptive - -生成视频的宽高比例。不同宽高比对应的宽高像素值见下方表格。 - -* 16:9 - -* 4:3 - -* 1:1 - -* 3:4 - -* 9:16 - -* 21:9 - -* adaptive:根据输入自动选择最合适的宽高比 - -> **adaptive 适配规则** -> -> 当配置 **ratio** 为 adaptive 时,模型会根据生成场景自动适配宽高比;实际生成的视频宽高比可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 **ratio** 字段获取。 -> -> * 文生视频:根据输入的提示词,智能选择最合适的宽高比。 -> -> * 首帧 / 首尾帧生视频:根据上传的首帧图片比例,自动选择最接近的宽高比。 -> -> * 多模态参考生视频:根据用户提示词意图判断,如果是首帧生视频/编辑视频/延长视频,以该图片/视频为准选择最接近的宽高比;否则,以传入的第一个媒体文件为准(优先级:视频>图片)选择最接近的宽高比。 - -*** - -**不同宽高比对应的宽高像素值:** - -| 分辨率 | 宽高比 | 宽高像素值 | -| ---- | ---- | -------- | -| 480p | 16:9 | 864×496 | -| | 4:3 | 752×560 | -| | 1:1 | 640×640 | -| | 3:4 | 560×752 | -| | 9:16 | 496×864 | -| | 21:9 | 992×432 | -| 720p | 16:9 | 1280×720 | -| | 4:3 | 1112×834 | -| | 1:1 | 960×960 | -| | 3:4 | 834×1112 | -| | 9:16 | 720×1280 | -| | 21:9 | 1470×630 | - - - -#### **duration** `integer` - -> Seedance 2.0 & 2.0 fast 默认值:5 - -生成视频时长,仅支持整数,单位:秒。 - -取值范围: - -* \[4,15] 或设置为-1 - -> **配置方法** -> -> * 指定具体时长:支持有效范围内的任一整数。 -> -> * 智能指定:设置为 -1,表示由模型在有效范围内自主选择合适的视频长度(整数秒)。实际生成视频的时长可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 **duration** 字段获取。注意视频时长与计费相关,请谨慎设置。 - - - -#### **frames** `integer` - -Seedance 2.0 & 2.0 fast 暂不支持 - - - -#### **camera\_fixed** `boolean` - - Seedance 2.0 & 2.0 fast 暂不支持 - - - -# Get/List-查询视频生成任务/列表 - -> 查询视频生成任务:GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{id} -> -> 查询视频生成任务列表:GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks?page\_num={page\_num}\&page\_size={page\_size}\&filter.status={filter.status}\&filter.task\_ids={filter.task\_ids}\&filter.model={filter.model} - -## 响应参数 - -#### **tools **`object[]` - -> 仅 Seedance 2.0 & 2.0 fast 支持 - -配置模型要调用的工具。 - -*** - -tools.**type **`string` - -指定使用的工具类型。 - -* web\_search:联网搜索工具。 - - - -#### **usage** `object` - -本次请求的 token 用量。 - -*** - -usage.**completion\_tokens** `integer` - -模型输出视频花费的 token 数量。 - -*** - -usage.**total\_tokens** `integer` - -本次请求消耗的总 token 数量。 - -*** - -usage.**tool\_usage **`object` - -> 仅 Seedance 2.0 & 2.0 fast 支持 - -使用工具的用量信息。 - -*** - -usage.tool\_usage.**web\_search **`integer` - -实际调用联网搜索工具的次数,仅开启联网搜索时返回。 - - - -# 调用简介及示例 - -## 流程简介 - -任务接口是异步接口,视频生成任务流程 - -1. 创建视频生成任务接口创建视频生成任务 - -2. 定时使用查询接口查询视频生成任务状态 - - 1. 任务 running,过段时间再查询任务状态 - - 2. 任务完成,返回视频链接,在24小时内下载生成的视频文件 - -## 1. 创建视频生成任务 - -> 以下示例仅展示 Seedance 2.0 & 2.0 fast 新增能力,更多视频生成示例详见 [创建视频生成任务 API](https://www.volcengine.com/docs/82379/1520757)。 - -### 多模态参考 - -### 编辑视频 - -### 延长视频 - -### 使用联网搜索 - -仅支持文本生视频 - -## 2. 查询视频生成任务 - -# 最佳实践-使用公共虚拟人像生成视频 - -平台提供公共虚拟人像素材库,目前您可以使用其中的图像素材来创建一个统一、完备的视频主角。帮助您更好地控制主角,并确保其形象在多段视频中保持一致,避免因为真人人脸限制导致角色无法统一的问题。 - -素材模态目前包含图片,并提供人物背景描述。每个素材对应一个独立素材 ID (asset ID),在体验中心的视频生成任务中,指定角色人脸生成视频。 - -1. 在浏览器中打开[体验中心](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128\&tab=GenVideo),点击输入框下方的 **虚拟人像库** 页签。 - -2. 检索需要使用的人像,支持使用自然语言检索及筛选框组合筛选。 - -| 输入:文本 | 输入:虚拟人像、图片 | 输出 | -| ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -- | -| **图片1**中美妆博主用中文进行介绍,妆容改为明艳大气,去掉脸部反光,笑容甜美,近景镜头,手持**图片2**的面霜面向镜头展示,清新简约背景,元气甜美风格。博主台词:挖到本命面霜了!质地像云朵一样软糯,一抹就吸收,熬夜急救、补水保湿全搞定,素颜都自带柔光感。 | ![Image Token: HTf6bPRukoWaW4xnCSlcvKtUn7c](images/HTf6bPRukoWaW4xnCSlcvKtUn7c.png)![Image Token: YfCDbzJlqo4yzZxCmdscWdsInCf](images/YfCDbzJlqo4yzZxCmdscWdsInCf.jpeg) | | - - - -在 [Video Generation API](https://www.volcengine.com/docs/82379/1520758) 的 **content.<模态>\_url.url** 字段中使用 素材 URI 生成视频。 - -> 输入的参考内容,包括人像素材,需符合视频生成限制,具体信息请查看使用限制。 -> -> **注意**: -> -> * 首次在 API 中使用虚拟人像素材 Asset URI 前,需先在[方舟体验中心](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128\&tab=GenVideo)提交一次视频生成任务,阅读并同意弹出的 **虚拟人像库使用协议**。 -> -> * 体验中心支持体验视频生成能力。默认单次生成 4 段视频,为节约成本,建议设置为每次生成 1 条,具体方式可参考[虚拟人像库](https://www.volcengine.com/docs/82379/2223965?lang=zh)。 - -同意协议的操作方式如下: - -![Image Token: LK8ybUN9Ko2KkQxq2FdclVQtnkh](images/LK8ybUN9Ko2KkQxq2FdclVQtnkh.gif) - -示例代码: - -# 使用自有虚拟人像素材生成视频(线下提交) - -方舟提供私域人像素材库,您可在视频生成中使用自有虚拟人物或真人(仅限素人)素材,生成短剧等更定制化的视频内容。平台将对您提供的素材进行审核,规避可能产生的法律风险。 - -* 自有素材需入库后使用,您可将虚拟人像或真人素材发送给销售代表,同时完成合规承诺函及其他证明材料的准备。 - -* 入库后,您可使用素材的 Asset ID,在视频生成 API 中使用自有素材。 - -> **重要**: -> -> * 对虚拟人像素材,您需签署虚拟人像素材合规承诺函,并提供签署承诺函所需的材料。 -> -> * 对真实人物素材,除承诺函外,您还需额外提供真人授权材料。 -> -> * 具体流程及所需材料,请和您的销售代表确认。 - -提交自有人像素材时,需按人物将素材分组: - -* 每个人物为一个素材组。 - -* 每组可包含多个素材文件,素材文件对应唯一 ID (asset ID)。 - -## 入库流程 - -提交自有虚拟人像素材方式大致如下,请联系您的销售代表了解详情。 - -1. 准备素材文件,完成承诺函签署,并准备其他证明材料。 - -2. 准备素材文件,完成承诺函签署,并准备其他证明材料。 - - * 每个人物素材需至少提供一张正面图片文件。此外,您可按需提供该人物的其他图片、视频素材。 - - * 需确保每个人物组中的素材与该正面图片为同一人物。 - - * 每个人物创建一个文件夹(命名:“*虚拟人像 1-<人像名>*”) - - 提交素材文件夹示例: - - ![Image Token: XMQ9bz6vhof7vxxsac8cqIZmneB](images/XMQ9bz6vhof7vxxsac8cqIZmneB.png) - - > **注意**: - > - > * 以上示例仅供参考,您可根据视频创作需求,提交虚拟人物素材。 - > - > * 您仅需上传视频生成任务中需要使用的素材。 - - * 素材文件需满足视频生成 API 对输入文件的要求: - - > **传入单张图片要求** - > - > * 格式:jpeg、png、webp、bmp、tiff、gif - > - > * 宽高比(宽/高): (0.4, 2.5) - > - > * 宽高长度(px):(300, 6000) - > - > * 大小:单张图片小于 30 MB。请求体大小不超过 64 MB。大文件请勿使用Base64编码。 - - - - > **传入单个视频要求** - > - > * 视频格式:mp4、mov。 - > - > * 分辨率:480p、720p - > - > * 时长:单个视频时长 \[2, 15] s,最多传入 3 个参考视频,所有视频总时长不超过 15s。 - > - > * 尺寸: - > - > * 宽高比(宽/高):\[0.4, 2.5] - > - > * 宽高长度(px):\[300, 6000] - > - > * 画面像素(宽 × 高):\[409600, 927408] ,示例: - > - > * 画面尺寸 640×640=409600 满足最小值 ; - > - > * 画面尺寸 834×1112=927408 满足最大值。 - > - > * 大小:单个视频不超过 50 MB。 - > - > * 帧率 (FPS):\[24, 60] - - - - > **注意**: - > - > 有关提交流程、承诺函签署所需材料的具体信息,请联系您的销售代表了解详情。 - -3. 方舟将对您提供的素材进行审核,通过审核的素材将被上传至虚拟人像库。 - -4. 入库后,每个人物组素材将通过以下示例中的形式返回,您可解压后查看: - - ![Image Token: PKu6b3391oUbVKxxEGjchxBVnbg](images/PKu6b3391oUbVKxxEGjchxBVnbg.png) - -示例中: - -* Andy 为您提交的人物名称 - -* group-20260310035119-9mzqn 为该人物组的 ID - -* 解压后,可查看每张素材的 Asset ID,如: - -![Image Token: VV0ybrxNfouEhZxTjqCcX1epnzb](images/VV0ybrxNfouEhZxTjqCcX1epnzb.png) - -* 您可按 `asset: //` 规则拼接 URI,在 API 中使用对应素材生成视频: - -具体调用方式请参考 [最佳实践-使用虚拟人像生成视频](https://bytedance.larkoffice.com/wiki/SANpwJ9bgiKgrykLaMTcAB0InWc#share-YurKdrLfAocLErxsTWDcKidPnGd)。 - -## **注意事项** - -1. 首次在 API 中使用虚拟人像素材 Asset URI 前,需先在[方舟体验中心](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128\&tab=GenVideo)提交一次视频生成任务,阅读并同意弹出的 **虚拟人像库使用协议**,操作方式如下: - -![Image Token: IFfPbDgceoFXZCxdriIcnwkPnUc](images/IFfPbDgceoFXZCxdriIcnwkPnUc.gif) - -* 仅支持使用已入库素材生成视频。 diff --git a/docs/API文档/old-「保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版).md b/docs/API文档/old-「保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版).md deleted file mode 100644 index 75cb4f3..0000000 --- a/docs/API文档/old-「保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版).md +++ /dev/null @@ -1,1201 +0,0 @@ -# 「⚠️保密信息」【申请权限填客户名称】Assets API 参考文档(邀测用户版) - -本文介绍素材资产(Assets)API 接口的参数。您可以使用以下 Assets API 接口创建、管理个人人像素材资产。 - -> 本文档仅限预览及邀测用户使用: -> -> * 不承诺正式 API 上线100%一致。 -> -> * 仅限邀测用户阅读,请勿截图/分享给其他人员。 -> -> * 您需确保上传的虚拟人像符合以下条件: -> -> * 您合法拥有该素材,并享有完整的使用及处分权限。素材不包含未获授权的第三方商标、标识类内容。 -> -> * 素材不得与任何自然人肖像或形象雷同,素材不存在抄袭、盗用情形,不会侵害任何第三方的人格权、知识产权等合法权益。 -> -> * 素材不包含违反法规、违背公序良俗、危害国家安全的内容。 - - - -# 素材资产(Assets)API 接口功能 - -> **素材资产的概念说明:** -> -> * **Asset(素材资产)**:一个素材文件(图片),是方舟 Seedance 2.0 系列模型可直接用于推理的可信资产。 -> -> * 仅需入库推理需使用的素材资产,不需使用的素材资产请勿入库。 -> -> * 仅可使用已入库素材资产的 Id (Asset ID) 进行视频生成,同一形象未入库素材无法使用。 -> -> * **Asset Group(素材资产组合)**:单个素材文件为一个 Asset,每个 Asset 属于一个 Asset Group。 -> -> * 可以使用素材组自由管理素材,例如可将同一人物、同一工作室或项目组的素材放入同一素材组合进行管理。 - -**Asset (Group) 创建接口:** - -1. CreateAssetGroup:创建素材资产组合。**首次创建素材资产组合时需在控制台签署授权函,详情参考 [ 私域虚拟人像素材库 (WIP)](https://bytedance.larkoffice.com/wiki/RtHgwpJgviwFXLkQ9hLcRooEnVe)** - -2. CreatAsset:创建素材资产。该接口可用于上传个人素材资产,创建素材资产后可利用返回字段中的素材 **Id (需处于 `active` 状态)**用于 Seedance 2.0 系列模型生成视频。 - - - -**Asset (Group) 管理接口:** - -* ListAssetGroups:查询素材资产组合列表。 - -* ListAssets:查询素材资产列表。 - -* GetAsset:查询素材资产信息。 - -* GetAssetGroup:查询素材资产组合信息。 - -* UpdateAssetGroup:更新素材资产组合信息。 - -* UpdateAsset:更新素材资产信息。 - - - -# 鉴权方式 - -调用素材资产(Assets)API 接口需使用 Access Key 鉴权,详情参考 [获取 API 访问密钥(AK/SK)](https://www.volcengine.com/docs/6257/64983?lang=zh)。 - - - -# 限流要求 - -* 并发数限制:账号下同一时刻在处理中的任务数量上限,超过此限制的任务将进入队列等待处理。**同时进行处理的asset创建任务不超过30。** - -* QPS 限流: API接口每秒查询请求的总数上限。超过此限制的查询请求会报错。 - -| 接口名 | 账号维度的 QPS 限流 | -| ---------------- | ------------ | -| CreateAssetGroup | 30 | -| CreateAsset | 30 | -| ListAssetGroups | 30 | -| ListAssets | 30 | -| GetAsset | 100 | -| GetAssetGroup | 100 | -| UpdateAsset | 30 | -| UpdateAssetGroup | 30 | - -# CreateAssetGroup - -> **POST **/open/CreateAssetGroup - -创建 Asset Group(素材资产组合)组合,用作素材资产管理。 - -> **首次创建 Asset Group(素材资产组合)需在控制台签署授权函,详情参考 [ 私域虚拟人像素材库 (WIP)](https://bytedance.larkoffice.com/wiki/RtHgwpJgviwFXLkQ9hLcRooEnVe)** - -### **Name** `string` `必填` - -Asset Group(素材资产组合)的名称,上限为 64 字符。 - -*** - -### **Description** `string` - -Asset Group(素材资产组合)的描述,上限为 300 字符。 - -*** - -### **GroupType **`string` - -Asset Group(素材资产组合)的类型。可选值: - -* AIGC:虚拟人像。 - -*** - -### **ProjectName **`string`** ** - -资源所属的项目名称,默认值为`default`。 - -若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 - -*** - - - -## 返回参数 - -### **Id** `string` - -Asset Group(素材资产组合)的 Id。 - -*** - -返回示例: - -```bash -{ - "Id": "group-2026**********-*****" -} -``` - - - -# CreateAsset - -> **POST **/open/CreateAsset - -向指定的Asset Group(素材资产组合)内创建Asset(素材资产)。 - - - -## 请求参数 - -### **GroupId** `string` `必填` - -Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 - -*** - -### **URL** `string` `必填` - -传入的Asset(素材资产)的公共可访问地址。 - -*** - -### **Name** `string` - -Asset(素材资产)的名称,上限为64个字符。 - -*** - -### **AssetType `string` `必填`** - -Asset(素材资产)的类型,当前仅支持传入图像。可选值: - -* Image:Asset(素材资产)的类型为图像。 - -> **传入图像的要求说明** -> -> * 格式:jpeg、png、webp、bmp、tiff、gif、heic/heif -> -> * 宽高比(宽/高): (0.4, 2.5) -> -> * 宽高长度(px):(300, 6000) -> -> * 大小:单张图片小于 30 MB。 - -*** - -### **ProjectName** `string` - -资源所属的项目名称,默认值为`default`。 - -若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 - -> 需要和待传入的 Asset Group(素材资产组合)的 **ProjectName **保持一致。 - -*** - - - -## 返回参数 - -### **Id **`string` - -Asset(素材资产)的 Id。 - -*** - -返回示例: - -```bash -{ - "Id": "Asset-2026**********-*****" -} -``` - - - -# ListAssets - -> **POST **/open/ListAssets - -查询符合筛选条件的Assets(素材资产)列表。 - -## 请求参数 - -### **Filter** `object` `必填` - -搜索的过滤条件。 - -*** - -Filter.**GroupIds** `array` - -Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 - -*** - -Filter.**GroupType** `string` `必填` - -Asset Group(素材资产组合)的类型。可选值: - -* AIGC:虚拟人像。 - -*** - -Filter.**Statuses** `array`** ** - -任务状态。 - -* Active:素材资产(Asset)已处理完毕,可以使用。 - -* Processing:素材资产(Asset)正在预处理,无法使用。 - -* Failed:素材资产(Asset)处理失败。 - -*** - -Filter.**Name** `string`** ** - -Asset(素材资产)的名称,上限为64个字符。 - -*** - -### **PageNumber** `int (i64)` `必填` - -搜索页码,可用于列表分页功能,从 1 开始。例如:"page\_number": 1,即返回第一页的搜索结果。 - -*** - -### **PageSize** `int (i64)` `必填` - -每页搜索结果的数量,上限为100。 - -*** - -### **SortBy** `string` - -用于排序的字段名称,默认值 `createTime`。支持以下类型: - -* CreateTime:根据创建时间排序。 - -* UpdateTime:根据更新时间排序。 - -* GroupId:根据资产素材组的 Id 排序。 - -*** - -### **SortOrder** `string` - -排序顺序,默认值 `Desc`。可选值: - -* Desc:降序 - -* Asc:升序 - -*** - -### **ProjectName** `string` - -资源所属的项目名称,默认值为`default`。 - -若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 - -*** - - - -## 返回参数 - -### **Items** `array[]` - -符合筛选条件的Asset(素材资产)数组。 - -*** - -Items.**Id** `string` - -Asset(素材资产)的 Id。 - -*** - -Items.**name** `string` - -Asset(素材资产)的名称,上限为64个字符。 - -*** - -Items.**URL** `string` - -Asset(素材资产)的公共可访问地址。 - -*** - -Items.**GroupId** `string` - -Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 - -*** - -Items.**AssetType** `string` - -Asset(素材资产)的类型。 - -* Image:Asset(素材资产)的类型为图像。 - -*** - -Items.**Status** `string` - -任务状态。 - -* Active:素材资产(Asset)已处理完毕,可以使用。 - -* Processing:素材资产(Asset)正在预处理,无法使用。 - -* Failed:素材资产(Asset)处理失败。 - -*** - -Items.**Error** `object` - -错误信息。 - -*** - -Items.Error.**Code** `string` - -错误码。 - -*** - -Items.Error.**Message** `string` - -错误信息。 - -*** - -Items.**ProjectName** `string` - -资源所属的项目名称。 - -*** - -Items.**CreateTime **`string` - -创建时间。 - -*** - -Items.**UpdateTime **`string` - -更新时间。 - -*** - -### **TotalCount **`int (i64)` - -返回总数。 - -*** - -### **PageNumber **`int (i64)` - -返回的页数。 - -*** - -### **PageSize **`int (i64)` - -每页搜索结果的数量,上限为100。 - - - -# ListAssetGroups - -> **POST **/open/ListAssetGroups - -查询符合筛选条件的Asset Groups(素材资产组合)列表。 - -## 请求参数 - -### **Filter** `object` `必填` - -搜索的过滤条件。 - -*** - -Filter.**name** `string` - -Asset Group(素材资产组合)的名称,上限为64个字符。 - -*** - -Filter.**GroupIds** `array` - -Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 - -*** - -Filter.**GroupType** `string`** **`必填` - -Asset Group(素材资产组合)的类型。可选值: - -* AIGC:虚拟人像。 - -*** - -### **PageNumber** `int (i64)` - -搜索页码,可用于列表分页功能,从 1 开始。例如:"page\_number": 1,即返回第一页的搜索结果。 - -*** - -### **PageSize** `int (i64)` - -每页搜索结果的数量,上限为100。 - -*** - -### **SortBy** `string` - -用于排序的字段名称,默认值 `createTime`。支持以下类型: - -* CreateTime:根据创建时间排序。 - -* UpdateTime:根据更新时间排序。 - -*** - -### **SortOrder** `string` - -排序顺序,默认值 `Desc`。可选值: - -* Desc:降序 - -* Asc:升序 - -*** - -### **ProjectName** `string` - -资源所属的项目名称,默认值为`default`。 - -若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 - -*** - - - -## 返回参数 - -### **TotalCount **`int (i64)` - -返回的 Asset Group(素材资产组合)的总数。 - -*** - -### **Items** `array[]` - -符合筛选条件的 Asset Group(素材资产组合)数组。 - -*** - -Items.**Id** `string` - -Asset Group(素材资产组合)的 Id。 - -*** - -Items.**Name** `string` - -Asset Group(素材资产组合)的名称,上限为64个字符。 - -*** - -Items.**Title** `string` - -Asset Group(素材资产组合)的标题。 - -> 即将下线,请直接使用参数 Name - -*** - -Items.**Description** `string` - -Asset Group(素材资产组合)的描述,上限为 300 字符。 - -*** - -Items.**GroupType** `string` - -Asset Group(素材资产组合)的类型。 - -* AIGC:虚拟人像。 - -*** - -Items.**ProjectName** `string` - -资源所属的项目名称。 - -*** - -Items.**CreateTime** `string` - -创建时间。 - -*** - -Items.**UpdateTime** `string` - -更新时间。 - -*** - -### **PageNumber **`int (i64)` - -返回的页数。 - -*** - -### **PageSize **`int (i64)` - -每页搜索结果的数量,上限为100。 - -*** - - - -# GetAssetGroup - -> **POST **/open/GetAssetGroup - -获取单个Asset Group(素材资产组合)信息。 - -## 请求参数 - - - -### **Id **`string` `必填` - -Asset Group(素材资产组合)的 Id - -*** - -### **ProjectName **`string` - -需要查询的 Asset Group(素材资产组合)所属的项目名称,默认值为`default`。 - -若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 - -*** - - - -## 返回参数 - -### **Id** `string` - -Asset Group(素材资产组合)的 Id。 - -*** - -### **Name** `string` - -Asset Group(素材资产组合)的名称,上限为64个字符。 - -*** - -### **Title** `string` - -Asset Group(素材资产组合)的标题。 - -> 即将下线,请直接使用参数 Name - -*** - -### **Description** `string` - -Asset Group(素材资产组合)的描述,上限为 300 字符。 - -*** - -### **GroupType **`string` - -Asset Group(素材资产组合)的类型。 - -* AIGC:虚拟人像 - -*** - -### **ProjectName **`string`** ** - -资源所属的项目名称。 - -*** - -### **CreateTime **`string` - -创建时间。 - -*** - -### **UpdateTime **`string` - -更新时间。 - -*** - -## - -# GetAsset - -> **POST **/open/GetAsset - -获取单个Asset(素材资产)信息。 - -## 请求参数 - - - -### **Id **`string` `必填` - -Asset(素材资产)的 Id。 - -*** - -### **ProjectName **`string` - -需要查询的 Asset(素材资产)所属的项目名称,默认值为`default`。 - -若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 - -*** - - - -## 返回参数 - -### **Id **`string` - -Asset(素材资产)的 Id。 - -*** - -### **Name** `string` - -Asset(素材资产)的名称,上限为64个字符。 - -*** - -### **URL **`string` - -Asset(素材资产)的访问地址。 - -*** - -### **AssetType **`string` - -Asset(素材资产)的类型。 - -* Image:Asset(素材资产)的类型为图像。 - -*** - -### **GroupId** `string` - -Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 - -*** - -### **Status** `string` - -任务状态。 - -* Active:素材资产(Asset)已处理完毕,可以使用。 - -* Processing:素材资产(Asset)正在预处理,无法使用。 - -* Failed:素材资产(Asset)处理失败。 - -*** - -### **Error** `object` - -错误信息。 - -*** - -Error.**Code** `string` - -错误码。 - -*** - -Error.**Message** `string` - -错误信息。 - -*** - -### **CreateTime **`string` - -创建时间。 - -*** - -### **UpdateTime ** `string` - -更新时间。 - -*** - -### **ProjectName** `string` - -资源所属的项目名称。 - -*** - -## - -# **UpdateAssetGroup** - -> **POST **/open/UpdateAssetGroup - -更新单个 Asset Group(素材资产组合)信息。当前仅支持更新 Asset Group(素材资产组合)的 Name 和 Description。 - -## 请求参数 - -### **Id **`string` `必填` - -需要更新的 Asset Group(素材资产组合)的 Id - -*** - -### **Name **`string` - -需要更新的 Asset Group(素材资产组合)的新名称,上限为64个字符。 - -*** - -### **Description** `string` - -需要更新的 Asset Group(素材资产组合)的新描述,上限为 300 字符。 - -*** - -### **ProjectName** `string` - -需要更新的 Asset Group(素材资产组合)所属的项目名称,默认值为`default`。 - -若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 - -*** - - - -## 返回参数 - -### **Id** `string` - -Asset Group(素材资产组合)的 Id。 - -*** - - - - - -# **UpdateAsset** - -> **POST **/open/UpdateAsset - -更新单个Asset(素材资产)信息。当前仅支持更新Asset(素材资产)的 Name。 - -## 请求参数 - -### **Id **`string` `必填` - -需要更新的 Asset(素材资产)的 Id - -*** - -### **Name **`string` - -需要更新的 Asset(素材资产)的新名称,上限为64个字符。 - -*** - -### **ProjectName** `string` - -需要更新的 Asset(素材资产)所属的项目名称,默认值为`default`。 - -若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 - -*** - - - -## 返回参数 - -### **Id** `string` - -Asset(素材资产)的 Id。 - -*** - - - -# 代码示例: - -> **以下示例为使用 Asset API 创建素材资产并用于视频生成的使用链路:** -> -> 1. **创建素材资产组合:**调用 **CreateAssetGroup** 接口创建一个素材资产组合(Asset Group),用于对同一项目或人物的素材进行统一管理。首次创建时需在控制台签署授权函。 -> -> 2. **上传素材资产并等待预处理完成:**调用 **CreateAsset** 接口上传图片素材,传入图片的公共访问URL及所属的Group ID,获得素材资产ID(Asset ID)。 -> 由于上传的素材资产需经过预处理后才能使用,可轮询调用 **GetAsset** 接口查询素材状态,直至状态变为 `Active`。若状态为 `Failed` 则表示处理失败。 -> -> 3. **在视频生成 API 中使用素材:**当素材资产状态为 `Active` 后,将素材ID按 `Asset://` 的格式拼接成URL,在视频生成API(如Seedance 2.0系列模型)的请求中,将该URL作为参考图像的 `image_url` 传入,即可使用该素材资产生成视频。 -> -> **API 鉴权方式区别说明** -> -> * **Asset API:**Access Key 鉴权,详情参考 [获取 API 访问密钥(AK/SK)](https://www.volcengine.com/docs/6257/64983?lang=zh)。 -> -> * **视频生成 API:**API Key 鉴权,详情参考 [管理 API Key](https://www.volcengine.com/docs/82379/1361424?lang=zh)。 -> -> **素材库[项目](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)(Project)隔离说明** -> -> * 向指定的 Asset Group(素材资产组合)内创建或查询 Asset(素材资产)时,需保证两者的 **ProjectName **一致 -> -> * Asset(素材资产)所属的 **ProjectName** 需与调用视频生成 API 接口时使用的 API key 所属的 **ProjectName** 一致 - -## 1. 创建素材资产组合 - -```go -package main - -import ( - "fmt" - - "github.com/bytedance/sonic" - "github.com/volcengine/volcengine-go-sdk/volcengine" - "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" - "github.com/volcengine/volcengine-go-sdk/volcengine/session" - "github.com/volcengine/volcengine-go-sdk/volcengine/universal" -) - -func main() { - config := volcengine.NewConfig().WithCredentials(credentials.NewStaticCredentials("", "", "")).WithRegion("cn-beijing") - sess, _ := session.NewSession(config) - resp, err := universal.New(sess).DoCall( - universal.RequestUniversal{ - ServiceName: "ark", - Action: "CreateAssetGroup", - Version: "2024-01-01", - HttpMethod: universal.POST, - ContentType: universal.ApplicationJSON, - }, - //根据实际情况填写 - &map[string]any{ - "Name": "test", - "Description": "test", - "GroupType": "AIGC", - }, - ) - if err != nil { - return - } - if resp == nil { - return - } - respData, err := sonic.Marshal(resp) - fmt.Println(string(respData)) -} -``` - -返回示例 - -```json -{"ResponseMetadata":{"RequestId":"20260318155041036F7CB6362358FB40FC","Action":"CreateAssetGroup","Version":"2024-01-01","Service":"ark","Region":"cn-beijing"},"Result":{"Id":"group-2026**********-*****"}} -``` - - - -## 2. 上传素材资产并获取素材资产信息 - -```go -package main - -import ( - "errors" - "fmt" - "time" - - "github.com/bytedance/sonic" - "github.com/volcengine/volcengine-go-sdk/volcengine" - "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" - "github.com/volcengine/volcengine-go-sdk/volcengine/session" - "github.com/volcengine/volcengine-go-sdk/volcengine/universal" -) - -const ( - region = "cn-beijing" - serviceName = "ark" - version = "2024-01-01" - - // 轮询配置 - pollInterval = 3 * time.Second - pollTimeout = 2 * time.Minute -) - -func main() { - // TODO: 替换为你的 AK / SK - ak := "" - sk := "" - - // TODO: 替换为你的实际参数 - groupID := "" - assetURL := "" - assetType := "Image" - projectName := "Default" - - config := volcengine.NewConfig(). - WithCredentials(credentials.NewStaticCredentials(ak, sk, "")). - WithRegion(region) - - sess, err := session.NewSession(config) - if err != nil { - fmt.Printf("create session failed: %v\n", err) - return - } - - client := universal.New(sess) - - // 1. 创建素材资产 - assetID, err := createAsset(client, groupID, assetURL, assetType, projectName) - if err != nil { - fmt.Printf("create asset failed: %v\n", err) - return - } - - fmt.Printf("asset created, AssetId = %s\n", assetID) - - // 2. 查询素材资产状态 - finalURL, err := waitForAssetActive(client, assetID, pollInterval, pollTimeout) - if err != nil { - fmt.Printf("poll asset failed: %v\n", err) - return - } - - fmt.Printf("asset is active, URL = %s\n", finalURL) -} - -// createAsset 调用 CreateAsset 并返回 AssetId -func createAsset(client *universal.Universal, groupID, url, assetType, projectName string) (string, error) { - resp, err := client.DoCall( - universal.RequestUniversal{ - ServiceName: serviceName, - Action: "CreateAsset", - Version: version, - HttpMethod: universal.POST, - ContentType: universal.ApplicationJSON, - }, - &map[string]any{ - "GroupId": groupID, - "URL": url, - "AssetType": assetType, - "ProjectName": projectName, - }, - ) - if err != nil { - return "", err - } - if resp == nil { - return "", errors.New("create asset response is nil") - } - - // 打印原始返回,便于排查 - respData, _ := sonic.Marshal(resp) - fmt.Printf("CreateAsset response: %s\n", string(respData)) - - assetID := extractString(resp, "Result", "Id") - if assetID == "" { - assetID = extractString(resp, "Result", "AssetId") - } - if assetID == "" { - assetID = extractString(resp, "Id") - } - if assetID == "" { - assetID = extractString(resp, "AssetId") - } - - if assetID == "" { - return "", fmt.Errorf("cannot find AssetId in response: %s", string(respData)) - } - - return assetID, nil -} - -// waitForAssetActive 查询 GetAsset,直到 Active / Failed / 超时 -func waitForAssetActive(client *universal.Universal, assetID string, interval, timeout time.Duration) (string, error) { - deadline := time.Now().Add(timeout) - - for { - if time.Now().After(deadline) { - return "", fmt.Errorf("polling timeout after %v, assetID=%s", timeout, assetID) - } - - status, url, errMsg, err := getAssetStatus(client, assetID) - if err != nil { - return "", err - } - - fmt.Printf("asset status: %s\n", status) - - switch status { - case "Processing": - time.Sleep(interval) - continue - case "Active": - if url == "" { - return "", fmt.Errorf("asset is Active but URL is empty, assetID=%s", assetID) - } - return url, nil - case "Failed": - if errMsg == "" { - errMsg = "unknown asset processing error" - } - return "", fmt.Errorf("asset processing failed: %s", errMsg) - default: - // 若返回其他状态,保守处理为继续查询 - fmt.Printf("unexpected status %q, continue polling...\n", status) - time.Sleep(interval) - } - } -} - -// getAssetStatus 调用 GetAsset,返回 Status / URL / Error -func getAssetStatus(client *universal.Universal, assetID string) (status, url, errMsg string, err error) { - resp, err := client.DoCall( - universal.RequestUniversal{ - ServiceName: serviceName, - Action: "GetAsset", - Version: version, - HttpMethod: universal.POST, - ContentType: universal.ApplicationJSON, - }, - &map[string]any{ - "Id": assetID, - }, - ) - if err != nil { - return "", "", "", err - } - if resp == nil { - return "", "", "", errors.New("get asset response is nil") - } - - // 打印原始返回,便于排查 - respData, _ := sonic.Marshal(resp) - fmt.Printf("GetAsset response: %s\n", string(respData)) - - // 兼容不同层级的字段位置 - status = extractString(resp, "Result", "Status") - if status == "" { - status = extractString(resp, "Status") - } - - url = extractString(resp, "Result", "URL") - if url == "" { - url = extractString(resp, "URL") - } - - errMsg = extractString(resp, "Result", "Error") - if errMsg == "" { - errMsg = extractString(resp, "Error") - } - - return status, url, errMsg, nil -} - -// extractString 从响应中按层级安全提取字符串 -func extractString(data any, keys ...string) string { - current := data - - for _, key := range keys { - switch v := current.(type) { - case map[string]any: - next, ok := v[key] - if !ok { - return "" - } - current = next - - case *map[string]any: - if v == nil { - return "" - } - next, ok := (*v)[key] - if !ok { - return "" - } - current = next - - default: - return "" - } - } - - switch v := current.(type) { - case string: - return v - case fmt.Stringer: - return v.String() - case nil: - return "" - default: - return fmt.Sprintf("%v", v) - } -} -``` - -返回示例 - -```json -CreateAsset response: {"ResponseMetadata":{"RequestId":"202603181520431F067112A17FC078A6DF","Action":"CreateAsset","Version":"2024-01-01","Service":"ark","Region":"cn-beijing"},"Result":{"Id":"Asset-2026**********-*****"}} -asset created, AssetId = asset-20260318072044-n8bcl -GetAsset response: {"ResponseMetadata":{"Service":"ark","Region":"cn-beijing","RequestId":"202603181520448A995106924553F77D0E","Action":"GetAsset","Version":"2024-01-01"},"Result":{"Name":"","GroupId":"group-2026**********-*****","CreateTime":"2026-03-18T07:20:44Z","ProjectName":"default","Id":"Asset-2026**********-*****","URL":"","AssetType":"Image","Status":"Processing","UpdateTime":"2026-03-18T07:20:44Z"}} -asset status: Processing -GetAsset response: {"Result":{"UpdateTime":"2026-03-18T07:20:47Z","ProjectName":"default","Id":"Asset-2026**********-*****","CreateTime":"2026-03-18T07:20:44Z","Name":"","URL":"","AssetType":"Image","GroupId":"group-2026**********-*****","Status":"Processing"},"ResponseMetadata":{"Version":"2024-01-01","Service":"ark","Region":"cn-beijing","RequestId":"2026031815204766FE2BA543E6FF666F66","Action":"GetAsset"}} -asset status: Processing -GetAsset response: {"ResponseMetadata":{"Version":"2024-01-01","Service":"ark","Region":"cn-beijing","RequestId":"202603181520511F067112A17FC078A75A","Action":"GetAsset"},"Result":{"Name":"","URL":"https://ark-media-asset-stg.tos-cn-beijing.volces.com/xxxx","AssetType":"Image","Status":"Active","Id":"Asset-2026**********-*****","GroupId":"group-2026**********-*****","CreateTime":"2026-03-18T07:20:44Z","UpdateTime":"2026-03-18T07:20:47Z","ProjectName":"default"}} -asset status: Active -asset is active, URL = https://ark-media-asset-stg.tos-cn-beijing.volces.com/xxxx -``` - - - -更多语言的示例代码详见: - -> 注意替换 Demo 中的 AK与SK,若需调用其他接口如 ListAssets,需替换 ACTION 与对应请求参数。 - -| **Python** | 创建素材资产组合: 上传素材资产并获取素材资产信息: | -| ---------- | --------------------------- | -| **Java** | 创建素材资产组合: 上传素材资产并获取素材资产信息: | -| **PHP** | 创建素材资产组合: 上传素材资产: | - - - -## 3. 素材资产用于视频生成 - -当上传的素材资产状态为 `Active` 时,可将素材 Id 按 `Asset: //` 的规则拼接 URL,以在 **视频生成 API **中使用对应的素材资产生成视频: - -```json - { - "type": "image_url", - "image_url": { - "url": "Asset://Asset-2026**********-*****" - }, - "role": "reference_image" - }, -``` - -使用素材资产生成视频的具体调用方式请参考[ 【申请权限填客户名称】Seedance 2.0 & 2.0 fast API文档(邀测用户版)](https://bytedance.larkoffice.com/wiki/SANpwJ9bgiKgrykLaMTcAB0InWc#share-ONSwd51ezoXCJqxkAm2cIC61nMX)。 - diff --git a/docs/API文档/seedance 2.0 系列教程.md b/docs/API文档/seedance 2.0 系列教程.md new file mode 100644 index 0000000..1b9e418 --- /dev/null +++ b/docs/API文档/seedance 2.0 系列教程.md @@ -0,0 +1,2233 @@ +seedance 2.0 系列模型(包括 seedance 2.0 和 seedance 2.0 fast )支持图像、视频、音频、文本等多种模态内容输入,具备视频生成、视频编辑、视频延长等能力,可高精度还原物品细节、音色、效果、风格、运镜等,保持稳定角色特征,赋予使用者如同导演般的掌控权。本文介绍 seedance 2.0 系列模型的专属能力,帮助您快速实现 [Video Generation API](https://www.volcengine.com/docs/82379/1520758) 调用。 +:::tip +请确保您的账户余额大于等于 200 元([前往充值](https://console.volcengine.com/finance/fund/recharge)),或已[购买资源包](https://console.volcengine.com/common-buy/fast/ark_bd%7C%7Cd682ppeeq1mp7kd5q0e0),否则无法开通 seedance 2.0 及 seedance 2.0 fast 模型。 +::: + +# 新手入门 +本入门教程专为 **API 新手用户** 设计,帮助您一键搭建 Python 开发环境、完成虚拟环境创建和方舟 SDK 安装,并提供直接可运行的 seedance 2.0 示例代码,您只需修改对应的输入素材,即可开始您的视频生成创作。 +**1. 准备工作** +在开始之前,请确保您已经完成以下准备: + +1. **注册账号**:确保您拥有火山引擎账号并已[登录](https://console.volcengine.com/)。 +2. **获取 API Key**:访问 [API Key 管理页面](https://console.volcengine.com/ark/region:ark+cn-beijing/apikey),点击 **创建 API Key**,并复制保存您的 API Key。注意请妥善保管您的 API Key,不要泄露给他人。 +3. [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&advancedActiveKey=model&projectName=default&tab=ComputerVision):请确保您的账户余额大于等于 200 元,否则无法开通 seedance 2.0 及 seedance 2.0 fast 模型。 +4. **下载并解压文件**:点击下载下方附件,将其解压到您的本地目录(如桌面或“下载”文件夹)。 + + +**2.操作步骤** + +```mixin-react +return ( + +); +``` + +**3.运行说明** +运行脚本后,您将看到如下流程: + +1. **API Key 校验**:脚本会自动检测您本地是否配置了`ARK_API_KEY`环境变量。如果没有,会提示您手动输入。 +2. **素材预览**:脚本会自动在您的默认浏览器中弹出一个本地生成的 HTML 页面,直观地展示本次任务的文本提示词、待替换的参考图片以及原始参考视频。 +3. **任务创建与轮询**:脚本向火山方舟服务器发起异步请求。由于视频生成需要一定时间,控制台会每隔 30 秒打印一次任务状态(如 `running`等)。 +4. **获取结果**:任务成功后,控制台会输出一段最终生成的视频 URL。您可以复制该链接到浏览器下载或在线播放。 + +**4.下一步** +在成功跑通本示例后,您可以尝试修改 `python/demo_standard.py`,来打造您专属的视频生成任务: + +1. 修改文本提示词 + +找到代码中的 `user_content` 变量,更改为您想要的画面描述。 + +2. 替换输入素材 (图片、视频、音频) + +您可以将 `reference_image_url`、`reference_video_url` 和 `reference_audio_url` 替换为您自己的素材链接。 +**注意**:请确保 URL 是公网可公开访问的链接(建议存放在 TOS 对象存储服务中,并配置为公共读)。 + +3. 继续学习下文中丰富的使用示例。 + + +# 模型能力 +seedance 2.0 fast 和 seedance 2.0 的模型能力相同。追求最高生成品质,推荐使用 seedance 2.0;更注重成本与生成速度,不要求极限品质,推荐使用 seedance 2.0 fast。 + + +|模型名称 | |[seedance 2.0](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-2-0&projectName=default) |[seedance 2.0 fast](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-2-0-fast&projectName=default) | +|---|---|---|---| +|Model ID | |doubao\-seedance\-2\-0\-260128 |doubao\-seedance\-2\-0\-fast\-260128 | +|文生视频 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|图生视频\-首帧 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|图生视频\-首尾帧 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|多模态参考【New】 |图片参考 |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|^^|视频参考 |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|^^|组合参考|![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |\ +| || | |\ +| |* 图片 + 音频| | |\ +| |* 图片 + 视频| | |\ +| |* 视频 + 音频| | |\ +| |* 图片 + 视频 + 音频 | | | +|编辑视频【New】 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|延长视频【New】 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|生成有声视频 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|联网搜索增强【New】 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|样片模式 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/f359753773c94d97885008ca1223c9bc~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/f359753773c94d97885008ca1223c9bc~tplv-goo7wpa0wc-image.image =20x) | +|返回视频尾帧 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/ee51ce32c1914aed81ff95080bb7db1d~tplv-goo7wpa0wc-image.image =20x) | +|输出视频规格 |输出分辨率 |480p, 720p |480p, 720p | +| |输出宽高比 |21:9, 16:9, 4:3, 1:1, 3:4, 9:16 || +| |输出时长 |4~15 秒 |4~15 秒 | +| |输出视频格式 |mp4 |mp4 | +|离线推理 | |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/f359753773c94d97885008ca1223c9bc~tplv-goo7wpa0wc-image.image =20x) |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/f359753773c94d97885008ca1223c9bc~tplv-goo7wpa0wc-image.image =20x) | +|在线推理限流 |最大 RPM |企业用户:600|企业用户:600|\ +| | |个人用户:180 |个人用户:180 | +| |最大并发数 |企业用户:10|企业用户:10|\ +| | |个人用户:3 |个人用户:3 | +|离线推理限流 |TPD |\- |\- | + + +# 基础使用 + +## 多模态参考 +输入文本、参考图、视频(可带音轨)和音频等内容,来生成一段新视频。可继承参考图片的角色形象、视觉风格、画面构图;参考视频的主体内容、运镜方式、动作表现、整体风格;以及参考音频的音色、音乐旋律、对话内容等核心信息。 +效果预览如下(访问[模型卡片](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-2-0)查看更多示例): + + +|输入:文本 |输入:图片、视频、音频 |输出 | +|---|---|---| +|全程使用**视频1**的第一视角构图,全程使用**音频1**作为背景音乐。第一人称视角果茶宣传广告,seedance牌「苹苹安安」苹果果茶限定款;首帧为**图片1**,你的手摘下一颗带晨露的阿克苏红苹果,轻脆的苹果碰撞声;2\-4 秒:快速切镜,你的手将苹果块投入雪克杯,加入冰块与茶底,用力摇晃,冰块碰撞声与摇晃声卡点轻快鼓点,背景音:「鲜切现摇」;4\-6 秒:第一人称成品特写,分层果茶倒入透明杯,你的手轻挤奶盖在顶部铺展,在杯身贴上粉红包标,镜头拉近看奶盖与果茶的分层纹理;6\-8 秒:第一人称手持举杯,你将**图片2**中的果茶举到镜头前(模拟递到观众面前的视角),杯身标签清晰可见,背景音「来一口鲜爽」,尾帧定格为**图片2**。背景声音统一为女生音色。 |||\ +| || |\ +| | | | + + +```mixin-react +return ( + + contents = new ArrayList<>(); + + // 1. Text prompt + contents.add(Content.builder() + .type("text") + .text(prompt) + .build()); + + // 2. Reference image 1 + contents.add(Content.builder() + .type("image_url") + .imageUrl(CreateContentGenerationTaskRequest.ImageUrl.builder() + .url(refImage1) + .build()) + .role("reference_image") + .build()); + + // 3. Reference image 2 + contents.add(Content.builder() + .type("image_url") + .imageUrl(CreateContentGenerationTaskRequest.ImageUrl.builder() + .url(refImage2) + .build()) + .role("reference_image") + .build()); + + // 4. Reference video + contents.add(Content.builder() + .type("video_url") + .videoUrl(CreateContentGenerationTaskRequest.VideoUrl.builder() + .url(refVideo) + .build()) + .role("reference_video") + .build()); + + // 5. Reference audio + contents.add(Content.builder() + .type("audio_url") + .audioUrl(CreateContentGenerationTaskRequest.AudioUrl.builder() + .url(refAudio) + .build()) + .role("reference_audio") + .build()); + + // Create video generation task + CreateContentGenerationTaskRequest createRequest = CreateContentGenerationTaskRequest.builder() + .generateAudio(generateAudio) + .model(modelId) + .content(contents) + .ratio(videoRatio) + .duration(videoDuration) + .watermark(showWatermark) + .build(); + + CreateContentGenerationTaskResult createResult = service.createContentGenerationTask(createRequest); + System.out.println("Task Created: " + createResult); + + // Get task details and poll status + String taskId = createResult.getId(); + pollTaskStatus(taskId); + \} + + /** + * Poll task status + * @param taskId Task ID + */ + + private static void pollTaskStatus(String taskId) \{ + GetContentGenerationTaskRequest getRequest = GetContentGenerationTaskRequest.builder() + .taskId(taskId) + .build(); + + System.out.println("----- polling task status -----"); + try \{ + while (true) \{ + GetContentGenerationTaskResponse getResponse = service.getContentGenerationTask(getRequest); + String status = getResponse.getStatus(); + + if ("succeeded".equalsIgnoreCase(status)) \{ + System.out.println("----- task succeeded -----"); + System.out.println(getResponse); + break; + \} else if ("failed".equalsIgnoreCase(status)) \{ + System.out.println("----- task failed -----"); + if (getResponse.getError() != null) \{ + System.out.println("Error: " + getResponse.getError().getMessage()); + \} + break; + \} else \{ + System.out.printf("Current status: %s, Retrying in 10 seconds...%n", status); + TimeUnit.SECONDS.sleep(10); + \} + \} + \} catch (InterruptedException ie) \{ + Thread.currentThread().interrupt(); + System.err.println("Polling interrupted"); + \} catch (Exception e) \{ + System.err.println("Error occurred: " + e.getMessage()); + \} finally \{ + service.shutdownExecutor(); + \} + \} +\} +\`\`\` + +`}> +); +``` + +:::tip + +* 您可任意组合以下模态内容,注意不支持“文本+音频”、“纯音频” 输入。 + * 文本 + * 图片:0~9 张 + * 视频:0~3 个 + * 音频:0~3 个 +* **进阶用法**:多模态生视频可通过提示词指定参考图片作为首帧/尾帧,间接实现“首尾帧+多模态参考”效果。若需严格保障首尾帧和指定图片一致,**优先使用图生视频\-首尾帧**(配置 role 为 first_frame/last_frame)。 +* 各个模态信息输入要求参见[多模态输入](/docs/82379/1366799#63a97f09)。 + +::: + +## 编辑视频 +您可以提供待编辑的视频、参考图片或音频,并结合使用提示词,完成多种视频编辑任务,例如:替换视频主体、视频中对象增删改、局部画面重绘/修复等。 +效果预览如下(访问[模型卡片](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-2-0)查看更多示例): + + +|输入:文本 |输入:视频&图片 |输出 | +|---|---|---| +|将**视频1**礼盒中的香水替换成**图像1**中的面霜,运镜不变 |||\ +| || |\ +| |![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/791b783fc6cd4394b13f41b66b5ff461~tplv-goo7wpa0wc-image.image =280x) | | + + +```mixin-react +return ( + + contents = new ArrayList<>(); + + // 1. Text prompt + contents.add(Content.builder() + .type("text") + .text(prompt) + .build()); + + // 2. Reference image 1 + contents.add(Content.builder() + .type("image_url") + .imageUrl(CreateContentGenerationTaskRequest.ImageUrl.builder() + .url(refImage1) + .build()) + .role("reference_image") + .build()); + + // 3. Reference video + contents.add(Content.builder() + .type("video_url") + .videoUrl(CreateContentGenerationTaskRequest.VideoUrl.builder() + .url(refVideo) + .build()) + .role("reference_video") + .build()); + + // Create video generation task + CreateContentGenerationTaskRequest createRequest = CreateContentGenerationTaskRequest.builder() + .generateAudio(generateAudio) + .model(modelId) + .content(contents) + .ratio(videoRatio) + .duration(videoDuration) + .watermark(showWatermark) + .build(); + + CreateContentGenerationTaskResult createResult = service.createContentGenerationTask(createRequest); + System.out.println("Task Created: " + createResult); + + // Get task details and poll status + String taskId = createResult.getId(); + pollTaskStatus(taskId); + \} + + /** + * Poll task status + * @param taskId Task ID + */ + + private static void pollTaskStatus(String taskId) \{ + GetContentGenerationTaskRequest getRequest = GetContentGenerationTaskRequest.builder() + .taskId(taskId) + .build(); + + System.out.println("----- polling task status -----"); + try \{ + while (true) \{ + GetContentGenerationTaskResponse getResponse = service.getContentGenerationTask(getRequest); + String status = getResponse.getStatus(); + + if ("succeeded".equalsIgnoreCase(status)) \{ + System.out.println("----- task succeeded -----"); + System.out.println(getResponse); + break; + \} else if ("failed".equalsIgnoreCase(status)) \{ + System.out.println("----- task failed -----"); + if (getResponse.getError() != null) \{ + System.out.println("Error: " + getResponse.getError().getMessage()); + \} + break; + \} else \{ + System.out.printf("Current status: %s, Retrying in 10 seconds...%n", status); + TimeUnit.SECONDS.sleep(10); + \} + \} + \} catch (InterruptedException ie) \{ + Thread.currentThread().interrupt(); + System.err.println("Polling interrupted"); + \} catch (Exception e) \{ + System.err.println("Error occurred: " + e.getMessage()); + \} finally \{ + service.shutdownExecutor(); + \} + \} +\} +\`\`\` + +`}> +); +``` + + +## 延长视频 +在原有视频基础上,向前或者向后延长视频,或多个视频片段(最多 3 个视频片段)串联成一个连贯视频。 +效果预览如下(访问[模型卡片](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedance-2-0)查看更多示例): + + +|输入:文本 |输入:待延长视频 |输出 | +|---|---|---| +|**视频1**中的拱形窗户打开,进入美术馆室内,接**视频2**,之后镜头进入画内,接**视频3** |||\ +| || |\ +| || |\ +| || |\ +| || |\ +| | | | + + +```mixin-react +return ( + + contents = new ArrayList<>(); + + // 1. Text prompt + contents.add(Content.builder() + .type("text") + .text(prompt) + .build()); + + // 2. Reference video 1 + contents.add(Content.builder() + .type("video_url") + .videoUrl(CreateContentGenerationTaskRequest.VideoUrl.builder() + .url(refVideo1) + .build()) + .role("reference_video") + .build()); + + // 3. Reference video 2 + contents.add(Content.builder() + .type("video_url") + .videoUrl(CreateContentGenerationTaskRequest.VideoUrl.builder() + .url(refVideo2) + .build()) + .role("reference_video") + .build()); + + // 4. Reference video 3 + contents.add(Content.builder() + .type("video_url") + .videoUrl(CreateContentGenerationTaskRequest.VideoUrl.builder() + .url(refVideo3) + .build()) + .role("reference_video") + .build()); + + // Create video generation task + CreateContentGenerationTaskRequest createRequest = CreateContentGenerationTaskRequest.builder() + .generateAudio(generateAudio) + .model(modelId) + .content(contents) + .ratio(videoRatio) + .duration(videoDuration) + .watermark(showWatermark) + .build(); + + CreateContentGenerationTaskResult createResult = service.createContentGenerationTask(createRequest); + System.out.println("Task Created: " + createResult); + + // Get task details and poll status + String taskId = createResult.getId(); + pollTaskStatus(taskId); + \} + + /** + * Poll task status + * @param taskId Task ID + */ + + private static void pollTaskStatus(String taskId) \{ + GetContentGenerationTaskRequest getRequest = GetContentGenerationTaskRequest.builder() + .taskId(taskId) + .build(); + + System.out.println("----- polling task status -----"); + try \{ + while (true) \{ + GetContentGenerationTaskResponse getResponse = service.getContentGenerationTask(getRequest); + String status = getResponse.getStatus(); + + if ("succeeded".equalsIgnoreCase(status)) \{ + System.out.println("----- task succeeded -----"); + System.out.println(getResponse); + break; + \} else if ("failed".equalsIgnoreCase(status)) \{ + System.out.println("----- task failed -----"); + if (getResponse.getError() != null) \{ + System.out.println("Error: " + getResponse.getError().getMessage()); + \} + break; + \} else \{ + System.out.printf("Current status: %s, Retrying in 10 seconds...%n", status); + TimeUnit.SECONDS.sleep(10); + \} + \} + \} catch (InterruptedException ie) \{ + Thread.currentThread().interrupt(); + System.err.println("Polling interrupted"); + \} catch (Exception e) \{ + System.err.println("Error occurred: " + e.getMessage()); + \} finally \{ + service.shutdownExecutor(); + \} + \} +\} +\`\`\` + +`}> +); +``` + +:::tip + +* 向前或向后延长 1 段视频,生成的视频一般只包含原视频的尾部画面。但您也可以通过提示词灵活控制,使其包含原视频内容。 例如:向前延长视频1,[延长内容描述...],**最后接视频1**。 +* 传入 2~3 段视频,补全中间过渡部分,生成的视频会包含原视频内容和新生成的视频内容。 + +::: + +## 使用联网搜索 +> 联网搜索能力仅适用于纯文本输入 + +seedance 2.0 新增支持调用联网搜索工具,通过配置 tools.**type** 参数为 web_search 即可开启联网搜索。 + +* 开启联网搜索后,模型会根据用户的提示词自主判断是否搜索互联网内容(如商品、天气等)。可提升生成视频的时效性,但也会增加一定的时延。 +* 实际搜索次数可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 usage.tool_usage.**web_search** 字段获取,如果为 0 表示未搜索。 + + + +|输入:文本 |输出 | +|---|---| +|微距镜头对准叶片上翠绿的玻璃蛙。焦点逐渐从它光滑的皮肤,转移到它完全透明的腹部,一颗鲜红的心脏正在有力地、规律地收缩扩张。||\ +|:::tip| |\ +|联网搜索玻璃蛙的容貌特征。| |\ +|| |\ +|:::| | + + +```mixin-react +return ( + + contents = new ArrayList<>(); + + // text prompt + contents.add(Content.builder() + .type("text") + .text(prompt) + .build()); + + // Create a video generation task + CreateContentGenerationTaskRequest createRequest = CreateContentGenerationTaskRequest.builder() + .model(modelId) + .content(contents) + .generateAudio(generateAudio) + .ratio(videoRatio) + .duration(videoDuration) + .watermark(showWatermark) + .tools(Collections.singletonList(webSearchTool)) + .build(); + CreateContentGenerationTaskResult createResult = service.createContentGenerationTask(createRequest); + System.out.println(createResult); + // Get the details of the task + String taskId = createResult.getId(); + GetContentGenerationTaskRequest getRequest = GetContentGenerationTaskRequest.builder() + .taskId(taskId) + .build(); + + // Polling query section + System.out.println("----- polling task status -----"); + while (true) \{ + try \{ + GetContentGenerationTaskResponse getResponse = service.getContentGenerationTask(getRequest); + String status = getResponse.getStatus(); + if ("succeeded".equalsIgnoreCase(status)) \{ + System.out.println("----- task succeeded -----"); + System.out.println(getResponse); + break; + \} else if ("failed".equalsIgnoreCase(status)) \{ + System.out.println("----- task failed -----"); + System.out.println("Error: " + getResponse.getStatus()); + break; + \} else \{ + System.out.printf("Current status: %s, Retrying in 10 seconds...", status); + TimeUnit.SECONDS.sleep(10); + \} + \} catch (InterruptedException ie) \{ + Thread.currentThread().interrupt(); + System.err.println("Polling interrupted"); + break; + \} + \} + \} +\} +\`\`\` + +`}> +); +``` + + +## 更多能力 +seedance 2.0 系列模型也支持文生视频、首帧图生视频、首尾帧图生视频、设置视频输出规格等通用基础能力,详情请参见 [视频生成教程-wip](/docs/82379/2298881)。 + +# 便利创作 +seedance 2.0 系列模型不支持直接上传含有真人人脸的参考图/视频。为便利创作者使用肖像,平台推出了以下解决方案。 + + +|方案 |介绍 | +|---|---| +|[使用虚拟人像](/docs/82379/2291680#2bf01416) |平台预置虚拟人像库,为创作者提供免费、合规、丰富多样的肖像素材。适用于需真人风格人脸但无需指定具体人物,追求零合规风险、快速创作的场景。 | +|[使用已授权真人素材](/docs/82379/2291680#f952d0c3) |支持使用已获得授权的真人肖像素材进行视频生成。 | +|[使用模型产物进行二创](/docs/82379/2291680#86c3831f) |本账号下部分模型生成的含人脸原始产物可作为输入素材,再次调用 seedance 2.0 系列模型进行二次创作,不会触发审核拦截。 | + + +## 使用虚拟人像 +对写实风格视频,可通过虚拟人像库预置人像来控制角色样貌。每个素材对应一个独立素材 ID (asset ID), 在 **content.<模态\>_url.url** 字段中传入 `asset://` 即可生成视频。浏览及检索虚拟人像请参见[虚拟人像库](/docs/82379/2223965)。 + + +|输入:文本 |输入:虚拟人像、图片 |输出 | +|---|---|---| +|固定机位,近景镜头,清新自然风格。在室内自然光下,**图片1**中美妆博主面带笑容,向镜头介绍**图片2**中的面霜。博主将手里的面霜展示给镜头,开心地说“挖到本命面霜了!”;接着她一边用手指轻轻蘸取面霜展示那种软糯感,一边说“质地像云朵一样软糯,一抹就吸收”;最后她把面霜涂抹在脸颊上,展示着水润透亮的皮肤,同时自信地说“熬夜急救、补水保湿全搞定”。要求画面中人物居中,完整展示人物的整个脑袋和上半身,始终对焦人脸,人脸始终清晰,纯净无任何字幕。|![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/946509d1f37f476c9ff29e0adaf187eb~tplv-goo7wpa0wc-image.image =200x) ||\ +|:::warning|> 虚拟人像| |\ +|Asset ID 仅用来向模型传入素材,提示词中仍需使用"**素材类型+序号**”格式引用素材,序号为请求体中该素材在同类素材中的排序。|| |\ +|正确用法:**图片1**中美妆博主|![图片](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/791b783fc6cd4394b13f41b66b5ff461~tplv-goo7wpa0wc-image.image =200x) | |\ +|错误用法:asset\-2026\*\*\*\*是美妆博主|> 产品图像 | |\ +|| | |\ +|:::| | | + + +```mixin-react +return ( + + contents = new ArrayList<>(); + + // 1. Text prompt + contents.add(Content.builder() + .type("text") + .text(prompt) + .build()); + + // 2. Reference image 1 + contents.add(Content.builder() + .type("image_url") + .imageUrl(CreateContentGenerationTaskRequest.ImageUrl.builder() + .url(refImage1) + .build()) + .role("reference_image") + .build()); + + // 3. Reference image 2 + contents.add(Content.builder() + .type("image_url") + .imageUrl(CreateContentGenerationTaskRequest.ImageUrl.builder() + .url(refImage2) + .build()) + .role("reference_image") + .build()); + + // Create video generation task + CreateContentGenerationTaskRequest createRequest = CreateContentGenerationTaskRequest.builder() + .generateAudio(generateAudio) + .model(modelId) + .content(contents) + .ratio(videoRatio) + .duration(videoDuration) + .watermark(showWatermark) + .build(); + + CreateContentGenerationTaskResult createResult = service.createContentGenerationTask(createRequest); + System.out.println("Task Created: " + createResult); + + // Get task details and poll status + String taskId = createResult.getId(); + pollTaskStatus(taskId); + \} + + /** + * Poll task status + * @param taskId Task ID + */ + + private static void pollTaskStatus(String taskId) \{ + GetContentGenerationTaskRequest getRequest = GetContentGenerationTaskRequest.builder() + .taskId(taskId) + .build(); + + System.out.println("----- polling task status -----"); + try \{ + while (true) \{ + GetContentGenerationTaskResponse getResponse = service.getContentGenerationTask(getRequest); + String status = getResponse.getStatus(); + + if ("succeeded".equalsIgnoreCase(status)) \{ + System.out.println("----- task succeeded -----"); + System.out.println(getResponse); + break; + \} else if ("failed".equalsIgnoreCase(status)) \{ + System.out.println("----- task failed -----"); + if (getResponse.getError() != null) \{ + System.out.println("Error: " + getResponse.getError().getMessage()); + \} + break; + \} else \{ + System.out.printf("Current status: %s, Retrying in 10 seconds...%n", status); + TimeUnit.SECONDS.sleep(10); + \} + \} + \} catch (InterruptedException ie) \{ + Thread.currentThread().interrupt(); + System.err.println("Polling interrupted"); + \} catch (Exception e) \{ + System.err.println("Error occurred: " + e.getMessage()); + \} finally \{ + service.shutdownExecutor(); + \} + \} +\} +\`\`\` + +`}> +); +``` + + +## 使用已授权真人素材 +通过真人认证和本人授权后,可将该真人的相关素材(例如该真人的图片、视频、音频)上传至方舟。素材入库成功后,每个素材将获得一个独立素材 ID (asset ID), 在 **content.<模态\>_url.url** 字段中传入 `asset://`即可使用该素材生成视频。真人认证及素材入库流程请参见[录入真人形象素材](/docs/82379/2315856)。 +```Shell +... +"content": [ + { + "type": "text", + "text": "" + }, + { + "type": "image_url", + "image_url": { + "url": "asset://" + }, + "role": "reference_image" + }, + { + "type": "video_url", + "video_url": { + "url": "asset://" + }, + "role": "reference_video" + }, + { + "type": "audio_url", + "audio_url": { + "url": "asset://" + }, + "role": "reference_audio" + } + ] +... +``` + +  + +## 使用模型产物进行二创 +seedance 2.0 系列模型不支持直接上传含有真人人脸的参考图/视频。为了便利创作者在含人脸场景的二次创作需求,方舟平台信任以下模型生成的含人脸产物,您可使用**本账号下近30天内由以下模型生成的含人脸原始产物**,作为输入素材,再次调用 seedance 2.0 系列模型进行二次创作。 + +|信任产物范围 |生效时间|有效期|\ +| |> 信任该时间之后|> 从产物生成时间|\ +| |> 生成的产物 |> 开始计算 | +|---|---|---| +|seedance 2.0 及 2.0 fast 生成的含人脸视频 |2026年03月11日起 |30天 | +|seedance 2.0 及 2.0 fast 生成的含人脸视频对应的尾帧图片 |2026年04月16日起 |30天 | +|[Seedream 5.0 lite 文生图](https://www.volcengine.com/docs/82379/1824121?lang=zh#9695d195)得到的含人脸图片 |2026年04月16日起 |30天 | + +:::warning + +* 对于含人脸场景,方舟平台仅信任模型原始产物,二次剪辑或超过有效期后均不可使用。 +* 对于不含人脸场景,模型产物不存在受信问题,支持自由剪辑后进行二次创作。 + + +::: + +|输入:同账号生成的视频 |输出 | +|---|---| +|||\ +|||\ +|> [使用虚拟人像](/docs/82379/2291680#2bf01416)示例生成的视频 |> 输入:将面霜的颜色修改为白色。|\ +| |> ratio 修改为16:9 | + + +```mixin-react +return ( + + contents = new ArrayList<>(); + + // 1. Text prompt + contents.add(Content.builder() + .type("text") + .text(prompt) + .build()); + + // 2. Reference video + contents.add(Content.builder() + .type("video_url") + .videoUrl(CreateContentGenerationTaskRequest.VideoUrl.builder() + .url(refVideo) + .build()) + .role("reference_video") + .build()); + + // Create video generation task + CreateContentGenerationTaskRequest createRequest = CreateContentGenerationTaskRequest.builder() + .generateAudio(generateAudio) + .model(modelId) + .content(contents) + .ratio(videoRatio) + .duration(videoDuration) + .watermark(showWatermark) + .build(); + + CreateContentGenerationTaskResult createResult = service.createContentGenerationTask(createRequest); + System.out.println("Task Created: " + createResult); + + // Get task details and poll status + String taskId = createResult.getId(); + pollTaskStatus(taskId); + \} + + /** + * Poll task status + * @param taskId Task ID + */ + + private static void pollTaskStatus(String taskId) \{ + GetContentGenerationTaskRequest getRequest = GetContentGenerationTaskRequest.builder() + .taskId(taskId) + .build(); + + System.out.println("----- polling task status -----"); + try \{ + while (true) \{ + GetContentGenerationTaskResponse getResponse = service.getContentGenerationTask(getRequest); + String status = getResponse.getStatus(); + + if ("succeeded".equalsIgnoreCase(status)) \{ + System.out.println("----- task succeeded -----"); + System.out.println(getResponse); + break; + \} else if ("failed".equalsIgnoreCase(status)) \{ + System.out.println("----- task failed -----"); + if (getResponse.getError() != null) \{ + System.out.println("Error: " + getResponse.getError().getMessage()); + \} + break; + \} else \{ + System.out.printf("Current status: %s, Retrying in 10 seconds...%n", status); + TimeUnit.SECONDS.sleep(10); + \} + \} + \} catch (InterruptedException ie) \{ + Thread.currentThread().interrupt(); + System.err.println("Polling interrupted"); + \} catch (Exception e) \{ + System.err.println("Error occurred: " + e.getMessage()); + \} finally \{ + service.shutdownExecutor(); + \} + \} +\} +\`\`\` + +`}> +); +``` + + +# 提示词技巧 +提示词中必须使用"**素材类型+序号**”格式引用素材,序号为请求体中该素材在同类素材中的排序。例如 「图片 n」指代`content`数组中第 n 个`type="image_url"`的参考图片(按数组顺序从1开始计数)。**注意不支持使用 Asset ID 指代素材。** +下文介绍多模态参考、编辑视频、延长视频的提示词典型公式,更多详细内容请参见[Seedance 2.0 系列提示词指南](/docs/82379/2222480)。 +:::tip +平台提供 **seedance 2.0 提示词优化技能**,方便您对提示词进行调优。 + +* 配置方式:可将技能文件配置到 Code Agent / AI Agent 中使用。以 OpenClaw 为例,下载该 SKILL.md 文件,复制完整内容至对话输入框中,并发送”请帮我安装这个技能”,等待工具自动完成安装。 +* 使用方式:在 AI 对话框输入 `/sd2-pe + 你的提示词内容`,开始调试提示词。 + + +::: +**多模态参考** + +* 图片参考:参考 / 提取 / 结合 +「图片 n」中的「主体 / 被参考元素描述」,生成「画面描述」,保持「主体 / 被参考元素描述」特征一致。 +* 视频参考:参考「视频 n」的「动作描述 / 运镜描述 / 特效描述」,生成「画面描述」,保持动作细节 / 运镜 / 特效一致。 +* 音频参考: + * 音色参考:「角色」说:“「台词」,音色参考「音频 n」。 + * 音频内容参考:理想出现时机 +「音频 n」。 + +**编辑视频** + +* 增加元素:清晰描述「元素特征」+「出现时机」+「出现位置」 +* 删除元素:点明需要删除的元素,对于保持不变的元素,在提示词中加以强调,表现更佳 +* 修改元素:清晰描述更换元素即可 + +**延长视频** + +* 延长视频:向前/向后延长「视频n」+「需延长的视频描述」 +* 轨道补全:「视频1」+「过渡画面描述」+接「视频2」+「过渡画面描述」+接「视频3」 + + +# 使用限制 +参见[使用限制](/docs/82379/1366799#66cb028f)。 + + diff --git a/docs/API文档/seedance模型价格.md b/docs/API文档/seedance模型价格.md new file mode 100644 index 0000000..4b3dd3c --- /dev/null +++ b/docs/API文档/seedance模型价格.md @@ -0,0 +1,481 @@ +不同模型服务支持的能力及单价各不相同,本文为您介绍各模型的计费公式及单价,方便您进行模型价格查阅和比较。 +:::tip + +* 如需了解计费方式及详细计费逻辑,请参见 [模型服务计费说明](/docs/82379/1544681)。 +* 支持通过 [价格计算器](https://www.volcengine.com/pricing?product=ark_bd&tab=2) **预估** 满足业务需求所需的费用。 +* 本文价格和 [定价详情页](https://www.volcengine.com/pricing?product=ark_bd&tab=1) 仅作为商品规格和价格的参考,具体可购买的商品规格及费用请以实际下单结果为准。 + +::: + +# 大语言模型 + +## 在线推理(常规) + + +|模型名称 |条件|输入|缓存存储|缓存输入|输出|\ +| |千 token |元/百万token |元/百万 token /小时 |元/百万token |元/百万token | +|---|---|---|---|---|---| +|doubao\-seed\-2.0\-pro |输入长度 [0, 32] |3.2 |0.017 |0.64 |16.0 | +|^^|输入长度 (32, 128] |4.8 |0.017 |0.96 |24.0 | +|^^|输入长度 (128, 256] |9.6 |0.017 |1.92 |48.0 | +|doubao\-seed\-2.0\-lite |输入长度 [0, 32] |0.6 |0.017 |0.12 |3.6 | +|^^|输入长度 (32, 128] |0.9 |0.017 |0.18 |5.4 | +|^^|输入长度 (128, 256] |1.8 |0.017 |0.36 |10.8 | +|doubao\-seed\-2.0\-mini |输入长度 [0, 32] |0.2 |0.017 |0.04 |2.0 | +|^^|输入长度 (32, 128] |0.4 |0.017 |0.08 |4.0 | +|^^|输入长度 (128, 256] |0.8 |0.017 |0.16 |8.0 | +|doubao\-seed\-2.0\-code |输入长度 [0, 32] |3.2 |0.017 |0.64 |16.0 | +|^^|输入长度 (32, 128] |4.8 |0.017 |0.96 |24.0 | +|^^|输入长度 (128, 256] |9.6 |0.017 |1.92 |48.0 | +|doubao\-seed\-1.8 |输入长度 [0, 32]|0.80 |0.017 |0.16 |2.00 |\ +| |且输出长度 [0, 0.2] | | | | | +|^^|输入长度 [0, 32]|0.80 |0.017 |0.16 |8.00 |\ +| |且输出长度 (0.2,+∞) | | | | | +|^^|输入长度 (32, 128] |1.20 |0.017 |0.16 |16.00 | +|^^|输入长度 (128, 256] |2.40 |0.017 |0.16 |24.00 | +|doubao\-seed\-character |输入长度 [0, 32] |0.80 |0.017 |0.16 |2.00 | +|^^|输入长度 (32, 128] |1.20 |0.017 |0.16 |6.00 | +|doubao\-seed\-code |输入长度 [0, 32] |1.20 |0.017 |0.24 |8.00 | +|^^|输入长度 (32, 128] |1.40 |0.017 |0.24 |12.00 | +|^^|输入长度 (128, 256] |2.80 |0.017 |0.24 |16.00 | +|doubao\-seed\-1.6 |输入长度 [0, 32]|0.80 |0.017 |0.16 |2.00 |\ +| |且输出长度 [0, 0.2] | | | | | +|^^|输入长度 [0, 32]|0.80 |0.017 |0.16 |8.00 |\ +| |且输出长度 (0.2,+∞) | | | | | +|^^|输入长度 (32, 128] |1.20 |0.017 |0.16 |16.00 | +|^^|输入长度 (128, 256] |2.40 |0.017 |0.16 |24.00 | +|doubao\-seed\-1.6\-lite |输入长度 [0, 32]|0.30 |0.017 |0.06 |0.60 |\ +| |且输出长度 [0, 0.2] | | | | | +|^^|输入长度 [0, 32]|0.30 |0.017 |0.06 |2.40 |\ +| |且输出长度 (0.2,+∞) | | | | | +|^^|输入长度 (32, 128] |0.60 |0.017 |0.06 |4.00 | +|^^|输入长度 (128, 256] |1.20 |0.017 |0.06 |12.00 | +|doubao\-seed\-1.6\-flash |输入长度 [0, 32] |0.15 |0.017 |0.03 |1.50 | +|^^|输入长度 (32, 128] |0.30 |0.017 |0.03 |3.00 | +|^^|输入长度 (128, 256] |0.60 |0.017 |0.03 |6.00 | +|doubao\-seed\-1.6\-vision |输入长度 [0, 32] |0.80 |0.017 |0.16 |8.00 | +|^^|输入长度 (32, 128] |1.20 |0.017 |0.16 |16.00 | +|^^|输入长度 (128, 256] |2.40 |0.017 |0.16 |24.00 | +|doubao\-seed\-translation |\- |1.20 |不支持 |不支持 |3.60 | +|doubao\-1.5\-pro\-32k |\- |0.80 |0.017 |0.16 |2.00 | +|doubao\-1.5\-lite\-32k |\- |0.30 |0.017 |0.06 |0.60 | +|doubao\-1.5\-vision\-pro |\- |3.00 |不支持 |不支持 |9.00 | +|glm\-4.7 |输入长度 [0, 32]|2.0 |0.017 |0.4 |8.0 |\ +| |且输出长度 [0, 0.2] | | | | | +|^^|输入长度 [0, 32]|3.0 |0.017 |0.6 |14.0 |\ +| |且输出长度 (0.2,+∞) | | | | | +|^^|输入长度 (32, 200] |4.0 |0.017 |0.8 |16.0 | +|deepseek\-v3.2 |输入长度 [0, 32] |2.00 |0.017 |0.4 |3.00 | +|^^|输入长度 (32, 128] |4.00 |0.017 |0.4 |6.00 | +|deepseek\-v3.1 |\- |4.00 |0.017 |0.80 |12.00 | +|deepseek\-v3 |\- |2.00 |0.017 |0.40 |8.00 | +|deepseek\-r1 |\- |4.00 |0.017 |0.80 |16.00 | + + +> * 按 token 后付费,计算公式: +> * `在线推理费用 = 输入单价 × 输入token + 缓存输入单价 × 缓存命中token + 缓存存储单价 × 缓存存储token × 时长 + 输出单价 × 输出token` +> * 分段计费:部分模型适用,不同的输入长度(和输出长度),token单价不同: +> * 举例:请求输入 200k tokens,输出 14k tokens,满足 **输入长度 (128, 256]** 条件,模型输入输出 token 按照:输入 2.4 元/百万 token,输出 24 元/百万 token 单价计费。 +> * 常见问题: [如何查看历史调用的输入输出长度的区间分布?](/docs/82379/1359411#fba666f2) + + +## 在线推理(低延迟) + + +|模型名称 |条件|输入|缓存输入|输出|\ +| |千 token |元/百万token |元/百万token |元/百万token | +|---|---|---|---|---| +|doubao\-seed\-2.0\-pro |输入长度 [0, 32] |9.6 |1.92 |48.0 | +|^^|输入长度 (32, 128] |14.4 |2.88 |72.0 | +|^^|输入长度 (128, 256] |28.8 |5.76 |144.0 | +|doubao\-seed\-2.0\-lite |输入长度 [0, 32] |1.2 |0.24 |7.2 | +|^^|输入长度 (32, 128] |1.8 |0.36 |10.8 | +|^^|输入长度 (128, 256] |3.6 |0.72 |21.6 | +|doubao\-seed\-2.0\-mini |输入长度 [0, 32] |0.4 |0.08 |4.0 | +|^^|输入长度 (32, 128] |0.8 |0.16 |8.0 | +|^^|输入长度 (128, 256] |1.6 |0.32 |16.0 | + + +## 在线推理(TPM 保障包) + + +|模型 |计费方式 |输入|输出|\ +| | |元/每10K TPM |元/每1K TPM | +|---|---|---|---| +|doubao\-seed\-1.8 |按购买时长后付费 |1.920 |0.480 | +|^^|包天预付费 |23.040 |5.760 | +|doubao\-seed\-1.6 |按购买时长后付费 |1.920 |0.480 | +|^^|包天预付费 |23.040 |5.760 | +|doubao\-seed\-1.6\-vision |按购买时长后付费 |1.920 |0.480 | +|^^|包天预付费 |23.040 |5.760 | +|doubao\-seed\-1.6\-flash|按购买时长后付费 |0.360 |0.360 |\ +|> 0615版本不支持 | | | | +|^^|包天预付费 |4.320 |4.320 | +|doubao\-1.5\-vision\-pro |按购买时长后付费 |7.200 |2.160 | +|^^|包天预付费 |86.400 |25.920 | +|doubao\-1.5\-pro\-32k|按购买时长后付费 |1.920 |0.480 |\ +|> 包含 character\-250715 版本 | | | | +|^^|包天预付费 |23.040 |5.760 | +|doubao\-1.5\-lite\-32k |按购买时长后付费 |0.72 |0.144 | +|^^|包天预付费 |8.64 |1.728 | +|doubao\-pro\-32k |按购买时长后付费 |1.920 |0.480 | +|^^|包天预付费 |23.040 |5.760 | +|deepseek\-v3.2 |按购买时长后付费 |7.2 |1.08 | +|^^|包天预付费 |86.4 |12.96 | +|deepseek\-v3.1 |按购买时长后付费 |9.60 |2.88 | +|^^|包天预付费 |115.20 |34.56 | +|deepseek\-v3 |按购买时长后付费 |4.80 |1.92 | +|^^|包天预付费 |57.60 |23.04 | +|deepseek\-r1 |按购买时长后付费 |9.60 |3.84 | +|^^|包天预付费 |115.20 |46.08 | + + +> * 相比普通的按token计费模式,TPM保障包具备更高并发,更低的延迟,更强稳定性。支持的模型,以[接入点创建页](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint/create)可选的付费方式为准。 +> * 支持「按购买时长后付费」和「包天预付费」两种方式叠加购买,可灵活组合。 +> * **doubao\-seed\-1.6 系列及之后模型,deepseek\-v3.2 模型,不同长度请求抵扣 TPM 速度不同**,可通过 TPM 计算器查看相应的抵扣系数,估算实际需购买的**可抵扣TPM**。 + + +## 批量推理 + + +|模型名称 |条件|输入|缓存命中|输出|\ +| |千 token |元/百万token |元/百万token |元/百万token | +|---|---|---|---|---| +|doubao\-seed\-2.0\-pro |输入长度 [0, 32] |1.6 |0.64 |8.0 | +|^^|输入长度 (32, 128] |2.4 |0.96 |12.0 | +|^^|输入长度 (128, 256] |4.8 |1.92 |24.0 | +|doubao\-seed\-2.0\-lite |输入长度 [0, 32] |0.3 |0.12 |1.8 | +|^^|输入长度 (32, 128] |0.45 |0.18 |2.7 | +|^^|输入长度 (128, 256] |0.9 |0.36 |5.4 | +|doubao\-seed\-2.0\-mini |输入长度 [0, 32] |0.1 |0.04 |1.0 | +|^^|输入长度 (32, 128] |0.2 |0.08 |2.0 | +|^^|输入长度 (128, 256] |0.4 |0.16 |4.0 | +|doubao\-seed\-2.0\-code |输入长度 [0, 32] |1.6 |0.64 |8.0 | +|^^|输入长度 (32, 128] |2.4 |0.96 |12.0 | +|^^|输入长度 (128, 256] |4.8 |1.92 |24.0 | +|doubao\-seed\-1.8 |输入长度 [0, 32]|0.40 |0.16 |1.00 |\ +| |且输出长度 [0, 0.2] | | | | +|^^|输入长度 [0, 32]|0.40 |0.16 |4.00 |\ +| |且输出长度 (0.2,+∞) | | | | +|^^|输入长度 (32, 128] |0.60 |0.16 |8.00 | +|^^|输入长度 (128, 256] |1.20 |0.16 |12.00 | +|doubao\-seed\-1.6\-vision |输入长度 [0, 32] |0.40 |0.16 |4.00 | +|^^|输入长度 (32, 128] |0.60 |0.16 |8.00 | +|^^|输入长度 (128, 256] |1.20 |0.16 |12.00 | +|doubao\-seed\-1.6\-lite |输入长度 [0, 32]|0.15 |0.06 |0.30 |\ +| |且输出长度 [0, 0.2] | | | | +|^^|输入长度 [0, 32]|0.15 |0.06 |1.20 |\ +| |且输出长度 (0.2,+∞) | | | | +|^^|输入长度 (32, 128] |0.30 |0.06 |2.00 | +|^^|输入长度 (128, 256] |0.60 |0.06 |6.00 | +|doubao\-seed\-1.6 |输入长度 [0, 32]|0.40 |0.16 |1.00 |\ +| |且输出长度 [0, 0.2] | | | | +|^^|输入长度 [0, 32]|0.40 |0.16 |4.00 |\ +| |且输出长度 (0.2,+∞) | | | | +|^^|输入长度 (32, 128] |0.60 |0.16 |8.00 | +|^^|输入长度 (128, 256] |1.20 |0.16 |12.00 | +|doubao\-seed\-1.6\-flash |输入长度 [0, 32] |0.075 |0.03 |0.75 | +|^^|输入长度 (32, 128] |0.150 |0.03 |1.50 | +|^^|输入长度 (128, 256] |0.300 |0.03 |3.00 | +|doubao\-seed\-translation |\- |0.60 |0.24 |1.80 | +|doubao\-1.5\-pro\-32k |\- |0.40 |0.16 |1.00 | +|doubao\-1.5\-lite\-32k |\- |0.15 |0.06 |0.30 | +|doubao\-pro\-32k |\- |0.80 |0.16 |2.00 | +|deepseek\-v3.2 |输入长度 [0, 32] |1.00 |0.40 |1.50 | +|^^|输入长度 (32, 128] |2.00 |0.40 |3.00 | +|deepseek\-v3.1 |\- |2.00 |0.80 |6.00 | +|deepseek\-v3 |\- |1.00 |0.40 |4.00 | +|deepseek\-r1 |\- |2.00 |0.80 |8.00 | + + +> * 按 token 后付费,计算公式:`批量推理费用 = 输入单价 × 输入token + 缓存命中单价 × 缓存命中token + 输出单价 × 输出token` +> * 部分模型已支持透明前缀缓存能力,无需任何配置,享受命中缓存后的更低单价。 +> * doubao\-seed\-1.6 系列支持分段计费,即根据每次请求的输入及输出长度,采用不同 token 单价。 +> * 举例:当某次请求的输入长度为 200k,输出长度为 14k 时,满足 **输入长度 (128, 256]** 条件,模型产生的所有 token 按照:输入2.4 元/百万 token,输出 24 元/百万 token 单价计费。 +> * 查看往期调用的输入输出长度分布,请查看常见问题 [如何查看历史调用的输入输出长度的区间分布?](/docs/82379/1359411#fba666f2) + + +# 视频生成模型 + +## 按token单价 + + +|模型 |在线推理|离线推理|\ +| |元/百万token |元/百万token | +|---|---|---| +|doubao\-seedance\-2.0|* 输出视频分辨率为 480p,720p|暂不支持 |\ +|> 按输出视频分辨率和输入是否包含视频区分定价 | * 输入不含视频:46.00| |\ +| | * 输入包含视频:28.00| |\ +| |* 输出视频分辨率为 1080p| |\ +| | * 输入不含视频:51.00| |\ +| | * 输入包含视频:31.00 | | +|doubao\-seedance\-2.0\-fast|* 输入不含视频:37.00|暂不支持 |\ +|> 按输入是否包含视频区分定价|* 输入包含视频:22.00 | |\ +|> 不支持输出 1080p 视频 | | | +|doubao\-seedance\-1.5\-pro|* 有声视频:16.00|* 有声视频:8.00|\ +|> 按输出视频是否包含声音区分定价 |* 无声视频:8.00 |* 无声视频:4.00 | +|doubao\-seedance\-1.0\-pro |15.00 |7.50 | +|doubao\-seedance\-1.0\-pro\-fast |4.20 |2.10 | +|doubao\-seedance\-1.0\-lite |10.00 |5.00 | + + +> * 仅对成功生成的视频计费。因审核等原因导致生成失败的,不收取费用。 +> * 视频价格估算公式:`按 token 单价 × token 用量` +> * 正常视频 token 用量估算:`(输入视频时长+输出视频时长) × 输出视频的宽 × 输出视频的高 × 输出视频的帧率/1024`,注意存在输入视频时, Seedance 2.0 和 Seedance 2.0 fast 模型针对不同的视频输出时长存在最低 Token 用量限制,详见下文表格。 +> * Draft 视频(仅480p)token 用量估算:`正常视频 token 用量公式 × 折算系数`,折算系数与模型相关,Seedance 1.5 pro 的 token 折算系数:无声 0.7;有声 0.6,其他模型暂不支持。 +> * 准确 token 用量:以调用 API 后返回信息中的 usage 字段为准。 + + +## 价格示例 +基于 token 用量公式估算的视频单价,方便您直观了解不同规格的视频成本。更多价格示例请参见[火山方舟视频生成模型价格快查表](https://bytedance.larkoffice.com/wiki/FXaYwxzJ5i5Zdik32ipcWzt7nxd?table=tblns3WjGMNbR8sL&view=vewPa39Do4#CategoryScheduledTask)。 + +### doubao\-seedance\-2.0 & 2.0 fast + +> * 视频价格估算公式:`按 token 单价 × token 用量`=`按 token 单价 × (输入视频时长+输出视频时长) × 输出视频的宽 × 输出视频的高 × 输出视频的帧率/1024` +> * 注意:输入包含视频时, Seedance 2.0 和 Seedance 2.0 fast 模型针对不同的视频输出时长存在最低 token 用量限制,如果 token 估算用量 < 最低 token 用量限制,则按最低 token 用量计算视频价格。 +* **输入不含视频** + + + +|分辨率 |宽高比 |输出视频时长(秒) |doubao\-seedance\-2.0|doubao\-seedance\-2.0\-fast|\ +| | | |视频价格(元/个) |视频价格(元/个) | +|---|---|---|---|---| +|480p |16:9 |5 |2.31 |1.86 | +|720p |16:9 |5 |4.97 |4.00 | +|1080p |16:9 |5 |12.39 |不支持 | + + +* **输入包含视频** + + + +|分辨率 |宽高比 |输入视频时长(秒) |输出视频时长(秒) |doubao\-seedance\-2.0|doubao\-seedance\-2.0\-fast|\ +| | | | |视频价格(元/个) |视频价格(元/个) | +|---|---|---|---|---|---| +|480p |16:9 |2~15 |5 |2.53~5.62|1.99~4.42|\ +| | | | |> 最低价对应输入2~4秒|> 最低价对应输入2~4秒|\ +| | | | |> 最高价对应输入15秒 |> 最高价对应输入15秒 | +|720p |16:9 |2~15 |5 |5.44~12.10|4.28~9.50|\ +| | | | |> 最低价对应输入2~4秒|> 最低价对应输入2~4秒|\ +| | | | |> 最高价对应输入15秒 |> 最高价对应输入15秒 | +|1080p |16:9 |2~15 |5 |13.56~30.13|不支持 |\ +| | | | |> 最低价对应输入2~4秒| |\ +| | | | |> 最高价对应输入15秒 | | + +附:输入包含视频时,Seedance 2.0 & 2.0 fast 的最低 token 用量限制。本表以 16:9 宽高比为例展示各分辨率下的最低 token 用量。不同宽高比的最低 token 用量存在少许差异,详情参见 [火山方舟视频生成模型价格快查表](https://bytedance.larkoffice.com/wiki/FXaYwxzJ5i5Zdik32ipcWzt7nxd?table=tblmNCuMjADrXtDf&view=vewPa39Do4#CategoryScheduledTask)。 + + +|输出视频秒数 |最低tokens\-480P |最低tokens\-720P |最低tokens\-1080P | +|---|---|---|---| +|4 |70308 |151200 |340200 | +|5 |90396 |194400 |437400 | +|6 |100440 |216000 |486000 | +|7 |120528 |259200 |583200 | +|8 |140616 |302400 |680400 | +|9 |150660 |324000 |729000 | +|10 |170748 |367200 |826200 | +|11 |190836 |410400 |923400 | +|12 |200880 |432000 |972000 | +|13 |220968 |475200 |1069200 | +|14 |241056 |518400 |1166400 | +|15 |251100 |540000 |1215000 | + + +### doubao\-seedance\-1.5\-pro + + +|分辨率 |宽高比 |时长(秒) |有声视频|Draft 有声|无声视频|Draft无声|\ +| | | |价格|视频价格|价格|视频价格|\ +| | | |(元/个) |(元/个) |(元/个) |(元/个) | +|---|---|---|---|---|---|---| +|480p |16:9 |5 |0.80 |0.48 |0.40 |0.28 | +|720p |16:9 |5 |1.73 |不支持 |0.86 |不支持 | +|1080p |16:9 |5 |3.89 |不支持 |1.94 |不支持 | + + +# 图片生成模型 + + +|模型名称 |单价|\ +| |元/张 | +|---|---| +|doubao\-seedream\-5.0\-lite |0.22 | +|doubao\-seedream\-4.5 |0.25 | +|doubao\-seedream\-4.0 |0.2 | +|doubao\-seedream\-3.0\-t2i |0.259 | + + +> * 按成功输出图片数量计费: +> * 组图场景按实际生成的图片数量计费。 +> * 因审核等原因未成功输出的图片不计费。 + +  + +# 向量模型 + + +|模型 |文本输入|图片输入|\ +| |元/百万 token |元/百万 token | +|---|---|---| +|doubao\-embedding\-vision |0.70 |1.80 | + +> 按输入的 tokens 计费: +> 费用 = `文本输入 tokens × 文本输入单价 + 图片输入 tokens × 图片输入单价` +> = `文本输入 tokens × 文本输入单价+ min((width × height)/784,1312 ) × 图片输入单价` + + +# 模型精调 + +## 精调\-按 token 后付费 + + +|基础模型 ID |LoRA精调|全量精调|\ +| |元/百万token |元/百万token | +|---|---|---| +|doubao\-seed\-1.6 |40 |80 | +|doubao\-seed\-1.6\-flash |7 |14 | +|doubao\-1\-5\-pro\-32k\-250115 |50 |100 | +|doubao\-1\-5\-lite\-32k\-250115 |30 |60 | + +> 训练费用 = 总 token 数 x 精调单价 =(用户训练集token数+混入token数+验证集token数)x 迭代轮次 x 精调token单价 +> * 若 token 数小于 1000,将会上取整为 1000 tokens 计算。 + + +## 精调\-按算力付费 + + +|算力规格 |计费方式 |定价|\ +| | |元/小时 | +|---|---|---| +|方舟A型模型单元 |按量后付费 |25 | +|方舟B型模型单元 |按量后付费 |15 | +|方舟C型模型单元 |按量后付费 |10 | +|方舟D型模型单元 |按量后付费 |20 | + +> 训练费用=训练计费时长*使用的模版单价=训练计费时长*模型单元数\*模型单元单价。 + + +## 推理\-在线推理 + + +|精调模型对应的基础模型 |条件(千 token) |输入|输出|\ +| | |元/百万token |元/百万token | +|---|---|---|---| +|doubao\-seed\-1.6 |输入长度 [0, 32] |1.60 |16.00 | +|^^|输入长度 (32, 128] |2.40 |32.00 | +|doubao\-seed\-1.6\-flash |输入长度 [0, 32] |0.30 |3.00 | +|^^|输入长度 (32, 128] |0.60 |6.00 | +|doubao\-1.5\-pro\-32k |\- |2.00 |5.00 | +|doubao\-1.5\-lite\-32k |\- |0.75 |1.50 | +|doubao\-pro\-32k |\- |0.80 |2.00 | + +> 按 token 后付费价格,仅部分 doubao 模型在精调后支持按 token 付费,以[接入点创建页](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint/create)可选的付费方式为准。 + + +## 推理\-批量推理 + + +|精调模型对应的基础模型 |条件(千 token) |输入|缓存命中|输出|\ +| | |元/百万token |元/百万token |元/百万token | +|---|---|---|---|---| +|doubao\-seed\-1.6 |输入长度 [0, 32] |0.40 |0.16 |4.00 | +|^^|输入长度 (32, 128] |0.60 |0.16 |8.00 | +|^^|输入长度 (128, 256] |1.20 |0.16 |12.00 | +|doubao\-seed\-1.6\-flash |输入长度 [0, 32] |0.075 |0.03 |0.75 | +|^^|输入长度 (32, 128] |0.15 |0.03 |1.50 | +|^^|输入长度 (128, 256] |0.30 |0.03 |3.00 | +|doubao\-1.5\-pro\-32k |\- |0.40 |0.16 |1.00 | +|doubao\-1.5\-lite\-32k |\- |0.15 |0.06 |0.30 | +|doubao\-pro\-32k |\- |0.80 |0.16 |2.00 | + +> 按token后付费,相比在线推理,价格低至50%。 + + +# 模型单元 + + +|机型 |计费方式 |定价|\ +| | |元/个 | +|---|---|---| +|方舟A型模型单元 |按购买时长后付费 |25.00 | +|^^|包月预付费 |16700.00 | +|方舟B型模型单元 |按购买时长后付费 |15.00 | +|^^|包月预付费 |10400.00 | +|方舟C型模型单元 |按购买时长后付费 |10.00 | +|^^|包月预付费 |7100.00 | +|方舟D型模型单元 |按购买时长后付费 |20.00 | +|^^|包月预付费 |12800.00 | + +> 支持「按购买时长后付费」和「包月预付费」两种方式叠加购买,可灵活组合。 +> **提供** [单元计算器](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint/create) 估算需要的机型数量。更推荐通过实际业务流量压测,计算需要的机型和数量。 + + +# 工具及插件 + +## 联网内容插件 + + +|服务项 |价格|说明 |\ +| |元/千次 | | +|---|---|---| +|联网资源 |4 |实时搜索互联网公开域内容,每月提供2万次免费额度。 | +|头条资源 |6 |实时搜索今日头条图文内容,并提供内容详情信息供展示交互卡片。 | +|抖音资源 |6 |实时搜索抖音百科内容,并提供内容详情信息供展示交互卡片。 | +|墨迹天气 |6 |实时搜索墨迹天气内容资源。 | + + +> * 出账及计费:按量后付费 +> * 用量:每次请求产生的调用次数,可返回结构体的 **source_type** 字段计算得到。 +> * 更多说明请参见 [联网内容插件功能说明](/docs/82379/1338552)。 + + +## 豆包助手 + + +|服务项 |价格|说明 |\ +| |元/次 | | +|---|---|---| +|日常沟通 |0.1 |全能助手,自然交流,多轮对话,高情商人格化聊天。 | +|深度沟通 |0.2 |深度理解,精准解析,先思考再回答,复杂问题尽在掌握。 | +|联网搜索 |0.2 |全网搜索,信源丰富,无需费力找资料,一键搜索实时资讯。 | +|边想边搜 |0.5 |逻辑缜密,深度洞察,遇难题问豆包,想得更深,答得更准。 | + + +> * 出账及计费:按量后付费 +> * 用量:每次请求产生的调用次数,可返回结构体的 **source_type** 字段计算得到。 +> * 更多说明请参见 [联网内容插件功能说明](/docs/82379/1338552)。 + + +## 知识库 + + +|服务项 |价格 | +|---|---| +|计算资源\-知识库【旗舰版】 |0.45 元/CU/小时 | +|离线存储资源\-知识库【旗舰版】 |0.0015 元/GB/小时 | +|标准计算资源\-知识库【标准版】 |0.0416 元/知识库/小时 | +|文本向量模型\-知识库【通用】 |0.0005 元/千token | +|文本向量模型(多功能版)\-知识库【通用】 |0.0005 元/千token | +|文本向量模型(Doubao\-embedding)\-知识库【通用】 |0.0005 元/千token | +|文本向量模型(Doubao\- embedding\-large)\-知识库【通用】 |0.0007 元/千token | +|多模态向量模型(Doubao\-embedding\-vision\-text)\-知识库【通用】 |0.0007 元/千token | +|多模态向量模型(Doubao\-embedding\-vision\-image)\-知识库【通用】 |0.0018 元/千token | +|重排模型\-知识库【通用】 |0.0005 元/千token | + +> 更多说明请参见 [知识库计费](/docs/82379/1263336)。 + + +# Coding Plan 个人版 + + +|套餐类型 |订阅时长 |价格 | +|---|---|---| +|Lite 套餐 |1 个月 |40 元/月 | +|^^|3 个月 |120 元/季 | +|Pro 套餐 |1 个月 |200 元/月 | +|^^|3 个月 |600 元/季 | + +> 套餐信息及特惠活动参见[套餐概览](/docs/82379/1925114)。 + + + diff --git a/docs/API文档/创建视频生成任务API.md b/docs/API文档/创建视频生成任务API.md new file mode 100644 index 0000000..a9c7c3c --- /dev/null +++ b/docs/API文档/创建视频生成任务API.md @@ -0,0 +1,648 @@ +`POST https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks` [ ](https://api.volcengine.com/api-explorer/?action=CreateContentsGenerationsTasks&data=%7B%7D&groupName=%E8%A7%86%E9%A2%91%E7%94%9F%E6%88%90API&query=%7B%7D&serviceCode=ark&version=2024-01-01)[运行](https://api.volcengine.com/api-explorer/?action=CreateContentsGenerationsTasks&data=%7B%7D&groupName=%E8%A7%86%E9%A2%91%E7%94%9F%E6%88%90API&query=%7B%7D&serviceCode=ark&version=2024-01-01) +本文介绍创建视频生成任务 API 的输入输出参数,供您使用接口时查阅字段含义。模型会依据传入的图片及文本信息生成视频,待生成完成后,您可以按条件查询任务并获取生成的视频。 +:::tip +请确保您的账户余额大于等于 200 元([前往充值](https://console.volcengine.com/finance/fund/recharge)),或已[购买资源包](https://console.volcengine.com/common-buy/fast/ark_bd%7C%7Cd682ppeeq1mp7kd5q0e0),否则无法开通 seedance 2.0 及 seedance 2.0 fast 模型。 + +::: +**模型能力==^new^==** + +* **seedance 2.0 & 2.0 fast==^new^==** ** (有声视频/无声视频)** + * **多模态参考生视频==^new^==**:输入++参考图片(0~9)+参考视频(0~3)+ 参考音频(0~3)+ 文本提示词(可选)++ 生成 1 个目标视频。注意不可单独输入音频,应至少包含 1 个参考视频或图片。支持生成全新视频、编辑视频、延长视频,[阅读教程](https://www.volcengine.com/docs/82379/2291680) 获取详细代码示例。 + * **图生视频\-首尾帧**:输入++首帧图片+尾帧图片+文本提示词(可选)++ 生成 1 个目标视频。 + * **图生视频\-首帧**:输入++首帧图片+文本提示词(可选)++ 生成 1 个目标视频。 + * **文生视频**:输入++文本提示词++生成 1 个目标视频。 +* **seedance 1.5 pro (有声视频/无声视频)** + 【图生视频\-首尾帧】【图生视频\-首帧】【文生视频】 +* **seedance 1.0 pro** + 【图生视频\-首尾帧】【图生视频\-首帧】【文生视频】 +* **seedance 1.0 pro fast** + 【图生视频\-首帧】【文生视频】 +* **seedance 1.0 lite** + * **doubao\-seedance\-1\-0\-lite\-t2v:** 文生视频 + * **doubao\-seedance\-1\-0\-lite\-i2v:** + * 参考图生视频:根据您输入的**++参考图片(1\-4张)++ ** +++文本提示词(可选)++ 生成 1 个目标视频。 + * 图生视频\-首尾帧 + * 图生视频\-首帧 + + +Tips:一键展开折叠,快速检索内容 +打开页面右上角开关,**ctrl ** + **f** 可检索页面内所有内容。 +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_cae7ddb0e1977b68b353f17897b8574c.png) + + +```mixin-react +return ( + +`}> + +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_2abecd05ca2779567c6d32f0ddc7874d.png =20x) [模型列表](https://www.volcengine.com/docs/82379/1330310?lang=zh#2705b333) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_a5fdd3028d35cc512a10bd71b982b6eb.png =20x) [模型计费](https://www.volcengine.com/docs/82379/1544106?redirect=1&lang=zh#02affcb8) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_afbcf38bdec05c05089d5de5c3fd8fc8.png =20x) [API Key](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D) + ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_57d0bca8e0d122ab1191b40101b5df75.png =20x) [调用教程](https://www.volcengine.com/docs/82379/1366799) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_f45b5cd5863d1eed3bc3c81b9af54407.png =20x) [接口文档](https://www.volcengine.com/docs/82379/1520758) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_1609c71a747f84df24be1e6421ce58f0.png =20x) [常见问题](https://www.volcengine.com/docs/82379/1359411) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_bef4bc3de3535ee19d0c5d6c37b0ffdd.png =20x) [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false) +`}>); +``` + + +--- + + + +## 请求参数 +> 跳转 [响应参数](#y2hhTyHB) + + +### 请求体 + +--- + + +**model** `string` %%require%% +您需要调用的模型的 ID (Model ID),[开通模型服务](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false),并[查询 Model ID](https://www.volcengine.com/docs/82379/1330310) 。 +您也可通过 Endpoint ID 来调用模型,获得限流、计费类型(前付费/后付费)、运行状态查询、监控、安全等高级能力,可参考[获取 Endpoint ID](https://www.volcengine.com/docs/82379/1099522)。 + +--- + + +**content** `object[]` %%require%% +输入给模型,生成视频的信息,支持文本、图片、音频、视频、样片任务 ID。 +:::warning +seedance 2.0 系列模型不支持直接上传含有真人人脸的参考图/视频。为了便利创作者对肖像的使用,平台推出了以下解决方案,详情参见 [教程](https://www.volcengine.com/docs/82379/2291680?lang=zh#5c67c9a1)。 + +* 支持使用部分模型的含人脸原始产物作为输入素材 +* 支持使用预置虚拟人像作为输入素材 +* 支持使用已授权真人素材作为输入 + +::: +支持以下几种组合: + +* **文本** +* **文本(可选)+ 图片** +* **文本(可选)+ 视频** +* **文本(可选)+ 图片 + 音频** +* **文本(可选)+ 图片 + 视频** +* **文本(可选)+ 视频 + 音频** +* **文本(可选)+ 图片 + 视频 + 音频** +* **样片任务 ID**:样片指使用 seedance 模型成功生成的样片视频,模型可基于样片生成高质量正式视频。 + + +信息类型 + +--- + + +**文本信息** `object` +输入给模型的提示词信息。 + +属性 + +--- + + +content.**type ** `string` %%require%% +输入内容的类型,此处应为 `text`。 + +--- + + +content.**text ** `string` %%require%% +输入给模型的文本提示词,描述期望生成的视频。 +:::tip + +* 提示词语言支持:所有模型均支持中英文提示词;seedance 2.0 及 seedance 2.0 fast 额外支持日语、印尼语、西班牙语、葡萄牙语。 +* 提示词字数建议:中文提示词不超过500字,英文提示词不超过1000词。字数过多易导致信息分散,模型可能忽略细节、仅关注重点,进而造成视频缺失部分元素。 +* 更多使用技巧:提示词的详细使用技巧,请参见 [seedance 提示词指南](https://www.volcengine.com/docs/82379/2222480?lang=zh)。 + + + +::: + +--- + + +**图片信息==^new^==** `object` +输入给模型的图片信息。 + +属性 + +--- + + +content.**type ** `string` %%require%% +输入内容的类型,此处应为 `image_url`。 + +--- + + +content.**image_url ** `object` %%require%% +输入给模型的图片对象。 + +属性 + +--- + + +content.image_url.**url ** `string` %%require%% +图片 URL 、图片 Base64 编码、素材 ID。 + +* 图片 URL:填入图片的公网 URL。 +* Base64 编码:将本地文件转换为 Base64 编码字符串,然后提交给大模型。遵循格式:`data:image/<图片格式>;base64,`,注意 `<图片格式>` 需小写,如 `data:image/png;base64,{base64_image}`。 +* 素材 ID:用于视频生成的预置素材及虚拟人像的 ID,遵循格式:asset://。可从 [素材&虚拟人像库](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128) 获取。 + +:::tip 传入单张图片要求 + +* 格式:jpeg、png、webp、bmp、tiff、gif。其中,seedance 1.5 pro 新增支持 heic 和 heif。 +* 宽高比(宽/高): (0.4, 2.5) +* 宽高长度(px):(300, 6000) +* 大小:单张图片小于 30 MB。请求体大小不超过 64 MB。大文件请勿使用Base64编码。 +* 图片数量: + * 图生视频\-首帧:1 张 + * 图生视频\-首尾帧:2 张 + * seedance 2.0&2.0 fast 多模态参考生视频:1~9 张 + * seedance 1.0 lite 参考图生视频:1~4 张 + +::: + +--- + + +content.**role ** `string` `条件必填` +图片的位置或用途。 +:::warning + +* **图生视频\-首帧**、**图生视频\-首尾帧**、**多模态参考生视频**(包括参考图、视频、音频)为 3 种互斥场景,**不可混用**。 +* **多模态参考生视频**可通过提示词指定参考图片作为首帧/尾帧,间接实现“首尾帧+多模态参考”效果。若需严格保障首尾帧和指定图片一致,**优先使用图生视频\-首尾帧**(配置 role 为 first_frame/last_frame)。 + + +::: +图生视频\-首帧 + +* **支持模型:** 所有图生视频模型 +* **字段role取值:** 需要传入1个 image_url 对象,字段 role 为 first_frame 或不填。 + + +图生视频\-首尾帧 + +* **支持模型:** seedance 2.0 & 2.0 fast,seedance 1.5 pro、seedance 1.0 pro、seedance 1.0 lite i2v +* **字段role取值:** 需要传入2个image_url对象,且字段 role 必填。 + * 首帧图片对应的字段 role 为:first_frame + * 尾帧图片对应的字段 role 为:last_frame + +:::tip +传入的首尾帧图片可相同。首尾帧图片的宽高比不一致时,以首帧图片为主,尾帧图片会自动裁剪适配。 + +::: + +图生视频\-参考图 + +* **支持模型:** seedance 2.0 & 2.0 fast(1~9 张图片),seedance 1.0 lite i2v(1~4 张图片) +* **字段role取值:** 必填,每张参考图对应的字段 role 均为:reference_image + +:::tip +参考图生视频功能的文本提示词,可以用自然语言指定多张图片的组合。但若想有更好的指令遵循效果,**推荐使用“[图1]xxx,[图2]xxx”的方式来指定图片**。 +示例1:戴着眼镜穿着蓝色T恤的男生和柯基小狗,坐在草坪上,3D卡通风格 +示例2:[图1]戴着眼镜穿着蓝色T恤的男生和[图2]的柯基小狗,坐在[图3]的草坪上,3D卡通风格 + +::: + + +--- + + +**视频信息==^new^==** `object` +输入给模型的视频信息。仅 seedance 2.0 & 2.0 fast 支持输入视频。 +方舟平台信任 seedance 2.0 及 2.0 fast 模型生成的含人脸视频,您可使用**本账号下近30天内由上述模型生成的含人脸原始视频**,作为输入素材进行二次创作,详情参见 [教程](https://www.volcengine.com/docs/82379/2291680?lang=zh#86c3831f)。 + +属性 +content.**type ** `string` %%require%% +输入内容的类型,此处应为`video_url`。 + +--- + + +content.**video_url** ** ** `object` %%require%% +输入给模型的视频对象。 + +属性 +content.video_url.**url ** `string` %%require%% +视频URL、素材 ID。 + +* 视频 URL:填入视频的公网 URL。 +* 素材 ID:用于视频生成的预置素材及虚拟人像视频的 ID,遵循格式:asset://。可从[素材&虚拟人像库](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128)获取。 + +:::tip 传入单个视频要求 + +* 视频格式:mp4、mov,支持编码格式见下表。 +* 分辨率:480p,720p,1080p +* 时长:单个视频时长 [2, 15] s,最多传入 3 个参考视频,所有视频总时长不超过 15s。 +* 尺寸: + * 宽高比(宽/高):[0.4, 2.5] + * 宽高长度(px):[300, 6000] + * 总像素数:[640×640=409600, 2206×946=2086876],即宽和高的乘积符合 [409600, 2086876] 的区间要求。 +* 大小:单个视频不超过 50 MB。 +* 帧率 (FPS):[24, 60] + +::: + +--- + + +content.**role ** `string` `条件必填` +视频的位置或用途。当前仅支持 reference_video:参考视频。 + + +--- + + +**音频信息==^new^==** `object` +输入给模型的音频信息。仅 seedance 2.0&2.0 fast 支持输入音频。 +注意不可单独输入音频,应至少包含 1 个参考视频或图片。 + +属性 +content.**type ** `string` %%require%% +输入内容的类型,此处应为`audio_url`。 + +--- + + +content.**audio_url** ** ** `object` %%require%% +输入给模型的音频对象。 + +属性 +content.audio_url.**url ** `string` %%require%% +音频 URL 、音频 Base64 编码、素材 ID。 + +* 音频 URL:填入音频的公网 URL。 +* Base64 编码:将本地文件转换为 Base64 编码字符串,然后提交给大模型。遵循格式:`data:audio/<音频格式>;base64,`,注意 `<音频格式>` 需小写,如 `data:audio/wav;base64,{base64_audio}`。 +* 素材 ID:用于视频生成的虚拟人的音频素材 ID,遵循格式:asset://。可从[素材&虚拟人像库](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128)获取。 + +:::tip 传入单个音频要求 + +* 格式:wav、mp3 +* 时长:单个音频时长 [2, 15] s,最多传入 3 段参考音频,所有音频总时长不超过 15 s。 +* 大小:单个音频不超过 15 MB,请求体大小不超过 64 MB。大文件请勿使用Base64编码。 + + + +::: + +--- + + +content.**role ** `string` `条件必填` +音频的位置或用途。当前仅支持 reference_audio:参考音频。 + + + +--- + + +**样片信息 ** `object` +基于样片任务 ID,生成正式视频。仅 seedance 1.5 pro 支持该功能。[阅读](https://www.volcengine.com/docs/82379/1366799?lang=zh#5acd28c8)[文档](https://www.volcengine.com/docs/82379/1366799?lang=zh#5acd28c8) 获取 draft 功能的使用教程和注意事项。 + +属性 + +--- + + +content.**type ** `string` %%require%% +输入内容的类型,此处应为 `draft_task`。 + +--- + + +content.**draft_task** ** ** `object` %%require%% +输入给模型的样片任务。 + +属性 + +--- + + +content.draft_task.**id ** `string` %%require%% +样片任务 ID。平台将自动复用 Draft 视频使用的用户输入(**model、** content.**text、** content.**image_url、generate_audio、seed、ratio、duration、camera_fixed ** ),生成正式视频。其余参数支持指定,不指定将使用本模型的默认值。 +使用分为两步:Step1: 调用本接口生成 Draft 视频。Step2: 如果确认 Draft 视频符合预期,可基于 Step1 返回的 Draft 视频任务 ID,调用本接口生成最终视频。[阅读文档](https://www.volcengine.com/docs/82379/1366799?lang=zh#5acd28c8) 获取详细教程。 + + + + +--- + + +**callback_url** `string` +填写本次生成任务结果的回调通知地址。当视频生成任务有状态变化时,方舟将向此地址推送 POST 请求。 +回调请求内容结构与[查询任务API](https://www.volcengine.com/docs/82379/1521309)的返回体一致。 +回调返回的 status 包括以下状态: + +* queued:排队中。 +* running:任务运行中。 +* succeeded: 任务成功。(如发送失败,即5秒内没有接收到成功发送的信息,回调三次) +* failed:任务失败。(如发送失败,即5秒内没有接收到成功发送的信息,回调三次) +* expired:任务超时,即任务处于**运行中或排队中**状态超过过期时间。可通过 **execution_expires_after ** 字段设置过期时间。 + + +--- + + +**return_last_frame** `boolean` `默认值 false` + +* true:返回生成视频的尾帧图像。设置为 `true` 后,可通过 [查询视频生成任务接口](https://www.volcengine.com/docs/82379/1521309) 获取视频的尾帧图像。尾帧图像的格式为 png,宽高像素值与生成的视频保持一致,无水印。 + 使用该参数可实现生成多个连续视频:以上一个生成视频的尾帧作为下一个视频任务的首帧,快速生成多个连续视频,调用示例详见 [教程](https://www.volcengine.com/docs/82379/1366799?lang=zh#141cf7fa)。 +* false:不返回生成视频的尾帧图像。 + + +--- + + +**service_tier** `string` `默认值 default` +> 不支持修改已提交任务的服务等级 +> seedance 2.0 & 2.0 fast 不支持离线推理 + +指定处理本次请求的服务等级类型,枚举值: + +* default:在线推理模式,RPM 和并发数配额较低(详见 [模型列表](https://www.volcengine.com/docs/82379/1330310?lang=zh#2705b333)),适合对推理时效性要求较高的场景。 +* flex:离线推理模式,TPD 配额更高(详见 [模型列表](https://www.volcengine.com/docs/82379/1330310?lang=zh#2705b333)),价格为在线推理的 50%, 适合对推理时延要求不高的场景。 + + +--- + + +**execution_expires_after ** `integer` `默认值 172800` +任务超时阈值。指定任务提交后的过期时间(单位:秒),从 **created at** 时间戳开始计算。默认值 172800 秒,即 48 小时。取值范围:[3600,259200]。 +不论使用哪种 **service_tier**,都建议根据业务场景设置合适的超时时间。超过该时间后任务会被自动终止,并标记为`expired`状态。 + +--- + + +**generate_audio ** `boolean` `默认值 true` +> 仅 seedance 2.0 & 2.0 fast、seedance 1.5 pro 支持 + +控制生成的视频是否包含与画面同步的声音。 + +* true:模型输出的视频包含同步音频。模型会基于文本提示词与视觉内容,自动生成与之匹配的人声、音效及背景音乐。建议将对话部分置于双引号内,以优化音频生成效果。例如:男人叫住女人说:“你记住,以后不可以用手指指月亮。” +* false:模型输出的视频为无声视频。 + +:::warning +生成的有声视频均为单声道,和传入的音频声道数无关。 + +::: +--- + + +**draft ** `boolean` `默认值 false` +> 仅 seedance 1.5 pro 支持 + +控制是否开启样片模式。[阅读文档](https://www.volcengine.com/docs/82379/1366799?lang=zh#5acd28c8) 获取使用教程和注意事项。 + +* true:开启样片模式,生成一段预览视频,快速验证场景结构、镜头调度、主体动作与 prompt 意图是否符合预期。消耗 token 数较正常视频更少,使用成本更低。 +* false:关闭样片模式,正常生成一段视频。 + +:::tip +开启样片模式后,将使用 480p 分辨率生成 Draft 视频(使用其他分辨率会报错),不支持返回尾帧功能,不支持离线推理功能。 + +::: +--- + + +**tools==^new^==** ** ** `object[]` +> 仅 seedance 2.0 & 2.0 fast 支持 + +配置模型要调用的工具。 + +属性 +tools.**type ** `string` +指定使用的工具类型。 + +* web_search:联网搜索工具。[阅读教程](https://www.volcengine.com/docs/82379/1366799?lang=zh#c40ed3ef) 获取详细代码示例。 + +:::tip + +* 开启联网搜索后,模型会根据用户的提示词自主判断是否搜索互联网内容(如商品、天气等)。可提升生成视频的时效性,但也会增加一定的时延。 +* 实际搜索次数可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 usage.tool_usage.**web_search** 字段获取,如果为 0 表示未搜索。 + +::: + +--- + + +**safety_identifier==^new^==** `string` +终端用户的唯一标识符,用于协助平台检测您的应用中可能违反火山方舟使用政策的用户。该标识符为英文字符串,需保证对单个用户固定且唯一,长度不超过 64 个字符。推荐传入对用户名、用户 ID 或邮箱进行哈希处理后生成的字符串,避免泄露用户隐私信息。 + +--- + + +  +:::warning 部分参数升级说明 + +* **对于 resolution、ratio、duration、frames、seed、camera_fixed、watermark 参数,平台升级了参数传入方式,示例如下。所有模型依然兼容支持旧方式。** +* 不同模型,可能对应支持不同的参数与取值,详见 [输出视频格式](https://www.volcengine.com/docs/82379/1366799?lang=zh#9fe4cce0)。当输入的参数或取值不符合所选的模型时,该参数将被忽略或触发报错: + * 新方式:在 request body 中直接传入参数。此方式为**强校验,** 若参数填写错误,模型会返回错误提示。 + * 旧方式:在文本提示词后追加 \-\-[parameters]。此方式为**弱校验,** 若参数填写错误,该参数将被忽略或触发报错。 + + +::: +**新方式(推荐):在 request body 中直接传入参数** +```JSON +... + // Specify the aspect ratio of the generated video as 16:9, duration as 5 seconds, resolution as 720p, seed as 11, and include a watermark. The camera is not fixed. + "model": "doubao-seedance-1-5-pro-251215", + "content": [ + { + "type": "text", + "text": "小猫对着镜头打哈欠" + } + ], + // All parameters must be written in full; abbreviations are not supported + "resolution": "720p", + "ratio":"16:9", + "duration": 5, + // "frames": 29, Either duration or frames is required + "seed": 11, + "camera_fixed": false, + "watermark": true +... +``` + + + + +**旧方式:在文本提示词后追加 \-\-[parameters]** +```JSON +... + // Specify the aspect ratio of the generated video as 16:9, duration as 5 seconds, resolution as 720p, seed as 11, and include a watermark. The camera is not fixed. + "model": "doubao-seedance-1-5-pro-251215", + "content": [ + { + "type": "text", + "text": "小猫对着镜头打哈欠 --rs 720p --rt 16:9 --dur 5 --seed 11 --cf false --wm true" + // "text": "小猫对着镜头打哈欠 --resolution 720p --ratio 16:9 --duration 5 --seed 11 --camerafixed false --watermark true" + } + ] +... +``` + + + + +--- + + +**resolution ** `string` +> seedance 2.0 & 2.0 fast、seedance 1.5 pro、seedance 1.0 lite 默认值:`720p` +> seedance 1.0 pro & pro\-fast 默认值:`1080p` + +视频分辨率,枚举值: + +* 480p +* 720p +* 1080p:seedance 1.0 lite 参考图场景、seedance 2.0 & 2.0 fast 不支持 + + +--- + + +**ratio ** `string` +> seedance 2.0 & 2.0 fast、seedance 1.5 pro 默认值为 `adaptive` +> seedance 1.0 lite 参考图场景默认值为 `16:9` +> 其他模型:文生视频默认值 `16:9`,图生视频默认值 `adaptive` + +生成视频的宽高比例。不同宽高比对应的宽高像素值见下方表格。 + +* 16:9 +* 4:3 +* 1:1 +* 3:4 +* 9:16 +* 21:9 +* adaptive:根据输入自动选择最合适的宽高比(详见下文说明) + +:::warning **adaptive ** 适配规则 +当配置 **ratio** 为 `adaptive` 时,模型会根据生成场景自动适配宽高比;实际生成的视频宽高比可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 **ratio** 字段获取。 +**支持模型:** + +* seedance 2.0 & 2.0 fast、seedance 1.5 Pro 支持 +* 其他模型仅图生视频场景支持,注意 seedance 1.0 lite 参考图场景不支持。 + +**取值规则:** + +* 文生视频:根据输入的提示词,智能选择最合适的宽高比。 +* 首帧 / 首尾帧生视频:根据上传的首帧图片比例,自动选择最接近的宽高比。 +* 多模态参考生视频:根据用户提示词意图判断,如果是首帧生视频/编辑视频/延长视频,以该图片/视频为准选择最接近的宽高比;否则,以传入的第一个媒体文件为准(优先级:视频>图片)选择最接近的宽高比。 + +::: +  + +不同宽高比对应的宽高像素值 +Note:图生视频,选择的宽高比与您上传的图片宽高比不一致时,方舟会对您的图片进行裁剪,裁剪时会居中裁剪,详细规则见 [图片裁剪规则](https://www.volcengine.com/docs/82379/1366799?lang=zh#f76aafc8)。 + +|分辨率 |宽高比|宽高像素值|宽高像素值|\ +| | |seedance 1.0 系列 |seedance 1.5 pro|\ +| | | |seedance 2.0 & 2.0 fast | +|---|---|---|---| +|480p |16:9 |864×480 |864×496 | +|^^|4:3 |736×544 |752×560 | +|^^|1:1 |640×640 |640×640 | +|^^|3:4 |544×736 |560×752 | +|^^|9:16 |480×864 |496×864 | +|^^|21:9 |960×416 |992×432 | +|720p |16:9 |1248×704 |1280×720 | +|^^|4:3 |1120×832 |1112×834 | +|^^|1:1 |960×960 |960×960 | +|^^|3:4 |832×1120 |834×1112 | +|^^|9:16 |704×1248 |720×1280 | +|^^|21:9 |1504×640 |1470×630 | +|1080p |16:9 |1920×1088 |1920×1080 |\ +|> 1.0 lite 参考图场景不支持,seedance 2.0 & 2.0 fast不支持 | | | | +|^^|4:3 |1664×1248 |1664×1248 | +|^^|1:1 |1440×1440 |1440×1440 | +|^^|3:4 |1248×1664 |1248×1664 | +|^^|9:16 |1088×1920 |1080×1920 | +|^^|21:9 |2176×928 |2206×946 | + + + + +--- + + +**duration** `integer` `默认值 5` +> duration 和 frames 二选一即可,frames 的优先级高于 duration。如果您希望生成整数秒的视频,建议指定 duration。 + +生成视频时长,仅支持整数,单位:秒。 + +* seedance 1.0 pro、seedance 1.0 pro fast、seedance 1.0 lite: [2, 12] s。 +* seedance 1.5 pro: [4,12] 或设置为`-1` +* seedance 2.0 & 2.0 fast: [4,15] 或设置为`-1` + +:::warning +seedance 2.0 & 2.0 fast、seedance 1.5 pro 支持两种配置方法 + + * 指定具体时长:支持有效范围内的任一整数。 + * 智能指定:设置为 `-1`,表示由模型在有效范围内自主选择合适的视频长度(整数秒)。实际生成视频的时长可通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309?lang=zh) 返回的 **duration** 字段获取。注意视频时长与计费相关,请谨慎设置。 + + +::: +--- + + +**frames** `integer` +> seedance 2.0 & 2.0 fast、seedance 1.5 pro 暂不支持 +> duration 和 frames 二选一即可,frames 的优先级高于 duration。如果您希望生成小数秒的视频,建议指定 frames。 + +生成视频的帧数。通过指定帧数,可以灵活控制生成视频的长度,生成小数秒的视频。 +由于 frames 的取值限制,仅能支持有限小数秒,您需要根据公式推算最接近的帧数。 + +* 计算公式:帧数 = 时长 × 帧率(24)。 +* 取值范围:支持 [29, 289] 区间内所有满足 `25 + 4n` 格式的整数值,其中 n 为正整数。 + +例如:假设需要生成 2.4 秒的视频,帧数=2.4×24=57.6。由于 frames 不支持 57.6,此时您只能选择一个最接近的值。根据 25+4n 计算出最接近的帧数为 57,实际生成的视频为 57/24=2.375 秒。 + +--- + + +**seed** `integer` `默认值 -1` +种子整数,用于控制生成内容的随机性。 +取值范围:[\-1, 2^32\-1]之间的整数。 +:::warning + +* 相同的请求下,模型收到不同的seed值,如:不指定seed值或令seed取值为\-1(会使用随机数替代)、或手动变更seed值,将生成不同的结果。 +* 相同的请求下,模型收到相同的seed值,会生成类似的结果,但不保证完全一致。 + + +::: +--- + + +**camera_fixed** `boolean` `默认值 false` +> 参考图场景不支持,seedance 2.0 & 2.0 fast 暂不支持 + +是否固定摄像头。枚举值: + +* true:固定摄像头。平台会在用户提示词中追加固定摄像头,实际效果不保证。 +* false:不固定摄像头。 + + +--- + + +**watermark** `boolean` `默认值 false` +生成视频是否包含水印。枚举值: + +* false:不含水印。 +* true:含有水印。 + + +--- + + + +## 响应参数 +> 跳转 [请求参数](#RxN8G2nH) + +**id ** `string` +视频生成任务 ID 。仅保存 7 天(从 **created at** 时间戳开始计算),超时后将自动清除。 + +* 设置`"draft": true`,为 Draft 视频任务 ID。 +* 设置 `"draft": false`,为正常视频任务 ID。 + +创建视频生成任务为异步接口,获取 ID 后,需要通过 [查询视频生成任务 API](https://www.volcengine.com/docs/82379/1521309) 来查询视频生成任务的状态。任务成功后,会输出生成视频的`video_url`。 + + diff --git a/docs/celery-polling-fix-20260404.md b/docs/celery-polling-fix-20260404.md deleted file mode 100644 index 6f6013a..0000000 --- a/docs/celery-polling-fix-20260404.md +++ /dev/null @@ -1,134 +0,0 @@ -# Celery 轮询机制修复报告 - -> 日期:2026-04-04 -> 版本:v0.16.0 -> 影响范围:backend/apps/generation/tasks.py, backend/config/settings.py - ---- - -## 一、问题现象 - -2026/4/1 下午,大量用户反馈视频生成任务长时间卡在"生成中",前端显示耗时 60~65 分钟。 -火山引擎侧确认视频实际生成仅需约 10 分钟,结果已就绪但未被平台及时同步。 - -**截图数据**(4/1 下午完成的任务): - -| 提交时间 | 显示耗时 | -|---------|---------| -| 2026/4/1 16:57:28 | 63 分 33 秒 | -| 2026/4/1 16:58:41 | 62 分 37 秒 | -| 2026/4/1 16:59:16 | 62 分 7 秒 | -| 2026/4/1 17:00:36 | 64 分 24 秒 | -| 2026/4/1 17:04:53 | 64 分 2 秒 | - -## 二、根因分析 - -### 2.1 状态同步链路 - -``` -用户提交任务 - → 后端调 create_task(火山 API) - → 获得 ark_task_id - → 派发 Celery 任务 poll_video_task - → Celery worker 每 5 秒查一次火山 API - → 火山返回完成 → 写 DB + 上传 TOS + 结算 - → 前端轮询 DB → 展示结果 -``` - -前端只读 DB 状态,**不直接调火山 API**。整个链路完全依赖 Celery worker 轮询。 - -### 2.2 旧实现缺陷 - -`poll_video_task` 使用 `while True` + `time.sleep(5)` 长驻循环: - -```python -# 旧代码 -while True: - time.sleep(POLL_INTERVAL) # 5 秒 - ark_resp = query_task(...) # 查一次 - if terminal: - break -``` - -**三个致命问题:** - -| 问题 | 影响 | -|------|------| -| 每个任务占死一个 worker 进程 | `concurrency=4` 最多同时轮询 4 个任务,第 5 个排队 | -| worker 重启后循环直接丢失 | 内存中的 `while True` 不可持久化,OOM/重启 = 任务丢失 | -| `time.sleep` 浪费进程资源 | worker 99% 时间在 sleep,实际有用工作不到 1% | - -### 2.3 OOM 重启链 - -``` -4 个任务同时轮询 - → 某些任务完成,触发 TOS 上传(下载视频 + 上传对象存储) - → 内存飙升超过 512Mi 限制 - → K8s OOM Kill → worker 重启(共重启 15 次) - → 4 个进程中的 while True 循环全部丢失 - → 等 recover_stuck_tasks(每 10 分钟)重新派发 - → 重新派发后 worker 又被占满 → 又 OOM → 循环 - → 实际恢复耗时 ≈ 50~60 分钟 -``` - -## 三、修复方案 - -### 3.1 核心改动:self.retry 替代 while True - -```python -# 新代码 -@shared_task(bind=True, max_retries=None, ignore_result=True) -def poll_video_task(self, record_id): - record = GenerationRecord.objects.get(pk=record_id) - - ark_resp = query_task(record.ark_task_id) - new_status = map_status(ark_resp.get('status', '')) - - if new_status in ('queued', 'processing'): - record.save(update_fields=['status', 'updated_at']) - raise self.retry(countdown=5) # 5 秒后重新入队 - - # 到达终态 → 处理结果 - ... -``` - -**原理对比:** - -| | 旧方式(while True) | 新方式(self.retry) | -|---|---|---| -| 任务生命周期 | 在 worker 进程内存中 | 在 Redis 队列中 | -| worker 占用 | 持续占用直到完成(分钟级) | 每次查询仅占用毫秒级 | -| worker 重启 | 任务丢失 | Redis 中的任务自动恢复 | -| 并发能力 | 最多 4 个(= concurrency) | 数百个(受 API RPM 限制) | - -### 3.2 recover_stuck_tasks 间隔缩短 - -| | 旧值 | 新值 | -|---|---|---| -| Beat 调度间隔 | 600 秒(10 分钟) | 180 秒(3 分钟) | -| stuck 判定门槛 | 10 分钟 | 3 分钟 | -| 最坏恢复时间 | ~20 分钟 | ~6 分钟 | - -### 3.3 变更文件 - -| 文件 | 改动 | -|------|------| -| `backend/apps/generation/tasks.py` | `poll_video_task`: while True → self.retry;`recover_stuck_tasks`: 门槛 10 → 3 分钟 | -| `backend/config/settings.py` | Beat schedule: 600 → 180 秒 | - -## 四、效果预估 - -| 指标 | 修复前 | 修复后 | -|------|--------|--------| -| 同时轮询任务数上限 | 4 | 数百 | -| worker 重启后任务恢复 | 丢失,等 10 分钟兜底 | 自动恢复,无需兜底 | -| 最坏同步延迟 | 60+ 分钟 | ~15 秒(= 查询间隔 + 网络延迟) | -| 内存占用 | 持续占满(sleep 期间不释放) | 脉冲式占用(查完释放) | -| OOM 风险 | 高(4 进程常驻 + TOS 上传峰值) | 低(进程闲置时内存极小) | - -## 五、部署注意 - -1. **无需数据库迁移** — 仅修改 Python 代码 -2. **部署后旧的 while True 任务会自然消亡** — 不需要手动干预 -3. **Redis 中可能有旧格式的任务** — 兼容无问题,新旧 `poll_video_task` 签名一致(`record_id` 参数不变) -4. **建议同步部署**:先部署代码,再重启 Celery worker(`kubectl rollout restart deployment celery-worker`) diff --git a/docs/changelog.md b/docs/changelog.md index be3cfd8..9b9a160 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,71 @@ --- +## 2026-04-17 — v0.18.3: 版权报错友好提示 + 图片删除即梦式连续重命名 + +**状态**: ✅ 已完成 | **验收**: 14 个自动化测试全过(11 单元 + 3 E2E) + +### 变更内容 + +#### 后端 +1. **版权限制错误友好提示** — `OutputVideoSensitiveContentDetected.PolicyViolation`(漫威等知名 IP 触发的版权拦截)加中文错误码映射:"生成的视频涉及版权限制内容(如知名IP、名人肖像等),已被系统拦截,请修改提示词后重试"。精确匹配 API 返回的 code,不影响父级 `OutputVideoSensitiveContentDetected`(敏感内容)的现有提示 + +#### 前端 +2. **图片删除即梦式连续重命名** — 现有逻辑:删图片2 后,图片3 保持原名称,再上传新图会出现"两个图片2"。修复后: + - `inputBar.ts::removeReference` 删除后,同类型(图片/视频/音频)剩余引用按顺序连续重命名(图片1/图片2/图片3 连续无空位) + - 用 `DOMParser` 解析 editorHtml,找到对应 `data-ref-id` 的 @mention span,更新 textContent(提示词栏里的 `@图片3` 自动变 `@图片2`) + - 缩略图区和提示词栏视觉同步刷新 + +#### 测试覆盖 +- **Vitest 单元测试 11 个**(test/unit/removeReferenceRelabeling.test.ts):图片三场景、视频/音频独立编号、空 editorHtml、无 @mention、传入无效 id、部分 @、连续快速删除等边界 +- **Playwright E2E 3 个**(test/e2e/bug2-rename.spec.ts):真实浏览器验证上传 3 张图 → 删中间 → 再上传,编号不冲突 + +#### 文档整理 +3. **AirDrama 根目录归档**:8 个过期 MD 文档移至 `archive/`(_settle_payment 双重结算/v0.15.1 部署/公告HTML/迁移到火山/全平台账务审计/火山耗时分析/图片上传blob/迭代需求_20260320) +4. **video-shuoshan/docs 归档**:6 个过期文档移至 `docs/archive/`(celery 轮询修复/design-review/PRD/test-report/两版旧 API 文档) +5. **新增 1080P Plan**:`AirDrama/1080P分辨率支持开发计划.md`,对照官方 API 文档完成 3 轮审查修正(21:9 像素值错误、_settle_payment 遗漏、VideoDetailModal 重新编辑、regenerate、API 响应 6 处、serializer 命名、GenerationRecord.resolution 字段已存在等),标注 5 项已知计费缺陷 + +### 变更文件 +- `backend/utils/airdrama_client.py` — ERROR_MESSAGES 加 PolicyViolation 映射 +- `web/src/store/inputBar.ts` — removeReference 重写(即梦逻辑 + editorHtml 同步) +- `web/test/unit/removeReferenceRelabeling.test.ts` — 11 个单元测试(新增) +- `web/test/e2e/bug2-rename.spec.ts` — 3 个 E2E 测试(新增) +- `AirDrama/1080P分辨率支持开发计划.md` — 1080P 开发 Plan(新增) +- `AirDrama/版本管理.md` — 添加 v0.18.3 记录 +- `AirDrama/项目总览与待办.md` — 完成项 + 1080P P0 待办 +- 16 个 MD 文档归档到两个 archive 目录 + +### 触发原因 +- 用户反馈:漫威素材生成失败时显示英文错误,不友好 +- 用户反馈:删除中间图片后再上传会出现重复编号(参考即梦交互) +- 火山 2026-04-16 1080P 上线,需提前规划开发 + +--- + +## 2026-04-13 — v0.18.2: 资产页修复 + 重新编辑素材泄漏 + 音频校验 + +**状态**: ✅ 已完成 | **验收**: 待测试 + +### 变更内容 + +#### 前端 +1. **资产页素材库引用不可查看** — Admin/Team 资产页的 `assetVideoToTask` 直接用了 `asset://` 协议 URL 作为 `previewUrl`,浏览器无法加载。改为检测 `asset://` 后使用 `thumb_url`(真实 TOS 缩略图地址),并标记 `isAssetRef`。同步修复 `BackendTask` 和 `AssetVideo` 类型定义补 `thumb_url` 字段 +2. **重新编辑素材泄漏** — `reEdit()` 把素材库引用混入 `references` 数组(注释写已过滤但实际没有),用户删除 @标签后旧素材仍通过 `filesToUpload` 路径发出。修复:`reEdit/regenerate` 加 `.filter(!isAssetRef)`;`PromptInput.extractText` 每次 DOM 变化时实时同步 `assetMentions` store +3. **音频不能作为唯一参考素材** — Seedance API 不支持"纯音频"和"文本+音频"。`canSubmit()` 去掉 `!hasText` 条件,同时检查 `references` 和 `assetMentions` 中的图片/视频;Toolbar 点击禁用按钮弹 toast 提示原因 +4. **素材库引用缩略图烂图** — `pollStatus` 跨项目素材保护 +5. **音频 ♫ 符号溢出** — 改用 CSS `::before` 渲染,不再污染 prompt 文本 + +### 变更文件 +- `web/src/pages/AdminAssetsPage.tsx` — isAssetUrl + thumb_url 处理 +- `web/src/pages/TeamAssetsPage.tsx` — 同上 +- `web/src/types/index.ts` — BackendTask/AssetVideo 补 thumb_url +- `web/src/store/generation.ts` — reEdit/regenerate 过滤 isAssetRef +- `web/src/components/PromptInput.tsx` — extractText 同步 assetMentions +- `web/src/store/inputBar.ts` — canSubmit 音频校验增强 +- `web/src/components/Toolbar.tsx` — 音频受限 toast 提示 + +--- + ## 2026-03-19 — v0.9.7: 登录风控第二期 — IP归属地解析 + 异常检测 + 飞书告警 + 自动封禁 **状态**: ✅ 已完成 | **验收**: ✅ 通过(本地验证,IP138 在线 API 需部署至阿里云后验证) diff --git a/docs/design-review.md b/docs/design-review.md deleted file mode 100644 index 259e0b0..0000000 --- a/docs/design-review.md +++ /dev/null @@ -1,134 +0,0 @@ -# 设计评审报告 - -## 评审结论: APPROVED - -**评审版本**: PRD v3.0 — Phase 3 (计量单位变更 + 管理后台重做 + 用户个人中心) -**评审日期**: 2026-03-12 -**评审范围**: Phase 3 新增/修改的原型页面(6 个新文件 + 1 个更新) - ---- - -## 功能覆盖检查 - -### Phase 3 P0 核心功能 - -| PRD 功能点 | 是否实现 | 备注 | -|-----------|---------|------| -| 管理后台布局 — 左侧 Sidebar 240px + 4 导航项 | ✅ | 所有 4 个管理页面共享一致的 Sidebar,active 高亮正确 | -| 管理后台路由 — dashboard/users/records/settings | ✅ | 4 个独立页面,导航链接互通 | -| 用户个人中心页面 `/profile` | ✅ | 消费概览 + 消费趋势 + 消费记录 + 配额警告 | -| 消费概览卡片 — 环形进度条 + 日/月配额 | ✅ | SVG 环形图 345s/600s,日额度 82.0%,月额度 39.1% | - -**P0 覆盖率: 4/4 (100%)** - -### Phase 3 P1 重要功能 - -| PRD 功能点 | 是否实现 | 备注 | -|-----------|---------|------| -| 仪表盘 — 核心指标卡片 (4 个) | ✅ | 总用户数 1,234 / 今日新增 +23 / 今日消费 4,560s / 本月消费 89,010s,含环比变化箭头 | -| 仪表盘 — 消费趋势折线图 | ✅ | SVG 折线图 + 面积填充,30 天数据,tooltip 显示 "3/28 188s" | -| 仪表盘 — 用户消费排行柱状图 | ✅ | Top 10 水平柱状图,数据递减 2,340s → 350s,前 3 名高亮 | -| 仪表盘 — 时间范围选择器 | ✅ | 今日/近7天/近30天/自定义,按钮可交互切换 | -| 仪表盘 — 图表 Mock 数据 | ✅ | 30 天数据有自然波动和上升趋势,排行榜数据合理 | -| 用户管理 — 用户列表表格 (分页) | ✅ | 9 列数据 + 7 条记录,分页 "共 56 条,第 1/3 页" | -| 用户管理 — 搜索和筛选 | ✅ | 关键字搜索 + 状态下拉 (全部/已启用/已禁用) + 刷新按钮 | -| 用户管理 — 配额编辑模态框 | ✅ | 点击"编辑配额"弹出 Modal,含日/月限额输入框 + 取消/保存按钮 | -| 用户管理 — 用户状态管理 | ✅ | 启用用户显示"禁用"(红色),禁用用户显示"启用"(绿色) | -| 用户管理 — 用户详情抽屉 | ✅ | 点击用户名打开 420px 右侧抽屉,含完整用户信息 + 近期 3 条消费记录 | -| 消费记录 — 消费明细表格 | ✅ | 6 列 (时间/用户名/消费秒数/视频描述/生成模式/状态),10 条记录,分页 "共 1,234 条,第 1/62 页" | -| 消费记录 — 时间范围筛选 | ✅ | 日期选择器 2026-03-01 ~ 2026-03-12 + 查询按钮 | -| 消费记录 — 用户筛选 | ✅ | "搜索用户名..." 搜索框 | -| 消费记录 — 导出 CSV | ✅ | 顶栏"导出 CSV"按钮 + 下载图标,主题色边框 | -| 系统设置 — 全局默认配额 | ✅ | 日限额 600s / 月限额 6000s 数字输入框 + 提示文字 + 保存按钮 | -| 系统设置 — 系统公告管理 | ✅ | 开关切换 (ON 状态) + 文本域含示例公告 + 保存按钮 + Toast 反馈 | -| 个人中心 — 消费记录列表 | ✅ | 6 条记录 (时间/秒数/prompt/模式/状态) + "加载更多"按钮 | -| 个人中心 — 消费趋势迷你图 | ✅ | Sparkline 折线图 7 天数据 (3/6-3/12),近7天/近30天切换按钮 | -| 个人中心 — 配额提示 | ✅ | 黄色警告横幅 "今日额度已消费 82%,请合理安排使用" + 日额度卡片 warning 样式 | - -**P1 覆盖率: 19/19 (100%)** - -### Phase 3 P2 锦上添花 - -| PRD 功能点 | 是否实现 | 备注 | -|-----------|---------|------| -| 页面切换过渡动画 | ✅ | fadeUp 入场动画 + 延迟序列 | -| 数据加载骨架屏 | ❌ | 未实现 (P2,不影响评审) | -| Sidebar 折叠模式 | ❌ | 未实现 (P2,不影响评审) | - -**P2 覆盖率: 1/3 (33%)** - -### 导航页更新 - -| PRD 功能点 | 是否实现 | 备注 | -|-----------|---------|------| -| index.html 更新 — Phase 3 导航卡片 | ✅ | 3 个分区:Phase 1 核心功能 + Phase 3 管理后台 (4 卡片) + Phase 3 用户端 (个人中心) | - ---- - -## 后台管理系统专项检查 - -| 检查项 | 结果 | 说明 | -|--------|------|------| -| 按功能模块拆分多个页面 + 侧边导航 | ✅ | 4 个独立页面 + 240px Sidebar 导航,active 状态正确 | -| 仪表盘图表有真实 Mock 数据 | ✅ | 折线图 30 天波动数据 + 排行榜 10 用户递减数据 + tooltip | -| 独立的用户管理页面 | ✅ | 完整表格 + 搜索/筛选 + 编辑配额模态框 + 用户详情抽屉 | -| 数据表格有搜索、分页、操作列 | ✅ | 搜索框 + 状态筛选 + 分页控件 + 编辑/禁用操作按钮 | - -**专项检查: 4/4 全部通过** - ---- - -## 素材使用质量 - -| 检查项 | 结果 | 问题描述 | -|--------|------|---------| -| 图片方向 | N/A | 原型为纯 HTML/CSS/SVG,无外部图片素材 | -| 精灵图集裁切 | N/A | 不涉及精灵图 | -| 尺寸比例 | ✅ | SVG 图表自适应宽度,卡片网格响应式 | -| 真实素材引用 | ✅ | 使用 SVG 内联图标(非 emoji 占位符) | -| 素材加载 | ✅ | 所有页面资源正常加载,无 broken image | -| 视觉层次 | ✅ | Sidebar → Content → Modal/Drawer 层叠正确 | - ---- - -## 设计质量 - -- **视觉一致性**: 5/5 — 所有 6 个 Phase 3 页面共享统一的深色主题 (Linear/Vercel 风格),CSS 变量一致 (#0a0a0f, #111118, #16161e, #2a2a38, #00b8e6),字体统一 (Noto Sans SC + Space Grotesk + JetBrains Mono) -- **交互合理性**: 5/5 — Drawer/Modal/Toggle/Toast 交互完整,时间选择器可切换,分页/搜索/筛选 UI 齐全 -- **响应式设计**: 4/5 — 仪表盘卡片使用 grid-cols-4 max-lg:grid-cols-2 响应式,个人中心 max-width 900px 居中。Sidebar 无折叠模式 (P2) -- **素材使用正确性**: 5/5 — 纯 SVG 图表和图标,无外部依赖,渲染准确 - -**综合评分: 4.8/5** - ---- - -## Playwright 验证截图 - -| 页面 | 截图文件 | 验证结果 | -|------|---------|---------| -| 仪表盘 | prototype-admin-dashboard.png | ✅ 4 统计卡片 + 折线图 + 排行榜 + 时间选择器 | -| 用户管理 | prototype-admin-users.png | ✅ 搜索/筛选栏 + 7 行用户表格 + 分页 | -| 用户详情抽屉 | prototype-admin-users-drawer.png | ✅ 右侧 420px 抽屉 + 用户信息 + 近期消费 | -| 消费记录 | prototype-admin-records.png | ✅ 筛选栏 + 10 行记录表格 + 导出 CSV + 分页 | -| 系统设置 | prototype-admin-settings.png | ✅ 配额表单 + 公告管理 + Toggle 开关 | -| 个人中心 | prototype-user-profile.png | ✅ 环形图 + 日/月额度 + Sparkline + 消费记录 + 配额警告 | -| 导航页 | prototype-index.png | ✅ Phase 1 + Phase 3 管理后台 + Phase 3 用户端分区 | - ---- - -## 评审总结 - -Phase 3 原型质量**极高**,完整覆盖了 PRD 中所有 P0 (4/4) 和 P1 (19/19) 功能需求。 - -**亮点**: -1. 管理后台 4 个子页面布局一致,Sidebar 导航交互正确 -2. 仪表盘图表使用真实感 Mock 数据(折线图有自然波动,排行榜递减合理) -3. 用户管理交互完整:表格 + 搜索 + 筛选 + 编辑模态框 + 详情抽屉 -4. 个人中心环形进度条 + Sparkline 趋势图 + 配额警告横幅,信息层次清晰 -5. 整体深色主题统一,Linear/Vercel 风格专业感强 -6. 所有页面计量单位正确使用「秒数」(非「次数」),与 Phase 3 需求一致 - -**可改进项** (不影响通过): -- P2: Sidebar 折叠模式未实现 -- P2: 骨架屏加载态未实现 -- 用户表格缺少头像列(可在开发阶段补充) diff --git a/docs/prd.md b/docs/prd.md deleted file mode 100644 index a598312..0000000 --- a/docs/prd.md +++ /dev/null @@ -1,1611 +0,0 @@ -# 产品需求文档 (PRD) - -## 1. 项目概述 - -- **项目名称**: Jimeng Clone — AI 视频生成平台 -- **一句话描述**: 1:1 还原即梦平台 (jimeng.jianying.com) 的 AI 视频生成页面,包含完整的用户认证体系、视频生成输入界面、后台管理系统和用户个人中心 -- **目标用户**: 前端开发学习者、AI 视频产品原型验证团队、平台管理员 -- **项目范围**: 视频生成输入栏(InputBar)及其交互逻辑 + Django 后端 API + 用户登录注册 + 后台管理(生成秒数统计与限制) + 用户个人中心(消费概览与记录) - -### 开发阶段划分 - -| 阶段 | 范围 | 状态 | -|------|------|------| -| **Phase 1** | 纯前端视频生成输入界面(InputBar、工具栏、上传、模式切换) | ✅ 已完成 | -| **Phase 2** | Django 后端 + 用户认证 + 前端路由 + 管理后台(基于调用次数) | ✅ 已完成 | -| **Phase 3** | 计量单位变更(次数→秒数)+ 管理后台重做(多页面 Sidebar)+ 用户个人中心 | 🔲 待开发 | - -> **当前迭代范围**: Phase 3。Phase 1 和 Phase 2 功能已完成。Phase 3 在 Phase 2 基础上进行重大重构和新增功能。文档中 `[Phase 3]` 标记用于区分本次迭代的新增/修改内容。 - -### Phase 3 核心变更摘要 - -1. **计量单位变更**: 所有「调用次数」改为「生成秒数」,用户每次生成视频消耗的是秒数(= 视频时长),不是次数 -2. **后台管理系统重做**: 从单页面改为多页面 + 左侧 Sidebar 导航(仪表盘/用户管理/消费记录/系统设置) -3. **新增用户个人中心** `/profile`: 消费概览 + 消费记录 + 趋势迷你图 -4. **图表库引入**: 使用 ECharts(echarts + echarts-for-react)展示趋势图、排行榜、环形进度条 -5. **设计升级**: 管理后台深色主题(Linear/Vercel 风格)、骨架屏加载、页面过渡动画 - -## 2. 功能需求 - -### 2.1 核心功能(P0) - -#### 已有前端功能 - -- [x] **深色主题全屏页面** — 背景色 `#0a0a0f`,页面无滚动,输入栏固定在底部居中,最大宽度 ~900px -- [x] **InputBar 容器** — 背景 `#16161e`,边框 `1px solid #2a2a38`,圆角 `20px`,内部分为上半区(上传+输入)和下半区(工具栏) -- [x] **提示词多行文本输入框** — 自适应高度(min 1行,max ~6行),支持换行输入,placeholder 根据当前模式动态切换 -- [x] **全能参考模式(默认模式)** — 左侧 [+ 参考内容] 上传按钮,支持上传 1-5 张图片/视频文件;上传后显示缩略图网格,每张标注「图片1」「图片2」等序号,每个缩略图右上角有 × 关闭按钮;上传区与文本输入框左右排列 -- [x] **首尾帧模式** — 上传区变为 [首帧缩略图] ↔ [+ 尾帧] 双框布局,中间双向箭头图标连接;首帧和尾帧各上传一张图片 -- [x] **模式切换下拉** — 工具栏中「全能参考/首尾帧」下拉菜单,点击切换模式,切换时联动:上传区 UI、比例选项、时长默认值 -- [x] **工具栏按钮行** — 一行横排按钮,透明背景,灰色文字 `#8a8a9a`,hover 时微亮背景 -- [x] **发送按钮** — 圆形按钮,内含上箭头图标;无内容时灰色不可点击,有内容(文字或上传文件)时变为蓝色 `#00b8e6` 可点击 - -#### [Phase 2] 用户认证系统 - -- [x] **用户注册页面** — 前端注册页 `/register`,包含用户名、邮箱、密码、确认密码字段,提交后调用后端注册 API -- [x] **用户登录页面** — 前端登录页 `/login`,包含用户名/邮箱、密码字段,登录成功后获取 JWT Token 并存储到 localStorage -- [x] **JWT 认证机制** — 后端签发 Access Token(有效期 2 小时)+ Refresh Token(有效期 7 天),前端请求自动附加 Authorization Bearer Token -- [x] **登录状态保持** — 前端全局 Auth 状态管理(Zustand),未登录用户自动跳转到登录页,已登录用户显示用户信息和退出按钮 -- [x] **Token 自动刷新** — Access Token 过期时自动使用 Refresh Token 刷新,刷新失败则跳转到登录页 - -#### [Phase 2] Django 后端服务 - -- [x] **Django 项目初始化** — 在 `backend/` 子目录创建 Django 项目,连接 MySQL 云数据库 -- [x] **RESTful API** — 使用 Django REST Framework (DRF) 提供用户认证、视频生成记录等 API -- [x] **CORS 配置** — 允许前端开发服务器(localhost:5173)跨域访问后端 API - -#### [Phase 3] 计量单位变更(次数 → 秒数) - -- [ ] **用户配额字段变更** — User 模型的 `daily_limit` / `monthly_limit`(次数)改为 `daily_seconds_limit` / `monthly_seconds_limit`(秒数),默认值分别为 600秒/日、6000秒/月 -- [ ] **消费记录增加秒数字段** — GenerationRecord 模型新增 `seconds_consumed` 字段(FloatField),记录每次生成消耗的秒数(= 视频时长 duration) -- [ ] **配额检查逻辑变更** — 后端配额检查从「今日调用次数 < daily_limit」改为「今日消费秒数 < daily_seconds_limit」 -- [ ] **前端展示变更** — 所有显示「剩余次数」的地方改为「剩余秒数」,UserInfoBar 组件配额显示改为秒数 - -#### [Phase 3] 后台管理系统重做 - -- [ ] **管理后台布局** — 采用左侧 Sidebar + 右侧内容区的经典管理后台布局,Sidebar 固定宽度 240px,支持折叠 -- [ ] **Sidebar 导航菜单** — 包含 4 个导航项:仪表盘、用户管理、消费记录、系统设置,当前项高亮,使用 react-router 嵌套路由 -- [ ] **管理后台路由** — `/admin` 为管理后台根路由(重定向到 `/admin/dashboard`),子路由:`/admin/dashboard`、`/admin/users`、`/admin/records`、`/admin/settings` - -#### [Phase 3] 用户个人中心 - -- [ ] **个人中心页面** — 新增 `/profile` 路由,已登录用户可访问,展示个人消费概览和历史记录 -- [ ] **消费概览卡片** — 显示已用秒数/总额度(环形进度条,使用 ECharts gauge)、今日已用/日限额、本月已用/月限额 - -### 2.2 重要功能(P1) - -#### 已有前端功能 - -- [x] **视频生成下拉按钮** — 蓝色文字 `#00b8e6` + 视频图标 + 下拉箭头,点击展开下拉菜单(菜单项仅 UI 展示,如"视频生成"/"图片生成"等) -- [x] **模型选择按钮** — 显示「Seedance 2.0」+ 钻石图标,点击展开模型选择下拉(仅 UI 展示,如 Seedance 2.0 / Seedance 2.0 Fast) -- [x] **比例选择按钮** — 全能参考模式下显示屏幕图标 + 当前比例值,点击弹出选项:`16:9` / `9:16` / `1:1` / `21:9` / `4:3` / `3:4`,默认 `21:9`;首尾帧模式下显示「自动匹配」不可选择 -- [x] **时长选择按钮** — 时钟图标 + 当前时长值,点击弹出选项:`5s` / `10s` / `15s`;全能参考模式默认 `15s`,首尾帧模式默认 `5s` -- [x] **@ 按钮** — 仅在全能参考模式下显示,点击插入 `@` 符号到文本输入框光标位置 -- [x] **文件上传交互** — 点击上传区触发文件选择器,accept 为 `image/*,video/*`;上传后生成本地预览缩略图;支持拖拽上传 -- [x] **上传数量限制** — 全能参考模式最多 5 张,超出时 toast 提示;首尾帧模式首帧/尾帧各 1 张 - -#### [Phase 2] 后台管理与生成接入 - -- [x] **Django Admin 集成** — 启用 Django Admin 面板(`/admin/`),管理员可查看和管理所有用户、视频生成记录 -- [x] **发送按钮接入后端** — 点击发送按钮时,调用后端 `/api/v1/video/generate` 接口(需登录),后端记录调用并检查配额 - -#### [Phase 3] 仪表盘页面(`/admin/dashboard`) - -- [ ] **核心指标卡片** — 4 个统计卡片:总用户数、今日新增用户、今日消费秒数、本月消费秒数。每个卡片显示数值 + 环比变化百分比 + 趋势箭头(↑绿色/↓红色) -- [ ] **消费趋势折线图** — 使用 ECharts 折线图展示近 30 天每日消费秒数趋势,支持 tooltip 悬浮显示具体数值,X轴为日期,Y轴为秒数 -- [ ] **用户消费排行柱状图** — 使用 ECharts 水平柱状图展示消费 Top 10 用户(按本月消费秒数降序),柱状图标签显示用户名和秒数 -- [ ] **时间范围选择器** — 支持「今日 / 近7天 / 近30天 / 自定义时间范围」切换,图表数据联动更新 -- [ ] **图表 Mock 数据** — 开发阶段使用 30 天的真实结构 Mock 数据(随机波动、周末低谷),确保图表有真实感 - -#### [Phase 3] 用户管理页面(`/admin/users`) - -- [ ] **用户列表表格** — 分页展示所有用户(每页 20 条),列:头像、用户名、邮箱、注册时间、状态(启用/禁用)、日限额(秒)、月限额(秒)、今日消费(秒)、本月消费(秒)、操作 -- [ ] **搜索和筛选** — 支持按用户名/邮箱关键字搜索 + 按状态筛选(全部/启用/禁用) -- [ ] **配额编辑** — 每个用户行的操作列有「编辑配额」按钮,点击弹出模态框,可修改日限额秒数和月限额秒数 -- [ ] **用户状态管理** — 操作列有「启用/禁用」开关按钮,点击后调用 API 切换用户 `is_active` 状态 -- [ ] **用户详情抽屉** — 点击用户名展开右侧抽屉面板,显示用户详情 + 该用户近期消费记录列表 - -#### [Phase 3] 消费记录页面(`/admin/records`) - -- [ ] **消费明细表格** — 分页展示所有用户的消费记录,列:时间、用户名、消费秒数、视频描述(prompt 截断)、生成模式、状态 -- [ ] **时间范围筛选** — 日期选择器,支持选择起止日期筛选记录 -- [ ] **用户筛选** — 支持按用户名搜索筛选特定用户的消费记录 -- [ ] **导出功能** — 「导出 CSV」按钮,将当前筛选条件下的消费记录导出为 CSV 文件 - -#### [Phase 3] 系统设置页面(`/admin/settings`) - -- [ ] **全局默认配额设置** — 表单修改全局默认日限额秒数和月限额秒数(新注册用户自动获得此配额) -- [ ] **系统公告管理** — 公告文本编辑框 + 启用/禁用开关,启用后公告内容展示在用户端页面顶部 - -#### [Phase 3] 用户个人中心详细功能 - -- [ ] **消费记录列表** — 分页展示当前用户的消费记录,每条记录显示:时间、消费秒数、生成的视频描述(prompt)、生成模式、状态 -- [ ] **消费趋势迷你图** — 使用 ECharts Sparkline 样式展示近 7 天 / 近 30 天每日消费秒数趋势,可切换时间范围 -- [ ] **配额提示** — 当日额度消费超过 80% 时显示黄色警告提示,超过 100% 时显示红色禁用提示 - -### 2.3 锦上添花(P2) - -- [x] **下拉菜单动画** — 下拉展开/收起有 fade + slide 动画过渡 -- [x] **文本输入框自动聚焦** — 页面加载后自动 focus 到文本输入框 -- [x] **键盘快捷键** — `Ctrl/Cmd + Enter` 触发发送(等同点击发送按钮) -- [ ] **上传进度条** — 文件上传时缩略图上显示加载进度 -- [ ] **拖拽排序** — 全能参考模式下已上传的缩略图支持拖拽调整顺序 -- [x] **响应式适配** — 移动端窄屏下工具栏按钮文字隐藏只显示图标,输入栏宽度自适应 -- [ ] **Tooltip 提示** — 工具栏按钮 hover 显示功能说明 tooltip -- [ ] **[Phase 2] 忘记密码** — 邮箱验证码找回密码流程 -- [ ] **[Phase 2] 用户个人资料编辑** — 查看和修改个人信息(头像、昵称) -- [ ] **[Phase 3] 页面切换过渡动画** — 路由切换时使用 fade/slide 过渡动画,提升体验流畅度 -- [ ] **[Phase 3] 数据加载骨架屏** — 管理后台和个人中心的数据加载使用骨架屏(Skeleton)替代 loading spinner -- [ ] **[Phase 3] Sidebar 折叠模式** — 管理后台 Sidebar 支持折叠为图标模式,增大内容区宽度 - -## 3. 技术栈建议 - -### 3.1 现有技术栈(保留) - -| 层级 | 技术选型 | 说明 | -|------|---------|------| -| 前端框架 | React 18 + TypeScript | 函数组件 + Hooks | -| 构建工具 | Vite 5 | 极速 HMR,原生 ESM | -| UI 组件库 | @arco-design/web-react | 字节跳动设计系统,即梦同款 | -| 状态管理 | Zustand | 轻量、TypeScript 友好 | -| 样式方案 | CSS Modules + Arco Design Token | 深色主题定制 | -| 图标 | @arco-design/web-react/icon + 自定义 SVG | 工具栏图标 | -| 文件处理 | 浏览器原生 File API | 本地预览、缩略图生成 | - -### 3.2 [Phase 2] 已有前端依赖(保留) - -| 依赖 | 说明 | -|------|------| -| react-router-dom v7 | 前端路由(登录页、注册页、管理页) | -| axios | HTTP 请求库,支持拦截器实现 Token 自动附加和刷新 | - -### 3.3 [Phase 3] 新增前端依赖 - -| 依赖 | 说明 | -|------|------| -| echarts | 图表库核心,用于折线图、柱状图、环形图等数据可视化 | -| echarts-for-react | ECharts 的 React 封装组件,声明式使用图表 | - -> **组件优先级**: 如果 Arco Design 内置了对应组件(如 Table、Modal、Skeleton、DatePicker),优先使用 Arco 组件,ECharts 仅用于复杂图表。 - -### 3.4 [Phase 2] 后端技术栈(保留) - -| 层级 | 技术选型 | 说明 | -|------|---------|------| -| 后端框架 | Django 4.2+ (LTS) | Python Web 框架 | -| API 框架 | Django REST Framework (DRF) | RESTful API 开发 | -| 认证方案 | djangorestframework-simplejwt | JWT Token 签发与验证 | -| 数据库 | MySQL 8.0(阿里云 RDS) | 云数据库,已提供连接信息 | -| 数据库驱动 | mysqlclient | Django 官方推荐的 MySQL 驱动 | -| CORS | django-cors-headers | 跨域请求支持 | -| 后端管理 | Django Admin | 内置管理后台 | -| 部署 | Gunicorn + Nginx | 生产环境部署方案 | - -> **后端代码目录**: 所有后端代码放在项目根目录下的 `backend/` 子目录中。 - -### 3.5 数据库连接配置(不变) - -```python -# backend/config/settings.py -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'video_auto', - 'USER': 'ai_video', - 'PASSWORD': 'JogNQdtrd3WY8CBCAiYfYEGx', - 'HOST': 'rm-7xv1uaw910558p1788o.mysql.rds.aliyuncs.com', - 'PORT': '3306', - 'OPTIONS': { - 'charset': 'utf8mb4', - 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", - }, - } -} -``` - -## 4. 页面列表 - -### 4.1 [已有] 视频生成页 (`/`) - -**整体布局**: -``` -┌─────────────────────────────────────────────┐ -│ [Phase 3 修改] 顶部用户信息: │ -│ 用户名 | 剩余: 345s/600s(日) | [个人中心] [退出]│ -│ │ -│ 深色背景空白区域 │ -│ #0a0a0f │ -│ │ -│ ┌─────────────────────────────────────┐ │ -│ │ InputBar (底部固定) │ │ -│ │ max-width: 900px, 居中 │ │ -│ │ 背景: #16161e │ │ -│ │ 边框: #2a2a38, 圆角: 20px │ │ -│ │ │ │ -│ │ ┌──────┐ ┌──────────────────────┐ │ │ -│ │ │上传区 │ │ 提示词文本输入框 │ │ │ -│ │ │ │ │ │ │ │ -│ │ └──────┘ └──────────────────────┘ │ │ -│ │ │ │ -│ │ ─────────── 工具栏按钮行 ────────── │ │ -│ │ [视频生成▼][模型][模式▼][比例][时长] │ │ -│ │ [@] flex空白 [发送⬆] │ │ -│ └─────────────────────────────────────┘ │ -└─────────────────────────────────────────────┘ -``` - -- [Phase 3 修改] 顶部用户信息区的配额显示从「剩余 N 次」改为「剩余 N 秒」,增加「个人中心」链接 -- [Phase 2] 需要登录才能访问,未登录重定向到 `/login` - -**全能参考模式 — 上传区细节**: -``` -┌──────────┐ -│ + │ ← 初始状态: [+ 参考内容] 按钮 -│ 参考内容 │ 虚线边框,点击触发上传 -└──────────┘ - -┌───┐┌───┐┌──────┐ -│图1││图2││ + │ ← 已上传状态: 缩略图网格 + 添加按钮 -│ × ││ × ││ │ 每张有序号标签和关闭按钮 -└───┘└───┘└──────┘ -``` - -**首尾帧模式 — 上传区细节**: -``` -┌──────┐ ┌──────┐ -│ 首帧 │ ↔ │+ 尾帧 │ ← 两个独立上传框 -│ │ │ │ 中间双向箭头 -└──────┘ └──────┘ -``` - -**工具栏按钮排布**: -``` -全能参考模式: -[🎬 视频生成 ▼] [💎 Seedance 2.0] [✨ 全能参考 ▼] [🖥 21:9] [🕐 15s] [@] ——flex空白—— [⬆ 发送] - -首尾帧模式: -[🎬 视频生成 ▼] [💎 Seedance 2.0] [🔀 首尾帧 ▼] [自动匹配] [🕐 5s] ——flex空白—— [⬆ 发送] -``` - -### 4.2 [已有] 登录页 (`/login`) - -``` -┌─────────────────────────────────────────────┐ -│ │ -│ 深色背景 #0a0a0f │ -│ │ -│ ┌──────────────────────┐ │ -│ │ Jimeng Clone │ │ -│ │ │ │ -│ │ 用户名/邮箱: [____] │ │ -│ │ 密码: [____] │ │ -│ │ │ │ -│ │ [ 登录 ] │ │ -│ │ │ │ -│ │ 没有账号?去注册 → │ │ -│ └──────────────────────┘ │ -│ │ -└─────────────────────────────────────────────┘ -``` - -- 表单验证:用户名/邮箱必填,密码最少 6 位 -- 登录成功后跳转到 `/`(视频生成页) -- 风格与主应用一致:深色主题,卡片式表单 - -### 4.3 [已有] 注册页 (`/register`) - -``` -┌─────────────────────────────────────────────┐ -│ │ -│ 深色背景 #0a0a0f │ -│ │ -│ ┌──────────────────────┐ │ -│ │ 创建账号 │ │ -│ │ │ │ -│ │ 用户名: [____] │ │ -│ │ 邮箱: [____] │ │ -│ │ 密码: [____] │ │ -│ │ 确认密码: [____] │ │ -│ │ │ │ -│ │ [ 注册 ] │ │ -│ │ │ │ -│ │ 已有账号?去登录 → │ │ -│ └──────────────────────┘ │ -│ │ -└─────────────────────────────────────────────┘ -``` - -- 表单验证:用户名 3-20 位、邮箱格式校验、密码最少 6 位、两次密码一致 -- 注册成功后自动登录并跳转到 `/` - -### 4.4 [Phase 3] 管理后台布局(`/admin/*`) - -``` -┌──────────────────────────────────────────────────────────────┐ -│ Jimeng Admin [管理员名] [退出] │ -├────────────┬─────────────────────────────────────────────────┤ -│ │ │ -│ SIDEBAR │ CONTENT AREA │ -│ 240px │ (根据子路由渲染不同页面) │ -│ │ │ -│ ┌──────┐ │ │ -│ │ 📊 │ │ │ -│ │仪表盘 │ │ │ -│ ├──────┤ │ │ -│ │ 👥 │ │ │ -│ │用户 │ │ │ -│ │管理 │ │ │ -│ ├──────┤ │ │ -│ │ 📋 │ │ │ -│ │消费 │ │ │ -│ │记录 │ │ │ -│ ├──────┤ │ │ -│ │ ⚙️ │ │ │ -│ │系统 │ │ │ -│ │设置 │ │ │ -│ └──────┘ │ │ -│ │ │ -│ ─────── │ │ -│ [返回首页] │ │ -│ │ │ -├────────────┴─────────────────────────────────────────────────┤ -│ Jimeng Clone Admin v3.0 │ -└──────────────────────────────────────────────────────────────┘ -``` - -**设计规范(管理后台专用)**: -- 整体风格参考 Linear / Vercel Dashboard,深色主题 -- 背景色 `#0a0a0f`,Sidebar 背景 `#111118`,内容区背景 `#0a0a0f` -- Sidebar 当前项背景 `rgba(255, 255, 255, 0.08)`,文字 `#ffffff` -- 非当前项文字 `#8a8a9a`,hover 背景 `rgba(255, 255, 255, 0.04)` -- 卡片背景 `#16161e`,边框 `1px solid #2a2a38`,圆角 `12px` -- 数据加载时显示 Arco Skeleton 骨架屏 - -### 4.5 [Phase 3] 仪表盘页面(`/admin/dashboard`) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ │ -│ 仪表盘 [今日] [近7天] [近30天] [自定义]│ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ 总用户数 │ │今日新增 │ │今日消费 │ │本月消费 │ │ -│ │ 1,234 │ │ +23 │ │ 4,560s │ │ 89,010s │ │ -│ │ ↑12% │ │ ↑15% │ │ ↓5% │ │ ↑8% │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 消费趋势 (ECharts 折线图) │ │ -│ │ │ │ -│ │ Y轴: 秒数 ___/\___ │ │ -│ │ / \___/\ │ │ -│ │ ___/\___/ \___ │ │ -│ │ │ │ -│ │ X轴: 日期 (3/1 3/5 3/10 3/15 3/20 3/25 3/30) │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 用户消费排行 Top 10 (ECharts 水平柱状图) │ │ -│ │ │ │ -│ │ user_a ████████████████████████ 2,340s │ │ -│ │ user_b ██████████████████ 1,890s │ │ -│ │ user_c ████████████████ 1,560s │ │ -│ │ ... │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -- 仅 `is_staff=True` 的用户可访问 -- 统计卡片使用 Arco Card 组件 + 自定义样式 -- 折线图使用 ECharts `line` 类型,开启 `tooltip`、`dataZoom` 交互 -- 柱状图使用 ECharts `bar` 类型,水平方向,标签显示用户名和秒数 -- 时间范围选择器使用 Arco DatePicker.RangePicker -- 数据来源:`GET /api/v1/admin/stats` - -### 4.6 [Phase 3] 用户管理页面(`/admin/users`) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ │ -│ 用户管理 │ -│ │ -│ [🔍 搜索用户名/邮箱___________] [状态: 全部 ▼] [刷新] │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 用户名 │ 邮箱 │ 注册时间 │ 状态 │ 日限额│ │ -│ │ │ │ │ │ (秒) │ │ -│ │ 月限额 │ 今日消费(秒) │ 本月消费(秒)│ 操作 │ │ -│ ├──────────┼──────────────┼──────────┼──────┼──────┤ │ -│ │ user_a │ a@test.com │ 3/1 │ ✅ │ 600 │ │ -│ │ 6000 │ 123 │ 2345 │[编辑][禁用] │ │ -│ ├──────────┼──────────────┼──────────┼──────┼──────┤ │ -│ │ user_b │ b@test.com │ 3/5 │ ❌ │ 300 │ │ -│ │ 3000 │ 0 │ 0 │[编辑][启用] │ │ -│ └──────────┴──────────────┴──────────┴──────┴──────┘ │ -│ │ -│ 共 56 条 [< 1 2 3 >] │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -- 使用 Arco Table 组件(分页、排序) -- 搜索使用 Arco Input.Search -- 状态筛选使用 Arco Select -- 编辑配额使用 Arco Modal 弹窗 -- 用户详情使用 Arco Drawer 右侧抽屉 -- 数据来源:`GET /api/v1/admin/users` - -### 4.7 [Phase 3] 消费记录页面(`/admin/records`) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ │ -│ 消费记录 [导出 CSV] │ -│ │ -│ [用户名搜索____] [时间: 2026-03-01 ~ 2026-03-12] [查询] │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 时间 │ 用户名 │ 消费秒数│ 视频描述 │ │ -│ │ │ │ │ (prompt截断) │ │ -│ │ 模式 │ 状态 │ │ -│ ├───────────────────┼────────┼────────┼─────────────┤ │ -│ │ 3/12 14:30:00 │ user_a │ 15s │ 一只猫在... │ │ -│ │ 全能参考 │ 已完成 │ │ -│ ├───────────────────┼────────┼────────┼─────────────┤ │ -│ │ 3/12 14:25:00 │ user_b │ 5s │ 日落海边... │ │ -│ │ 首尾帧 │ 生成中 │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -│ 共 1,234 条 [< 1 2 3 ... 62 >] │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -- 使用 Arco Table 组件(分页) -- 时间范围筛选使用 Arco DatePicker.RangePicker -- 导出 CSV:前端调用 API 获取全部数据并生成 CSV 文件下载 -- 数据来源:`GET /api/v1/admin/records` - -### 4.8 [Phase 3] 系统设置页面(`/admin/settings`) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ │ -│ 系统设置 │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 全局默认配额 │ │ -│ │ │ │ -│ │ 默认每日限额 (秒): [____600____] │ │ -│ │ 默认每月限额 (秒): [____6000___] │ │ -│ │ │ │ -│ │ [保存配额设置] │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 系统公告 [启用公告: ON] │ │ -│ │ │ │ -│ │ ┌──────────────────────────────────────────────┐ │ │ -│ │ │ │ │ │ -│ │ │ 公告内容 (支持纯文本) │ │ │ -│ │ │ │ │ │ -│ │ └──────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ [保存公告] │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -- 使用 Arco Form、InputNumber、Switch、Input.TextArea 组件 -- 保存后 showToast 提示「设置已保存」 -- 数据来源:`GET/PUT /api/v1/admin/settings` - -### 4.9 [Phase 3] 用户个人中心(`/profile`) - -``` -┌─────────────────────────────────────────────────────────────┐ -│ │ -│ [← 返回首页] 个人中心 [用户名] [退出] │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 消费概览 │ │ -│ │ │ │ -│ │ ┌────────┐ ┌──────────────┐ ┌──────────────┐ │ │ -│ │ │ (环形图)│ │ 今日额度 │ │ 本月额度 │ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ 已用 │ │ 已用: 123s │ │ 已用: 2345s │ │ │ -│ │ │ 345s │ │ 限额: 600s │ │ 限额: 6000s │ │ │ -│ │ │ /600s │ │ ████████░░░ │ │ ████░░░░░░░ │ │ │ -│ │ │ │ │ 20.5% │ │ 39.1% │ │ │ -│ │ └────────┘ └──────────────┘ └──────────────┘ │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 消费趋势 [近7天] [近30天] │ │ -│ │ ___/\___/\___ (Sparkline 迷你折线图) │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ 消费记录 │ │ -│ │ │ │ -│ │ 3/12 14:30 │ 15s │ 一只猫在花园... │ 全能参考 │ 完成│ │ -│ │ 3/12 14:25 │ 5s │ 日落海边散步... │ 首尾帧 │ 完成│ │ -│ │ 3/12 13:00 │ 10s │ 城市夜景延时... │ 全能参考 │ 失败│ │ -│ │ ... │ │ -│ │ │ │ -│ │ [加载更多] │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -- 环形进度条使用 ECharts `gauge` 类型(半环或全环) -- 进度条使用 Arco Progress 组件 -- 消费记录列表使用 Arco List 或 Table 组件,支持「加载更多」分页 -- Sparkline 迷你图使用 ECharts `line` 类型(无坐标轴,仅曲线) -- 数据来源:`GET /api/v1/profile/overview` + `GET /api/v1/profile/records` -- 深色主题风格与主应用一致 - -## 5. API 设计 - -### 5.1 [已有] 视频生成(Phase 3 修改配额返回字段) - -``` -POST /api/v1/video/generate -Content-Type: multipart/form-data -Authorization: Bearer - -Request: -{ - "prompt": string, - "mode": "universal" | "keyframe", - "model": "seedance_2.0" | "seedance_2.0_fast", - "aspect_ratio": "16:9" | "9:16" | "1:1" | "21:9" | "4:3" | "3:4" | "auto", - "duration": 5 | 10 | 15, - "references": File[], - "first_frame": File | null, - "last_frame": File | null -} - -Response: 202 Accepted -{ - "task_id": "uuid", - "status": "queued", - "estimated_time": 120, - "seconds_consumed": 15, ← [Phase 3] 本次消耗秒数 - "remaining_seconds_today": 345 ← [Phase 3] 今日剩余秒数 -} - -Error Response: 429 Too Many Requests -{ - "error": "quota_exceeded", - "message": "您今日的生成额度已用完", ← [Phase 3] 改为秒数描述 - "daily_seconds_limit": 600, - "daily_seconds_used": 600, - "reset_at": "2026-03-13T00:00:00+08:00" -} -``` - -### 5.2 [已有] 文件上传(不变) - -``` -POST /api/v1/upload -Content-Type: multipart/form-data -Authorization: Bearer - -Request: -{ - "file": File, - "type": "image" | "video" -} - -Response: 200 OK -{ - "file_id": "uuid", - "url": "https://cdn.example.com/...", - "thumbnail_url": "https://cdn.example.com/.../thumb.jpg", - "width": 1920, - "height": 1080, - "duration": 10.5 -} -``` - -### 5.3 [已有] 用户注册(不变) - -``` -POST /api/v1/auth/register -Content-Type: application/json - -Request: -{ - "username": "string (3-20字符)", - "email": "string (合法邮箱)", - "password": "string (最少6位)" -} - -Response: 201 Created -{ - "user": { - "id": 1, - "username": "johndoe", - "email": "john@example.com" - }, - "tokens": { - "access": "eyJ...", - "refresh": "eyJ..." - } -} - -Error Response: 400 Bad Request -{ - "username": ["该用户名已被注册"], - "email": ["该邮箱已被注册"] -} -``` - -### 5.4 [已有] 用户登录(不变) - -``` -POST /api/v1/auth/login -Content-Type: application/json - -Request: -{ - "username": "string (用户名或邮箱)", - "password": "string" -} - -Response: 200 OK -{ - "user": { - "id": 1, - "username": "johndoe", - "email": "john@example.com", - "is_staff": false - }, - "tokens": { - "access": "eyJ...", - "refresh": "eyJ..." - } -} - -Error Response: 401 Unauthorized -{ - "error": "invalid_credentials", - "message": "用户名或密码错误" -} -``` - -### 5.5 [已有] Token 刷新(不变) - -``` -POST /api/v1/auth/token/refresh -Content-Type: application/json - -Request: -{ - "refresh": "eyJ..." -} - -Response: 200 OK -{ - "access": "eyJ..." -} -``` - -### 5.6 [已有] 获取当前用户信息(Phase 3 修改配额字段) - -``` -GET /api/v1/auth/me -Authorization: Bearer - -Response: 200 OK -{ - "id": 1, - "username": "johndoe", - "email": "john@example.com", - "is_staff": false, - "quota": { - "daily_seconds_limit": 600, ← [Phase 3] 改为秒数 - "daily_seconds_used": 123, - "monthly_seconds_limit": 6000, - "monthly_seconds_used": 2345 - } -} -``` - -### 5.7 [Phase 3] 管理后台 — 仪表盘统计 - -``` -GET /api/v1/admin/stats?period=30d -Authorization: Bearer -(requires is_staff=True) - -Query Parameters: - period: "today" | "7d" | "30d" | "custom" - start_date: "2026-03-01" (当 period=custom 时必填) - end_date: "2026-03-12" (当 period=custom 时必填) - -Response: 200 OK -{ - "total_users": 1234, - "new_users_today": 23, - "seconds_consumed_today": 4560, - "seconds_consumed_this_month": 89010, - "today_change_percent": -5.0, - "month_change_percent": 8.0, - "daily_trend": [ - {"date": "2026-02-11", "seconds": 3200}, - {"date": "2026-02-12", "seconds": 4100}, - ... - ], - "top_users": [ - { - "user_id": 1, - "username": "user_a", - "seconds_consumed": 2340 - }, - ... - ] -} -``` - -### 5.8 [Phase 3] 管理后台 — 用户列表 - -``` -GET /api/v1/admin/users?page=1&page_size=20&search=&status=&sort_by=created_at&order=desc -Authorization: Bearer -(requires is_staff=True) - -Query Parameters: - page: int (默认 1) - page_size: int (默认 20,最大 100) - search: string (按用户名或邮箱模糊搜索) - status: "active" | "disabled" | "" (空表示全部) - sort_by: "created_at" | "seconds_today" | "seconds_month" (排序字段) - order: "asc" | "desc" - -Response: 200 OK -{ - "total": 56, - "page": 1, - "page_size": 20, - "results": [ - { - "id": 1, - "username": "user_a", - "email": "a@test.com", - "is_active": true, - "date_joined": "2026-03-01T10:00:00+08:00", - "daily_seconds_limit": 600, - "monthly_seconds_limit": 6000, - "seconds_today": 123, - "seconds_this_month": 2345 - }, - ... - ] -} -``` - -### 5.9 [Phase 3] 管理后台 — 用户详情 + 消费记录 - -``` -GET /api/v1/admin/users/:id -Authorization: Bearer -(requires is_staff=True) - -Response: 200 OK -{ - "id": 1, - "username": "user_a", - "email": "a@test.com", - "is_active": true, - "is_staff": false, - "date_joined": "2026-03-01T10:00:00+08:00", - "daily_seconds_limit": 600, - "monthly_seconds_limit": 6000, - "seconds_today": 123, - "seconds_this_month": 2345, - "seconds_total": 5678, - "recent_records": [ - { - "id": 101, - "created_at": "2026-03-12T14:30:00+08:00", - "seconds_consumed": 15, - "prompt": "一只猫在花园里追蝴蝶", - "mode": "universal", - "model": "seedance_2.0", - "status": "completed" - }, - ... - ] -} -``` - -### 5.10 [Phase 3] 管理后台 — 修改用户配额 - -``` -PUT /api/v1/admin/users/:id/quota -Authorization: Bearer -(requires is_staff=True) -Content-Type: application/json - -Request: -{ - "daily_seconds_limit": 900, - "monthly_seconds_limit": 9000 -} - -Response: 200 OK -{ - "user_id": 1, - "username": "user_a", - "daily_seconds_limit": 900, - "monthly_seconds_limit": 9000, - "updated_at": "2026-03-12T14:30:00+08:00" -} -``` - -### 5.11 [Phase 3] 管理后台 — 启用/禁用用户 - -``` -PATCH /api/v1/admin/users/:id/status -Authorization: Bearer -(requires is_staff=True) -Content-Type: application/json - -Request: -{ - "is_active": false -} - -Response: 200 OK -{ - "user_id": 1, - "username": "user_a", - "is_active": false, - "updated_at": "2026-03-12T14:30:00+08:00" -} -``` - -### 5.12 [Phase 3] 管理后台 — 消费记录列表 - -``` -GET /api/v1/admin/records?page=1&page_size=20&search=&start_date=&end_date= -Authorization: Bearer -(requires is_staff=True) - -Query Parameters: - page: int (默认 1) - page_size: int (默认 20,最大 100) - search: string (按用户名搜索) - start_date: "2026-03-01" (起始日期) - end_date: "2026-03-12" (结束日期) - -Response: 200 OK -{ - "total": 1234, - "page": 1, - "page_size": 20, - "results": [ - { - "id": 101, - "created_at": "2026-03-12T14:30:00+08:00", - "user_id": 1, - "username": "user_a", - "seconds_consumed": 15, - "prompt": "一只猫在花园里追蝴蝶", - "mode": "universal", - "model": "seedance_2.0", - "aspect_ratio": "16:9", - "status": "completed" - }, - ... - ] -} -``` - -### 5.13 [Phase 3] 管理后台 — 系统设置 - -``` -GET /api/v1/admin/settings -Authorization: Bearer -(requires is_staff=True) - -Response: 200 OK -{ - "default_daily_seconds_limit": 600, - "default_monthly_seconds_limit": 6000, - "announcement": "系统将于今晚 22:00 进行维护", - "announcement_enabled": true -} - -PUT /api/v1/admin/settings -Authorization: Bearer -(requires is_staff=True) -Content-Type: application/json - -Request: -{ - "default_daily_seconds_limit": 600, - "default_monthly_seconds_limit": 6000, - "announcement": "系统将于今晚 22:00 进行维护", - "announcement_enabled": true -} - -Response: 200 OK -{ - "default_daily_seconds_limit": 600, - "default_monthly_seconds_limit": 6000, - "announcement": "系统将于今晚 22:00 进行维护", - "announcement_enabled": true, - "updated_at": "2026-03-12T14:30:00+08:00" -} -``` - -### 5.14 [Phase 3] 用户个人中心 — 消费概览 - -``` -GET /api/v1/profile/overview?period=7d -Authorization: Bearer - -Query Parameters: - period: "7d" | "30d" (趋势数据的时间范围) - -Response: 200 OK -{ - "daily_seconds_limit": 600, - "daily_seconds_used": 123, - "monthly_seconds_limit": 6000, - "monthly_seconds_used": 2345, - "total_seconds_used": 5678, - "daily_trend": [ - {"date": "2026-03-06", "seconds": 45}, - {"date": "2026-03-07", "seconds": 120}, - {"date": "2026-03-08", "seconds": 0}, - {"date": "2026-03-09", "seconds": 88}, - {"date": "2026-03-10", "seconds": 200}, - {"date": "2026-03-11", "seconds": 156}, - {"date": "2026-03-12", "seconds": 123} - ] -} -``` - -### 5.15 [Phase 3] 用户个人中心 — 消费记录 - -``` -GET /api/v1/profile/records?page=1&page_size=20 -Authorization: Bearer - -Query Parameters: - page: int (默认 1) - page_size: int (默认 20) - -Response: 200 OK -{ - "total": 45, - "page": 1, - "page_size": 20, - "results": [ - { - "id": 101, - "created_at": "2026-03-12T14:30:00+08:00", - "seconds_consumed": 15, - "prompt": "一只猫在花园里追蝴蝶", - "mode": "universal", - "model": "seedance_2.0", - "aspect_ratio": "16:9", - "status": "completed" - }, - ... - ] -} -``` - -## 6. 数据模型 - -### 6.1 已有前端 Store 类型(保留) - -```typescript -// 创作模式 -type CreationMode = 'universal' | 'keyframe'; - -// 模型选项 -type ModelOption = 'seedance_2.0' | 'seedance_2.0_fast'; - -// 宽高比 -type AspectRatio = '16:9' | '9:16' | '1:1' | '21:9' | '4:3' | '3:4'; - -// 时长 -type Duration = 5 | 10 | 15; - -// 上传文件 -interface UploadedFile { - id: string; - file: File; - type: 'image' | 'video'; - previewUrl: string; - label: string; -} - -// 输入栏状态 Store -interface InputBarStore { - mode: CreationMode; - setMode: (mode: CreationMode) => void; - model: ModelOption; - setModel: (model: ModelOption) => void; - aspectRatio: AspectRatio; - setAspectRatio: (ratio: AspectRatio) => void; - duration: Duration; - setDuration: (duration: Duration) => void; - prompt: string; - setPrompt: (prompt: string) => void; - references: UploadedFile[]; - addReference: (file: File) => void; - removeReference: (id: string) => void; - clearReferences: () => void; - firstFrame: UploadedFile | null; - lastFrame: UploadedFile | null; - setFirstFrame: (file: File | null) => void; - setLastFrame: (file: File | null) => void; - canSubmit: () => boolean; - switchMode: (mode: CreationMode) => void; - submit: () => void; - reset: () => void; -} -``` - -### 6.2 已有下拉菜单配置(保留) - -```typescript -interface DropdownOption { - label: string; - value: string; - icon?: string; - disabled?: boolean; -} - -const generationTypes: DropdownOption[] = [ - { label: '视频生成', value: 'video', icon: 'video' }, - { label: '图片生成', value: 'image', icon: 'image' }, -]; - -const modelOptions: DropdownOption[] = [ - { label: 'Seedance 2.0', value: 'seedance_2.0', icon: 'diamond' }, - { label: 'Seedance 2.0 Fast', value: 'seedance_2.0_fast', icon: 'diamond' }, -]; - -const modeOptions: DropdownOption[] = [ - { label: '全能参考', value: 'universal', icon: 'sparkle' }, - { label: '首尾帧', value: 'keyframe', icon: 'swap' }, -]; - -const aspectRatioOptions: DropdownOption[] = [ - { label: '16:9', value: '16:9' }, - { label: '9:16', value: '9:16' }, - { label: '1:1', value: '1:1' }, - { label: '21:9', value: '21:9' }, - { label: '4:3', value: '4:3' }, - { label: '3:4', value: '3:4' }, -]; - -const durationOptions: DropdownOption[] = [ - { label: '5s', value: '5' }, - { label: '10s', value: '10' }, - { label: '15s', value: '15' }, -]; -``` - -### 6.3 [Phase 2] 前端 Auth Store 类型(Phase 3 修改配额字段) - -```typescript -interface User { - id: number; - username: string; - email: string; - is_staff: boolean; -} - -// [Phase 3] 配额字段改为秒数 -interface Quota { - daily_seconds_limit: number; // 原 daily_limit - daily_seconds_used: number; // 原 daily_used - monthly_seconds_limit: number; // 原 monthly_limit - monthly_seconds_used: number; // 原 monthly_used -} - -interface AuthStore { - // 状态 - user: User | null; - accessToken: string | null; - refreshToken: string | null; - isAuthenticated: boolean; - isLoading: boolean; - - // 操作 - login: (username: string, password: string) => Promise; - register: (username: string, email: string, password: string) => Promise; - logout: () => void; - refreshAccessToken: () => Promise; - fetchUserInfo: () => Promise; - - // 配额 - quota: Quota | null; - fetchQuota: () => Promise; -} -``` - -### 6.4 [Phase 3] 前端新增类型 - -```typescript -// 管理后台统计数据 -interface AdminStats { - total_users: number; - new_users_today: number; - seconds_consumed_today: number; // 原 calls_today - seconds_consumed_this_month: number; // 原 calls_this_month - today_change_percent: number; - month_change_percent: number; - daily_trend: { date: string; seconds: number }[]; - top_users: { user_id: number; username: string; seconds_consumed: number }[]; -} - -// 管理后台用户列表项 -interface AdminUser { - id: number; - username: string; - email: string; - is_active: boolean; - date_joined: string; - daily_seconds_limit: number; - monthly_seconds_limit: number; - seconds_today: number; - seconds_this_month: number; -} - -// 管理后台消费记录 -interface AdminRecord { - id: number; - created_at: string; - user_id: number; - username: string; - seconds_consumed: number; - prompt: string; - mode: CreationMode; - model: ModelOption; - aspect_ratio: string; - status: 'queued' | 'processing' | 'completed' | 'failed'; -} - -// 系统设置 -interface SystemSettings { - default_daily_seconds_limit: number; - default_monthly_seconds_limit: number; - announcement: string; - announcement_enabled: boolean; -} - -// 用户个人中心概览 -interface ProfileOverview { - daily_seconds_limit: number; - daily_seconds_used: number; - monthly_seconds_limit: number; - monthly_seconds_used: number; - total_seconds_used: number; - daily_trend: { date: string; seconds: number }[]; -} - -// 用户消费记录 -interface ProfileRecord { - id: number; - created_at: string; - seconds_consumed: number; - prompt: string; - mode: CreationMode; - model: ModelOption; - aspect_ratio: string; - status: 'queued' | 'processing' | 'completed' | 'failed'; -} - -// 分页响应 -interface PaginatedResponse { - total: number; - page: number; - page_size: number; - results: T[]; -} -``` - -### 6.5 后端数据模型(Django Models) - -#### [Phase 2 → Phase 3 修改] 用户模型 - -```python -# backend/apps/accounts/models.py - -from django.contrib.auth.models import AbstractUser -from django.db import models - -class User(AbstractUser): - """扩展用户模型 — Phase 3: 配额单位改为秒数""" - email = models.EmailField(unique=True, verbose_name='邮箱') - # [Phase 3] 改为秒数限制 - daily_seconds_limit = models.IntegerField(default=600, verbose_name='每日秒数上限') - monthly_seconds_limit = models.IntegerField(default=6000, verbose_name='每月秒数上限') - created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') - updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') - - class Meta: - verbose_name = '用户' - verbose_name_plural = '用户' -``` - -> **迁移说明**: 需要创建 Django migration 将 `daily_limit` → `daily_seconds_limit`,`monthly_limit` → `monthly_seconds_limit`,并将现有数据按「原次数 × 15」换算为秒数(假设平均每次生成 15 秒)。 - -#### [Phase 2 → Phase 3 修改] 生成记录模型 - -```python -# backend/apps/generation/models.py - -import uuid -from django.db import models -from django.conf import settings - -class GenerationRecord(models.Model): - """视频生成记录 — Phase 3: 新增消费秒数字段""" - MODE_CHOICES = [ - ('universal', '全能参考'), - ('keyframe', '首尾帧'), - ] - MODEL_CHOICES = [ - ('seedance_2.0', 'Seedance 2.0'), - ('seedance_2.0_fast', 'Seedance 2.0 Fast'), - ] - STATUS_CHOICES = [ - ('queued', '排队中'), - ('processing', '生成中'), - ('completed', '已完成'), - ('failed', '失败'), - ] - - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name='generation_records', - verbose_name='用户' - ) - task_id = models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='任务ID') - prompt = models.TextField(blank=True, verbose_name='提示词') - mode = models.CharField(max_length=20, choices=MODE_CHOICES, verbose_name='创作模式') - model = models.CharField(max_length=30, choices=MODEL_CHOICES, verbose_name='模型') - aspect_ratio = models.CharField(max_length=10, verbose_name='宽高比') - duration = models.IntegerField(verbose_name='视频时长(秒)') - seconds_consumed = models.FloatField(default=0, verbose_name='消费秒数') # [Phase 3] 新增 - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='queued', verbose_name='状态') - created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间') - - class Meta: - verbose_name = '生成记录' - verbose_name_plural = '生成记录' - ordering = ['-created_at'] - indexes = [ - models.Index(fields=['user', 'created_at']), - ] -``` - -> **消费秒数逻辑**: `seconds_consumed` = 视频 `duration`(5/10/15秒),在调用生成 API 时自动设置。 - -#### [Phase 2 → Phase 3 修改] 配额配置模型 - -```python -# backend/apps/generation/models.py (续) - -class QuotaConfig(models.Model): - """全局配额配置 — Phase 3: 改为秒数""" - default_daily_seconds_limit = models.IntegerField(default=600, verbose_name='默认每日秒数上限') - default_monthly_seconds_limit = models.IntegerField(default=6000, verbose_name='默认每月秒数上限') - announcement = models.TextField(blank=True, default='', verbose_name='系统公告') # [Phase 3] 新增 - announcement_enabled = models.BooleanField(default=False, verbose_name='启用公告') # [Phase 3] 新增 - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - verbose_name = '系统配置' - verbose_name_plural = '系统配置' - - def save(self, *args, **kwargs): - # 单例模式,确保只有一条记录 - self.pk = 1 - super().save(*args, **kwargs) -``` - -## 7. 非功能需求 - -### 性能要求 -- 首屏加载 < 2s(Vite 构建,代码分割) -- 文件上传后缩略图预览 < 200ms(使用 URL.createObjectURL 本地生成) -- 下拉菜单展开/收起动画 < 150ms -- 文本输入无感知延迟(非受控组件或 debounce) -- [Phase 2] 后端 API 响应时间 < 500ms(除文件上传外) -- [Phase 2] JWT Token 验证 < 50ms -- [Phase 3] ECharts 图表渲染 < 300ms(含数据加载) -- [Phase 3] 管理后台页面切换 < 200ms(路由级代码分割) -- [Phase 3] 分页列表请求 < 300ms - -### 安全要求 -- 文件上传仅限 image/* 和 video/* MIME 类型 -- 文件大小限制:图片 < 20MB,视频 < 100MB -- 使用 URL.createObjectURL 生成预览,组件卸载时调用 URL.revokeObjectURL 释放内存 -- [Phase 2] 密码使用 Django 默认的 PBKDF2 算法加密存储 -- [Phase 2] JWT Access Token 有效期 2 小时,Refresh Token 有效期 7 天 -- [Phase 2] 数据库密码等敏感配置通过环境变量管理(生产环境),开发环境可硬编码在 settings 中 -- [Phase 2] API 接口对未认证请求返回 401,对权限不足返回 403 -- [Phase 2] 管理接口仅 `is_staff=True` 用户可访问 -- [Phase 3] 管理后台操作(修改配额、禁用用户)需记录操作日志 -- [Phase 3] CSV 导出需要防止 CSV 注入攻击(prompt 字段可能包含特殊字符) - -### 响应式设计要求 -- 桌面端(≥1024px):InputBar 最大宽度 900px 居中;管理后台 Sidebar 240px + 内容区自适应 -- 平板端(768px-1023px):InputBar 宽度 90% 居中;管理后台 Sidebar 折叠为图标模式 -- 移动端(<768px):InputBar 宽度 95%,工具栏按钮文字隐藏仅显示图标;管理后台 Sidebar 隐藏,使用汉堡菜单 - -### 浏览器兼容 -- Chrome 90+、Firefox 90+、Safari 15+、Edge 90+ - -## 8. 验收标准 - -> **重要**: Phase 1 和 Phase 2 功能已完成。当前验收范围为 **Phase 3 全部功能**。Phase 1/2 功能保持不变,不重复验收。 - -### Phase 1 验收标准(已通过) - -#### P0(已全部通过) -1. 页面打开后显示深色全屏背景 `#0a0a0f`,底部居中显示 InputBar -2. InputBar 样式与参考截图视觉一致:背景 `#16161e`、边框 `#2a2a38`、圆角 `20px` -3. 默认处于「全能参考」模式,显示 [+ 参考内容] 上传按钮 + 提示词输入框 -4. 点击 [+ 参考内容] 可选择图片/视频文件,上传后显示带序号的缩略图 -5. 上传 1-5 张文件后缩略图正确显示,每个有 × 关闭按钮可删除 -6. 切换到「首尾帧」模式后,上传区变为首帧 ↔ 尾帧双框布局 -7. 工具栏所有按钮正确显示,布局与参考截图一致 -8. 发送按钮状态正确:无内容时灰色,有内容时蓝色 `#00b8e6` - -#### P1(已全部通过) -9. 「视频生成」下拉、「模型选择」下拉、「模式切换」下拉均可正常展开和选择 -10. 比例选择按钮点击弹出 6 个选项,选中后按钮文字更新 -11. 时长选择按钮点击弹出 3 个选项,选中后按钮文字更新 -12. 切换模式时联动正确:全能参考→首尾帧时比例变为「自动匹配」、时长变为 5s、隐藏 @ 按钮 -13. 文件上传支持拖拽 - -#### P2(已全部通过) -14. 下拉菜单有动画过渡效果 -15. `Ctrl/Cmd + Enter` 可触发发送 -16. 页面加载后文本输入框自动聚焦 -17. 移动端下工具栏按钮自适应 - -### Phase 2 验收标准(已通过) - -#### P0(已全部通过) -18. 未登录用户访问 `/` 自动跳转到 `/login` -19. 注册页表单验证正确,注册成功后自动登录跳转到首页 -20. 登录页输入正确凭据后成功登录,获取 JWT Token -21. 后端 Django 服务正常启动,能连接 MySQL 数据库 -22. 发送按钮点击后调用后端 API,后端记录调用并返回剩余配额 - -#### P1(已全部通过) -23. 管理员登录后可访问管理后台,普通用户无法访问 -24. 管理后台正确显示用户总数、调用统计等指标 -25. 管理员可修改用户的每日/每月调用限额 -26. 超出限额时前端显示友好提示,后端返回 429 状态码 -27. Django Admin 后台(`/admin/`)可管理用户和生成记录 - -### Phase 3 验收标准(当前迭代) - -#### P0(必须全部通过) -28. 所有「调用次数」展示改为「生成秒数」—— UserInfoBar 显示「剩余 Ns/Ns(日)」而非「剩余 N 次」 -29. 后端 User 模型字段 `daily_limit`/`monthly_limit` 迁移为 `daily_seconds_limit`/`monthly_seconds_limit` -30. GenerationRecord 模型新增 `seconds_consumed` 字段,生成 API 返回 `seconds_consumed` 和 `remaining_seconds_today` -31. 管理后台使用左侧 Sidebar + 右侧内容区布局,Sidebar 包含 4 个导航项(仪表盘/用户管理/消费记录/系统设置) -32. `/admin/dashboard` 仪表盘页面显示 4 个统计卡片(总用户、今日新增、今日消费秒数、本月消费秒数)+ ECharts 消费趋势折线图 + 用户消费排行柱状图 -33. `/admin/users` 用户管理页面支持分页列表、搜索筛选、编辑配额、启用/禁用用户 -34. `/profile` 用户个人中心显示消费概览(环形进度条)+ 消费记录列表 - -#### P1 -35. `/admin/records` 消费记录页面显示所有用户消费明细,支持时间范围筛选和导出 CSV -36. `/admin/settings` 系统设置页面支持修改全局默认配额和管理系统公告 -37. 用户个人中心显示消费趋势 Sparkline 迷你图(近 7天/30天 切换) -38. 仪表盘统计卡片显示环比变化百分比 + 趋势箭头 -39. 用户管理页面点击用户名展开详情抽屉,显示用户详情 + 近期消费记录 -40. 管理后台深色主题与 Linear/Vercel Dashboard 风格一致 -41. 数据加载使用骨架屏(Skeleton) - -#### P2 -42. 管理后台 Sidebar 支持折叠为图标模式 -43. 页面切换有 fade/slide 过渡动画 -44. 当日额度消费超过 80% 时用户端显示黄色警告提示 - -## 9. 模式切换联动逻辑 - -| 切换动作 | 上传区 | 比例 | 时长 | @ 按钮 | Placeholder | -|---------|--------|------|------|--------|-------------| -| → 全能参考 | [+ 参考内容] 多文件上传 | 恢复用户之前选择(默认 21:9) | 恢复用户之前选择(默认 15s) | 显示 | "上传1-5张参考图或视频,输入文字,自由组合图、文、音、视频多元素,定义精彩互动。" | -| → 首尾帧 | [首帧] ↔ [+ 尾帧] | 自动匹配(灰色不可选) | 5s(可切换) | 隐藏 | "输入描述,定义首帧到尾帧的运动过程" | - -> **注意**: 切换模式时不清空已输入的提示词文本,但上传的文件会被清空(因为两种模式的上传逻辑不同)。 - -## 10. 设计规范(Design Token) - -### 颜色 - -| Token | 值 | 用途 | -|-------|-----|------| -| `--color-bg-page` | `#0a0a0f` | 页面背景 | -| `--color-bg-input-bar` | `#16161e` | InputBar 背景 | -| `--color-border-input-bar` | `#2a2a38` | InputBar 边框 | -| `--color-primary` | `#00b8e6` | 主强调色(发送按钮、视频生成文字) | -| `--color-text-primary` | `#ffffff` | 主文字 | -| `--color-text-secondary` | `#8a8a9a` | 次文字(工具栏按钮、placeholder) | -| `--color-text-disabled` | `#4a4a5a` | 禁用文字 | -| `--color-bg-hover` | `rgba(255, 255, 255, 0.06)` | 按钮 hover 背景 | -| `--color-bg-dropdown` | `#1e1e2a` | 下拉菜单背景 | -| `--color-bg-upload` | `rgba(255, 255, 255, 0.04)` | 上传区背景 | -| `--color-border-upload` | `#2a2a38` | 上传区虚线边框 | -| `--color-btn-send-disabled` | `#3a3a4a` | 发送按钮禁用状态 | -| `--color-btn-send-active` | `#00b8e6` | 发送按钮激活状态 | - -### [Phase 3] 管理后台专用颜色 - -| Token | 值 | 用途 | -|-------|-----|------| -| `--color-bg-sidebar` | `#111118` | Sidebar 背景 | -| `--color-sidebar-active` | `rgba(255, 255, 255, 0.08)` | Sidebar 当前项背景 | -| `--color-sidebar-hover` | `rgba(255, 255, 255, 0.04)` | Sidebar hover 背景 | -| `--color-bg-card` | `#16161e` | 卡片/面板背景 | -| `--color-border-card` | `#2a2a38` | 卡片边框 | -| `--color-success` | `#00b894` | 正向指标(↑绿色) | -| `--color-danger` | `#e74c3c` | 负向指标(↓红色) / 禁用状态 | -| `--color-warning` | `#f39c12` | 警告提示(额度 80%+) | - -### 圆角 - -| Token | 值 | 用途 | -|-------|-----|------| -| `--radius-input-bar` | `20px` | InputBar 容器 | -| `--radius-btn` | `8px` | 工具栏按钮 | -| `--radius-send-btn` | `50%` | 发送按钮(圆形) | -| `--radius-thumbnail` | `8px` | 缩略图 | -| `--radius-dropdown` | `12px` | 下拉菜单 | -| `--radius-card` | `12px` | [Phase 3] 管理后台卡片 | - -### 尺寸 - -| Token | 值 | 用途 | -|-------|-----|------| -| `--input-bar-max-width` | `900px` | InputBar 最大宽度 | -| `--send-btn-size` | `36px` | 发送按钮直径 | -| `--thumbnail-size` | `80px` | 上传缩略图尺寸 | -| `--toolbar-height` | `44px` | 工具栏行高 | -| `--toolbar-btn-height` | `32px` | 工具栏按钮高度 | -| `--sidebar-width` | `240px` | [Phase 3] Sidebar 宽度 | -| `--sidebar-collapsed-width` | `64px` | [Phase 3] Sidebar 折叠宽度 | - -## 11. 组件树结构 - -``` -App -├── AuthProvider // [Phase 2] 认证上下文 -├── Router // [Phase 2] 路由 -│ ├── /login → LoginPage // [Phase 2] 登录页 -│ ├── /register → RegisterPage // [Phase 2] 注册页 -│ ├── / → ProtectedRoute // [Phase 2] 需要登录 -│ │ └── VideoGenerationPage -│ │ ├── UserInfoBar // [Phase 2] 顶部用户信息 + [Phase 3] 秒数配额 + 个人中心链接 -│ │ ├── PageBackground -│ │ └── InputBar -│ │ ├── InputArea -│ │ │ ├── UploadSection -│ │ │ │ ├── UniversalUpload -│ │ │ │ │ ├── UploadButton -│ │ │ │ │ └── ThumbnailGrid -│ │ │ │ │ └── ThumbnailItem -│ │ │ │ └── KeyframeUpload -│ │ │ │ ├── FrameUpload -│ │ │ │ ├── ArrowIcon -│ │ │ │ └── FrameUpload -│ │ │ └── PromptInput -│ │ └── Toolbar -│ │ ├── GenerationTypeDropdown -│ │ ├── ModelSelector -│ │ ├── ModeDropdown -│ │ ├── AspectRatioSelector -│ │ ├── DurationSelector -│ │ ├── AtButton -│ │ ├── FlexSpacer -│ │ └── SendButton -│ ├── /profile → ProtectedRoute // [Phase 3] 用户个人中心 -│ │ └── ProfilePage -│ │ ├── ProfileHeader // 返回首页 + 用户信息 -│ │ ├── ConsumptionOverview // 消费概览卡片 -│ │ │ ├── EChartsGauge // 环形进度条 -│ │ │ ├── DailyQuotaCard // 今日额度进度条 -│ │ │ └── MonthlyQuotaCard // 本月额度进度条 -│ │ ├── ConsumptionTrend // Sparkline 迷你趋势图 -│ │ └── ConsumptionRecordList // 消费记录列表 -│ ├── /admin → ProtectedRoute (requireAdmin) // [Phase 3] 管理后台 -│ │ └── AdminLayout // Sidebar + Content 布局 -│ │ ├── AdminSidebar // 左侧导航栏 -│ │ │ ├── SidebarLogo // Logo -│ │ │ ├── SidebarNav // 导航菜单 -│ │ │ │ ├── NavItem (仪表盘) -│ │ │ │ ├── NavItem (用户管理) -│ │ │ │ ├── NavItem (消费记录) -│ │ │ │ └── NavItem (系统设置) -│ │ │ └── SidebarFooter // 返回首页链接 -│ │ └── AdminContent // 右侧内容区 (Outlet) -│ │ ├── /admin/dashboard → DashboardPage -│ │ │ ├── StatsCards // 4 个指标卡片 -│ │ │ ├── TrendLineChart // ECharts 折线图 -│ │ │ └── TopUsersBarChart // ECharts 柱状图 -│ │ ├── /admin/users → UsersPage -│ │ │ ├── UserSearchBar // 搜索 + 筛选 -│ │ │ ├── UserTable // Arco Table -│ │ │ ├── QuotaEditModal // 配额编辑弹窗 -│ │ │ └── UserDetailDrawer // 用户详情抽屉 -│ │ ├── /admin/records → RecordsPage -│ │ │ ├── RecordFilters // 筛选条件 -│ │ │ ├── RecordTable // Arco Table -│ │ │ └── ExportButton // 导出 CSV -│ │ └── /admin/settings → SettingsPage -│ │ ├── QuotaSettingsCard // 全局配额表单 -│ │ └── AnnouncementCard // 公告管理 -│ └── * → Navigate to / -``` - -## 12. 后端项目结构 - -``` -backend/ -├── manage.py -├── requirements.txt -├── config/ # Django 项目配置 -│ ├── __init__.py -│ ├── settings.py -│ ├── urls.py -│ ├── wsgi.py -│ └── asgi.py -├── apps/ -│ ├── accounts/ # 用户认证模块 -│ │ ├── __init__.py -│ │ ├── models.py # User 扩展模型 (Phase 3: 秒数字段) -│ │ ├── serializers.py # 注册/登录序列化器 -│ │ ├── views.py # 注册/登录/Token刷新 API -│ │ ├── urls.py -│ │ └── admin.py # 用户管理 Admin -│ └── generation/ # 视频生成模块 -│ ├── __init__.py -│ ├── models.py # GenerationRecord (Phase 3: +seconds_consumed) + QuotaConfig (Phase 3: +announcement) -│ ├── serializers.py # [Phase 3] 新增管理后台 + 个人中心序列化器 -│ ├── views.py # [Phase 3] 新增管理后台 API (stats/users/records/settings) + 个人中心 API (overview/records) -│ ├── urls.py # [Phase 3] 新增路由 -│ ├── admin.py # 生成记录 Admin -│ └── middleware.py # 配额检查中间件 (Phase 3: 改为秒数检查) -``` - -## 13. 参考截图说明 - -参考截图存放于 `/Users/maidong/Desktop/zyc/研究openclaw/视频生成平台/` 目录: - -| 文件 | 内容描述 | -|------|---------| -| `20260311-154443.jpeg` | **空状态全貌** — 完整展示了 InputBar 在无输入时的样式 | -| `20260311-154432.jpeg` | **已上传状态** — 展示了上传 1 张图片后的 InputBar | -| `20260311-154407.jpeg` | **有内容状态** — 展示了输入完整提示词 + 多张图片引用后的 InputBar | - -## 14. 修订历史 - -| 日期 | 版本 | 变更内容 | -|------|------|---------| -| 2026-03-11 | v1.0 | 初始版本 — 纯前端视频生成输入界面 | -| 2026-03-12 | v2.0 | 增量迭代 — 新增 Django 后端、用户认证系统、后台管理系统 | -| 2026-03-12 | v2.1 | **需求修订(BUG-002)** — 引入开发阶段划分(Phase 1 / Phase 2),验收标准按阶段分组 | -| 2026-03-12 | v2.2 | **需求修订(BUG-002 后续)** — Phase 2 功能已完成开发,更新阶段状态和验收范围 | -| 2026-03-12 | v3.0 | **重大迭代 — Phase 3** — 计量单位从「调用次数」改为「生成秒数」;管理后台从单页面重做为多页面 Sidebar 布局(仪表盘/用户管理/消费记录/系统设置);新增用户个人中心(/profile)含消费概览、环形进度条、消费趋势 Sparkline、消费记录列表;引入 ECharts 图表库;管理后台深色主题升级(参考 Linear/Vercel 风格);新增骨架屏加载和页面过渡动画;新增 8 个 API 端点、修改 3 个已有 API 端点 | diff --git a/docs/todo/提示词AI优化功能.md b/docs/todo/提示词AI优化功能.md new file mode 100644 index 0000000..8274c4b --- /dev/null +++ b/docs/todo/提示词AI优化功能.md @@ -0,0 +1,65 @@ +# 提示词 AI 优化功能 + +**状态**:待开发 +**创建日期**:2026-04-17 + +## 需求背景 + +用户写提示词时,经常写得过于简单或不符合 Seedance 2.0 的提示词规范(如没用「图片n」引用素材、缺少核心要素、镜头语言模糊等),导致生成效果不理想。 + +引入火山官方的 SKILL.md(Seedance 2.0 Prompt Optimizer)能力,让用户在写完提示词后一键优化。 + +## 功能设计 + +### 用户视角 +1. 用户在提示词输入框输入原始提示词(带 @素材引用) +2. 点击输入框旁的「AI 优化」按钮 +3. 弹出预览弹窗,显示优化后的提示词 +4. 用户点「采纳」→ 替换原提示词;点「取消」→ 保留原文 +5. 消耗一定 token 数(计入团队 token 池) + +### 技术方案 + +**后端** +- 新接口:`POST /api/v1/prompt/optimize` +- 入参:`prompt`(原始提示词,含 `@素材` 标记)、`asset_refs`(素材引用列表:label + type + url) +- 调用豆包模型(推荐 `doubao-seed-2.0` 最新版本,具体 model id 需确认) +- System prompt:基于 SKILL.md 改造成**一次性输出**模式(不做多轮交互) +- 返回:`optimized_prompt`(优化后的文本)+ `token_used`(消耗 token 数) +- 同时扣减团队 token 池 + +**前端** +- `PromptInput` 组件右侧加「AI 优化」按钮(带 ✨ 图标) +- 点击后:loading 状态 → 调用后端接口 → 弹出 `PromptOptimizeModal` 预览弹窗 +- 弹窗显示:原始 vs 优化对比、token 消耗提示、采纳/取消按钮 +- 采纳后:把优化结果写回 editor(保持 @mention 标签正确渲染) + +**SKILL.md 改造要点** +- 去掉 Step 0(主动引导提问)→ 一次输入一次输出 +- 去掉 Step 3 的「多选模板交互」→ 如遇歧义/冲突,在输出里以备注形式标注(如 `【注:检测到 X 冲突,已按 Y 处理】`) +- 保留 Step 2(素材自动映射 `@图N`)、Step 4(结构化输出:优化后提示词 / 优化问题 / 相关原则) + +## 计费设计 +- 提示词优化和视频生成共用**同一个 token 池**(用户已熟悉的计费机制) +- 不单独限额,按实际 token 消耗扣减 +- 前端展示:"本次优化消耗约 X token" + +## 模型选择 +- **首选**:豆包 2.0 系列最强模型(需查火山文档确认最新 model id) +- 备选:`doubao-1-5-pro-32k`(成本更低,任务够用) + +## 待确认事项 +- [ ] 豆包 2.0 系列当前最强模型的具体 model id +- [ ] Token 池扣减逻辑是否需要团队/个人双重配额 +- [ ] 优化失败时(LLM 报错、token 超限)的前端兜底提示 + +## 验收标准 +1. 用户输入粗糙提示词(如「美女跳舞」)→ 优化后符合 SKILL.md 的三段式结构(全局设定 / 时间线脚本 / 质感风格与约束) +2. 带 `@素材` 的提示词 → 优化后正确使用 `@图1/@图2/@视频1` 等标记 +3. 冲突/缺失场景 → 在输出中以备注标明,不擅自填充 +4. Token 消耗正确扣减到团队池 +5. 用户可在弹窗中选择采纳或取消 + +## 参考文件 +- SKILL.md(火山提供的原始技能文件) +- `docs/API文档/seedance 2.0 系列教程.MD` 第 2152 行起的「提示词技巧」部分 diff --git a/test-report.md b/test-report.md deleted file mode 100644 index cab323d..0000000 --- a/test-report.md +++ /dev/null @@ -1,175 +0,0 @@ -# 测试报告 - -## 测试结论: ALL_PASSED - -## Bug 路由摘要 -- CODE_BUG: 0 个(路由到开发 Agent) -- DESIGN_BUG: 0 个(路由到设计 Agent) -- REQUIREMENT_BUG: 0 个(路由到产品 Agent) - -## 测试概要 -- 单元测试: 224 通过 / 0 失败(9 个测试文件) -- E2E 测试: 49 通过 / 0 失败(3 个测试文件) -- 视觉质量检查: 7 通过 / 0 失败 -- 后端 API 验证: 12 端点全部通过 -- Django 系统检查: 0 问题 -- 数据库迁移: 完整 - -## 通过的测试 - -### 单元测试(224 个) - -#### 既有测试文件(已更新适配 Phase 3) -- ✅ test/unit/apiClient.test.ts — API 客户端测试(authApi、videoApi、adminApi、profileApi) -- ✅ test/unit/phase2Components.test.tsx — 组件类型测试(已更新为秒数配额格式) -- ✅ test/unit/auth.test.ts — 认证流程测试 -- ✅ test/unit/videoGeneration.test.ts — 视频生成测试 -- ✅ test/unit/adminPanel.test.ts — 管理面板测试 -- ✅ test/unit/uiComponents.test.tsx — UI 组件测试 -- ✅ test/unit/routerSetup.test.tsx — 路由配置测试 -- ✅ test/unit/phase2Features.test.ts — Phase 2 功能测试 - -#### Phase 3 新增测试(62 个,test/unit/phase3Features.test.ts) -- ✅ 秒数配额系统验证(7 个)— Quota 类型字段、默认值 600s/6000s、使用量追踪 -- ✅ 管理后台多页面布局验证(7 个)— AdminLayout 侧边栏、4 个导航链接、折叠功能 -- ✅ 仪表盘页面验证(8 个)— ECharts 图表、统计卡片、趋势数据、骨架屏加载 -- ✅ 用户管理页面验证(6 个)— 用户表格、搜索过滤、配额编辑、状态切换 -- ✅ 消费记录页面验证(5 个)— 记录表格、日期范围过滤、CSV 导出、分页 -- ✅ 系统设置页面验证(4 个)— 全局配额默认值、公告管理、开关切换 -- ✅ 个人中心页面验证(8 个)— 仪表盘图表、进度条、趋势图、消费记录列表 -- ✅ 后端 API 路由验证(4 个)— profile 和 admin URL 配置完整 -- ✅ QuotaConfig 模型验证(3 个)— 单例模式、默认值 -- ✅ ProtectedRoute 管理员守卫(1 个)— requireAdmin 属性 -- ✅ UserInfoBar 导航(2 个)— 秒数显示、个人中心链接 -- ✅ ECharts 集成(3 个)— echarts-for-react 依赖、图表组件配置 -- ✅ 路由配置(4 个)— 嵌套路由、重定向、通配符 - -### E2E 测试(49 个) - -#### 既有 E2E 测试(26 个) -- ✅ test/e2e/video-generation.spec.ts — 视频生成页 P0/P1/P2 验收 + Sidebar(14 个) -- ✅ test/e2e/auth-flow.spec.ts — 认证流程(注册、登录、登出、路由保护)(12 个) - -#### Phase 3 新增 E2E 测试(23 个,test/e2e/phase3-admin-profile.spec.ts) - -**个人中心页面(7 个)** -- ✅ 已认证用户可访问个人中心 -- ✅ 显示消费概览(今日额度、本月额度) -- ✅ 显示消费趋势(近 7 天 / 近 30 天切换) -- ✅ 显示消费记录(新用户显示"暂无记录") -- ✅ 返回首页导航 -- ✅ 退出按钮可见 -- ✅ 未认证用户重定向到登录页 - -**UserInfoBar 配额与导航(2 个)** -- ✅ 显示秒数格式配额(剩余: Xs/Xs(日)) -- ✅ 个人中心链接导航到 /profile - -**管理后台权限控制(6 个)** -- ✅ 非管理员用户无法访问 /admin/dashboard -- ✅ 非管理员用户无法访问 /admin/users -- ✅ 非管理员用户无法访问 /admin/records -- ✅ 非管理员用户无法访问 /admin/settings -- ✅ /admin 重定向到 /admin/dashboard -- ✅ 未认证访问 /admin 重定向到 /login - -**后端 API 集成(7 个)** -- ✅ GET /api/v1/auth/me 返回秒数配额(daily_seconds_limit=600, monthly_seconds_limit=6000) -- ✅ GET /api/v1/profile/overview 返回消费数据(7 天趋势) -- ✅ GET /api/v1/profile/overview 支持 30 天周期 -- ✅ GET /api/v1/profile/records 返回分页记录 -- ✅ POST /api/v1/video/generate 消耗秒数(seconds_consumed=10, remaining=590) -- ✅ 管理端点对非管理员返回 403 -- ✅ 未认证请求返回 401 - -**趋势切换(1 个)** -- ✅ 点击 30 天标签更新趋势周期 - -### 后端 API 验证(curl 直接测试) -- ✅ POST /api/v1/auth/register — 201 Created -- ✅ POST /api/v1/auth/login — 200 OK -- ✅ GET /api/v1/auth/me — 返回秒数配额 -- ✅ GET /api/v1/profile/overview?period=7d — 7 天趋势 -- ✅ GET /api/v1/profile/overview?period=30d — 30 天趋势 -- ✅ GET /api/v1/profile/records — 分页记录 -- ✅ GET /api/v1/admin/stats — 管理统计数据 -- ✅ GET /api/v1/admin/users — 用户列表(分页、搜索) -- ✅ GET /api/v1/admin/records — 消费记录(日期过滤) -- ✅ GET /api/v1/admin/settings — 系统设置 -- ✅ POST /api/v1/video/generate — 202 Accepted,seconds_consumed=10 -- ✅ Django system check: 0 issues, migrations up-to-date - -## 失败的测试(Bug 列表) - -无 - -## 视觉质量检查 - -| 检查项 | 状态 | 说明 | -|-------|------|------| -| 登录页面 | ✅ | 暗色主题,表单完整,注册链接可见 | -| 主页面(已登录) | ✅ | 显示"剩余: 600s/600s(日)"秒数配额,个人中心和管理后台入口可见 | -| 管理后台 — 仪表盘 | ✅ | 侧边栏 4 个导航项,统计卡片(用户数、今日消费、本月消费、活跃用户),ECharts 折线图和柱状图 | -| 管理后台 — 用户管理 | ✅ | 用户表格含秒数配额列,搜索过滤,分页功能 | -| 管理后台 — 消费记录 | ✅ | 记录表格含时间/用户/秒数/描述/模式/状态列,日期范围过滤,导出 CSV 按钮 | -| 管理后台 — 系统设置 | ✅ | 全局默认配额(600s/6000s),系统公告开关和文本输入 | -| 个人中心 | ✅ | 仪表盘图表(0s/600s),今日/本月额度卡片含进度条,消费趋势(近7天/近30天),消费记录(暂无记录) | - -### 视觉截图文件 -- screenshot-login-phase3.png — 登录页 -- screenshot-main-page-phase3.png — 主页面 -- screenshot-admin-dashboard-phase3.png — 管理仪表盘 -- screenshot-admin-users-phase3.png — 用户管理 -- screenshot-admin-records-phase3.png — 消费记录 -- screenshot-admin-settings-phase3.png — 系统设置 -- screenshot-profile-phase3.png — 个人中心 - -## 上一轮 Bug 修复验证 — 全部 ✅ - -| 原 Bug | 状态 | 验证方式 | -|--------|------|---------| -| API 拦截器在登录端点 401 重定向 | ✅ 已修复 | `api.ts:24-25` 排除 `/auth/login`、`/auth/register`、`/auth/token/refresh` 端点 | -| 音频类型死代码(types/store/components) | ✅ 已修复 | 源码审查确认 `UploadedFile.type` 为 `'image' \| 'video'`,音频分支已全部移除 | -| GenerationCard 硬编码模型名 | ✅ 已修复 | `GenerationCard.tsx:54` 使用 `task.model` 动态渲染 | -| 无文件大小验证 | ✅ 已修复 | UniversalUpload、InputBar、KeyframeUpload 均有 20MB/100MB 限制 | - -## Phase 3 功能覆盖总结 - -| 功能模块 | 前端 | 后端 API | 单元测试 | E2E 测试 | 视觉检查 | -|---------|------|---------|---------|---------|---------| -| 秒数配额系统 | ✅ | ✅ | ✅ | ✅ | ✅ | -| 管理后台侧边栏 | ✅ | — | ✅ | ✅ | ✅ | -| 仪表盘(ECharts) | ✅ | ✅ | ✅ | — | ✅ | -| 用户管理 | ✅ | ✅ | ✅ | — | ✅ | -| 消费记录 | ✅ | ✅ | ✅ | — | ✅ | -| 系统设置 | ✅ | ✅ | ✅ | — | ✅ | -| 个人中心 | ✅ | ✅ | ✅ | ✅ | ✅ | -| 权限控制 | ✅ | ✅ | ✅ | ✅ | — | -| CSV 导出 | ✅ | — | ✅ | — | ✅ | - -## 测试环境 -- 前端: Vite 5 + React 18 + TypeScript -- 后端: Django 4.2 + DRF + SimpleJWT -- 单元测试: Vitest 3.x (jsdom) -- E2E 测试: Playwright 1.50 -- 数据库: SQLite (开发环境) -- Node.js: v22+ -- Python: 3.12+ - -## 测试文件清单 - -| 文件 | 类型 | 测试数 | -|------|------|--------| -| `test/unit/inputBarStore.test.ts` | Store 单元测试 | 31 | -| `test/unit/generationStore.test.ts` | Store 单元测试 | 15 | -| `test/unit/authStore.test.ts` | Store 单元测试 | 15 | -| `test/unit/apiClient.test.ts` | API 客户端测试 | 20 | -| `test/unit/phase2Components.test.tsx` | Phase 2 组件测试 | 17 | -| `test/unit/designTokens.test.ts` | CSS/设计规范测试 | 30 | -| `test/unit/components.test.tsx` | 组件渲染测试 | 16 | -| `test/unit/bugfixVerification.test.ts` | Bug 修复验证 | 15 | -| `test/unit/phase3Features.test.ts` | Phase 3 功能测试 | 62 | -| `test/e2e/video-generation.spec.ts` | E2E 视频生成 | 14 | -| `test/e2e/auth-flow.spec.ts` | E2E 认证流程 | 12 | -| `test/e2e/phase3-admin-profile.spec.ts` | E2E Phase 3 | 23 | -| **总计** | | **270** | From 39667ff19ce03177fb18498a78b189cc6e4f15eb Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 17 Apr 2026 18:23:52 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20v0.19.0=201080P=20=E5=88=86?= =?UTF-8?q?=E8=BE=A8=E7=8E=87=E6=94=AF=E6=8C=81=20=E2=80=94=20=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E5=89=8D=E5=90=8E=E7=AB=AF=20+=20=E4=B8=A5=E6=A0=BC?= =?UTF-8?q?=E8=AE=A1=E8=B4=B9=E5=87=86=E7=A1=AE=E6=80=A7=20+=2047=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 火山 Seedance 2.0 于 2026-04-16 上线 1080P 支持。本次实现前端 UI、 后端校验/计费、数据库迁移,并严格遵守三原则: 1. 禁止兜底/静默降级 — Fast+1080P 组合在 UI/store/serializer/view/计价 五层防御,任一层穿透都 fail loud,不悄悄按 720P 扣费 2. 钱的计算绝对准确 — 前端预估公式与后端 estimate_tokens 完全一致 `(输入时长+输出时长) × 宽 × 高 × fps / 1024`;实际扣费按火山返回 total_tokens × 官方单价;预估端不维护最低 token 修正表 3. 不隐藏 bug — 无 `or '720p'` / `|| '720p'` 兜底;类型严格;异常暴露 ## 后端(7 处 + 1 次迁移) - models.py: QuotaConfig 加 base_token_price_1080p(51)/base_token_price_1080p_video(31); GenerationRecord.resolution 加 RESOLUTION_CHOICES 约束 + default='720p' - migrations/0020: 含 RunPython data migration 回填历史 resolution='' → '720p' - utils/billing.py: * RESOLUTION_MAP 加 1080P 六种宽高比(21:9 是 2206×946,不是 seedance 1.0 值) * get_resolution 去掉 tier 默认值,非法组合 raise KeyError 不静默降级 * estimate_tokens 纯官方公式,加 input_video_duration 参数(公式完整) - utils/airdrama_client.py: create_task 加 resolution 必填参数(无默认值) - apps/generation/serializers.py: * VideoGenerateSerializer 加 resolution ChoiceField * aspect_ratio 改 ChoiceField 显式拒绝 adaptive * SystemSettingsSerializer 加 2 个 1080P 单价 - apps/generation/views.py: * _get_token_price 加 resolution 必填参数,Fast+1080P raise ValueError * _sum_video_duration 累加视频参考时长 * video_generate_view 读 resolution、400 拒绝 Fast+1080P 组合、 传给 get_resolution/estimate_tokens/_get_token_price/create_task/ GenerationRecord.resolution(移除 L450 硬编码 '720p') * _settle_payment 按 record.resolution 取单价(1080P 结算按 1080P 价) * _serialize_task + 5 处手工序列化加 resolution 字段(无 `or '720p'`) - apps/accounts/views.py: team 接口返回 token_price_1080p/_video ## 前端(10 处) - types/index.ts: Resolution 类型;GenerationTask/BackendTask/Team/ QuotaConfig/AssetVideo 加字段(全部必填,无 optional) - store/inputBar.ts: resolution state;setModel/setResolution 双向拦截 Fast+1080P 组合,toast 提示引导,不静默降级 - store/generation.ts: addTask/backendToFrontend/reEdit/regenerate 全链路 携带 resolution;mapErrorMessage 改 '今日生成次数或团队余额不足' - components/Toolbar.tsx: * 加分辨率选择器 Dropdown(位置:比例和时长之间) * modelItems/resolutionItems 双向 disabled(Fast 下 1080P 灰 / 1080P 下 Fast 灰) * estimatedTokens 对齐后端公式(含输入视频时长 + assetMentions 视频时长) * estimatedCost 按 resolution 选单价(Fast→fast_*、1080p→1080p_*、其他→基础) * tooltip 明示"实际费用以火山 API 返回的 token 数为准" - components/Dropdown.tsx: 加 disabled 属性支持 - components/VideoDetailModal.tsx: 重新编辑恢复 resolution - components/GenerationCard.tsx: 动态显示 task.resolution.toUpperCase() - pages/SettingsPage.tsx: 加 2 个 1080P 单价输入框(独立分组) - pages/AdminAssetsPage.tsx / TeamAssetsPage.tsx: 去 || '720p' 兜底 - lib/api.ts: videoApi.generate 参数 resolution 必填 ## 测试(47 个用例) ### 后端(28 个) - tests/test_1080p_billing.py(23): RESOLUTION_MAP 像素、estimate_tokens 公式(含/不含输入视频、不做最低 token 修正)、_get_token_price 六种 组合、Fast+1080P 抛异常、calculate_cost 对齐官方示例 4.97 / 12.39 元 - tests/test_1080p_api.py(5): video_generate_view 拒绝 Fast+1080P (400) + 拒绝 adaptive + 拒绝非法 resolution + 默认值兼容 + 合法组合通过 ### 前端(19 个) - test/unit/resolution1080p.test.ts(14): store 状态、双向拦截 (1080P 下切 Fast 被阻止 model 不变、反向同样)、官方像素契约测试、 价格示例对齐(720P 4.97 / 1080P 12.39) - test/e2e/resolution-1080p.spec.ts(5): 真实浏览器验证默认 720P、 Dropdown 双向置灰、tooltip 明示以火山为准 ## 与官方文档对齐 - 参数:resolution (480p/720p/1080p 小写)、ratio、duration、generate_audio - 像素:来自 docs/API文档/创建视频生成任务API.md Seedance 2.0 & 2.0 fast 列 - 单价:来自 docs/API文档/seedance模型价格.md (46/28/51/31/37/22) - Fast 不支持 1080P:来自 docs/API文档/Seedance 2.0 1080P.md Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/apps/accounts/views.py | 2 + ...aconfig_base_token_price_1080p_and_more.py | 41 ++++ backend/apps/generation/models.py | 9 +- backend/apps/generation/serializers.py | 7 +- backend/apps/generation/views.py | 64 +++++- backend/tests/test_1080p_api.py | 131 +++++++++++ backend/tests/test_1080p_billing.py | 208 ++++++++++++++++++ backend/utils/airdrama_client.py | 6 +- backend/utils/billing.py | 54 ++++- web/src/components/Dropdown.tsx | 5 +- web/src/components/GenerationCard.tsx | 2 +- web/src/components/Toolbar.tsx | 85 +++++-- web/src/components/VideoDetailModal.tsx | 1 + web/src/lib/api.ts | 1 + web/src/pages/AdminAssetsPage.tsx | 1 + web/src/pages/SettingsPage.tsx | 27 ++- web/src/pages/TeamAssetsPage.tsx | 1 + web/src/store/generation.ts | 8 +- web/src/store/inputBar.ts | 30 ++- web/src/types/index.ts | 8 + web/test/e2e/resolution-1080p.spec.ts | 136 ++++++++++++ web/test/unit/resolution1080p.test.ts | 160 ++++++++++++++ 22 files changed, 950 insertions(+), 37 deletions(-) create mode 100644 backend/apps/generation/migrations/0020_quotaconfig_base_token_price_1080p_and_more.py create mode 100644 backend/tests/test_1080p_api.py create mode 100644 backend/tests/test_1080p_billing.py create mode 100644 web/test/e2e/resolution-1080p.spec.ts create mode 100644 web/test/unit/resolution1080p.test.ts diff --git a/backend/apps/accounts/views.py b/backend/apps/accounts/views.py index db8cecd..a11d299 100644 --- a/backend/apps/accounts/views.py +++ b/backend/apps/accounts/views.py @@ -241,6 +241,8 @@ def me_view(request): 'token_price_video': float(config.base_token_price_video) * markup_mult, 'token_price_fast': float(config.base_token_price_fast) * markup_mult, 'token_price_fast_video': float(config.base_token_price_fast_video) * markup_mult, + 'token_price_1080p': float(config.base_token_price_1080p) * markup_mult, + 'token_price_1080p_video': float(config.base_token_price_1080p_video) * markup_mult, 'is_active': team.is_active, } data['team_disabled'] = not team.is_active diff --git a/backend/apps/generation/migrations/0020_quotaconfig_base_token_price_1080p_and_more.py b/backend/apps/generation/migrations/0020_quotaconfig_base_token_price_1080p_and_more.py new file mode 100644 index 0000000..7183cd5 --- /dev/null +++ b/backend/apps/generation/migrations/0020_quotaconfig_base_token_price_1080p_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.29 on 2026-04-17 18:09 + +from django.db import migrations, models + + +def backfill_empty_resolution(apps, schema_editor): + """将历史 resolution='' 的记录回填为 '720p'(choices 约束前的旧数据)。""" + GenerationRecord = apps.get_model('generation', 'GenerationRecord') + GenerationRecord.objects.filter(resolution='').update(resolution='720p') + + +def reverse_backfill(apps, schema_editor): + """回滚时不恢复为空字符串(历史数据无法精确识别)。""" + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('generation', '0019_duration_nullable'), + ] + + operations = [ + migrations.AddField( + model_name='quotaconfig', + name='base_token_price_1080p', + field=models.DecimalField(decimal_places=2, default=51, max_digits=10, verbose_name='1080P单价-不含视频(元/百万tokens)'), + ), + migrations.AddField( + model_name='quotaconfig', + name='base_token_price_1080p_video', + field=models.DecimalField(decimal_places=2, default=31, max_digits=10, verbose_name='1080P单价-含视频(元/百万tokens)'), + ), + # 先回填历史空值,再改 choices 约束,避免 MySQL 严格模式 IntegrityError + migrations.RunPython(backfill_empty_resolution, reverse_backfill), + migrations.AlterField( + model_name='generationrecord', + name='resolution', + field=models.CharField(choices=[('480p', '480P'), ('720p', '720P'), ('1080p', '1080P')], default='720p', max_length=10, verbose_name='分辨率'), + ), + ] diff --git a/backend/apps/generation/models.py b/backend/apps/generation/models.py index ffa19a3..f7f3ee0 100644 --- a/backend/apps/generation/models.py +++ b/backend/apps/generation/models.py @@ -19,6 +19,11 @@ class GenerationRecord(models.Model): ('completed', '已完成'), ('failed', '失败'), ] + RESOLUTION_CHOICES = [ + ('480p', '480P'), + ('720p', '720P'), + ('1080p', '1080P'), + ] user = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -39,7 +44,7 @@ class GenerationRecord(models.Model): cost_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='用户费用(元)') base_cost_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='平台成本(元)') frozen_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='冻结金额(元)') - resolution = models.CharField(max_length=10, blank=True, default='', verbose_name='分辨率') + resolution = models.CharField(max_length=10, choices=RESOLUTION_CHOICES, default='720p', verbose_name='分辨率') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='queued', verbose_name='状态') result_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='生成结果URL') thumbnail_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='视频缩略图URL') @@ -97,6 +102,8 @@ class QuotaConfig(models.Model): base_token_price_video = models.DecimalField(max_digits=10, decimal_places=2, default=28, verbose_name='基础token单价-含视频(元/百万tokens)') base_token_price_fast = models.DecimalField(max_digits=10, decimal_places=2, default=37, verbose_name='Fast单价-不含视频(元/百万tokens)') base_token_price_fast_video = models.DecimalField(max_digits=10, decimal_places=2, default=22, verbose_name='Fast单价-含视频(元/百万tokens)') + base_token_price_1080p = models.DecimalField(max_digits=10, decimal_places=2, default=51, verbose_name='1080P单价-不含视频(元/百万tokens)') + base_token_price_1080p_video = models.DecimalField(max_digits=10, decimal_places=2, default=31, verbose_name='1080P单价-含视频(元/百万tokens)') updated_at = models.DateTimeField(auto_now=True) class Meta: diff --git a/backend/apps/generation/serializers.py b/backend/apps/generation/serializers.py index 0b75884..2775497 100644 --- a/backend/apps/generation/serializers.py +++ b/backend/apps/generation/serializers.py @@ -5,8 +5,11 @@ class VideoGenerateSerializer(serializers.Serializer): prompt = serializers.CharField(required=False, allow_blank=True, default='') mode = serializers.ChoiceField(choices=['universal', 'keyframe']) model = serializers.ChoiceField(choices=['seedance_2.0', 'seedance_2.0_fast']) - aspect_ratio = serializers.CharField(max_length=10) + # 显式枚举拒绝 adaptive(火山默认值)— 估算/计费需要明确宽高 + aspect_ratio = serializers.ChoiceField(choices=['16:9', '9:16', '4:3', '1:1', '3:4', '21:9']) duration = serializers.IntegerField() + # 1080p 仅 Seedance 2.0 支持,Fast 不支持 — 上层 video_generate_view 会做 model/resolution 组合校验 + resolution = serializers.ChoiceField(choices=['480p', '720p', '1080p'], required=False, default='720p') references = serializers.ListField(child=serializers.DictField(), required=False, default=list) @@ -40,6 +43,8 @@ class SystemSettingsSerializer(serializers.Serializer): base_token_price_video = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=0, required=False) base_token_price_fast = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=0, required=False) base_token_price_fast_video = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=0, required=False) + base_token_price_1080p = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=0, required=False) + base_token_price_1080p_video = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=0, required=False) announcement = serializers.CharField(required=False, allow_blank=True, default='') announcement_enabled = serializers.BooleanField(required=False, default=False) max_desktop_sessions = serializers.IntegerField(min_value=1, required=False, default=1) diff --git a/backend/apps/generation/views.py b/backend/apps/generation/views.py index 92774d0..5f184e7 100644 --- a/backend/apps/generation/views.py +++ b/backend/apps/generation/views.py @@ -55,10 +55,38 @@ def _has_video_reference(references): return any(ref.get('type') == 'video' for ref in references) -def _get_token_price(config, model, has_video_ref): - """根据模型和是否有视频参考选择单价。""" +def _sum_video_duration(references): + """累加所有视频类型参考素材的 duration(秒),用于 token 估算的输入时长。""" + if not references: + return 0.0 + total = 0.0 + for ref in references: + if ref.get('type') == 'video': + try: + total += float(ref.get('duration') or 0) + except (ValueError, TypeError): + continue + return total + + +def _get_token_price(config, model, has_video_ref, resolution): + """根据模型、是否含视频、分辨率选择单价。 + + 约束(与官方文档一致): + - Seedance 2.0 Fast 不支持 1080p — 此组合在 UI 层已阻止、VideoGenerateSerializer + 也会在 video_generate_view 中拒绝。若仍进到这里,表示前端约束失效或绕过前端 + 直接调 API,应 fail loud,绝不按 720p 价静默降级(那是欺骗用户)。 + - 1080p 仅 Seedance 2.0 使用独立单价(51/31) + - 480p 和 720p 共享同一单价 + """ + if model == 'seedance_2.0_fast' and resolution == '1080p': + raise ValueError( + 'Seedance 2.0 Fast 不支持 1080p — 前端应阻止此组合,不应进到计价函数' + ) if model == 'seedance_2.0_fast': return config.base_token_price_fast_video if has_video_ref else config.base_token_price_fast + if resolution == '1080p': + return config.base_token_price_1080p_video if has_video_ref else config.base_token_price_1080p return config.base_token_price_video if has_video_ref else config.base_token_price @@ -175,15 +203,26 @@ def video_generate_view(request): mode = serializer.validated_data['mode'] model = serializer.validated_data['model'] aspect_ratio = serializer.validated_data['aspect_ratio'] + # serializer 已设 default='720p' + choices 约束,validated_data 必有合法值 + resolution = serializer.validated_data['resolution'] search_mode = request.data.get('search_mode', 'off') seed = _safe_int(request.data.get('seed', -1), -1) + # 1080P 仅 Seedance 2.0 支持,Fast 不支持 + if resolution == '1080p' and model == 'seedance_2.0_fast': + return Response({ + 'error': 'invalid_resolution', + 'message': '1080P 仅支持 AirDrama 模型,AirDrama Fast 不支持 1080P,请切换模型或选择 720P', + }, status=status.HTTP_400_BAD_REQUEST) + # ── 预估 token 和费用 ── config = QuotaConfig.objects.get_or_create(pk=1)[0] - w, h = get_resolution(aspect_ratio) - estimated_tokens = estimate_tokens(w, h, duration) - has_video_ref = _has_video_reference(request.data.get('references', [])) - token_price = _get_token_price(config, model, has_video_ref) + references = request.data.get('references', []) + w, h = get_resolution(aspect_ratio, resolution) + has_video_ref = _has_video_reference(references) + input_video_dur = _sum_video_duration(references) if has_video_ref else 0 + estimated_tokens = estimate_tokens(w, h, duration, input_video_duration=input_video_dur) + token_price = _get_token_price(config, model, has_video_ref, resolution) estimated_cost = calculate_cost(estimated_tokens, token_price, team.markup_percentage) # ── 所有额度检查在 transaction 内完成,select_for_update 串行化同团队请求 ── @@ -447,7 +486,7 @@ def video_generate_view(request): duration=duration, seconds_consumed=duration, frozen_amount=estimated_cost, - resolution='720p', + resolution=resolution, tokens_consumed=0, cost_amount=0, base_cost_amount=0, @@ -471,6 +510,7 @@ def video_generate_view(request): duration=duration, search_mode=search_mode, seed=seed, + resolution=resolution, ) ark_task_id = ark_response.get('id', '') record.ark_task_id = ark_task_id @@ -550,7 +590,9 @@ def _settle_payment(record, total_tokens): return config = QuotaConfig.objects.get_or_create(pk=1)[0] has_video_ref = _has_video_reference(record.reference_urls) - token_price = _get_token_price(config, record.model, has_video_ref) + # 按任务实际 resolution 取单价(1080P 任务用 1080P 单价结算) + # record.resolution 有 model 层 default='720p' + choices 约束 + data migration 回填,永远不为空 + token_price = _get_token_price(config, record.model, has_video_ref, record.resolution) actual_cost = calculate_cost(total_tokens, token_price, team.markup_percentage) base_cost = calculate_base_cost(total_tokens, token_price) frozen = record.frozen_amount @@ -634,6 +676,7 @@ def _serialize_task(record): 'mode': record.mode, 'model': record.model, 'aspect_ratio': record.aspect_ratio, + 'resolution': record.resolution, 'duration': record.duration, 'seconds_consumed': record.seconds_consumed, 'tokens_consumed': record.tokens_consumed, @@ -1705,6 +1748,7 @@ def admin_records_view(request): 'mode': r.mode, 'model': r.model, 'aspect_ratio': r.aspect_ratio, + 'resolution': r.resolution, 'status': r.status, 'error_message': r.error_message or '', 'raw_error': r.raw_error or '', @@ -1768,6 +1812,7 @@ def team_records_view(request): 'mode': r.mode, 'model': r.model, 'aspect_ratio': r.aspect_ratio, + 'resolution': r.resolution, 'status': r.status, 'error_message': r.error_message or '', 'raw_error': r.raw_error or '', @@ -2656,6 +2701,7 @@ def profile_records_view(request): 'mode': r.mode, 'model': r.model, 'aspect_ratio': r.aspect_ratio, + 'resolution': r.resolution, 'status': r.status, 'error_message': r.error_message or '', }) @@ -2788,6 +2834,7 @@ def admin_assets_user_videos(request, user_id): 'duration': r.duration, 'seconds_consumed': r.seconds_consumed, 'aspect_ratio': r.aspect_ratio, + 'resolution': r.resolution, 'reference_urls': r.reference_urls or [], 'created_at': r.created_at.isoformat(), }) @@ -2869,6 +2916,7 @@ def team_assets_member_videos(request, member_id): 'duration': r.duration, 'seconds_consumed': r.seconds_consumed, 'aspect_ratio': r.aspect_ratio, + 'resolution': r.resolution, 'reference_urls': r.reference_urls or [], 'created_at': r.created_at.isoformat(), }) diff --git a/backend/tests/test_1080p_api.py b/backend/tests/test_1080p_api.py new file mode 100644 index 0000000..72e348a --- /dev/null +++ b/backend/tests/test_1080p_api.py @@ -0,0 +1,131 @@ +""" +1080P API 集成测试 — 验证 video_generate_view 的入口校验。 +""" +import os +import sys +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +django.setup() + +import unittest +from django.test import TestCase +from django.contrib.auth import get_user_model +from rest_framework.test import APIClient +from apps.accounts.models import Team +from apps.generation.models import QuotaConfig + +User = get_user_model() + + +class TestVideoGenerateResolution(TestCase): + """video_generate_view 的分辨率+模型组合校验。""" + + def setUp(self): + # 初始化 QuotaConfig + QuotaConfig.objects.get_or_create(pk=1) + + # 建测试 team + user + self.team = Team.objects.create( + name='test-1080p', + is_active=True, + monthly_spending_limit=1000, + markup_percentage=0, + balance=1000, + frozen_amount=0, + ) + self.user = User.objects.create_user( + username='test_1080p_user', + email='test1080p@example.com', + password='testpass123', + team=self.team, + spending_limit=-1, + daily_generation_limit=-1, + monthly_generation_limit=-1, + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + def test_reject_fast_plus_1080p(self): + """原则 1:Fast + 1080P 组合必须 400 拒绝,不能静默降级。""" + resp = self.client.post('/api/v1/video/generate', { + 'prompt': '测试', + 'mode': 'universal', + 'model': 'seedance_2.0_fast', + 'aspect_ratio': '16:9', + 'duration': 5, + 'resolution': '1080p', + 'references': [], + }, format='json') + self.assertEqual(resp.status_code, 400) + body = resp.json() + self.assertEqual(body.get('error'), 'invalid_resolution') + # 提示信息要明确告知用户原因 + self.assertIn('1080P', body.get('message', '')) + self.assertIn('Fast', body.get('message', '')) + + def test_reject_adaptive_ratio(self): + """原则 1:adaptive 不在 6 选 1 白名单,拒绝。""" + resp = self.client.post('/api/v1/video/generate', { + 'prompt': '测试', + 'mode': 'universal', + 'model': 'seedance_2.0', + 'aspect_ratio': 'adaptive', + 'duration': 5, + 'resolution': '720p', + 'references': [], + }, format='json') + self.assertEqual(resp.status_code, 400) + # serializer 错误:aspect_ratio 不在 choices + self.assertIn('aspect_ratio', str(resp.content)) + + def test_reject_invalid_resolution(self): + """resolution 不在 480p/720p/1080p 白名单,拒绝。""" + resp = self.client.post('/api/v1/video/generate', { + 'prompt': '测试', + 'mode': 'universal', + 'model': 'seedance_2.0', + 'aspect_ratio': '16:9', + 'duration': 5, + 'resolution': '4K', + 'references': [], + }, format='json') + self.assertEqual(resp.status_code, 400) + + def test_resolution_default_720p_when_missing(self): + """旧客户端不传 resolution 字段时,serializer default='720p' 生效。""" + # 不传 resolution(兼容旧客户端) + resp = self.client.post('/api/v1/video/generate', { + 'prompt': '测试', + 'mode': 'universal', + 'model': 'seedance_2.0', + 'aspect_ratio': '16:9', + 'duration': 5, + 'references': [], + }, format='json') + # serializer 应该接受(default='720p');可能因火山 API 未开通等其他原因失败, + # 但不该是 resolution 相关的 400 错误 + if resp.status_code == 400: + body = resp.json() + self.assertNotEqual(body.get('error'), 'invalid_resolution') + + def test_accept_valid_1080p_airdrama(self): + """原则:AirDrama + 1080P 组合合法,不被 400 拒绝。""" + resp = self.client.post('/api/v1/video/generate', { + 'prompt': '测试', + 'mode': 'universal', + 'model': 'seedance_2.0', + 'aspect_ratio': '16:9', + 'duration': 5, + 'resolution': '1080p', + 'references': [], + }, format='json') + # 不应该因为分辨率被 400(可能因余额/API 未开通等其他原因失败) + if resp.status_code == 400: + body = resp.json() + self.assertNotEqual(body.get('error'), 'invalid_resolution') + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/backend/tests/test_1080p_billing.py b/backend/tests/test_1080p_billing.py new file mode 100644 index 0000000..229e974 --- /dev/null +++ b/backend/tests/test_1080p_billing.py @@ -0,0 +1,208 @@ +""" +1080P 分辨率支持的计费逻辑测试 — 严格对齐用户三原则: +1. 不兜底/静默降级 +2. 钱的计算绝对准确(纯官方公式) +3. 不隐藏 bug(非法组合 fail loud) + +运行方式: + cd backend && source venv/Scripts/activate && python -m pytest tests/test_1080p_billing.py -v +或 Django test runner: + python manage.py test tests.test_1080p_billing +""" +import os +import sys +import django + +# Django setup +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +django.setup() + +import unittest +from utils.billing import ( + RESOLUTION_MAP, + get_resolution, + estimate_tokens, + calculate_cost, + calculate_base_cost, +) + + +class TestResolutionMap(unittest.TestCase): + """验证 RESOLUTION_MAP 的 18 个组合像素值与官方文档一致。""" + + def test_1080p_pixels(self): + # 来自 docs/API文档/创建视频生成任务API.md Seedance 2.0 & 2.0 fast 列 + self.assertEqual(get_resolution('16:9', '1080p'), (1920, 1080)) + self.assertEqual(get_resolution('9:16', '1080p'), (1080, 1920)) + self.assertEqual(get_resolution('4:3', '1080p'), (1664, 1248)) + self.assertEqual(get_resolution('1:1', '1080p'), (1440, 1440)) + self.assertEqual(get_resolution('3:4', '1080p'), (1248, 1664)) + # 21:9 特别注意:是 2206×946,不是 seedance 1.0 的 2176×928 + self.assertEqual(get_resolution('21:9', '1080p'), (2206, 946)) + + def test_720p_pixels(self): + self.assertEqual(get_resolution('16:9', '720p'), (1280, 720)) + self.assertEqual(get_resolution('21:9', '720p'), (1470, 630)) + + def test_480p_pixels(self): + self.assertEqual(get_resolution('16:9', '480p'), (864, 496)) + self.assertEqual(get_resolution('21:9', '480p'), (992, 432)) + + def test_invalid_combo_raises(self): + """原则 1:非法组合必须 fail loud,不静默降级。""" + with self.assertRaises(KeyError): + get_resolution('adaptive', '720p') # adaptive 不在 map + with self.assertRaises(KeyError): + get_resolution('16:9', '4K') # 不存在的 tier + with self.assertRaises(KeyError): + get_resolution('unknown', 'unknown') + + def test_tier_required(self): + """tier 参数必填,不允许默认 720p 静默降级。""" + with self.assertRaises(TypeError): + get_resolution('16:9') # type: ignore - 故意漏参数 + + +class TestEstimateTokens(unittest.TestCase): + """ + 严格对齐官方公式:`(输入视频时长+输出时长) × 宽 × 高 × 帧率 / 1024` + 预估端不做最低 token 修正(那是火山计费侧逻辑)。 + """ + + def test_formula_no_input_video(self): + # 720P 16:9 (1280×720), 5s 输出, 24fps + # 1280 × 720 × 24 × 5 / 1024 = 108000 + self.assertEqual(estimate_tokens(1280, 720, 5), 108000) + + def test_formula_with_input_video(self): + # 720P 16:9, 5s 输出 + 5s 输入 = 10s 总时长 + # 1280 × 720 × 24 × 10 / 1024 = 216000 + self.assertEqual(estimate_tokens(1280, 720, 5, input_video_duration=5), 216000) + + def test_1080p_formula(self): + # 1080P 16:9 (1920×1080), 5s 输出, 无输入视频 + # 1920 × 1080 × 24 × 5 / 1024 = 243000 + self.assertEqual(estimate_tokens(1920, 1080, 5), 243000) + + def test_1080p_with_input_video(self): + # 1080P 16:9, 5s 输出 + 2s 输入 = 7s + # 1920 × 1080 × 24 × 7 / 1024 = 340200 + self.assertEqual(estimate_tokens(1920, 1080, 5, input_video_duration=2), 340200) + + def test_no_silent_min_token_adjustment(self): + """原则 2:预估端严格按公式,不做最低 token 修正。 + 火山文档说 1080p 5s 输入含视频最低 437400 tokens,但那是火山计费侧的事, + 我们预估就老老实实按公式算 (5s+2s)×1920×1080×24/1024 = 340200,不擅自拉高。 + """ + # 1080p 5s 输出 + 2s 输入 = 7s 总时长 + # 公式值 340200,官方最低 437400 + # 我们应该返回公式值,不主动调到最低值 + result = estimate_tokens(1920, 1080, 5, input_video_duration=2) + self.assertEqual(result, 340200, "预估端不应主动修正到火山最低 token") + + def test_float_input_duration(self): + """输入视频时长可能是浮点数(前端 getMediaInfo 读取),要正确累加。""" + # 720P 16:9, 5s 输出 + 3.5s 输入 = 8.5s + # 1280 × 720 × 24 × 8.5 / 1024 = 183600 + self.assertEqual(estimate_tokens(1280, 720, 5, input_video_duration=3.5), 183600) + + +class TestGetTokenPrice(unittest.TestCase): + """验证单价选择逻辑 — 4 种模型×视频组合 + 1080p 独立单价 + Fast+1080P fail loud。""" + + def setUp(self): + # Mock QuotaConfig — 用官方文档默认值 + from types import SimpleNamespace + from decimal import Decimal + self.config = SimpleNamespace( + base_token_price=Decimal('46'), + base_token_price_video=Decimal('28'), + base_token_price_fast=Decimal('37'), + base_token_price_fast_video=Decimal('22'), + base_token_price_1080p=Decimal('51'), + base_token_price_1080p_video=Decimal('31'), + ) + from apps.generation.views import _get_token_price + self._get_token_price = _get_token_price + + def test_seedance_2_0_720p_no_video(self): + """AirDrama 720P 不含视频 = 46 元/百万 tokens.""" + price = self._get_token_price(self.config, 'seedance_2.0', False, '720p') + self.assertEqual(price, 46) + + def test_seedance_2_0_720p_with_video(self): + """AirDrama 720P 含视频 = 28.""" + price = self._get_token_price(self.config, 'seedance_2.0', True, '720p') + self.assertEqual(price, 28) + + def test_seedance_2_0_480p_same_as_720p(self): + """480p 和 720p 共享同一单价(官方价格一致)。""" + price_480 = self._get_token_price(self.config, 'seedance_2.0', False, '480p') + price_720 = self._get_token_price(self.config, 'seedance_2.0', False, '720p') + self.assertEqual(price_480, price_720) + + def test_seedance_2_0_1080p_no_video(self): + """AirDrama 1080P 不含视频 = 51(独立单价,不是 720p 的 46).""" + price = self._get_token_price(self.config, 'seedance_2.0', False, '1080p') + self.assertEqual(price, 51) + + def test_seedance_2_0_1080p_with_video(self): + """AirDrama 1080P 含视频 = 31(独立单价,不是 720p 的 28).""" + price = self._get_token_price(self.config, 'seedance_2.0', True, '1080p') + self.assertEqual(price, 31) + + def test_fast_720p_no_video(self): + """Fast 720P 不含视频 = 37.""" + price = self._get_token_price(self.config, 'seedance_2.0_fast', False, '720p') + self.assertEqual(price, 37) + + def test_fast_720p_with_video(self): + """Fast 720P 含视频 = 22.""" + price = self._get_token_price(self.config, 'seedance_2.0_fast', True, '720p') + self.assertEqual(price, 22) + + def test_fast_480p_uses_fast_price(self): + """Fast 不分 480p/720p,都用 fast 单价。""" + price_480 = self._get_token_price(self.config, 'seedance_2.0_fast', False, '480p') + price_720 = self._get_token_price(self.config, 'seedance_2.0_fast', False, '720p') + self.assertEqual(price_480, price_720) + self.assertEqual(price_480, 37) + + def test_fast_1080p_raises_value_error(self): + """原则 1 + 3:Fast + 1080P 必须 fail loud,不能静默按 720p 价(欺骗用户)。""" + with self.assertRaises(ValueError) as ctx: + self._get_token_price(self.config, 'seedance_2.0_fast', False, '1080p') + self.assertIn('1080p', str(ctx.exception).lower()) + + with self.assertRaises(ValueError): + self._get_token_price(self.config, 'seedance_2.0_fast', True, '1080p') + + +class TestCalculateCost(unittest.TestCase): + """验证扣费金额计算 = tokens × 单价 × (1 + 加价%),精确到分.""" + + def test_720p_cost_matches_official_example(self): + """官方示例:720P 5s 16:9 = 4.97 元(无加价).""" + # 720p 5s 公式值 108000 tokens + tokens = estimate_tokens(1280, 720, 5) + # 46 元/百万 × 108000 / 1000000 = 4.968 ≈ 4.97 + cost = calculate_cost(tokens, 46, 0) + self.assertEqual(str(cost), '4.97') + + def test_1080p_no_video_cost(self): + """1080P 5s 16:9 不含视频 = 1920×1080×24×5/1024 × 51 / 1000000 = 12.393 ≈ 12.39 元.""" + tokens = estimate_tokens(1920, 1080, 5) + cost = calculate_cost(tokens, 51, 0) + self.assertEqual(str(cost), '12.39') + + def test_markup_applied(self): + """团队加价 20% 的情况。""" + tokens = estimate_tokens(1280, 720, 5) # 108000 + cost = calculate_cost(tokens, 46, 20) + # 4.968 × 1.2 = 5.9616 → 5.96 + self.assertEqual(str(cost), '5.96') + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/backend/utils/airdrama_client.py b/backend/utils/airdrama_client.py index a3b548d..eb800fb 100644 --- a/backend/utils/airdrama_client.py +++ b/backend/utils/airdrama_client.py @@ -92,7 +92,7 @@ def _headers(): } -def create_task(prompt, model, content_items, aspect_ratio, duration, +def create_task(prompt, model, content_items, aspect_ratio, duration, resolution, generate_audio=True, search_mode='off', seed=-1): """Create a video generation task. @@ -102,6 +102,9 @@ def create_task(prompt, model, content_items, aspect_ratio, duration, content_items: List of media content dicts (image_url, video_url, audio_url). aspect_ratio: Video aspect ratio ('16:9', '9:16', etc.). duration: Video duration in seconds. + resolution: Output video resolution ('480p'|'720p'|'1080p'). 必填,不设默认值避免调用者遗漏导致 + 静默降级(1080p 任务若因默认值被意外降为 720p 会产生计费偏差,违反准确性原则)。 + 注意:1080p 仅 Seedance 2.0 支持。 generate_audio: Whether to generate audio with the video. search_mode: 'smart' to enable internet search, 'off' to disable. @@ -120,6 +123,7 @@ def create_task(prompt, model, content_items, aspect_ratio, duration, 'content': content, 'generate_audio': generate_audio, 'ratio': aspect_ratio, + 'resolution': resolution, 'duration': duration, 'watermark': False, 'seed': seed, diff --git a/backend/utils/billing.py b/backend/utils/billing.py index ee34db2..c4d650e 100644 --- a/backend/utils/billing.py +++ b/backend/utils/billing.py @@ -22,20 +22,62 @@ RESOLUTION_MAP = { ('480p', '1:1'): (640, 640), ('480p', '3:4'): (560, 752), ('480p', '21:9'): (992, 432), + # 1080p (来自火山 API 文档,Seedance 2.0 & 2.0 fast 列) + ('1080p', '16:9'): (1920, 1080), + ('1080p', '9:16'): (1080, 1920), + ('1080p', '4:3'): (1664, 1248), + ('1080p', '1:1'): (1440, 1440), + ('1080p', '3:4'): (1248, 1664), + ('1080p', '21:9'): (2206, 946), } # 默认帧率 DEFAULT_FPS = 24 -def get_resolution(aspect_ratio: str, tier: str = '720p') -> tuple: - """根据宽高比和分辨率档位返回 (width, height) 像素值。""" - return RESOLUTION_MAP.get((tier, aspect_ratio), (1280, 720)) +def get_resolution(aspect_ratio: str, tier: str) -> tuple: + """根据宽高比和分辨率档位返回 (width, height) 像素值。 + + tier 必填,不设默认值 — 避免调用者遗漏时静默降级为 720p(违反计费准确性原则)。 + 若 (tier, aspect_ratio) 组合不在 RESOLUTION_MAP(如 adaptive),raise KeyError, + 让上游感知并 fail loud。上游(serializer/前端)负责保证合法组合。 + """ + key = (tier, aspect_ratio) + if key not in RESOLUTION_MAP: + raise KeyError( + f'不支持的分辨率组合: tier={tier!r}, aspect_ratio={aspect_ratio!r}. ' + f'仅支持 480p/720p/1080p × 16:9/9:16/4:3/1:1/3:4/21:9' + ) + return RESOLUTION_MAP[key] -def estimate_tokens(width: int, height: int, duration: int, fps: int = DEFAULT_FPS) -> int: - """预估视频生成消耗的 tokens。""" - return round(width * height * fps * duration / 1024) +def estimate_tokens( + width: int, + height: int, + duration: int, + fps: int = DEFAULT_FPS, + input_video_duration: float = 0, +) -> int: + """预估视频生成消耗的 tokens。 + + 火山官方公式:`(输入视频时长 + 输出视频时长) × 宽 × 高 × 帧率 / 1024` + + ⚠️ 这是预估值,仅用于前端展示和额度冻结。 + 真实费用以火山 API 返回的 usage.total_tokens 为准(`_settle_payment` 中按实际值结算)。 + 最低 token 用量限制是火山计费端的逻辑,我方不在预估端维护该表(避免与官方脱钩)。 + + Args: + width: 输出视频宽度(像素) + height: 输出视频高度(像素) + duration: 输出视频时长(秒) + fps: 帧率,默认 24 + input_video_duration: 输入参考视频的总时长(秒),默认 0 + + Returns: + token 估算值(整数) + """ + total_duration = duration + (input_video_duration or 0) + return round(width * height * fps * total_duration / 1024) def calculate_cost(tokens: int, base_price, markup_percentage) -> Decimal: diff --git a/web/src/components/Dropdown.tsx b/web/src/components/Dropdown.tsx index 9cd116c..4c0a2b5 100644 --- a/web/src/components/Dropdown.tsx +++ b/web/src/components/Dropdown.tsx @@ -5,6 +5,7 @@ interface DropdownItem { label: string; value: string; icon?: ReactNode; + disabled?: boolean; } interface DropdownProps { @@ -41,8 +42,10 @@ export function Dropdown({ items, value, onSelect, trigger, minWidth = 150 }: Dr {items.map((item) => (
{ + if (item.disabled) return; onSelect(item.value); setOpen(false); }} diff --git a/web/src/components/GenerationCard.tsx b/web/src/components/GenerationCard.tsx index 3ba65d9..5136943 100644 --- a/web/src/components/GenerationCard.tsx +++ b/web/src/components/GenerationCard.tsx @@ -389,7 +389,7 @@ export function GenerationCard({ task, onOpenDetail }: Props) { 时长{task.duration}s
- 分辨率720p + 分辨率{task.resolution.toUpperCase()}
模型 diff --git a/web/src/components/Toolbar.tsx b/web/src/components/Toolbar.tsx index 2f431a5..4af9680 100644 --- a/web/src/components/Toolbar.tsx +++ b/web/src/components/Toolbar.tsx @@ -4,7 +4,8 @@ import { useGenerationStore } from '../store/generation'; import { useAuthStore } from '../store/auth'; import { Dropdown } from './Dropdown'; import { showToast } from './Toast'; -import type { CreationMode, AspectRatio, Duration, GenerationType, ModelOption } from '../types'; +import { parseAssetMentions } from '../lib/assetMentions'; +import type { CreationMode, AspectRatio, Duration, Resolution, GenerationType, ModelOption } from '../types'; import styles from './Toolbar.module.css'; const VideoIcon = () => ( @@ -71,10 +72,7 @@ const generationTypeItems = [ { label: '视频生成', value: 'video' as GenerationType, icon: }, ]; -const modelItems = [ - { label: 'AirDrama', value: 'seedance_2.0' as ModelOption, icon: }, - { label: 'AirDrama Fast', value: 'seedance_2.0_fast' as ModelOption, icon: }, -]; +// NOTE: modelItems 在组件内部按 resolution 动态构建(1080P 下 Fast 置灰) const modeItems = [ { label: '全能参考', value: 'universal' as CreationMode, icon: }, @@ -99,9 +97,20 @@ const durationItems = Array.from({ length: 12 }, (_, i) => { return { label: `${v}s`, value: String(v) }; }); -const RESOLUTION_MAP: Record = { - '16:9': [1280, 720], '9:16': [720, 1280], '4:3': [1112, 834], - '1:1': [960, 960], '3:4': [834, 1112], '21:9': [1470, 630], +// 对照 billing.py::RESOLUTION_MAP — 前端预估与后端计费保持一致 +const RESOLUTION_PIXELS: Record> = { + '480p': { + '16:9': [864, 496], '9:16': [496, 864], '4:3': [752, 560], + '1:1': [640, 640], '3:4': [560, 752], '21:9': [992, 432], + }, + '720p': { + '16:9': [1280, 720], '9:16': [720, 1280], '4:3': [1112, 834], + '1:1': [960, 960], '3:4': [834, 1112], '21:9': [1470, 630], + }, + '1080p': { + '16:9': [1920, 1080], '9:16': [1080, 1920], '4:3': [1664, 1248], + '1:1': [1440, 1440], '3:4': [1248, 1664], '21:9': [2206, 946], + }, }; const modeLabels: Record = { @@ -120,30 +129,67 @@ export function Toolbar() { const setAspectRatio = useInputBarStore((s) => s.setAspectRatio); const duration = useInputBarStore((s) => s.duration); const setDuration = useInputBarStore((s) => s.setDuration); + const resolution = useInputBarStore((s) => s.resolution); + const setResolution = useInputBarStore((s) => s.setResolution); const isSubmittable = useInputBarStore((s) => s.canSubmit()); const triggerInsertAt = useInputBarStore((s) => s.triggerInsertAt); const isKeyframe = mode === 'keyframe'; const references = useInputBarStore((s) => s.references); + const editorHtml = useInputBarStore((s) => s.editorHtml); const team = useAuthStore((s) => s.team); const addTask = useGenerationStore((s) => s.addTask); const estimatedTokens = useMemo(() => { - const res = RESOLUTION_MAP[aspectRatio] || [1280, 720]; - return Math.round((res[0] * res[1] * 24 * duration) / 1024); - }, [aspectRatio, duration]); + // 官方公式:`(输入视频时长 + 输出视频时长) × 宽 × 高 × 24fps / 1024` + // 前后端必须一致(和 backend/utils/billing.py::estimate_tokens 对齐)。 + // 输入视频时长 = 直接上传的视频 references.duration + 素材库 @ 视频的 duration + // resolution / aspectRatio 都是严格类型枚举,不做 || 兜底 — bug 直接暴露。 + const [w, h] = RESOLUTION_PIXELS[resolution][aspectRatio]; + const refVideoDur = references + .filter((r) => r.type === 'video' && typeof r.duration === 'number') + .reduce((sum, r) => sum + (r.duration || 0), 0); + const mentionVideoDur = parseAssetMentions(editorHtml).durations.video; + const totalDuration = duration + refVideoDur + mentionVideoDur; + return Math.round((w * h * 24 * totalDuration) / 1024); + }, [aspectRatio, duration, resolution, references, editorHtml]); + + // 分辨率 Dropdown:Fast 模式下 1080P 置灰 + const resolutionItems = useMemo(() => [ + { label: '480P', value: '480p' as Resolution }, + { label: '720P', value: '720p' as Resolution }, + { + label: model === 'seedance_2.0_fast' ? '1080P(Fast 不支持)' : '1080P', + value: '1080p' as Resolution, + disabled: model === 'seedance_2.0_fast', + }, + ], [model]); + + // 模型 Dropdown:当前 1080P 时,Fast 置灰(1080P 仅 AirDrama 支持) + const modelItems = useMemo(() => [ + { label: 'AirDrama', value: 'seedance_2.0', icon: }, + { + label: resolution === '1080p' ? 'AirDrama Fast(不支持 1080P)' : 'AirDrama Fast', + value: 'seedance_2.0_fast', + icon: , + disabled: resolution === '1080p', + }, + ], [resolution]); const estimatedCost = useMemo(() => { const hasVideoRef = references.some((r) => r.type === 'video'); let price = team?.token_price || 0; if (model === 'seedance_2.0_fast') { + // Fast 不支持 1080p,单价不分分辨率 price = hasVideoRef ? (team?.token_price_fast_video || 0) : (team?.token_price_fast || 0); + } else if (resolution === '1080p') { + price = hasVideoRef ? (team?.token_price_1080p_video || 0) : (team?.token_price_1080p || 0); } else { price = hasVideoRef ? (team?.token_price_video || 0) : (team?.token_price || 0); } return (estimatedTokens * price / 1000000).toFixed(2); - }, [estimatedTokens, model, references, team]); + }, [estimatedTokens, model, resolution, references, team]); const handleSend = useCallback(() => { if (!isSubmittable) { @@ -224,6 +270,19 @@ export function Toolbar() { } /> + {/* Resolution */} + setResolution(v as Resolution)} + minWidth={100} + trigger={ + + } + /> + {/* Duration */} 0 && ( 预估消耗:{estimatedTokens.toLocaleString()} tokens / ¥{estimatedCost} diff --git a/web/src/components/VideoDetailModal.tsx b/web/src/components/VideoDetailModal.tsx index 9780a50..f4e1b22 100644 --- a/web/src/components/VideoDetailModal.tsx +++ b/web/src/components/VideoDetailModal.tsx @@ -220,6 +220,7 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele if (task.model) store.setModel(task.model as 'seedance_2.0' | 'seedance_2.0_fast'); if (task.aspectRatio) store.setAspectRatio(task.aspectRatio as any); if (task.duration) store.setDuration(task.duration); + if (task.resolution) store.setResolution(task.resolution); // Load references from task (exclude asset library refs — they restore via @mentions in editorHtml) if (task.references && task.references.length > 0) { const refs = task.references.filter(r => r.previewUrl && !r.isAssetRef).map(r => ({ diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index d9d52b7..60e0916 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -146,6 +146,7 @@ export const videoApi = { model: string; aspect_ratio: string; duration: number; + resolution: string; references: { url: string; type: string; role: string; label: string; thumb_url?: string; duration?: string }[]; search_mode?: string; seed?: number; diff --git a/web/src/pages/AdminAssetsPage.tsx b/web/src/pages/AdminAssetsPage.tsx index 0e2a45d..41c7ff6 100644 --- a/web/src/pages/AdminAssetsPage.tsx +++ b/web/src/pages/AdminAssetsPage.tsx @@ -57,6 +57,7 @@ function assetVideoToTask(v: AssetVideo): GenerationTask { model: 'seedance_2.0', aspectRatio: (v.aspect_ratio as any) || '16:9', duration: v.duration as any, + resolution: v.resolution, references, assetMentions: [], status: 'completed', diff --git a/web/src/pages/SettingsPage.tsx b/web/src/pages/SettingsPage.tsx index a3c13da..18eb9c8 100644 --- a/web/src/pages/SettingsPage.tsx +++ b/web/src/pages/SettingsPage.tsx @@ -14,6 +14,8 @@ export function SettingsPage() { base_token_price_video: 0, base_token_price_fast: 0, base_token_price_fast_video: 0, + base_token_price_1080p: 0, + base_token_price_1080p_video: 0, announcement: '', announcement_enabled: false, max_desktop_sessions: 1, @@ -143,7 +145,7 @@ export function SettingsPage() { />
-

Seedance 2.0

+

Seedance 2.0(480P / 720P)

@@ -164,7 +166,28 @@ export function SettingsPage() { />
-

Seedance 2.0 Fast

+

Seedance 2.0(1080P)

+
+
+ + setSettings({ ...settings, base_token_price_1080p: Number(e.target.value) })} + /> +
+
+ + setSettings({ ...settings, base_token_price_1080p_video: Number(e.target.value) })} + /> +
+
+

Seedance 2.0 Fast(不支持 1080P)

diff --git a/web/src/pages/TeamAssetsPage.tsx b/web/src/pages/TeamAssetsPage.tsx index 9c37006..54e1e0e 100644 --- a/web/src/pages/TeamAssetsPage.tsx +++ b/web/src/pages/TeamAssetsPage.tsx @@ -57,6 +57,7 @@ function assetVideoToTask(v: AssetVideo): GenerationTask { model: 'seedance_2.0', aspectRatio: (v.aspect_ratio as any) || '16:9', duration: v.duration as any, + resolution: v.resolution, references, assetMentions: [], status: 'completed', diff --git a/web/src/store/generation.ts b/web/src/store/generation.ts index 4c6d4b4..5a62325 100644 --- a/web/src/store/generation.ts +++ b/web/src/store/generation.ts @@ -32,7 +32,7 @@ function mapErrorMessage(raw?: string): string | undefined { // Model / generation errors if (s.includes('quota') || s.includes('insufficient')) - return '额度不足,请联系管理员'; + return '今日生成次数或团队余额不足,请联系管理员'; // If already Chinese, return as-is if (/[\u4e00-\u9fa5]/.test(raw)) return raw; @@ -121,6 +121,7 @@ function backendToFrontend(bt: BackendTask): GenerationTask { model: bt.model, aspectRatio: bt.aspect_ratio as GenerationTask['aspectRatio'], duration: bt.duration as GenerationTask['duration'], + resolution: bt.resolution, references, assetMentions, status: mapStatus(bt.status), @@ -402,6 +403,7 @@ export const useGenerationStore = create((set, get) => ({ model: input.model, aspectRatio: input.aspectRatio, duration: input.duration, + resolution: input.resolution, references: localRefs, assetMentions: placeholderAssetMentions, status: 'generating', @@ -521,6 +523,7 @@ export const useGenerationStore = create((set, get) => ({ model: input.model, aspect_ratio: input.aspectRatio, duration: input.duration, + resolution: input.resolution, references: uploadedRefs, search_mode: input.searchMode || 'off', seed: input.seed ?? -1, @@ -638,6 +641,7 @@ export const useGenerationStore = create((set, get) => ({ editorHtml: task.prompt, aspectRatio: task.aspectRatio, duration: task.duration, + resolution: task.resolution, references, assetMentions: task.assetMentions || [], // 如果 seed 开关打开且 task 有有效 seed,填入;否则不动 @@ -652,6 +656,7 @@ export const useGenerationStore = create((set, get) => ({ editorHtml: task.editorHtml || task.prompt, aspectRatio: task.aspectRatio, duration: task.duration, + resolution: task.resolution, assetMentions: [], firstFrame: firstRef ? { id: firstRef.id, type: firstRef.type, previewUrl: firstRef.previewUrl, label: '首帧', tosUrl: firstRef.previewUrl } : null, lastFrame: lastRef ? { id: lastRef.id, type: lastRef.type, previewUrl: lastRef.previewUrl, label: '尾帧', tosUrl: lastRef.previewUrl } : null, @@ -688,6 +693,7 @@ export const useGenerationStore = create((set, get) => ({ model: task.model, aspectRatio: task.aspectRatio, duration: task.duration, + resolution: task.resolution, references: task.mode === 'universal' ? references : [], assetMentions: task.assetMentions || [], }); diff --git a/web/src/store/inputBar.ts b/web/src/store/inputBar.ts index 7eb18ce..9dcfd23 100644 --- a/web/src/store/inputBar.ts +++ b/web/src/store/inputBar.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import type { CreationMode, ModelOption, AspectRatio, Duration, GenerationType, UploadedFile } from '../types'; +import type { CreationMode, ModelOption, AspectRatio, Duration, Resolution, GenerationType, UploadedFile } from '../types'; import { showToast } from '../components/Toast'; import { mediaApi } from '../lib/api'; import { parseAssetMentions } from '../lib/assetMentions'; @@ -88,6 +88,10 @@ interface InputBarState { setDuration: (duration: Duration) => void; prevDuration: Duration; + // Resolution (480p/720p/1080p) — 1080p 仅 Seedance 2.0 支持 + resolution: Resolution; + setResolution: (resolution: Resolution) => void; + // Prompt prompt: string; setPrompt: (prompt: string) => void; @@ -145,7 +149,17 @@ export const useInputBarStore = create((set, get) => ({ setMode: (mode) => set({ mode }), model: 'seedance_2.0', - setModel: (model) => set({ model }), + setModel: (model) => { + // Fast + 1080P 为非法组合(官方文档约束)。UI Dropdown 已对 Fast 项置灰, + // 此处为 UI 被绕过时的防御性拦截(depth defense),不做静默降级: + // 阻止切换 + toast 引导用户手动改分辨率,让用户选择始终被尊重。 + const state = get(); + if (model === 'seedance_2.0_fast' && state.resolution === '1080p') { + showToast('1080P 仅 AirDrama 模型支持,请先切换分辨率到 720P 或 480P'); + return; + } + set({ model }); + }, aspectRatio: '21:9', setAspectRatio: (aspectRatio) => set({ aspectRatio, prevAspectRatio: aspectRatio }), @@ -162,6 +176,17 @@ export const useInputBarStore = create((set, get) => ({ }, prevDuration: 15, + resolution: '720p' as Resolution, + setResolution: (resolution) => { + // Fast + 1080P 非法组合(对称 setModel 的拦截)— UI Dropdown 已置灰,此处防御性拦截 + const state = get(); + if (resolution === '1080p' && state.model === 'seedance_2.0_fast') { + showToast('AirDrama Fast 不支持 1080P,请先切换模型到 AirDrama'); + return; + } + set({ resolution }); + }, + prompt: '', setPrompt: (prompt) => set({ prompt }), @@ -398,6 +423,7 @@ export const useInputBarStore = create((set, get) => ({ prevAspectRatio: '21:9', duration: 15, prevDuration: 15, + resolution: '720p', prompt: '', editorHtml: '', references: [], diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 9b6e6d0..f187af8 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -2,6 +2,7 @@ export type CreationMode = 'universal' | 'keyframe'; export type ModelOption = 'seedance_2.0' | 'seedance_2.0_fast'; export type AspectRatio = '16:9' | '9:16' | '1:1' | '21:9' | '4:3' | '3:4'; export type Duration = 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15; +export type Resolution = '480p' | '720p' | '1080p'; export type GenerationType = 'video' | 'image'; export type UserRole = 'super_admin' | 'team_admin' | 'member'; @@ -44,6 +45,7 @@ export interface GenerationTask { model: ModelOption; aspectRatio: AspectRatio; duration: Duration; + resolution: Resolution; references: ReferenceSnapshot[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any assetMentions: Record[]; @@ -67,6 +69,7 @@ export interface BackendTask { mode: CreationMode; model: ModelOption; aspect_ratio: string; + resolution: Resolution; duration: number; seconds_consumed: number; tokens_consumed: number; @@ -113,6 +116,8 @@ export interface TeamInfo { token_price_video: number; token_price_fast: number; token_price_fast_video: number; + token_price_1080p: number; + token_price_1080p_video: number; is_active: boolean; } @@ -222,6 +227,8 @@ export interface SystemSettings { base_token_price_video: number; base_token_price_fast: number; base_token_price_fast_video: number; + base_token_price_1080p: number; + base_token_price_1080p_video: number; announcement: string; announcement_enabled: boolean; max_desktop_sessions: number; @@ -407,6 +414,7 @@ export interface AssetVideo { seconds_consumed: number; cost_amount?: number; aspect_ratio: string; + resolution: Resolution; reference_urls?: { url: string; type: string; role: string; label: string; thumb_url?: string }[]; created_at: string; } diff --git a/web/test/e2e/resolution-1080p.spec.ts b/web/test/e2e/resolution-1080p.spec.ts new file mode 100644 index 0000000..978ada4 --- /dev/null +++ b/web/test/e2e/resolution-1080p.spec.ts @@ -0,0 +1,136 @@ +/** + * 1080P 分辨率支持 E2E — 真实浏览器验证 UI 双向约束和预估费用。 + * 针对本地开发环境(localhost:5173 + 127.0.0.1:8000)。 + */ +import { test, expect, Page } from '@playwright/test'; + +const BASE_URL = 'http://localhost:5173'; +const API_URL = 'http://127.0.0.1:8000'; +const USERNAME = 'admin'; +const PASSWORD = 'admin123'; + +async function login(page: Page) { + const resp = await page.request.post(`${API_URL}/api/v1/auth/login`, { + data: { username: USERNAME, password: PASSWORD }, + }); + if (!resp.ok()) { + const err = await resp.text(); + throw new Error(`Login failed: ${resp.status()} ${err}`); + } + const body = await resp.json(); + + await page.goto(BASE_URL); + await page.evaluate(({ access, refresh }) => { + localStorage.setItem('access_token', access); + localStorage.setItem('refresh_token', refresh); + }, { access: body.tokens.access, refresh: body.tokens.refresh }); + await page.goto(`${BASE_URL}/app`); + await page.waitForTimeout(1500); + + // 关闭公告弹窗 + const knowBtn = page.getByRole('button', { name: /我知道了|知道了|关闭/ }).first(); + if (await knowBtn.isVisible().catch(() => false)) { + await knowBtn.click(); + await page.waitForTimeout(300); + } +} + +test.describe.serial('1080P 分辨率支持', () => { + test('默认分辨率显示 720P', async ({ page }) => { + await login(page); + // 找到 Toolbar 里的分辨率按钮(label 应显示 720P) + const resolutionBtn = page.getByRole('button', { name: '720P', exact: true }).first(); + await expect(resolutionBtn).toBeVisible(); + }); + + test('AirDrama 模式下可切换到 1080P', async ({ page }) => { + await login(page); + // 点分辨率按钮展开 dropdown + await page.getByRole('button', { name: '720P', exact: true }).first().click(); + await page.waitForTimeout(200); + + // 选 1080P + await page.getByText('1080P', { exact: true }).click(); + await page.waitForTimeout(300); + + // 分辨率按钮应显示 1080P + await expect(page.getByRole('button', { name: '1080P', exact: true }).first()).toBeVisible(); + }); + + test('1080P 下 Fast 模型置灰(UI 不可达 Fast+1080P)', async ({ page }) => { + await login(page); + // 先切到 1080P + await page.getByRole('button', { name: '720P', exact: true }).first().click(); + await page.waitForTimeout(200); + await page.getByText('1080P', { exact: true }).click(); + await page.waitForTimeout(300); + + // 打开模型 dropdown + await page.getByRole('button', { name: /AirDrama$/, exact: false }).first().click(); + await page.waitForTimeout(200); + + // Fast 项应包含 "不支持 1080P" 且有 disabled 视觉 + const fastItem = page.getByText(/AirDrama Fast.*不支持 1080P/); + await expect(fastItem).toBeVisible(); + + // 点击 Fast 不应切换(Dropdown 的 disabled 阻止了 onSelect) + await fastItem.click({ force: true }); + await page.waitForTimeout(300); + + // 模型应仍是 AirDrama + await expect(page.getByRole('button', { name: /AirDrama$/, exact: false }).first()).toBeVisible(); + }); + + test('Fast 模式下 1080P 置灰(UI 不可达 Fast+1080P,反向)', async ({ page }) => { + await login(page); + // 先确保 resolution 是 720P(reset) + await page.reload(); + await page.waitForTimeout(1500); + + // 切到 Fast 模型 + await page.getByRole('button', { name: /AirDrama$/, exact: false }).first().click(); + await page.waitForTimeout(200); + await page.getByText('AirDrama Fast', { exact: true }).click(); + await page.waitForTimeout(300); + + // 打开分辨率 dropdown + await page.getByRole('button', { name: '720P', exact: true }).first().click(); + await page.waitForTimeout(200); + + // 1080P 项应带 "Fast 不支持" 标注 + const disabled1080p = page.getByText(/1080P.*Fast 不支持/); + await expect(disabled1080p).toBeVisible(); + + // 点击不生效 + await disabled1080p.click({ force: true }); + await page.waitForTimeout(300); + + // 分辨率仍为 720P(可能 Dropdown 保持打开或关闭,但按钮不该变) + const bodyText = await page.textContent('body'); + expect(bodyText).toContain('720P'); + }); + + test('预估费用 tooltip 明示「以火山为准」', async ({ page }) => { + await login(page); + // 需要让按钮栏里的"预估"显示出来(需要有 prompt 或素材) + // 输入一个简单 prompt + const promptArea = page.locator('[contenteditable]').first(); + if (await promptArea.isVisible().catch(() => false)) { + await promptArea.click(); + await promptArea.type('测试提示词'); + await page.waitForTimeout(300); + } + + // 找到"预估消耗"文案 + const estSpan = page.getByText(/预估消耗/).first(); + if (await estSpan.isVisible().catch(() => false)) { + const title = await estSpan.getAttribute('title'); + expect(title).toBeTruthy(); + expect(title!).toContain('实际'); + expect(title!).toContain('火山'); + } else { + // 如果没有预估显示(比如 team 没配单价),跳过 + console.log('跳过:预估未显示(team 可能未配单价)'); + } + }); +}); diff --git a/web/test/unit/resolution1080p.test.ts b/web/test/unit/resolution1080p.test.ts new file mode 100644 index 0000000..98a02a8 --- /dev/null +++ b/web/test/unit/resolution1080p.test.ts @@ -0,0 +1,160 @@ +/** + * 1080P 分辨率支持 — 前端单元测试 + * + * 验证用户三原则: + * 1. 不兜底/静默降级 — setModel/setResolution 拦截 Fast+1080P 组合 + * 2. 钱的计算绝对准确 — 前端 estimatedTokens 公式与后端一致 + * 3. 不隐藏 bug — 无 || '720p' 兜底 + */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { useInputBarStore } from '../../src/store/inputBar'; + +// Mock Toast 避免真实 DOM 调用 +vi.mock('../../src/components/Toast', () => ({ + showToast: vi.fn(), +})); + +describe('1080P — Store 分辨率状态', () => { + beforeEach(() => { + useInputBarStore.getState().reset(); + }); + + it('默认分辨率是 720p', () => { + expect(useInputBarStore.getState().resolution).toBe('720p'); + }); + + it('setResolution 能设为 480p / 720p / 1080p', () => { + const { setResolution } = useInputBarStore.getState(); + setResolution('480p'); + expect(useInputBarStore.getState().resolution).toBe('480p'); + setResolution('1080p'); + expect(useInputBarStore.getState().resolution).toBe('1080p'); + setResolution('720p'); + expect(useInputBarStore.getState().resolution).toBe('720p'); + }); + + it('reset 把分辨率恢复为 720p', () => { + const { setResolution, reset } = useInputBarStore.getState(); + setResolution('1080p'); + reset(); + expect(useInputBarStore.getState().resolution).toBe('720p'); + }); +}); + +describe('1080P — 双向拦截(原则 1:不静默降级)', () => { + beforeEach(() => { + useInputBarStore.getState().reset(); + }); + + it('1080P 下切 Fast 模型应被阻止,resolution 不变,model 也不变', () => { + const { setResolution, setModel } = useInputBarStore.getState(); + setResolution('1080p'); + setModel('seedance_2.0_fast'); + // 拦截成功:model 保持原值,resolution 不变(不降级为 720p) + const state = useInputBarStore.getState(); + expect(state.model).toBe('seedance_2.0'); + expect(state.resolution).toBe('1080p'); + }); + + it('Fast 模式下切 1080P 分辨率应被阻止,model 不变,resolution 不变', () => { + const { setModel, setResolution } = useInputBarStore.getState(); + setModel('seedance_2.0_fast'); + setResolution('1080p'); + const state = useInputBarStore.getState(); + expect(state.model).toBe('seedance_2.0_fast'); + expect(state.resolution).toBe('720p'); // 仍是默认 720p,没被改到 1080p + }); + + it('AirDrama 下切 1080P 正常生效', () => { + const { setResolution } = useInputBarStore.getState(); + setResolution('1080p'); + expect(useInputBarStore.getState().resolution).toBe('1080p'); + }); + + it('1080P 下切回 AirDrama 正常生效(同模型不拦截)', () => { + const { setModel, setResolution } = useInputBarStore.getState(); + setResolution('1080p'); + setModel('seedance_2.0'); + expect(useInputBarStore.getState().model).toBe('seedance_2.0'); + expect(useInputBarStore.getState().resolution).toBe('1080p'); + }); + + it('Fast 下切 480p/720p 正常生效(不是 1080p 不拦截)', () => { + const { setModel, setResolution } = useInputBarStore.getState(); + setModel('seedance_2.0_fast'); + setResolution('480p'); + expect(useInputBarStore.getState().resolution).toBe('480p'); + setResolution('720p'); + expect(useInputBarStore.getState().resolution).toBe('720p'); + }); +}); + +describe('1080P — 官方像素值(与后端 RESOLUTION_MAP 对齐)', () => { + // 这里硬编码官方文档的像素表,作为前端契约测试 + // 如果 Toolbar.tsx 的 RESOLUTION_PIXELS 改动,这些测试应该跟着更新 + // 对应 backend/utils/billing.py::RESOLUTION_MAP + const EXPECTED_PIXELS = { + '480p': { + '16:9': [864, 496], + '9:16': [496, 864], + '4:3': [752, 560], + '1:1': [640, 640], + '3:4': [560, 752], + '21:9': [992, 432], + }, + '720p': { + '16:9': [1280, 720], + '9:16': [720, 1280], + '4:3': [1112, 834], + '1:1': [960, 960], + '3:4': [834, 1112], + '21:9': [1470, 630], + }, + '1080p': { + '16:9': [1920, 1080], + '9:16': [1080, 1920], + '4:3': [1664, 1248], + '1:1': [1440, 1440], + '3:4': [1248, 1664], + '21:9': [2206, 946], // 关键:不是 2176×928(seedance 1.0 值) + }, + }; + + // estimate_tokens 官方公式实现(对齐前端 Toolbar 和后端 billing.py) + function estimateTokens(w: number, h: number, duration: number, inputVideoDuration = 0) { + return Math.round((w * h * 24 * (duration + inputVideoDuration)) / 1024); + } + + it('1080P 5s 16:9 无输入视频 = 243000 tokens', () => { + const [w, h] = EXPECTED_PIXELS['1080p']['16:9']; + expect(estimateTokens(w, h, 5)).toBe(243000); + }); + + it('1080P 5s 16:9 含 2s 输入视频 = 340200 tokens(纯公式,不修正到最低 437400)', () => { + const [w, h] = EXPECTED_PIXELS['1080p']['16:9']; + expect(estimateTokens(w, h, 5, 2)).toBe(340200); + }); + + it('720P 5s 16:9 无输入视频 = 108000 tokens', () => { + const [w, h] = EXPECTED_PIXELS['720p']['16:9']; + expect(estimateTokens(w, h, 5)).toBe(108000); + }); + + it('1080P 21:9 像素 = 2206×946(不是 seedance 1.0 的 2176×928)', () => { + expect(EXPECTED_PIXELS['1080p']['21:9']).toEqual([2206, 946]); + }); + + it('价格示例:1080P 5s 16:9 × 51 元/百万 = 12.39 元', () => { + const tokens = 243000; + const price = 51; + const cost = (tokens * price) / 1_000_000; + expect(cost.toFixed(2)).toBe('12.39'); + }); + + it('价格示例:720P 5s 16:9 × 46 元/百万 = 4.97 元', () => { + const tokens = 108000; + const price = 46; + const cost = (tokens * price) / 1_000_000; + expect(cost.toFixed(2)).toBe('4.97'); + }); +}); From 6b22e1fa3f2eb0fa9036957aeed8dc8c69f47110 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 17 Apr 2026 19:07:02 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=E7=94=A8=E6=88=B7=E7=AB=AF=E6=96=87?= =?UTF-8?q?=E6=A1=88=E4=BB=8E=E3=80=8C=E9=A2=9D=E5=BA=A6=E3=80=8D=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=85=B7=E4=BD=93=E5=8D=95=E4=BD=8D=E3=80=8C=E6=AC=A1?= =?UTF-8?q?=E6=95=B0=E3=80=8D=E2=80=94=20=E6=B6=88=E9=99=A4=E7=82=B9?= =?UTF-8?q?=E6=95=B0=E6=A6=82=E5=BF=B5=E6=B7=B7=E6=B7=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar 左下角:去钻石图标(避免用户带入即梦/豆包的"点数"概念)+ 数据从 daily_seconds (秒数池残留) 改为 daily_generation_limit (次数); 文案 "剩余额度"→"今日剩余次数"(必须写全,用户不猜); 数字字号放大 14→18,tabular-nums 稳定排版 - ProfilePage 预警 banner: "今日额度已使用 X%"→"今日生成次数已用 X%"; "今日额度已用完"→"今日生成次数已用完" - generation.ts 错误映射: "额度不足,请联系管理员"→ "今日生成次数或团队余额不足,请联系管理员"(两种可能都列出) 秒数池(daily_seconds_limit)是 v0.10.0 计费改次数+金额前的遗留概念, 这次把用户端可见的"额度"全部替换为明确的"生成次数/余额"单位,避免用户 把"额度"理解成即梦/豆包的"点数"来找客服问问题。 Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/components/Sidebar.module.css | 16 ++++++++-------- web/src/components/Sidebar.tsx | 17 ++++++++++------- web/src/pages/ProfilePage.tsx | 4 ++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/web/src/components/Sidebar.module.css b/web/src/components/Sidebar.module.css index 5cf0c88..b37a0f9 100644 --- a/web/src/components/Sidebar.module.css +++ b/web/src/components/Sidebar.module.css @@ -62,37 +62,37 @@ padding-bottom: 8px; } -/* Quota display */ +/* Quota display — 今日剩余生成次数(v0.10.0 起次数制) */ .quota { display: flex; flex-direction: column; align-items: center; - gap: 2px; + gap: 3px; cursor: pointer; padding: 8px 4px; border-radius: 8px; transition: background 0.15s; + min-width: 56px; } .quota:hover { background: rgba(255, 255, 255, 0.04); } -.diamondIcon { - flex-shrink: 0; -} - .quotaNumber { - font-size: 14px; + font-size: 18px; font-weight: 600; color: var(--color-text-primary); line-height: 1; + font-variant-numeric: tabular-nums; + letter-spacing: 0.5px; } .quotaLabel { - font-size: 9px; + font-size: 10px; color: var(--color-text-secondary); white-space: nowrap; + letter-spacing: 0.5px; } /* Admin button */ diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index 79805b0..2096876 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -12,8 +12,11 @@ export function Sidebar() { const isActive = (path: string) => location.pathname === path; const role = user?.role; + // 今日剩余生成次数(v0.10.0 起计费体系为次数+金额,不再是秒数池) const dailyRemaining = quota - ? (quota.daily_seconds_limit === -1 ? Infinity : Math.max(0, quota.daily_seconds_limit - quota.daily_seconds_used)) + ? (quota.daily_generation_limit === -1 + ? Infinity + : Math.max(0, quota.daily_generation_limit - quota.daily_generation_used)) : 0; return ( @@ -70,15 +73,15 @@ export function Sidebar() {
{/* Quota display - not for super admin */} {role !== 'super_admin' && ( -
navigate('/profile')}> - - - - +
navigate('/profile')} + title="今日剩余生成次数(实际扣费以火山 token 消耗为准)" + > {dailyRemaining === Infinity ? '∞' : dailyRemaining.toLocaleString()} - 剩余额度 + 今日剩余次数
)} diff --git a/web/src/pages/ProfilePage.tsx b/web/src/pages/ProfilePage.tsx index 260488c..59faab0 100644 --- a/web/src/pages/ProfilePage.tsx +++ b/web/src/pages/ProfilePage.tsx @@ -153,10 +153,10 @@ export function ProfilePage() { {/* Quota warning */} {dailyPercent >= 80 && dailyPercent < 100 && ( -
今日额度已使用 {dailyPercent.toFixed(0)}%,请合理使用
+
今日生成次数已用 {dailyPercent.toFixed(0)}%,请合理使用
)} {dailyPercent >= 100 && ( -
今日额度已用完,请明天再试
+
今日生成次数已用完,请明天再试
)} {/* Consumption Overview */} From 27bfa689ce4e47f0d7f66a5c8719d60e7aad4924 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 17 Apr 2026 19:14:28 +0800 Subject: [PATCH 6/6] =?UTF-8?q?test:=20=E5=8A=A0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=9C=8D=20E2E=20=E2=80=94=201080P=20=E5=88=86=E8=BE=A8?= =?UTF-8?q?=E7=8E=87=E6=94=AF=E6=8C=81=E7=BA=BF=E4=B8=8A=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=20(8/8=20=E9=80=9A=E8=BF=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对 airflow-studio.test.airlabs.art 使用团管账号 tudou 真实验证: - Sidebar「今日剩余次数」文案 + 无钻石图标 - Toolbar 默认 720P - AirDrama 可切 1080P - 1080P 下 Fast Dropdown 置灰(UI 不可达 Fast+1080P) - Fast 下 1080P Dropdown 置灰(反向) - ProfilePage 预警文案无「今日额度」老称谓 - API 拒绝 Fast+1080P 组合(400 invalid_resolution) - API 拒绝 adaptive ratio(400) 已跑通,附带 resolution-1080p.spec.ts (本地版,admin 账号,5/5 通过) 和 backend tests (23 unit + 5 integration 全过)。 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../e2e/resolution-1080p-test-env.spec.ts | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 web/test/e2e/resolution-1080p-test-env.spec.ts diff --git a/web/test/e2e/resolution-1080p-test-env.spec.ts b/web/test/e2e/resolution-1080p-test-env.spec.ts new file mode 100644 index 0000000..448f0e9 --- /dev/null +++ b/web/test/e2e/resolution-1080p-test-env.spec.ts @@ -0,0 +1,154 @@ +/** + * 1080P E2E — 针对**测试服**(airflow-studio.test.airlabs.art)使用团管账号 tudou。 + * 这是对 resolution-1080p.spec.ts 的测试服版本,验证 CI/CD 部署后线上真实行为。 + */ +import { test, expect, Page } from '@playwright/test'; + +const BASE_URL = 'https://airflow-studio.test.airlabs.art'; +const API_URL = 'https://airflow-studio-api.test.airlabs.art'; +const USERNAME = 'tudou'; +const PASSWORD = 'seaislee'; + +async function login(page: Page) { + const resp = await page.request.post(`${API_URL}/api/v1/auth/login`, { + data: { username: USERNAME, password: PASSWORD }, + }); + if (!resp.ok()) { + const err = await resp.text(); + throw new Error(`Login failed: ${resp.status()} ${err}`); + } + const body = await resp.json(); + + await page.goto(BASE_URL); + await page.evaluate(({ access, refresh }) => { + localStorage.setItem('access_token', access); + localStorage.setItem('refresh_token', refresh); + }, { access: body.tokens.access, refresh: body.tokens.refresh }); + await page.goto(`${BASE_URL}/app`); + await page.waitForTimeout(2000); + + // 关闭公告 + const knowBtn = page.getByRole('button', { name: /我知道了|知道了|关闭/ }).first(); + if (await knowBtn.isVisible().catch(() => false)) { + await knowBtn.click(); + await page.waitForTimeout(300); + } +} + +test.describe.serial('[测试服] 1080P 分辨率支持 — tudou 团管账号', () => { + test('Sidebar 显示「今日剩余次数」(无钻石图标)', async ({ page }) => { + await login(page); + // 含"今日剩余次数"文案 + await expect(page.getByText('今日剩余次数')).toBeVisible(); + // 确认钻石 SVG 不存在(旧的 diamond path) + const diamondPath = page.locator('path[d^="M6 3h12l4 8"]'); + expect(await diamondPath.count()).toBe(0); + }); + + test('Toolbar 默认分辨率显示 720P', async ({ page }) => { + await login(page); + const resolutionBtn = page.getByRole('button', { name: '720P', exact: true }).first(); + await expect(resolutionBtn).toBeVisible(); + }); + + test('AirDrama 模式可切换到 1080P', async ({ page }) => { + await login(page); + await page.getByRole('button', { name: '720P', exact: true }).first().click(); + await page.waitForTimeout(200); + await page.getByText('1080P', { exact: true }).click(); + await page.waitForTimeout(300); + await expect(page.getByRole('button', { name: '1080P', exact: true }).first()).toBeVisible(); + }); + + test('1080P 下 Fast 模型在 Dropdown 中置灰', async ({ page }) => { + await login(page); + // 先切 1080P + await page.getByRole('button', { name: '720P', exact: true }).first().click(); + await page.waitForTimeout(200); + await page.getByText('1080P', { exact: true }).click(); + await page.waitForTimeout(300); + + // 打开模型 Dropdown + await page.getByRole('button', { name: /AirDrama$/, exact: false }).first().click(); + await page.waitForTimeout(200); + + // Fast 项应带"不支持 1080P" + await expect(page.getByText(/AirDrama Fast.*不支持 1080P/)).toBeVisible(); + }); + + test('Fast 模式下 1080P 在 Dropdown 中置灰', async ({ page }) => { + await login(page); + // 切 Fast 模型 + await page.getByRole('button', { name: /AirDrama$/, exact: false }).first().click(); + await page.waitForTimeout(200); + await page.getByText('AirDrama Fast', { exact: true }).click(); + await page.waitForTimeout(300); + + // 打开分辨率 Dropdown + await page.getByRole('button', { name: '720P', exact: true }).first().click(); + await page.waitForTimeout(200); + + // 1080P 项应带"Fast 不支持" + await expect(page.getByText(/1080P.*Fast 不支持/)).toBeVisible(); + }); + + test('ProfilePage 预警文案显示「今日生成次数」而非「额度」', async ({ page }) => { + await login(page); + await page.goto(`${BASE_URL}/profile`); + await page.waitForTimeout(1500); + // Page 应不含"今日额度"这种老文案 + const body = await page.textContent('body'); + // 能找到"今日"相关字样,不是"额度" + if (body && body.includes('今日')) { + // 如果出现"今日",必须是跟"次数"搭配,不是跟"额度" + expect(body).not.toMatch(/今日额度/); + } + }); + + test('提交 Fast+1080P 组合被后端 400 拒绝', async ({ page }) => { + await login(page); + // 直接调 API 测试(绕过前端 UI 约束,验证后端 fail loud) + const loginResp = await page.request.post(`${API_URL}/api/v1/auth/login`, { + data: { username: USERNAME, password: PASSWORD }, + }); + const { tokens } = await loginResp.json(); + const resp = await page.request.post(`${API_URL}/api/v1/video/generate`, { + headers: { Authorization: `Bearer ${tokens.access}` }, + data: { + prompt: 'E2E 测试 Fast+1080P', + mode: 'universal', + model: 'seedance_2.0_fast', + aspect_ratio: '16:9', + duration: 5, + resolution: '1080p', + references: [], + }, + }); + expect(resp.status()).toBe(400); + const body = await resp.json(); + expect(body.error).toBe('invalid_resolution'); + expect(body.message).toContain('1080P'); + expect(body.message).toContain('Fast'); + }); + + test('提交 adaptive ratio 被后端 400 拒绝', async ({ page }) => { + await login(page); + const loginResp = await page.request.post(`${API_URL}/api/v1/auth/login`, { + data: { username: USERNAME, password: PASSWORD }, + }); + const { tokens } = await loginResp.json(); + const resp = await page.request.post(`${API_URL}/api/v1/video/generate`, { + headers: { Authorization: `Bearer ${tokens.access}` }, + data: { + prompt: 'E2E adaptive', + mode: 'universal', + model: 'seedance_2.0', + aspect_ratio: 'adaptive', + duration: 5, + resolution: '720p', + references: [], + }, + }); + expect(resp.status()).toBe(400); + }); +});