feat(video): 添加三阶段视频生成流程
This commit is contained in:
parent
560f844dce
commit
5ef9a1d4cd
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
name: video-generation-flow-update
|
||||
overview: 修改 VideoExplanation.vue 的视频生成流程,新增阶段二(调用 nano-banana 生成首尾帧图片),然后用首尾帧图片调用万相模型生成视频
|
||||
todos:
|
||||
- id: import-config
|
||||
content: 导入 GRS_API_KEY,添加 storyboardImages 状态和 phase 枚举扩展
|
||||
status: completed
|
||||
- id: add-image-functions
|
||||
content: 新增 generateImageForShot 和 generateImagesFromStoryboard 函数
|
||||
status: completed
|
||||
dependencies:
|
||||
- import-config
|
||||
- id: modify-video-api
|
||||
content: 修改 generateSingleClip 为万相首尾帧视频 API 调用
|
||||
status: completed
|
||||
dependencies:
|
||||
- import-config
|
||||
- id: update-main-flow
|
||||
content: 修改 generateVideoFromScript 整合三个阶段
|
||||
status: completed
|
||||
dependencies:
|
||||
- add-image-functions
|
||||
- modify-video-api
|
||||
- id: update-ui
|
||||
content: 更新 UI 阶段指示器、进度显示和图片预览
|
||||
status: completed
|
||||
dependencies:
|
||||
- update-main-flow
|
||||
---
|
||||
|
||||
## 用户需求
|
||||
|
||||
将视频生成流程修改为三阶段:
|
||||
|
||||
1. **阶段一(保持不变)**:解析试题,生成讲解文案 + 分镜脚本
|
||||
2. **阶段二(新增)**:根据分镜调用 nano-banana 生成首尾帧分镜图片
|
||||
|
||||
- 每个分镜生成 2 张图片(首帧 + 尾帧)
|
||||
- prompt 直接使用 storyboard[].visual
|
||||
- 使用 nano-banana-pro 模型
|
||||
|
||||
3. **阶段三(修改)**:根据首尾帧图片调用万相 API 生成视频
|
||||
|
||||
- 使用万相首尾帧视频模型 wan2.2-kf2v-flash
|
||||
- 视频时长固定 5 秒
|
||||
|
||||
## 关键变更
|
||||
|
||||
- storyboard 新增 firstFrameUrl、lastFrameUrl 字段存储图片
|
||||
- phase 新增枚举值:firstFrame、lastFrame、video
|
||||
- 视频时长从 15 秒改为 5 秒(万相模型限制)
|
||||
- progress 重新分配:脚本 0-15%,首帧 15-40%,尾帧 40-65%,视频 65-98%
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue3 Composition API
|
||||
- axios HTTP 客户端
|
||||
- Vite 代理(已配置 dashscope-api)
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 新增状态
|
||||
|
||||
- `storyboardImages`: 存储每个分镜的首尾帧图片 URL 列表
|
||||
|
||||
### 新增/修改函数
|
||||
|
||||
1. **generateImageForShot(shot, frameType)**: 调用 nano-banana 生成单张图片
|
||||
2. **generateImagesFromStoryboard()**: 遍历分镜生成所有首尾帧图片
|
||||
3. **generateSingleClip()**: 改为调用万相首尾帧视频 API(wan2.2-kf2v-flash)
|
||||
|
||||
### API 调用
|
||||
|
||||
1. **nano-banana 图片生成**:
|
||||
|
||||
- URL: `https://grsai.dakka.com.cn/v1/draw/nano-banana`
|
||||
- 参数: model="nano-banana-pro", prompt=visual, aspectRatio="16:9", imageSize="1K"
|
||||
|
||||
2. **万相首尾帧视频**:
|
||||
|
||||
- URL: `/dashscope-api/api/v1/services/aigc/image2video/video-synthesis`
|
||||
- 参数: model="wan2.2-kf2v-flash", first_frame_url, last_frame_url
|
||||
- resolution: "720P"
|
||||
|
||||
### 配置
|
||||
|
||||
- 复用 `src/config/index.js` 中的 GRS_API_KEY
|
||||
|
|
@ -6,6 +6,8 @@
|
|||
// ── GRS AI(作文批改 / 试题分析)──
|
||||
export const GRS_API_KEY = "sk-6ba4b57a0b034d9388f7a7a2d477d637";
|
||||
|
||||
|
||||
export const DOUBAO_KEY = "2d46f767-6476-4858-8041-540fcae99808";
|
||||
// 作文批改接口
|
||||
export const ESSAY_API_URL = "https://grsai.dakka.com.cn/v1/draw/nano-banana";
|
||||
export const ESSAY_MODEL = "nano-banana-pro";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,603 @@
|
|||
`POST https://ark.cn-beijing.volces.com/api/v3/images/generations` [运行](https://api.volcengine.com/api-explorer/?action=ImageGenerations&groupName=%E5%9B%BE%E7%89%87%E7%94%9F%E6%88%90API&serviceCode=ark&version=2024-01-01&tab=2#N4IgTgpgzgDg9gOyhA+gMzmAtgQwC4gBcIArmADYgA0IUAlgF4REgBMA0tSAO74TY4wAayJoc5ZDSxwAJhErEZcEgCMccALTIIMyDiwaALBoAMG1gFYTADlbWuMMHCwwCxQPhmgUTTA-l6Ao2MAw-4CLeYB4tkHBgDOJgE2KgF+KgABygGHxgNf6gPSmgN2egCwegHEegCFugLCagCfKgOhKgGbx-oBFRoBjkYCTkZGA34qA2Ur+gKyugI76gOSagOJO-oDU5oCnpoBHphWA+Ib+gBVKI4Cf2oAr1oBOQf5wAMaATHaAy+b+gJKKgP1+gL-xgFRxY4CABoCEVoBTPv6A9maAj7b+gKGxgA3OgHnagNxygJJy-peAuyH+gNyugEbpgFgJgHH4wBjfoBvQOygAY5QAz2tkZoBLfUAQjqAQmtAIoagAIEp6AZXlAHBygC51c7+QAUsUNAPjuD38gHSzQKAOYzADMB52y6xagAlTQA55oBSELR0UA2DaAF7V-IAXU0xgB9FQDuioAvIMA9OaAbz1AM8GI0AHJqAAn1soB-PUAS5GAeASKmz-IAAAPW-kAs8qAEB1-IBA80AL4GMlr+QBc+oBUfUagDwVQA2aiAAL5AA)
|
||||
本文介绍图片生成模型如 Seedream 5.0 lite 的调用 API ,包括输入输出参数,取值范围,注意事项等信息,供您使用接口时查阅字段含义。
|
||||
|
||||
**不同模型支持的图片生成能力简介**
|
||||
|
||||
* **doubao\-seedream\-5.0\-lite==^new^==** **、doubao\-seedream\-4.5/4.0**
|
||||
* 生成组图(组图:基于您输入的内容,生成的一组内容关联的图片;需配置 **sequential_image_generation ** 为`auto` **)**
|
||||
* 多图生组图,根据您输入的 **++多张参考图片(2\-14)++ ** +++文本提示词++ 生成一组内容关联的图片(输入的参考图数量+最终生成的图片数量≤15张)。
|
||||
* 单图生组图,根据您输入的 ++单张参考图片+文本提示词++ 生成一组内容关联的图片(最多生成14张图片)。
|
||||
* 文生组图,根据您输入的 ++文本提示词++ 生成一组内容关联的图片(最多生成15张图片)。
|
||||
* 生成单图(配置 **sequential_image_generation ** 为`disabled` **)**
|
||||
* 多图生图,根据您输入的 **++多张参考图片(2\-14)++ ** +++文本提示词++ 生成单张图片。
|
||||
* 单图生图,根据您输入的 ++单张参考图片+文本提示词++ 生成单张图片。
|
||||
* 文生图,根据您输入的 ++文本提示词++ 生成单张图片。
|
||||
* **doubao\-seedream\-3.0\-t2i**
|
||||
* 文生图,根据您输入的 ++文本提示词++ 生成单张图片。
|
||||
* **doubao\-seededit\-** **3.0** **\-i2i**
|
||||
* 图生图,根据您输入的 ++单张参考图片+文本提示词++ 生成单张图片。
|
||||
|
||||
|
||||
|
||||
```mixin-react
|
||||
return (<Tabs>
|
||||
<Tabs.TabPane title="鉴权说明" key="oOTdY3Sn"><RenderMd content={`本接口仅支持 API Key 鉴权,请在 [获取 API Key](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey) 页面,获取长效 API Key。
|
||||
`}></RenderMd></Tabs.TabPane>
|
||||
<Tabs.TabPane title="快速入门" key="HHCpvO5jKo"><RenderMd content={` [ ](#)[体验中心](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?type=GenImage) <span> </span>[模型列表](https://www.volcengine.com/docs/82379/1330310?lang=zh#d3e5e0eb) <span> </span>[模型计费](https://www.volcengine.com/docs/82379/1544106?lang=zh#457edfd0) <span> </span>[API Key](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D)
|
||||
<span> </span>[调用教程](https://www.volcengine.com/docs/82379/1548482) <span> </span>[接口文档](https://www.volcengine.com/docs/82379/1666945) <span> </span>[常见问题](https://www.volcengine.com/docs/82379/1359411) <span> </span>[开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false)
|
||||
`}></RenderMd></Tabs.TabPane></Tabs>);
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
<span id="7thx2dVa"></span>
|
||||
## 请求参数
|
||||
<span id="BFVUvDi6"></span>
|
||||
### 请求体
|
||||
|
||||
---
|
||||
|
||||
|
||||
**model** `string` %%require%%
|
||||
本次请求使用模型的 [Model ID](https://www.volcengine.com/docs/82379/1099455?lang=zh#fc299dc6) 或[推理接入点](https://www.volcengine.com/docs/82379/1099522) (Endpoint ID)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
**prompt ** `string` %%require%%
|
||||
用于生成图像的提示词,支持中英文。(查看提示词指南:[Seedream 4.0](https://www.volcengine.com/docs/82379/1829186) 、[Seedream 3.0](https://www.volcengine.com/docs/82379/1795150))
|
||||
建议不超过300个汉字或600个英文单词。字数过多信息容易分散,模型可能因此忽略细节,只关注重点,造成图片缺失部分元素。
|
||||
|
||||
---
|
||||
|
||||
|
||||
**image** `string/array`
|
||||
> doubao\-seedream\-3.0\-t2i 不支持该参数
|
||||
|
||||
输入的图片信息,支持 URL 或 Base64 编码。其中,doubao\-seedream\-5.0\-lite/4.5/4.0 支持单图或多图输入([查看多图融合示例](https://www.volcengine.com/docs/82379/1824121?lang=zh#4a35e28f)),doubao\-seededit\-3.0\-i2i 仅支持单图输入。
|
||||
|
||||
* 图片URL:请确保图片URL可被访问。
|
||||
* Base64编码:请遵循此格式`data:image/<图片格式>;base64,<Base64编码>`。注意 `<图片格式>` 需小写,如 `data:image/png;base64,<base64_image>`。
|
||||
|
||||
:::tip
|
||||
|
||||
* 传入图片需要满足以下条件:
|
||||
* 图片格式:jpeg、png(doubao\-seedream\-5.0\-lite/4.5/4.0 模型新增支持 webp、bmp、tiff、gif 格式**==^new^==**)
|
||||
* 宽高比(宽/高)范围:
|
||||
* [1/16, 16] (适用模型:doubao\-seedream\-5.0\-lite/4.5/4.0)
|
||||
* [1/3, 3] (适用模型:doubao\-seedream\-3.0\-t2i/seededit\-3.0\-i2i)
|
||||
* 宽高长度(px) \> 14
|
||||
* 大小:不超过 10MB
|
||||
* 总像素:不超过 `6000x6000=36000000` px (对单张图宽度和高度的像素乘积限制,而不是对宽度或高度的单独值进行限制)
|
||||
* doubao\-seedream\-5.0\-lite/4.5/4.0 最多支持传入 14 张参考图。
|
||||
|
||||
|
||||
:::
|
||||
---
|
||||
|
||||
|
||||
**size ** `string`
|
||||
|
||||
```mixin-react
|
||||
return (<Tabs>
|
||||
<Tabs.TabPane title="doubao-seedream-5.0-lite" key="BMB6AP1M"><RenderMd content={`指定生成图像的尺寸信息,支持以下两种方式,不可混用。
|
||||
|
||||
* 方式 1 | 指定生成图像的分辨率,并在prompt中用自然语言描述图片宽高比、图片形状或图片用途,最终由模型判断生成图片的大小。
|
||||
* 可选值:\`2K\`、\`3K\`
|
||||
* 方式 2 | 指定生成图像的宽高像素值:
|
||||
* 默认值:\`2048x2048\`
|
||||
* 总像素取值范围:[\`2560x1440=3686400\`, \`3072x3072x1.1025=10404496\`]
|
||||
* 宽高比取值范围:[1/16, 16]
|
||||
|
||||
:::tip
|
||||
采用方式 2 时,需同时满足总像素取值范围和宽高比取值范围。其中,总像素是对单张图宽度和高度的像素乘积限制,而不是对宽度或高度的单独值进行限制。
|
||||
|
||||
* **有效示例**:\`3750x1250\`
|
||||
|
||||
总像素值 3750x1250=4687500,符合 [3686400, 10404496] 的区间要求;宽高比 3750/1250=3,符合 [1/16, 16] 的区间要求,故该示例值有效。
|
||||
|
||||
* **无效示例**:\`1500x1500\`
|
||||
|
||||
总像素值 1500x1500=2250000,未达到 3686400 的最低要求;宽高 1500/1500=1,虽符合 [1/16, 16] 的区间要求,但因其未同时满足两项限制,故该示例值无效。
|
||||
:::
|
||||
推荐的宽高像素值:
|
||||
|
||||
|分辨率 |宽高比 |宽高像素值 |
|
||||
|---|---|---|
|
||||
|<div style="text-align: center">|1:1 |2048x2048 |\\
|
||||
|2K</div>| | |\\
|
||||
| | | |
|
||||
|^^|4:3 |2304x1728 |
|
||||
|^^|3:4 |1728x2304 |
|
||||
|^^|16:9 |2848x1600 |
|
||||
|^^|9:16 |1600x2848 |
|
||||
|^^|3:2 |2496x1664 |
|
||||
|^^|2:3 |1664x2496 |
|
||||
|^^|21:9 |3136x1344 |
|
||||
|<div style="text-align: center">|1:1 |3072x3072 |\\
|
||||
|3K</div>| | |\\
|
||||
| | | |
|
||||
|^^|4:3 |3456x2592 |
|
||||
|^^|3:4 |2592x3456 |
|
||||
|^^|16:9 |4096x2304 |
|
||||
|^^|9:16 |2304x4096 |
|
||||
|^^|2:3 |2496x3744 |
|
||||
|^^|3:2 |3744x2496 |
|
||||
|^^|21:9 |4704x2016 |
|
||||
|
||||
|
||||
`}></RenderMd></Tabs.TabPane>
|
||||
<Tabs.TabPane title="doubao-seedream-4.5" key="kghENadO"><RenderMd content={`指定生成图像的尺寸信息,支持以下两种方式,不可混用。
|
||||
|
||||
* 方式 1 | 指定生成图像的分辨率,并在prompt中用自然语言描述图片宽高比、图片形状或图片用途,最终由模型判断生成图片的大小。
|
||||
* 可选值:\`2K\`、\`4K\`
|
||||
* 方式 2 | 指定生成图像的宽高像素值:
|
||||
* 默认值:\`2048x2048\`
|
||||
* 总像素取值范围:[\`2560x1440=3686400\`, \`4096x4096=16777216\`]
|
||||
* 宽高比取值范围:[1/16, 16]
|
||||
|
||||
:::tip
|
||||
采用方式 2 时,需同时满足总像素取值范围和宽高比取值范围。其中,总像素是对单张图宽度和高度的像素乘积限制,而不是对宽度或高度的单独值进行限制。
|
||||
|
||||
* **有效示例**:\`3750x1250\`
|
||||
|
||||
总像素值 3750x1250=4687500,符合 [3686400, 16777216] 的区间要求;宽高比 3750/1250=3,符合 [1/16, 16] 的区间要求,故该示例值有效。
|
||||
|
||||
* **无效示例**:\`1500x1500\`
|
||||
|
||||
总像素值 1500x1500=2250000,未达到 3686400 的最低要求;宽高 1500/1500=1,虽符合 [1/16, 16] 的区间要求,但因其未同时满足两项限制,故该示例值无效。
|
||||
:::
|
||||
推荐的宽高像素值:
|
||||
|
||||
|分辨率 |宽高比 |宽高像素值 |
|
||||
|---|---|---|
|
||||
|<div style="text-align: center">|1:1 |2048x2048 |\\
|
||||
|2K</div>| | |\\
|
||||
| | | |
|
||||
|^^|4:3 |2304x1728 |
|
||||
|^^|3:4 |1728x2304 |
|
||||
|^^|16:9 |2848x1600 |
|
||||
|^^|9:16 |1600x2848 |
|
||||
|^^|3:2 |2496x1664 |
|
||||
|^^|2:3 |1664x2496 |
|
||||
|^^|21:9 |3136x1344 |
|
||||
|<div style="text-align: center">|1:1 |4096x4096 |\\
|
||||
|4K</div>| | |\\
|
||||
| | | |
|
||||
|^^|3:4 |3520x4704 |
|
||||
|^^|4:3 |4704x3520 |
|
||||
|^^|16:9 |5504x3040 |
|
||||
|^^|9:16 |3040x5504 |
|
||||
|^^|2:3 |3328x4992 |
|
||||
|^^|3:2 |4992x3328 |
|
||||
|^^|21:9 |6240x2656 |
|
||||
|
||||
|
||||
`}></RenderMd></Tabs.TabPane>
|
||||
<Tabs.TabPane title="doubao-seedream-4.0" key="MKsftGMr"><RenderMd content={`指定生成图像的尺寸信息,支持以下两种方式,不可混用。
|
||||
|
||||
* 方式 1 | 指定生成图像的分辨率,并在prompt中用自然语言描述图片宽高比、图片形状或图片用途,最终由模型判断生成图片的大小。
|
||||
* 可选值:\`1K\`、\`2K\`、\`4K\`
|
||||
* 方式 2 | 指定生成图像的宽高像素值:
|
||||
* 默认值:\`2048x2048\`
|
||||
* 总像素取值范围:[\`1280x720=921600\`, \`4096x4096=16777216\`]
|
||||
* 宽高比取值范围:[1/16, 16]
|
||||
|
||||
:::tip
|
||||
采用方式 2 时,需同时满足总像素取值范围和宽高比取值范围。其中,总像素是对单张图宽度和高度的像素乘积限制,而不是对宽度或高度的单独值进行限制。
|
||||
|
||||
* **有效示例**:\`1600x600\`
|
||||
|
||||
总像素值 1600x600=960000,符合 [921600, 16777216] 的区间要求;宽高比 1600/600=8/3,符合 [1/16, 16] 的区间要求,故该示例值有效。
|
||||
|
||||
* **无效示例**:\`800x800\`
|
||||
|
||||
总像素值 800x800=640000,未达到 921600 的最低要求;宽高 800/800=1,虽符合 [1/16, 16] 的区间要求,但因其未同时满足两项限制,故该示例值无效。
|
||||
:::
|
||||
推荐的宽高像素值:
|
||||
|
||||
|分辨率 |宽高比 |宽高像素值 |
|
||||
|---|---|---|
|
||||
|<div style="text-align: center">|1:1 |1024x1024 |\\
|
||||
|1K</div>| | |\\
|
||||
| | | |
|
||||
|^^|4:3 |1152x864 |
|
||||
|^^|3:4 |864x1152 |
|
||||
|^^|16:9 |1280x720 |
|
||||
|^^|9:16 |720x1280 |
|
||||
|^^|3:2 |832x1248 |
|
||||
|^^|2:3 |1248x832 |
|
||||
|^^|21:9 |1512x648 |
|
||||
|<div style="text-align: center">|1:1 |2048x2048 |\\
|
||||
|2K</div>| | |\\
|
||||
| | | |
|
||||
|^^|4:3 |2304x1728 |
|
||||
|^^|3:4 |1728x2304 |
|
||||
|^^|16:9 |2848x1600 |
|
||||
|^^|9:16 |1600x2848 |
|
||||
|^^|3:2 |2496x1664 |
|
||||
|^^|2:3 |1664x2496 |
|
||||
|^^|21:9 |3136x1344 |
|
||||
|<div style="text-align: center">|1:1 |4096x4096 |\\
|
||||
|4K</div>| | |\\
|
||||
| | | |
|
||||
|^^|3:4 |3520x4704 |
|
||||
|^^|4:3 |4704x3520 |
|
||||
|^^|16:9 |5504x3040 |
|
||||
|^^|9:16 |3040x5504 |
|
||||
|^^|2:3 |3328x4992 |
|
||||
|^^|3:2 |4992x3328 |
|
||||
|^^|21:9 |6240x2656 |
|
||||
|
||||
|
||||
`}></RenderMd></Tabs.TabPane>
|
||||
<Tabs.TabPane title="doubao-seedream-3.0-t2i" key="dUuqsxPhNL"><RenderMd content={`指定生成图像的宽高像素值。
|
||||
|
||||
* 默认值:\`1024x1024\`
|
||||
* 单张图片像素取值范围: [\`512x512\`, \`2048x2048\`]
|
||||
|
||||
推荐的宽高像素值:
|
||||
|
||||
|宽高比 |宽高像素值 |
|
||||
|---|---|
|
||||
|1:1 |1024x1024 |
|
||||
|4:3 |864x1152 |
|
||||
|3:4 |1152x864 |
|
||||
|16:9 |1280x720 |
|
||||
|9:16 |720x1280 |
|
||||
|3:2 |832x1248 |
|
||||
|2:3 |1248x832 |
|
||||
|21:9 |1512x648 |
|
||||
|
||||
`}></RenderMd></Tabs.TabPane>
|
||||
<Tabs.TabPane title="doubao-seededit-3.0-i2i" key="pOay7SY7u2"><RenderMd content={`指定生成图像的宽高像素值。**当前仅支持 adaptive。**
|
||||
|
||||
* adaptive。将您的输入图片尺寸与下表中的尺寸进行对比,选择最接近的,作为输出图片的尺寸。具体而言,会按顺序从可选比例中,选取与原图宽高比**差值最小**的**第一个**,作为生成图片的比例。
|
||||
* 预设的高宽像素
|
||||
|
||||
|
||||
|宽/高 |宽 |高 |
|
||||
|---|---|---|
|
||||
|0.33 |512 |1536 |
|
||||
|0.35 |544 |1536 |
|
||||
|0.38 |576 |1536 |
|
||||
|0.4 |608 |1536 |
|
||||
|0.42 |640 |1536 |
|
||||
|0.47 |640 |1376 |
|
||||
|0.51 |672 |1312 |
|
||||
|0.55 |704 |1280 |
|
||||
|0.56 |736 |1312 |
|
||||
|0.6 |768 |1280 |
|
||||
|0.63 |768 |1216 |
|
||||
|
||||
|
||||
`}></RenderMd></Tabs.TabPane></Tabs>);
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
**seed** `integer` `默认值 -1`
|
||||
> 仅 doubao\-seedream\-3.0\-t2i/seededit\-3.0\-i2i 支持该参数
|
||||
|
||||
随机数种子,用于控制模型生成内容的随机性。取值范围为 [\-1, 2147483647]。
|
||||
:::warning
|
||||
|
||||
* 相同的请求下,模型收到不同的seed值,如:不指定seed值或令seed取值为\-1(会使用随机数替代)、或手动变更seed值,将生成不同的结果。
|
||||
* 相同的请求下,模型收到相同的seed值,会生成类似的结果,但不保证完全一致。
|
||||
|
||||
|
||||
:::
|
||||
---
|
||||
|
||||
|
||||
**sequential_image_generation** `string` `默认值 disabled`
|
||||
> 仅 doubao\-seedream\-5.0\-lite/4.5/4.0 支持该参数 | [查看组图输出示例](https://www.volcengine.com/docs/82379/1824121?lang=zh#fc9f85e4)
|
||||
|
||||
控制是否关闭组图功能。
|
||||
:::tip
|
||||
组图:基于您输入的内容,生成的一组内容关联的图片。
|
||||
|
||||
:::
|
||||
* `auto`:自动判断模式,模型会根据用户提供的提示词自主判断是否返回组图以及组图包含的图片数量。
|
||||
* `disabled`:关闭组图功能,模型只会生成一张图。
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
**sequential_image_generation_options ** `object`
|
||||
> 仅 doubao\-seedream\-5.0\-lite/4.5/4.0 支持该参数
|
||||
|
||||
组图功能的配置。仅当 **sequential_image_generation ** 为 `auto` 时生效。
|
||||
|
||||
属性
|
||||
|
||||
---
|
||||
|
||||
|
||||
sequential_image_generation_options.**max_images ** ** ** `integer` `默认值 15`
|
||||
指定本次请求,最多可生成的图片数量。
|
||||
|
||||
* 取值范围: [1, 15]
|
||||
|
||||
:::tip
|
||||
实际可生成的图片数量,除受到 **max_images ** 影响外 **,** 还受到输入的参考图数量影响。**输入的参考图数量+最终生成的图片数量≤15张**。
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
|
||||
**tools==^new^==** ** ** `array of object`
|
||||
> 仅 doubao\-seedream\-5.0\-lite 支持该参数
|
||||
|
||||
配置模型要调用的工具。
|
||||
|
||||
属性
|
||||
|
||||
---
|
||||
|
||||
|
||||
tools.**type ** `string`
|
||||
指定使用的工具类型。
|
||||
|
||||
* `web_search`:联网搜索功能。
|
||||
|
||||
:::tip
|
||||
|
||||
* 开启联网搜索后,模型会根据用户的提示词自主判断是否搜索互联网内容(如商品、天气等),提升生成图片的时效性,但也会增加一定的时延。
|
||||
* 实际搜索次数可通过字段 usage.tool_usage.**web_search** 查询,如果为 0 表示未搜索。
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
|
||||
**stream** `Boolean` `默认值 false`
|
||||
> 仅 doubao\-seedream\-5.0\-lite/4.5/4.0 支持该参数 | [查看流式输出示例](https://www.volcengine.com/docs/82379/1824121?lang=zh#e5bef0d7)
|
||||
|
||||
控制是否开启流式输出模式。
|
||||
|
||||
* `false`:非流式输出模式,等待所有图片全部生成结束后再一次性返回所有信息。
|
||||
* `true`:流式输出模式,即时返回每张图片输出的结果。在生成单图和组图的场景下,流式输出模式均生效。
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
**guidance_scale ** `Float`
|
||||
> doubao\-seedream\-3.0\-t2i 默认值 2.5
|
||||
> doubao\-seededit\-3.0\-i2i 默认值 5.5
|
||||
> doubao\-seedream\-5.0\-lite/4.5/4.0 不支持
|
||||
|
||||
模型输出结果与prompt的一致程度,生成图像的自由度,又称为文本权重;值越大,模型自由度越小,与用户输入的提示词相关性越强。
|
||||
取值范围:[`1`, `10`] 。
|
||||
|
||||
---
|
||||
|
||||
|
||||
**output_format==^new^==**`string` `默认值 jpeg`
|
||||
> 仅 doubao\-seedream\-5.0\-lite 支持该参数
|
||||
|
||||
指定生成图像的文件格式。可选值:
|
||||
|
||||
* `png`
|
||||
* `jpeg`
|
||||
|
||||
:::tip
|
||||
doubao\-seedream\-4.5/4.0、doubao\-seedream\-3.0\-t2i/seededit\-3.0\-i2i 模型生成图像的文件格式默认为 jpeg,不支持自定义设置。
|
||||
|
||||
:::
|
||||
---
|
||||
|
||||
|
||||
**response_format** `string` `默认值 url`
|
||||
指定生成图像的返回格式。支持以下两种返回方式:
|
||||
|
||||
* `url`:返回图片下载链接;**链接在图片生成后24小时内有效,请及时下载图片。**
|
||||
* `b64_json`:以 Base64 编码字符串的 JSON 格式返回图像数据。
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
**watermark** `Boolean` `默认值 true`
|
||||
是否在生成的图片中添加水印。
|
||||
|
||||
* `false`:不添加水印。
|
||||
* `true`:在图片右下角添加“AI生成”字样的水印标识。
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
**optimize_prompt_options ** `object`
|
||||
> 仅 doubao\-seedream\-5.0\-lite/4.5/4.0 支持该参数
|
||||
|
||||
提示词优化功能的配置。
|
||||
|
||||
属性
|
||||
optimize_prompt_options.**mode ** `string` `默认值 standard`
|
||||
设置提示词优化功能使用的模式。
|
||||
|
||||
* `standard`:标准模式,生成内容的质量更高,耗时较长。
|
||||
* `fast`:快速模式,生成内容的耗时更短,质量一般;doubao\-seedream\-5.0\-lite/4.5 当前不支持。
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
<span id="7P96iLnc"></span>
|
||||
## 响应参数
|
||||
<span id="Hrya4y9k"></span>
|
||||
### 流式响应参数
|
||||
请参见[文档](https://www.volcengine.com/docs/82379/1824137?lang=zh)。
|
||||
|
||||
<span id="1AxnwQZN"></span>
|
||||
### 非流式响应参数
|
||||
|
||||
---
|
||||
|
||||
|
||||
**model** `string`
|
||||
本次请求使用的模型 ID (`模型名称-版本`)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
**created** `integer`
|
||||
本次请求创建时间的 Unix 时间戳(秒)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
**data** `array`
|
||||
输出图像的信息。
|
||||
:::tip
|
||||
doubao\-seedream\-5.0\-lite/4.5/4.0 模型生成组图场景下,组图生成过程中某张图生成失败时:
|
||||
|
||||
* 若失败原因为审核不通过:仍会继续请求下一个图片生成任务,即不影响同请求内其他图片的生成流程。
|
||||
* 若失败原因为内部服务异常(500):不会继续请求下一个图片生成任务。
|
||||
|
||||
|
||||
:::
|
||||
可能类型
|
||||
图片信息 `object`
|
||||
生成成功的图片信息。
|
||||
|
||||
属性
|
||||
data.**url ** `string`
|
||||
图片的 url 信息,当 **response_format ** 指定为 `url` 时返回。该链接将在生成后 **24 小时内失效**,请务必及时保存图像。
|
||||
|
||||
---
|
||||
|
||||
|
||||
data.**b64_json** `string`
|
||||
图片的 base64 信息,当 **response_format ** 指定为 `b64_json` 时返回。
|
||||
|
||||
---
|
||||
|
||||
|
||||
data.**size** `string`
|
||||
> 仅 doubao\-seedream\-5.0\-lite/4.5/4.0 支持该字段。
|
||||
|
||||
图像的宽高像素值,格式 `<宽像素>x<高像素>`,如`2048×2048`。
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
错误信息 `object`
|
||||
某张图片生成失败,错误信息。
|
||||
|
||||
属性
|
||||
data.**error** `object`
|
||||
错误信息结构体。
|
||||
|
||||
属性
|
||||
|
||||
---
|
||||
|
||||
|
||||
data.error.**code**
|
||||
某张图片生成错误的错误码,请参见[错误码](https://www.volcengine.com/docs/82379/1299023)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
data.error.**message**
|
||||
某张图片生成错误的提示信息。
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
**tools** `array of object`
|
||||
本次请求,配置的模型调用工具
|
||||
|
||||
属性
|
||||
|
||||
---
|
||||
|
||||
|
||||
tools.**type ** `string`
|
||||
配置的调用工具类型。
|
||||
|
||||
* web_search:联网搜索工具。
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
**usage** `object`
|
||||
本次请求的用量信息。
|
||||
|
||||
属性
|
||||
|
||||
---
|
||||
|
||||
|
||||
usage.**generated_images ** `integer`
|
||||
模型成功生成的图片张数,不包含生成失败的图片。
|
||||
仅对成功生成图片按张数进行计费。
|
||||
|
||||
---
|
||||
|
||||
|
||||
usage.**output_tokens** `integer`
|
||||
模型生成的图片花费的 token 数量。
|
||||
计算逻辑为:计算 `sum(图片长*图片宽)/256` ,然后取整。
|
||||
|
||||
---
|
||||
|
||||
|
||||
usage.**total_tokens** `integer`
|
||||
本次请求消耗的总 token 数量。
|
||||
当前不计算输入 token,故与 **output_tokens** 值一致。
|
||||
|
||||
---
|
||||
|
||||
|
||||
usage.**tool_usage ** `object`
|
||||
使用工具的用量信息。
|
||||
|
||||
属性
|
||||
|
||||
---
|
||||
|
||||
|
||||
usage.tool_usage.**web_search ** `integer`
|
||||
调用联网搜索工具次数,仅开启联网搜索时返回。
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
**error** `object`
|
||||
本次请求,如发生错误,对应的错误信息。
|
||||
|
||||
属性
|
||||
|
||||
---
|
||||
|
||||
|
||||
error.**code** `string`
|
||||
请参见[错误码](https://www.volcengine.com/docs/82379/1299023)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
error.**message** `string`
|
||||
错误提示信息
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,653 @@
|
|||
万相首尾帧生视频模型基于**首帧图像**、**尾帧图像和文本提示词**,生成一段平滑过渡的视频。
|
||||
|
||||
**相关文档**:[使用指南](https://help.aliyun.com/zh/model-studio/image-to-video-first-and-last-frames-guide)
|
||||
|
||||
## 适用范围
|
||||
|
||||
为确保调用成功,请务必保证模型、Endpoint URL 和 API Key 均属于**同一地域**。跨地域调用将会失败。
|
||||
|
||||
- [**选择模型**](https://help.aliyun.com/zh/model-studio/image-to-video-first-and-last-frames-guide#06f39eafa2dwt):确认模型所属的地域。
|
||||
|
||||
- **选择 URL**:选择对应的地域 Endpoint URL,支持HTTP URL或 DashScope SDK URL。
|
||||
|
||||
- **配置 API Key**:选择地域并[获取API Key](https://help.aliyun.com/zh/model-studio/get-api-key),再[配置API Key到环境变量](https://help.aliyun.com/zh/model-studio/configure-api-key-through-environment-variables)。
|
||||
|
||||
- **安装 SDK**:如需通过SDK进行调用,请[安装DashScope SDK](https://help.aliyun.com/zh/model-studio/install-sdk)。
|
||||
|
||||
|
||||
**说明**
|
||||
|
||||
本文的示例代码适用于**北京地域**。
|
||||
|
||||
## HTTP调用
|
||||
|
||||
由于图生视频任务耗时较长(通常为1-5分钟),API采用异步调用。整个流程包含 **“创建任务 -> 轮询获取”** 两个核心步骤,具体如下:
|
||||
|
||||
### 步骤1:创建任务获取任务ID
|
||||
|
||||
## **北京**
|
||||
|
||||
`POST https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis`
|
||||
|
||||
## **新加坡**
|
||||
|
||||
`POST https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis`
|
||||
|
||||
**说明**
|
||||
|
||||
- 创建成功后,使用接口返回的 `task_id` 查询结果,task\_id 有效期为 24 小时。**请勿重复创建任务**,轮询获取即可。
|
||||
|
||||
- 新手指引请参见[Postman](https://help.aliyun.com/zh/model-studio/first-call-to-image-and-video-api)。
|
||||
|
||||
|
||||
| #### 请求参数 | ## 首尾帧生视频 根据首帧、尾帧和prompt生成视频。 ``` curl --location 'https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis' \\ -H 'X-DashScope-Async: enable' \\ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \\ -H 'Content-Type: application/json' \\ -d '{ "model": "wan2.2-kf2v-flash", "input": { "first_frame_url": "https://wanx.alicdn.com/material/20250318/first_frame.png", "last_frame_url": "https://wanx.alicdn.com/material/20250318/last_frame.png", "prompt": "写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。" }, "parameters": { "resolution": "480P", "prompt_extend": true } }' ``` ## 使用Base64 首帧first\\_frame\\_url和尾帧last\\_frame\\_url参数支持传入图像的 Base64 编码字符串。先下载[first\\_frame\\_base64](https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250722/vzjwiv/first_frame_base64.txt)和[last\\_frame\\_base64](https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250722/qbtevh/last_frame_base64.txt)文件,并将完整内容粘贴至对应参数中。 格式参见[如何输入图像](https://help.aliyun.com/zh/model-studio/image-to-video-first-and-last-frames-guide#69308edfbebnx)。 ``` curl --location 'https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis' \\ -H 'X-DashScope-Async: enable' \\ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \\ -H 'Content-Type: application/json' \\ -d '{ "model": "wanx2.1-kf2v-plus", "input": { "first_frame_url": "data:image/png;base64,GDU7MtCZzEbTbmRZ......", "last_frame_url": "data:image/png;base64,VBORw0KGgoAAAANSUh......", "prompt": "写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。" }, "parameters": { "resolution": "720P", "prompt_extend": true } }' ``` ## 使用视频特效 必须传入`first_frame_url`和`template`,无需传入prompt和last\\_frame\\_url。 不同模型支持不同的特效模板。调用前请查阅[万相-图生视频-视频特效](https://help.aliyun.com/zh/model-studio/wanx-video-effects),以免调用失败。 ``` curl --location 'https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis' \\ -H 'X-DashScope-Async: enable' \\ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \\ -H 'Content-Type: application/json' \\ -d '{ "model": "wanx2.1-kf2v-plus", "input": { "first_frame_url": "https://ty-yuanfang.oss-cn-hangzhou.aliyuncs.com/lizhengjia.lzj/tmp/11.png", "template": "hanfu-1" }, "parameters": { "resolution": "720P", "prompt_extend": true } }' ``` ## 使用反向提示词 通过 negative\\_prompt 指定生成的视频避免出现“人物”元素。 ``` curl --location 'https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis' \\ -H 'X-DashScope-Async: enable' \\ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \\ -H 'Content-Type: application/json' \\ -d '{ "model": "wanx2.1-kf2v-plus", "input": { "first_frame_url": "https://wanx.alicdn.com/material/20250318/first_frame.png", "last_frame_url": "https://wanx.alicdn.com/material/20250318/last_frame.png", "prompt": "写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。", "negative_prompt": "人物" }, "parameters": { "resolution": "720P", "prompt_extend": true } }' ``` |
|
||||
| --- | --- |
|
||||
| ##### 请求头(Headers) |
|
||||
| **Content-Type** `*string*` **(必选)** 请求内容类型。此参数必须设置为`application/json`。 |
|
||||
| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API-Key进行身份认证。示例值:Bearer sk-xxxx。 |
|
||||
| **X-DashScope-Async** `*string*` **(必选)** 异步处理配置参数。HTTP请求只支持异步,**必须设置为**`**enable**`。 **重要** 缺少此请求头将报错:“current user api does not support synchronous calls”。 |
|
||||
| ##### 请求体(Request Body) |
|
||||
| **model** `*string*` **(必选)** 模型名称。示例值:wan2.2-kf2v-flash。 详情参见[模型列表与价格](https://help.aliyun.com/zh/model-studio/models#3511187847vep)。 |
|
||||
| **input** `*object*` **(必选)** 输入的基本信息,如提示词等。 **属性** **prompt** `*string*` (可选) 文本提示词。支持中英文,长度不超过800个字符,每个汉字/字母占一个字符,超过部分会自动截断。 如果首尾帧的主体和场景变化较大,建议描写变化过程,例如运镜过程(镜头向左移动)或者主体运动过程(人向前奔跑)。 示例值:一只黑色小猫好奇地看向天空,**镜头从平视逐渐上升**,最后**俯拍**它的好奇的眼神。 提示词的使用技巧请参见[文生视频/图生视频Prompt指南](https://help.aliyun.com/zh/model-studio/text-to-video-prompt)。 **negative\\_prompt** `*string*` (可选) 反向提示词,用来描述不希望在视频画面中看到的内容,可以对视频画面进行限制。 支持中英文,长度不超过500个字符,超过部分会自动截断。 示例值:低分辨率、错误、最差质量、低质量、残缺、多余的手指、比例不良等。 **first\\_frame\\_url** `*string*` **(必选)** 首帧图像的URL或 Base64 编码数据。**输出视频的宽高比将以此图像为基准。** 图像限制: - 图像格式:JPEG、JPG、PNG(不支持透明通道)、BMP、WEBP。 - 图像分辨率:图像的宽度和高度范围为\\[240,8000\\],单位为像素。 - 文件大小:不超过10MB。 支持输入的格式: 1. 公网URL: - 支持 HTTP 或 HTTPS 协议。 - 示例值:https://wanx.alicdn.com/xxx/first\\_frame.png。 2. 临时URL: - 支持OSS协议,必须通过[上传文件获取临时 URL](https://help.aliyun.com/zh/model-studio/get-temporary-file-url)。 - 示例值:oss://dashscope-instant/xxx/xxx.png。 3. Base64 编码图像后的字符串: - 数据格式:`data:{MIME_type};base64,{base64_data}`。 - 示例值:data:image/png;base64,GDU7MtCZzEbTbmRZ......。(编码字符串过长,仅展示片段) - 详情请参见[如何输入图像](https://help.aliyun.com/zh/model-studio/image-to-video-first-and-last-frames-guide#69308edfbebnx)。 **last\\_frame\\_url** `*string*` (可选) 尾帧图像的URL或 Base64 编码数据。 图像限制: - 图像格式:JPEG、JPG、PNG(不支持透明通道)、BMP、WEBP。 - 图像分辨率:图像的宽度和高度范围为\\[240,8000\\],单位为像素。尾帧图像分辨率可与首帧不同,无需强制对齐。 - 文件大小:不超过10MB。 支持输入的格式: 1. 公网URL: - 支持 HTTP 或 HTTPS 协议。 - 示例值:https://wanx.alicdn.com/xxxx/last\\_frame.png。 2. 临时URL: - 支持OSS协议,必须通过[上传文件获取临时 URL](https://help.aliyun.com/zh/model-studio/get-temporary-file-url)。 - 示例值:oss://dashscope-instant/xxx/xxx.png。 3. Base64 编码图像后的字符串: - 数据格式:`data:{MIME_type};base64,{base64_data}`。 - 示例值:data:image/png;base64,GDU7MtCZ......。(编码字符串过长,仅展示片段) - 详情请参见[如何输入图像](https://help.aliyun.com/zh/model-studio/image-to-video-first-and-last-frames-guide#69308edfbebnx)。 **template** `*string*` (可选) 视频特效模板的名称。使用此参数时,仅需传入 `first_frame_url`。 不同模型支持不同的特效模板。调用前请查阅[万相-图生视频-视频特效](https://help.aliyun.com/zh/model-studio/wanx-video-effects),以免调用失败。 示例值:hufu-1,表示使用“唐韵翩然”特效。 |
|
||||
| **parameters** `*object*` (可选) 视频处理参数。 **属性** **resolution** `*string*` (可选) **重要** **resolution**直接影响费用,同一模型:1080P > 720P > 480P,调用前请确认[模型价格](https://help.aliyun.com/zh/model-studio/models#3511187847vep)。 生成的视频分辨率档位。仅用于调整视频的清晰度(总像素),不改变视频的宽高比,**视频宽高比将与首帧图像 first\\_frame\\_url 的宽高比保持一致**。 此参数的默认值和可用枚举值依赖于 model 参数,规则如下: - wan2.2-kf2v-flash:可选值:480P、720P、1080P。默认值为`720P`。 - wanx2.1-kf2v-plus:可选值:720P。默认值为`720P`。 示例值:720P。 **duration** `*integer*` (可选) **重要** duration直接影响费用,按秒计费,调用前请确认[模型价格](https://help.aliyun.com/zh/model-studio/models#3511187847vep)。 视频生成时长,单位为秒。当前参数值固定为5,且不支持修改。模型将始终生成5秒时长的视频。 **prompt\\_extend** `*bool*` (可选) 是否开启prompt智能改写。开启后使用大模型对输入prompt进行智能改写。对于较短的prompt生成效果提升明显,但会增加耗时。 - true:默认值,开启智能改写。 - false:不开启智能改写。 示例值:true。 **watermark** `*bool*` (可选) 是否添加水印标识,水印位于图片右下角,文案为“AI生成”。 - false:默认值,不添加水印。 - true:添加水印。 示例值:false。 **seed** `*integer*` (可选) 随机数种子,取值范围为`[0, 2147483647]`。 未指定时,系统自动生成随机种子。若需提升生成结果的可复现性,建议固定seed值。 请注意,由于模型生成具有概率性,即使使用相同 seed,也不能保证每次生成结果完全一致。 示例值:12345。 |
|
||||
|
||||
| #### 响应参数 | ### 成功响应 请保存 task\\_id,用于查询任务状态与结果。 ``` { "output": { "task_status": "PENDING", "task_id": "0385dc79-5ff8-4d82-bcb6-xxxxxx" }, "request_id": "4909100c-7b5a-9f92-bfe5-xxxxxx" } ``` ### 异常响应 创建任务失败,请参见[错误信息](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "code": "InvalidApiKey", "message": "No API-key provided.", "request_id": "7438d53d-6eb8-4596-8835-xxxxxx" } ``` |
|
||||
| --- | --- |
|
||||
| **output** `*object*` 任务输出信息。 属性 **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 |
|
||||
| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 |
|
||||
| **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误信息](https://help.aliyun.com/zh/model-studio/error-code)。 | |
|
||||
| **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误信息](https://help.aliyun.com/zh/model-studio/error-code)。 | |
|
||||
|
||||
### 步骤2:根据任务ID查询结果
|
||||
|
||||
## **北京**
|
||||
|
||||
`GET https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}`
|
||||
|
||||
## **新加坡**
|
||||
|
||||
`GET https://dashscope-intl.aliyuncs.com/api/v1/tasks/{task_id}`
|
||||
|
||||
**说明**
|
||||
|
||||
- **轮询建议**:视频生成过程约需数分钟,建议采用**轮询**机制,并设置合理的查询间隔(如 15 秒)来获取结果。
|
||||
|
||||
- **任务状态流转**:PENDING(排队中)→ RUNNING(处理中)→ SUCCEEDED(成功)/ FAILED(失败)。
|
||||
|
||||
- **结果链接**:任务成功后返回视频链接,有效期为 **24 小时**。建议在获取链接后立即下载并转存至永久存储(如[阿里云 OSS](https://help.aliyun.com/zh/oss/user-guide/what-is-oss))。
|
||||
|
||||
- **task\_id 有效期**:**24小时**,超时后将无法查询结果,接口将返回任务状态为`UNKNOWN`。
|
||||
|
||||
- **RPS 限制**:查询接口默认RPS为20。如需更高频查询或事件通知,建议[配置异步任务回调](https://help.aliyun.com/zh/model-studio/async-task-api)。
|
||||
|
||||
- **更多操作**:如需批量查询、取消任务等操作,请参见[管理异步任务](https://help.aliyun.com/zh/model-studio/manage-asynchronous-tasks#f26499d72adsl)。
|
||||
|
||||
|
||||
| #### 请求参数 | ## 查询任务结果 请将`86ecf553-d340-4e21-xxxxxxxxx`替换为真实的task\\_id。 > 若使用新加坡地域的模型,需将base\\_url替换为https://dashscope-intl.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx ``` curl -X GET https://dashscope.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx \\ --header "Authorization: Bearer $DASHSCOPE_API_KEY" ``` |
|
||||
| --- | --- |
|
||||
| ##### **请求头(Headers)** |
|
||||
| **Authorization** `*string*`**(必选)** 请求身份认证。接口使用阿里云百炼API-Key进行身份认证。示例值:Bearer sk-xxxx。 |
|
||||
| ##### **URL路径参数(Path parameters)** |
|
||||
| **task\\_id** `*string*`**(必选)** 任务ID。 |
|
||||
|
||||
| #### **响应参数** | ## 任务执行成功 视频URL仅保留24小时,超时后会被自动清除,请及时保存生成的视频。 ``` { "request_id": "ec016349-6b14-9ad6-8009-xxxxxx", "output": { "task_id": "3f21a745-9f4b-4588-b643-xxxxxx", "task_status": "SUCCEEDED", "submit_time": "2025-04-18 10:36:58.394", "scheduled_time": "2025-04-18 10:37:13.802", "end_time": "2025-04-18 10:45:23.004", "video_url": "https://dashscope-result-wlcb.oss-cn-wulanchabu.aliyuncs.com/xxx.mp4?xxxxx", "orig_prompt": "写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。", "actual_prompt": "写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。小猫的黄色眼睛明亮有神,毛发光滑,胡须清晰可见。背景是简单的浅色墙面,突显小猫的黑色身影。近景特写,强调小猫的表情变化和眼神细节。" }, "usage": { "video_duration": 5, "video_count": 1, "SR": 480 } } ``` ## 任务执行失败 若任务执行失败,task\\_status将置为 FAILED,并提供错误码和信息。请参见[错误信息](https://help.aliyun.com/zh/model-studio/error-code)进行解决。 ``` { "request_id": "e5d70b02-ebd3-98ce-9fe8-759d7d7b107d", "output": { "task_id": "86ecf553-d340-4e21-af6e-a0c6a421c010", "task_status": "FAILED", "code": "InvalidParameter", "message": "The size is not match xxxxxx" } } ``` ## 任务查询过期 task\\_id查询有效期为 24 小时,超时后将无法查询,返回以下报错信息。 ``` { "request_id": "a4de7c32-7057-9f82-8581-xxxxxx", "output": { "task_id": "502a00b1-19d9-4839-a82f-xxxxxx", "task_status": "UNKNOWN" } } ``` |
|
||||
| --- | --- |
|
||||
| **output** `*object*` 任务输出信息。 **属性** **task\\_id** `*string*` 任务ID。查询有效期24小时。 **task\\_status** `*string*` 任务状态。 **枚举值** - PENDING:任务排队中 - RUNNING:任务处理中 - SUCCEEDED:任务执行成功 - FAILED:任务执行失败 - CANCELED:任务已取消 - UNKNOWN:任务不存在或状态未知 **轮询过程中的状态流转:** - PENDING(排队中) → RUNNING(处理中)→ SUCCEEDED(成功)/ FAILED(失败)。 - 初次查询状态通常为 PENDING(排队中)或 RUNNING(处理中)。 - 当状态变为 SUCCEEDED 时,响应中将包含生成的视频url。 - 若状态为 FAILED,请检查错误信息并重试。 **submit\\_time** `*string*` 任务提交时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **scheduled\\_time** `*string*` 任务执行时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **end\\_time** `*string*` 任务完成时间。格式为 YYYY-MM-DD HH:mm:ss.SSS。 **video\\_url** `*string*` 视频URL。仅在 task\\_status 为 SUCCEEDED 时返回。 链接有效期24小时,可通过此URL下载视频。视频格式为MP4(H.264 编码)。 **orig\\_prompt** `*string*` 原始输入的prompt,对应请求参数`prompt`。 **actual\\_prompt** `*string*` 开启 prompt 智能改写后,返回实际使用的优化后 prompt。若未开启该功能,则不返回此字段。 **code** `*string*` 请求失败的错误码。请求成功时不会返回此参数,详情请参见[错误信息](https://help.aliyun.com/zh/model-studio/error-code)。 **message** `*string*` 请求失败的详细信息。请求成功时不会返回此参数,详情请参见[错误信息](https://help.aliyun.com/zh/model-studio/error-code)。 |
|
||||
| **usage** `*object*` 输出信息统计。只对成功的结果计数。 **属性** **video\\_duration** `*integer*` 生成视频的时长,单位秒。枚举值为5。计费公式:费用 = 视频秒数 × 单价。 **video\\_count** `*integer*` 生成视频的数量。固定为1。 **video\\_ratio** `*string*` 当前仅当2.1模型返回该值。生成视频的比例,固定为standard。 **SR** `*integer*` 当前仅当2.2模型返回该值。生成视频的分辨率档位,枚举值为480、720、1080。 | |
|
||||
| **request\\_id** `*string*` 请求唯一标识。可用于请求明细溯源和问题排查。 | |
|
||||
|
||||
## DashScope SDK调用
|
||||
|
||||
SDK 的参数命名与[HTTP接口](https://help.aliyun.com/zh/model-studio/text-to-video-api-reference#42703589880ts)基本一致,参数结构根据语言特性进行封装。
|
||||
|
||||
由于图生视频任务耗时较长(通常为1-5分钟),SDK 在底层封装了 HTTP 异步调用流程,支持同步、异步两种调用方式。
|
||||
|
||||
> 具体耗时受限于排队任务数和服务执行情况,请在获取结果时耐心等待。
|
||||
|
||||
### Python SDK调用
|
||||
|
||||
**重要**
|
||||
|
||||
请确保 DashScope Python SDK 版本**不低于** `**1.23.8**`,再运行以下代码。
|
||||
|
||||
若版本过低,可能会触发 “url error, please check url!” 等错误。请参考[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)进行更新。
|
||||
|
||||
根据模型所在地域设置 `**base_http_api_url**`:
|
||||
|
||||
## **北京**
|
||||
|
||||
`dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1'`
|
||||
|
||||
## **新加坡**
|
||||
|
||||
`dashscope.base_http_api_url = 'https://dashscope-intl.aliyuncs.com/api/v1'`
|
||||
|
||||
#### **示例代码**
|
||||
|
||||
## 同步调用
|
||||
|
||||
本示例展示三种图像输入方式:公网URL、Base64编码、本地文件路径。
|
||||
|
||||
##### 请求示例
|
||||
|
||||
```
|
||||
import base64
|
||||
import os
|
||||
from http import HTTPStatus
|
||||
from dashscope import VideoSynthesis
|
||||
import mimetypes
|
||||
import dashscope
|
||||
|
||||
# 以下为北京地域URL,各地域的URL不同,获取URL:https://help.aliyun.com/zh/model-studio/text-to-video-api-reference
|
||||
dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1'
|
||||
|
||||
|
||||
# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx"
|
||||
# 各地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
|
||||
api_key = os.getenv("DASHSCOPE_API_KEY")
|
||||
|
||||
# --- 辅助函数:用于 Base64 编码 ---
|
||||
# 格式为 data:{MIME_type};base64,{base64_data}
|
||||
def encode_file(file_path):
|
||||
mime_type, _ = mimetypes.guess_type(file_path)
|
||||
if not mime_type or not mime_type.startswith("image/"):
|
||||
raise ValueError("不支持或无法识别的图像格式")
|
||||
with open(file_path, "rb") as image_file:
|
||||
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
||||
return f"data:{mime_type};base64,{encoded_string}"
|
||||
|
||||
"""
|
||||
图像输入方式说明:
|
||||
以下提供了三种图片输入方式,三选一即可
|
||||
|
||||
1. 使用公网URL - 适合已有公开可访问的图片
|
||||
2. 使用本地文件 - 适合本地开发测试
|
||||
3. 使用Base64编码 - 适合私有图片或需要加密传输的场景
|
||||
"""
|
||||
|
||||
# 【方式一】使用公网图片 URL
|
||||
first_frame_url = "https://wanx.alicdn.com/material/20250318/first_frame.png"
|
||||
last_frame_url = "https://wanx.alicdn.com/material/20250318/last_frame.png"
|
||||
|
||||
# 【方式二】使用本地文件(支持绝对路径和相对路径)
|
||||
# 格式要求:file:// + 文件路径
|
||||
# 示例(绝对路径):
|
||||
# first_frame_url = "file://" + "/path/to/your/first_frame.png" # Linux/macOS
|
||||
# last_frame_url = "file://" + "C:/path/to/your/last_frame.png" # Windows
|
||||
# 示例(相对路径):
|
||||
# first_frame_url = "file://" + "./first_frame.png" # 以实际路径为准
|
||||
# last_frame_url = "file://" + "./last_frame.png" # 以实际路径为准
|
||||
|
||||
# 【方式三】使用Base64编码的图片
|
||||
# first_frame_url = encode_file("./first_frame.png") # 以实际路径为准
|
||||
# last_frame_url = encode_file("./last_frame.png") # 以实际路径为准
|
||||
|
||||
def sample_sync_call_kf2v():
|
||||
print('please wait...')
|
||||
rsp = VideoSynthesis.call(api_key=api_key,
|
||||
model="wan2.2-kf2v-flash",
|
||||
prompt="写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。",
|
||||
first_frame_url=first_frame_url,
|
||||
last_frame_url=last_frame_url,
|
||||
resolution="720P",
|
||||
prompt_extend=True)
|
||||
print(rsp)
|
||||
if rsp.status_code == HTTPStatus.OK:
|
||||
print(rsp.output.video_url)
|
||||
else:
|
||||
print('Failed, status_code: %s, code: %s, message: %s' %
|
||||
(rsp.status_code, rsp.code, rsp.message))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sample_sync_call_kf2v()
|
||||
```
|
||||
|
||||
##### 响应示例
|
||||
|
||||
> video\_url 有效期24小时,请及时下载视频。
|
||||
|
||||
```
|
||||
{
|
||||
"status_code": 200,
|
||||
"request_id": "efa545b3-f95c-9e3a-a3b6-xxxxxx",
|
||||
"code": null,
|
||||
"message": "",
|
||||
"output": {
|
||||
"task_id": "721164c6-8619-4a35-a6d9-xxxxxx",
|
||||
"task_status": "SUCCEEDED",
|
||||
"video_url": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxx.mp4?xxxxx",
|
||||
"submit_time": "2025-02-12 11:03:30.701",
|
||||
"scheduled_time": "2025-02-12 11:06:05.378",
|
||||
"end_time": "2025-02-12 11:12:18.853",
|
||||
"orig_prompt": "写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。",
|
||||
"actual_prompt": "写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。小猫毛发乌黑光亮,眼睛大而明亮,瞳孔呈金黄色。它抬头仰望,耳朵竖立,显得格外专注。镜头上移后,小猫转头直视镜头,眼神中充满好奇与警觉。背景简洁,突出小猫的细节特征。近景特写,自然光线柔和。"
|
||||
},
|
||||
"usage": {
|
||||
"video_count": 1,
|
||||
"video_duration": 5,
|
||||
"video_ratio": "standard"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 异步调用
|
||||
|
||||
本示例展示异步调用方式。该方式会立即返回任务ID,需要自行轮询或等待任务完成。
|
||||
|
||||
##### 请求示例
|
||||
|
||||
```
|
||||
import os
|
||||
from http import HTTPStatus
|
||||
from dashscope import VideoSynthesis
|
||||
import dashscope
|
||||
|
||||
# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1
|
||||
dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1'
|
||||
|
||||
|
||||
"""
|
||||
环境要求:
|
||||
dashscope python SDK >= 1.23.8
|
||||
安装/升级SDK:
|
||||
pip install -U dashscope
|
||||
"""
|
||||
|
||||
# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx"
|
||||
# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
|
||||
api_key = os.getenv("DASHSCOPE_API_KEY")
|
||||
|
||||
# 使用公网可访问的图片URL
|
||||
first_frame_url = "https://wanx.alicdn.com/material/20250318/first_frame.png"
|
||||
last_frame_url = "https://wanx.alicdn.com/material/20250318/last_frame.png"
|
||||
|
||||
|
||||
def sample_async_call_kf2v():
|
||||
# 异步调用,返回一个task_id
|
||||
rsp = VideoSynthesis.async_call(api_key=api_key,
|
||||
model="wan2.2-kf2v-flash",
|
||||
prompt="写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。",
|
||||
first_frame_url=first_frame_url,
|
||||
last_frame_url=last_frame_url,
|
||||
resolution="720P",
|
||||
prompt_extend=True)
|
||||
print(rsp)
|
||||
if rsp.status_code == HTTPStatus.OK:
|
||||
print("task_id: %s" % rsp.output.task_id)
|
||||
else:
|
||||
print('Failed, status_code: %s, code: %s, message: %s' %
|
||||
(rsp.status_code, rsp.code, rsp.message))
|
||||
|
||||
# 获取异步任务信息
|
||||
status = VideoSynthesis.fetch(task=rsp, api_key=api_key)
|
||||
if status.status_code == HTTPStatus.OK:
|
||||
print(status.output.task_status) # check the task status
|
||||
else:
|
||||
print('Failed, status_code: %s, code: %s, message: %s' %
|
||||
(status.status_code, status.code, status.message))
|
||||
|
||||
# 等待异步任务结束
|
||||
rsp = VideoSynthesis.wait(task=rsp, api_key=api_key)
|
||||
print(rsp)
|
||||
if rsp.status_code == HTTPStatus.OK:
|
||||
print(rsp.output.video_url)
|
||||
else:
|
||||
print('Failed, status_code: %s, code: %s, message: %s' %
|
||||
(rsp.status_code, rsp.code, rsp.message))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sample_async_call_kf2v()
|
||||
```
|
||||
|
||||
##### 响应示例
|
||||
|
||||
1、创建任务的响应示例
|
||||
|
||||
```
|
||||
{
|
||||
"status_code": 200,
|
||||
"request_id": "c86ff7ba-8377-917a-90ed-xxxxxx",
|
||||
"code": "",
|
||||
"message": "",
|
||||
"output": {
|
||||
"task_id": "721164c6-8619-4a35-a6d9-xxxxxx",
|
||||
"task_status": "PENDING",
|
||||
"video_url": ""
|
||||
},
|
||||
"usage": null
|
||||
}
|
||||
```
|
||||
|
||||
2、查询任务结果的响应示例
|
||||
|
||||
> video\_url 有效期24小时,请及时下载视频。
|
||||
|
||||
```
|
||||
{
|
||||
"status_code": 200,
|
||||
"request_id": "efa545b3-f95c-9e3a-a3b6-xxxxxx",
|
||||
"code": null,
|
||||
"message": "",
|
||||
"output": {
|
||||
"task_id": "721164c6-8619-4a35-a6d9-xxxxxx",
|
||||
"task_status": "SUCCEEDED",
|
||||
"video_url": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxx.mp4?xxxxx",
|
||||
"submit_time": "2025-02-12 11:03:30.701",
|
||||
"scheduled_time": "2025-02-12 11:06:05.378",
|
||||
"end_time": "2025-02-12 11:12:18.853",
|
||||
"orig_prompt": "写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。",
|
||||
"actual_prompt": "写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。小猫毛发乌黑光亮,眼睛大而明亮,瞳孔呈金黄色。它抬头仰望,耳朵竖立,显得格外专注。镜头上移后,小猫转头直视镜头,眼神中充满好奇与警觉。背景简洁,突出小猫的细节特征。近景特写,自然光线柔和。"
|
||||
},
|
||||
"usage": {
|
||||
"video_count": 1,
|
||||
"video_duration": 5,
|
||||
"video_ratio": "standard"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Java SDK调用
|
||||
|
||||
**重要**
|
||||
|
||||
请确保 DashScope Java SDK 版本**不低于** `**2.20.9**`,再运行以下代码。
|
||||
|
||||
若版本过低,可能会触发 “url error, please check url!” 等错误。请参考[安装SDK](https://help.aliyun.com/zh/model-studio/install-sdk)进行更新。
|
||||
|
||||
#### **示例代码**
|
||||
|
||||
## 同步调用
|
||||
|
||||
本示例展示同步调用方式,并支持三种图像输入方式:公网URL、Base64编码、本地文件路径。
|
||||
|
||||
##### 请求示例
|
||||
|
||||
```
|
||||
// Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesis;
|
||||
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesisParam;
|
||||
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesisResult;
|
||||
import com.alibaba.dashscope.exception.ApiException;
|
||||
import com.alibaba.dashscope.exception.InputRequiredException;
|
||||
import com.alibaba.dashscope.exception.NoApiKeyException;
|
||||
import com.alibaba.dashscope.utils.Constants;
|
||||
import com.alibaba.dashscope.utils.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 环境要求
|
||||
* dashscope java SDK >= 2.20.9
|
||||
* 更新maven依赖:
|
||||
* https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java
|
||||
*/
|
||||
public class Kf2vSync {
|
||||
|
||||
static {
|
||||
// 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1
|
||||
Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1";
|
||||
}
|
||||
|
||||
// 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx"
|
||||
// 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
|
||||
static String apiKey = System.getenv("DASHSCOPE_API_KEY");
|
||||
|
||||
/**
|
||||
* 图像输入方式说明:三选一即可
|
||||
*
|
||||
* 1. 使用公网URL - 适合已有公开可访问的图片
|
||||
* 2. 使用本地文件 - 适合本地开发测试
|
||||
* 3. 使用Base64编码 - 适合私有图片或需要加密传输的场景
|
||||
*/
|
||||
|
||||
//【方式一】公网URL
|
||||
static String firstFrameUrl = "https://wanx.alicdn.com/material/20250318/first_frame.png";
|
||||
static String lastFrameUrl = "https://wanx.alicdn.com/material/20250318/last_frame.png";
|
||||
|
||||
//【方式二】本地文件路径(file://+绝对路径 or file:///+绝对路径)
|
||||
// static String firstFrameUrl = "file://" + "/your/path/to/first_frame.png"; // Linux/macOS
|
||||
// static String lastFrameUrl = "file:///" + "C:/path/to/your/img.png"; // Windows
|
||||
|
||||
//【方式三】Base64编码
|
||||
// static String firstFrameUrl = Kf2vSync.encodeFile("/your/path/to/first_frame.png");
|
||||
// static String lastFrameUrl = Kf2vSync.encodeFile("/your/path/to/last_frame.png");
|
||||
|
||||
|
||||
public static void syncCall() {
|
||||
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("prompt_extend", true);
|
||||
parameters.put("resolution", "720P");
|
||||
|
||||
VideoSynthesis videoSynthesis = new VideoSynthesis();
|
||||
VideoSynthesisParam param =
|
||||
VideoSynthesisParam.builder()
|
||||
.apiKey(apiKey)
|
||||
.model("wan2.2-kf2v-flash")
|
||||
.prompt("写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。")
|
||||
.firstFrameUrl(firstFrameUrl)
|
||||
.lastFrameUrl(lastFrameUrl)
|
||||
.parameters(parameters)
|
||||
.build();
|
||||
VideoSynthesisResult result = null;
|
||||
try {
|
||||
System.out.println("---sync call, please wait a moment----");
|
||||
result = videoSynthesis.call(param);
|
||||
} catch (ApiException | NoApiKeyException e){
|
||||
throw new RuntimeException(e.getMessage());
|
||||
} catch (InputRequiredException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
System.out.println(JsonUtils.toJson(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文件编码为Base64字符串
|
||||
* @param filePath 文件路径
|
||||
* @return Base64字符串,格式为 data:{MIME_type};base64,{base64_data}
|
||||
*/
|
||||
public static String encodeFile(String filePath) {
|
||||
Path path = Paths.get(filePath);
|
||||
if (!Files.exists(path)) {
|
||||
throw new IllegalArgumentException("文件不存在: " + filePath);
|
||||
}
|
||||
// 检测MIME类型
|
||||
String mimeType = null;
|
||||
try {
|
||||
mimeType = Files.probeContentType(path);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("无法检测文件类型: " + filePath);
|
||||
}
|
||||
if (mimeType == null || !mimeType.startsWith("image/")) {
|
||||
throw new IllegalArgumentException("不支持或无法识别的图像格式");
|
||||
}
|
||||
// 读取文件内容并编码
|
||||
byte[] fileBytes = null;
|
||||
try{
|
||||
fileBytes = Files.readAllBytes(path);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("无法读取文件内容: " + filePath);
|
||||
}
|
||||
|
||||
String encodedString = Base64.getEncoder().encodeToString(fileBytes);
|
||||
return "data:" + mimeType + ";base64," + encodedString;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
syncCall();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 响应示例
|
||||
|
||||
> video\_url 有效期24小时,请及时下载视频。
|
||||
|
||||
```
|
||||
{
|
||||
"request_id": "e6bb4517-c073-9c10-b748-dedb8c11bb41",
|
||||
"output": {
|
||||
"task_id": "984784fe-83c1-4fc4-88c7-52c2c1fa92a2",
|
||||
"task_status": "SUCCEEDED",
|
||||
"video_url": "https://dashscope-result-wlcb-acdr-1.oss-cn-wulanchabu-acdr-1.aliyuncs.com/xxx.mp4?xxxxx"
|
||||
},
|
||||
"usage": {
|
||||
"video_count": 1,
|
||||
"video_duration": 5,
|
||||
"video_ratio": "standard"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 异步调用
|
||||
|
||||
本示例展示异步调用方式。该方式会立即返回任务ID,需要自行轮询或等待任务完成。
|
||||
|
||||
##### 请求示例
|
||||
|
||||
```
|
||||
// Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesis;
|
||||
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesisListResult;
|
||||
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesisParam;
|
||||
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesisResult;
|
||||
import com.alibaba.dashscope.exception.ApiException;
|
||||
import com.alibaba.dashscope.exception.InputRequiredException;
|
||||
import com.alibaba.dashscope.exception.NoApiKeyException;
|
||||
import com.alibaba.dashscope.task.AsyncTaskListParam;
|
||||
import com.alibaba.dashscope.utils.JsonUtils;
|
||||
import com.alibaba.dashscope.utils.Constants;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 环境要求
|
||||
* dashscope java SDK >= 2.20.9
|
||||
* 更新maven依赖:
|
||||
* https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java
|
||||
*/
|
||||
|
||||
public class Kf2vAsync {
|
||||
|
||||
static {
|
||||
// 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1
|
||||
Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1";
|
||||
}
|
||||
|
||||
// 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey="sk-xxx"
|
||||
// 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
|
||||
static String apiKey = System.getenv("DASHSCOPE_API_KEY");
|
||||
|
||||
// 公网URL
|
||||
static String firstFrameUrl = "https://wanx.alicdn.com/material/20250318/first_frame.png";
|
||||
static String lastFrameUrl = "https://wanx.alicdn.com/material/20250318/last_frame.png";
|
||||
|
||||
public static void asyncCall(){
|
||||
|
||||
// 设置parameters参数
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("prompt_extend", true);
|
||||
parameters.put("resolution", "720P");
|
||||
|
||||
VideoSynthesis videoSynthesis = new VideoSynthesis();
|
||||
VideoSynthesisParam param =
|
||||
VideoSynthesisParam.builder()
|
||||
.apiKey(apiKey)
|
||||
.model("wan2.2-kf2v-flash")
|
||||
.prompt("写实风格,一只黑色小猫好奇地看向天空,镜头从平视逐渐上升,最后俯拍它的好奇的眼神。")
|
||||
.firstFrameUrl(firstFrameUrl)
|
||||
.lastFrameUrl(lastFrameUrl)
|
||||
.parameters(parameters)
|
||||
.build();
|
||||
VideoSynthesisResult result = null;
|
||||
try {
|
||||
System.out.println("---async call, please wait a moment----");
|
||||
result = videoSynthesis.asyncCall(param);
|
||||
} catch (ApiException | NoApiKeyException e){
|
||||
throw new RuntimeException(e.getMessage());
|
||||
} catch (InputRequiredException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
System.out.println(JsonUtils.toJson(result));
|
||||
|
||||
String taskId = result.getOutput().getTaskId();
|
||||
|
||||
System.out.println("taskId=" + taskId);
|
||||
|
||||
try {
|
||||
result = videoSynthesis.wait(taskId, apiKey);
|
||||
} catch (ApiException | NoApiKeyException e){
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
System.out.println(JsonUtils.toJson(result));
|
||||
System.out.println(JsonUtils.toJson(result.getOutput()));
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
public static void listTask() throws ApiException, NoApiKeyException {
|
||||
VideoSynthesis is = new VideoSynthesis();
|
||||
AsyncTaskListParam param = AsyncTaskListParam.builder().build();
|
||||
param.setApiKey(apiKey);
|
||||
VideoSynthesisListResult result = is.list(param);
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
// 获取单个任务结果
|
||||
public static void fetchTask(String taskId) throws ApiException, NoApiKeyException {
|
||||
VideoSynthesis is = new VideoSynthesis();
|
||||
// 如果已设置 DASHSCOPE_API_KEY 为环境变量,apiKey 可为空
|
||||
VideoSynthesisResult result = is.fetch(taskId, apiKey);
|
||||
System.out.println(result.getOutput());
|
||||
System.out.println(result.getUsage());
|
||||
}
|
||||
|
||||
public static void main(String[] args){
|
||||
asyncCall();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 响应示例
|
||||
|
||||
1、创建任务的响应示例
|
||||
|
||||
```
|
||||
{
|
||||
"request_id": "5dbf9dc5-4f4c-9605-85ea-xxxxxxxx",
|
||||
"output": {
|
||||
"task_id": "7277e20e-aa01-4709-xxxxxxxx",
|
||||
"task_status": "PENDING"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2、查询任务结果的响应示例
|
||||
|
||||
> video\_url 有效期24小时,请及时下载视频。
|
||||
|
||||
```
|
||||
{
|
||||
"request_id": "1625235c-c13e-93ec-aff7-xxxxxxxx",
|
||||
"output": {
|
||||
"task_id": "464a5e46-79a6-46fd-9823-xxxxxxxx",
|
||||
"task_status": "SUCCEEDED",
|
||||
"video_url": "https://dashscope-result-sh.oss-cn-shanghai.aliyuncs.com/xxx.mp4?xxxxxx"
|
||||
},
|
||||
"usage": {
|
||||
"video_count": 1,
|
||||
"video_duration": 5,
|
||||
"video_ratio": "standard"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -1,21 +1,46 @@
|
|||
<script setup>
|
||||
import { ref, computed, onUnmounted } from "vue";
|
||||
import { ref, computed, onUnmounted, watch, nextTick } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import axios from "axios";
|
||||
import { GRS_API_KEY, ESSAY_API_URL } from "@/config";
|
||||
import { DOUBAO_KEY } from "@/config/index.js";
|
||||
|
||||
// ── 状态 ──
|
||||
const status = ref("idle"); // idle | generating | done | error
|
||||
// 生成阶段: script(解析试题/生成脚本)| firstFrame(生成首帧)| lastFrame(生成尾帧)| video(生成视频)
|
||||
const phase = ref("script");
|
||||
const textInput = ref("");
|
||||
const errorMsg = ref("");
|
||||
const videoUrl = ref("");
|
||||
const progress = ref(0);
|
||||
const progressText = ref("");
|
||||
const cancelTokenSource = ref(null);
|
||||
|
||||
// ── API 配置 (nano-banana) ──
|
||||
const API_KEY = GRS_API_KEY;
|
||||
const API_URL = ESSAY_API_URL;
|
||||
// 脚本数据
|
||||
const scriptContent = ref(""); // 讲解文案
|
||||
const storyboard = ref([]); // 分镜脚本列表 [{ index, desc, visual }]
|
||||
|
||||
// 多片段视频
|
||||
const CLIP_DURATION = 5; // 每段视频时长(秒),万相模型固定5秒
|
||||
const MAX_CLIPS = 16; // 最多片段数(上限约 80 秒)
|
||||
const videoClips = ref([]); // 已生成的视频片段 URL 列表
|
||||
const currentClipIndex = ref(0); // 当前正在生成第几段
|
||||
|
||||
// 分镜图片(首尾帧)
|
||||
const storyboardImages = ref([]); // [{ index, firstFrameUrl, lastFrameUrl }]
|
||||
const videoPlayerRef = ref(null); // video 元素引用
|
||||
const currentPlayIndex = ref(0); // 当前播放到第几段
|
||||
|
||||
// ── API 配置 ──
|
||||
// 脚本生成 API(GPT 兼容接口)
|
||||
const SCRIPT_API_KEY = "sk-aVLnOpoEsktYJh0A51aVnwqM3o5WF6JUf9icmWkOXMnZKvOM";
|
||||
const SCRIPT_API_URL = "https://api.oaibest.com/v1/chat/completions";
|
||||
const SCRIPT_MODEL = "gpt-4o";
|
||||
|
||||
// 视频生成 API(阿里云 DashScope 万相首尾帧模型)
|
||||
const VIDEO_API_KEY = "sk-74fa3459e6a84dda85135fcbb4cf0f29"; // 替换为阿里云百炼 API Key
|
||||
const VIDEO_API_URL =
|
||||
"/dashscope-api/api/v1/services/aigc/image2video/video-synthesis";
|
||||
const VIDEO_TASK_URL = "/dashscope-api/api/v1/tasks";
|
||||
const VIDEO_MODEL = "wan2.2-kf2v-flash";
|
||||
|
||||
// ── Router ──
|
||||
const router = useRouter();
|
||||
|
|
@ -27,116 +52,313 @@ const canGenerate = computed(() => {
|
|||
|
||||
const goBack = () => router.back();
|
||||
|
||||
// ── 阶段一:解析试题,生成讲解文案 + 分镜脚本 ──
|
||||
const generateScript = async () => {
|
||||
phase.value = "script";
|
||||
progress.value = 5;
|
||||
progressText.value = "正在解析试题...";
|
||||
|
||||
const systemPrompt = `你是一位专业的英语教师,擅长制作试题讲解视频。
|
||||
请根据用户提供的英语试题,完成以下两项任务,并严格按照 JSON 格式返回:
|
||||
{
|
||||
"script": "完整的讲解文案(中文,包含题目分析、解题思路、答案解析)",
|
||||
"storyboard": [
|
||||
{ "index": 1, "desc": "分镜描述", "visual": "画面/动画/图表/板书说明" },
|
||||
...
|
||||
]
|
||||
}
|
||||
storyboard 每个分镜对应一个知识点或解题步骤,visual 字段描述该分镜的视觉呈现方式(如:板书写出语法结构、动画展示句子成分、图表对比选项等)。
|
||||
**请根据试题的复杂度自行决定分镜数量(建议 3~12 个),每个分镜对应约 15 秒视频内容。简单题目可少分镜,复杂题目适当增加分镜,确保讲解充分。**`;
|
||||
|
||||
const response = await axios.post(
|
||||
SCRIPT_API_URL,
|
||||
{
|
||||
model: SCRIPT_MODEL,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: `请解析以下英语试题:\n\n${textInput.value}` },
|
||||
],
|
||||
temperature: 0.7,
|
||||
response_format: { type: "json_object" },
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${SCRIPT_API_KEY}`,
|
||||
},
|
||||
cancelToken: cancelTokenSource.value.token,
|
||||
timeout: 60000,
|
||||
}
|
||||
);
|
||||
|
||||
const raw = response.data?.choices?.[0]?.message?.content || "{}";
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
scriptContent.value = parsed.script || "";
|
||||
storyboard.value = Array.isArray(parsed.storyboard) ? parsed.storyboard : [];
|
||||
|
||||
if (!scriptContent.value) {
|
||||
throw new Error("脚本生成失败,请重试");
|
||||
}
|
||||
|
||||
progress.value = 35;
|
||||
progressText.value = "脚本生成完成,准备生成视频...";
|
||||
};
|
||||
|
||||
// ── 阶段二:调用豆包 Seedream 生成分镜首尾帧图片 ──
|
||||
const IMAGE_API_URL = "/ark-api/api/v3/images/generations";
|
||||
const IMAGE_MODEL = "doubao-seedream-5-0-260128";
|
||||
|
||||
const generateImageForShot = async (shot, frameType) => {
|
||||
const frameLabel = frameType === "first" ? "首帧" : "尾帧";
|
||||
const prompt = shot.visual;
|
||||
|
||||
try {
|
||||
const createResponse = await axios.post(
|
||||
IMAGE_API_URL,
|
||||
{
|
||||
model: IMAGE_MODEL,
|
||||
prompt: prompt,
|
||||
sequential_image_generation: "disabled",
|
||||
response_format: "url",
|
||||
size: "2K",
|
||||
stream: false,
|
||||
watermark: true,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${DOUBAO_KEY}`,
|
||||
},
|
||||
cancelToken: cancelTokenSource.value.token,
|
||||
timeout: 180000,
|
||||
}
|
||||
);
|
||||
|
||||
const data = createResponse.data;
|
||||
|
||||
if (data?.error) {
|
||||
throw new Error(
|
||||
`分镜 ${shot.index} ${frameLabel}图片生成失败:${
|
||||
data.error.message || data.error.code
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const imgUrl = (Array.isArray(data?.data) && data.data[0]?.url) || null;
|
||||
|
||||
if (!imgUrl) {
|
||||
throw new Error(
|
||||
`分镜 ${shot.index} ${frameLabel}图片生成失败,未获取到图片地址`
|
||||
);
|
||||
}
|
||||
|
||||
return imgUrl;
|
||||
} catch (err) {
|
||||
// 打印完整错误信息以便排查
|
||||
console.error("[Seedream 生图失败]", {
|
||||
status: err?.response?.status,
|
||||
statusText: err?.response?.statusText,
|
||||
data: err?.response?.data,
|
||||
url: IMAGE_API_URL,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const generateImagesFromStoryboard = async () => {
|
||||
phase.value = "firstFrame";
|
||||
storyboardImages.value = [];
|
||||
|
||||
const shots = storyboard.value.slice(0, MAX_CLIPS);
|
||||
const totalClips = shots.length;
|
||||
|
||||
for (let i = 0; i < totalClips; i++) {
|
||||
if (status.value !== "generating") return;
|
||||
|
||||
const shot = shots[i];
|
||||
currentClipIndex.value = i + 1;
|
||||
|
||||
// 生成首帧
|
||||
progress.value = 15 + Math.round((i / totalClips) * 25);
|
||||
progressText.value = `正在生成第 ${i + 1}/${totalClips} 段首帧图片...`;
|
||||
|
||||
const firstFrameUrl = await generateImageForShot(shot, "first");
|
||||
|
||||
// 生成尾帧
|
||||
progress.value = 15 + Math.round(((i + 0.5) / totalClips) * 25);
|
||||
progressText.value = `正在生成第 ${i + 1}/${totalClips} 段尾帧图片...`;
|
||||
|
||||
const lastFrameUrl = await generateImageForShot(shot, "last");
|
||||
|
||||
storyboardImages.value.push({
|
||||
index: shot.index,
|
||||
firstFrameUrl,
|
||||
lastFrameUrl,
|
||||
});
|
||||
}
|
||||
|
||||
if (storyboardImages.value.length === 0) {
|
||||
throw new Error("所有分镜图片生成失败");
|
||||
}
|
||||
|
||||
progress.value = 40;
|
||||
progressText.value = "图片生成完成,准备生成视频...";
|
||||
};
|
||||
|
||||
// ── 阶段三:根据首尾帧图片调用万相模型生成视频 ──
|
||||
const generateSingleClip = async (shot, firstFrameUrl, lastFrameUrl) => {
|
||||
const videoPrompt = `英语试题讲解视频片段(第 ${shot.index} 段,约5秒),教学风格,纯动画板书形式,不出现任何真人或人物形象。
|
||||
整体讲解文案:${scriptContent.value}
|
||||
当前分镜内容:${shot.desc}
|
||||
视觉要求:${shot.visual}`;
|
||||
|
||||
const createResponse = await axios.post(
|
||||
VIDEO_API_URL,
|
||||
{
|
||||
model: VIDEO_MODEL,
|
||||
input: {
|
||||
first_frame_url: firstFrameUrl,
|
||||
last_frame_url: lastFrameUrl,
|
||||
prompt: videoPrompt,
|
||||
},
|
||||
parameters: {
|
||||
resolution: "720P",
|
||||
prompt_extend: true,
|
||||
watermark: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${VIDEO_API_KEY}`,
|
||||
"X-DashScope-Async": "enable",
|
||||
},
|
||||
cancelToken: cancelTokenSource.value.token,
|
||||
timeout: 30000,
|
||||
}
|
||||
);
|
||||
|
||||
const taskId = createResponse.data?.output?.task_id;
|
||||
if (!taskId) {
|
||||
throw new Error(`分镜 ${shot.index} 创建视频任务失败`);
|
||||
}
|
||||
|
||||
// 轮询任务状态(建议间隔 15 秒)
|
||||
const maxPollAttempts = 24; // 最多 6 分钟
|
||||
let pollCount = 0;
|
||||
|
||||
while (pollCount < maxPollAttempts) {
|
||||
if (status.value !== "generating") return null;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||
pollCount++;
|
||||
|
||||
const statusResponse = await axios.get(`${VIDEO_TASK_URL}/${taskId}`, {
|
||||
headers: { Authorization: `Bearer ${VIDEO_API_KEY}` },
|
||||
cancelToken: cancelTokenSource.value.token,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const taskStatus = statusResponse.data?.output?.task_status;
|
||||
|
||||
if (taskStatus === "SUCCEEDED") {
|
||||
return statusResponse.data?.output?.video_url || null;
|
||||
}
|
||||
|
||||
if (taskStatus === "FAILED") {
|
||||
throw new Error(
|
||||
`分镜 ${shot.index} 视频生成失败:${
|
||||
statusResponse.data?.output?.message || "未知错误"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`分镜 ${shot.index} 视频生成超时`);
|
||||
};
|
||||
|
||||
const generateVideoFromScript = async () => {
|
||||
// 阶段二:生成分镜首尾帧图片
|
||||
await generateImagesFromStoryboard();
|
||||
|
||||
if (status.value !== "generating") return;
|
||||
|
||||
// 阶段三:根据首尾帧图片生成视频
|
||||
phase.value = "video";
|
||||
videoClips.value = [];
|
||||
|
||||
const totalClips = storyboardImages.value.length;
|
||||
|
||||
for (let i = 0; i < totalClips; i++) {
|
||||
if (status.value !== "generating") return;
|
||||
|
||||
const shot = storyboard.value[i];
|
||||
const imageData = storyboardImages.value[i];
|
||||
currentClipIndex.value = i + 1;
|
||||
const baseProgress = 65;
|
||||
const clipProgress = Math.round((i / totalClips) * 33);
|
||||
progress.value = baseProgress + clipProgress;
|
||||
progressText.value = `正在生成第 ${i + 1}/${totalClips} 段视频...`;
|
||||
|
||||
const url = await generateSingleClip(
|
||||
shot,
|
||||
imageData.firstFrameUrl,
|
||||
imageData.lastFrameUrl
|
||||
);
|
||||
if (url) {
|
||||
videoClips.value.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
if (videoClips.value.length === 0) {
|
||||
throw new Error("所有视频片段生成失败");
|
||||
}
|
||||
|
||||
progress.value = 99;
|
||||
progressText.value = "视频片段全部生成完成!";
|
||||
};
|
||||
|
||||
// ── 主入口 ──
|
||||
const generateVideo = async () => {
|
||||
if (!canGenerate.value) return;
|
||||
|
||||
if (!API_KEY) {
|
||||
errorMsg.value = "请先在设置中配置 API Key";
|
||||
status.value = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
status.value = "generating";
|
||||
errorMsg.value = "";
|
||||
videoUrl.value = "";
|
||||
videoClips.value = [];
|
||||
scriptContent.value = "";
|
||||
storyboard.value = [];
|
||||
storyboardImages.value = [];
|
||||
currentClipIndex.value = 0;
|
||||
progress.value = 0;
|
||||
progressText.value = "正在连接 AI 模型...";
|
||||
|
||||
cancelTokenSource.value = axios.CancelToken.source();
|
||||
|
||||
// 模拟生成进度(与真实 API 进度同步)
|
||||
const progressSteps = [
|
||||
{ progress: 10, text: "正在分析试题内容..." },
|
||||
{ progress: 25, text: "正在生成讲解脚本..." },
|
||||
{ progress: 45, text: "正在调用 nano-banana 生成视频..." },
|
||||
{ progress: 70, text: "视频渲染中..." },
|
||||
{ progress: 90, text: "视频处理完成..." },
|
||||
];
|
||||
|
||||
let stepIndex = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
if (stepIndex < progressSteps.length && status.value === "generating") {
|
||||
progress.value = progressSteps[stepIndex].progress;
|
||||
progressText.value = progressSteps[stepIndex].text;
|
||||
stepIndex++;
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
try {
|
||||
// 调用 nano-banana API 生成视频
|
||||
const response = await axios.post(
|
||||
API_URL,
|
||||
{
|
||||
model: "nano-banana-pro",
|
||||
prompt: `请生成一个英语试题讲解视频。试题内容:${textInput.value}`,
|
||||
aspectRatio: "16:9",
|
||||
duration: 30,
|
||||
webHook: "",
|
||||
shutProgress: true,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${API_KEY}`,
|
||||
},
|
||||
cancelToken: cancelTokenSource.value.token,
|
||||
timeout: 300000, // 5分钟超时
|
||||
}
|
||||
);
|
||||
// 阶段一:解析试题,生成讲解文案 + 分镜脚本
|
||||
await generateScript();
|
||||
|
||||
clearInterval(progressInterval);
|
||||
if (status.value !== "generating") return;
|
||||
|
||||
// 解析 API 响应
|
||||
let responseData = response.data;
|
||||
// 阶段二:生成分镜首尾帧图片
|
||||
// 阶段三:按分镜逐段生成视频
|
||||
await generateVideoFromScript();
|
||||
|
||||
// 处理 SSE 格式响应 (data: {...})
|
||||
if (typeof responseData === "string") {
|
||||
const jsonStr = responseData.replace(/^data:\s*/m, "").trim();
|
||||
try {
|
||||
responseData = JSON.parse(jsonStr);
|
||||
} catch {
|
||||
// 解析失败,使用原始数据
|
||||
}
|
||||
}
|
||||
|
||||
// 支持多种响应格式
|
||||
let generatedVideoUrl = "";
|
||||
if (responseData?.data?.video_url) {
|
||||
// 格式1: { data: { video_url: "..." } }
|
||||
generatedVideoUrl = responseData.data.video_url;
|
||||
} else if (responseData?.data?.url) {
|
||||
// 格式2: { data: { url: "..." } }
|
||||
generatedVideoUrl = responseData.data.url;
|
||||
} else if (responseData?.video_url) {
|
||||
// 格式3: { video_url: "..." }
|
||||
generatedVideoUrl = responseData.video_url;
|
||||
} else if (responseData?.url) {
|
||||
// 格式4: { url: "..." }
|
||||
generatedVideoUrl = responseData.url;
|
||||
} else if (responseData?.results?.[0]?.url) {
|
||||
// 格式5: { results: [{ url: "..." }] }
|
||||
generatedVideoUrl = responseData.results[0].url;
|
||||
} else if (responseData?.output) {
|
||||
// 格式6: { output: "..." }
|
||||
generatedVideoUrl = responseData.output;
|
||||
} else {
|
||||
throw new Error("无法解析视频 URL,请检查 API 响应格式");
|
||||
}
|
||||
if (status.value !== "generating") return;
|
||||
|
||||
progress.value = 100;
|
||||
progressText.value = "视频生成完成!";
|
||||
videoUrl.value = generatedVideoUrl;
|
||||
status.value = "done";
|
||||
} catch (err) {
|
||||
clearInterval(progressInterval);
|
||||
if (axios.isCancel(err)) return;
|
||||
console.error("视频生成失败:", err);
|
||||
console.error("生成失败:", err);
|
||||
errorMsg.value =
|
||||
err?.response?.data?.error?.message ||
|
||||
err?.response?.data?.message ||
|
||||
err?.response?.data?.error ||
|
||||
err.message ||
|
||||
"视频生成失败,请稍后重试";
|
||||
"生成失败,请稍后重试";
|
||||
status.value = "error";
|
||||
}
|
||||
};
|
||||
|
|
@ -148,17 +370,48 @@ const cancelGeneration = () => {
|
|||
status.value = "idle";
|
||||
progress.value = 0;
|
||||
progressText.value = "";
|
||||
currentClipIndex.value = 0;
|
||||
};
|
||||
|
||||
const resetAll = () => {
|
||||
status.value = "idle";
|
||||
phase.value = "script";
|
||||
textInput.value = "";
|
||||
videoUrl.value = "";
|
||||
videoClips.value = [];
|
||||
errorMsg.value = "";
|
||||
progress.value = 0;
|
||||
progressText.value = "";
|
||||
scriptContent.value = "";
|
||||
storyboard.value = [];
|
||||
storyboardImages.value = [];
|
||||
currentClipIndex.value = 0;
|
||||
};
|
||||
|
||||
// ── 多片段视频顺序播放 ──
|
||||
const setupPlayer = () => {
|
||||
if (!videoPlayerRef.value || videoClips.value.length === 0) return;
|
||||
|
||||
const video = videoPlayerRef.value;
|
||||
currentPlayIndex.value = 0;
|
||||
video.src = videoClips.value[0];
|
||||
|
||||
video.addEventListener("ended", () => {
|
||||
currentPlayIndex.value++;
|
||||
if (currentPlayIndex.value < videoClips.value.length) {
|
||||
video.src = videoClips.value[currentPlayIndex.value];
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 状态变为 done 时初始化播放器
|
||||
watch(status, async (newVal) => {
|
||||
if (newVal === "done" && videoClips.value.length > 0) {
|
||||
await nextTick();
|
||||
setupPlayer();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cancelTokenSource.value) {
|
||||
cancelTokenSource.value.cancel("组件卸载,取消生成");
|
||||
|
|
@ -166,6 +419,7 @@ onUnmounted(() => {
|
|||
});
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- Nav Bar -->
|
||||
|
|
@ -315,6 +569,80 @@ onUnmounted(() => {
|
|||
<!-- Generating State -->
|
||||
<div v-else-if="status === 'generating'" class="generating-state">
|
||||
<div class="generating-card">
|
||||
<!-- 阶段步骤指示器 -->
|
||||
<div class="phase-steps">
|
||||
<div
|
||||
class="phase-step"
|
||||
:class="{
|
||||
active: phase === 'script',
|
||||
done: phase !== 'script',
|
||||
}"
|
||||
>
|
||||
<div class="phase-dot">
|
||||
<svg
|
||||
v-if="phase !== 'script'"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
<span v-else>1</span>
|
||||
</div>
|
||||
<span class="phase-label">解析试题</span>
|
||||
</div>
|
||||
<div
|
||||
class="phase-line"
|
||||
:class="{
|
||||
done:
|
||||
phase === 'firstFrame' ||
|
||||
phase === 'lastFrame' ||
|
||||
phase === 'video',
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="phase-step"
|
||||
:class="{
|
||||
active: phase === 'firstFrame' || phase === 'lastFrame',
|
||||
done: phase === 'video',
|
||||
}"
|
||||
>
|
||||
<div class="phase-dot">
|
||||
<svg
|
||||
v-if="phase === 'video'"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
<span v-else>2</span>
|
||||
</div>
|
||||
<span class="phase-label">生成图片</span>
|
||||
</div>
|
||||
<div
|
||||
class="phase-line"
|
||||
:class="{ done: phase === 'video' }"
|
||||
></div>
|
||||
<div class="phase-step" :class="{ active: phase === 'video' }">
|
||||
<div class="phase-dot">
|
||||
<span>3</span>
|
||||
</div>
|
||||
<span class="phase-label">生成视频</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 动画图标 -->
|
||||
<div class="generating-icon">
|
||||
<div class="video-icon-wrapper">
|
||||
<svg
|
||||
|
|
@ -327,14 +655,47 @@ onUnmounted(() => {
|
|||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polygon points="5 3 19 12 5 21 5 3" />
|
||||
<polygon
|
||||
v-if="phase === 'video'"
|
||||
points="5 3 19 12 5 21 5 3"
|
||||
/>
|
||||
<template v-else>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<polyline points="21 15 16 10 5 21" />
|
||||
</template>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="generating-title">AI 正在生成视频</p>
|
||||
<p class="generating-model">nano-banana</p>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<p class="generating-title">
|
||||
{{
|
||||
phase === "script"
|
||||
? "AI 正在解析试题"
|
||||
: phase === "firstFrame"
|
||||
? `AI 正在生成第 ${currentClipIndex || 1}/${
|
||||
storyboard.length || MAX_CLIPS
|
||||
} 段首帧图片`
|
||||
: phase === "lastFrame"
|
||||
? `AI 正在生成第 ${currentClipIndex || 1}/${
|
||||
storyboard.length || MAX_CLIPS
|
||||
} 段尾帧图片`
|
||||
: `AI 正在生成第 ${currentClipIndex || 1}/${
|
||||
storyboard.length || MAX_CLIPS
|
||||
} 段视频`
|
||||
}}
|
||||
</p>
|
||||
<p class="generating-model">
|
||||
{{
|
||||
phase === "script"
|
||||
? "GPT-4o · 脚本生成"
|
||||
: phase === "firstFrame" || phase === "lastFrame"
|
||||
? `${IMAGE_MODEL} · 图片生成`
|
||||
: `${VIDEO_MODEL} · 视频生成`
|
||||
}}
|
||||
</p>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
|
|
@ -347,13 +708,101 @@ onUnmounted(() => {
|
|||
<span class="progress-text">{{ progressText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 脚本预览(图片/视频阶段时展示) -->
|
||||
<div
|
||||
v-if="
|
||||
(phase === 'firstFrame' ||
|
||||
phase === 'lastFrame' ||
|
||||
phase === 'video') &&
|
||||
scriptContent
|
||||
"
|
||||
class="script-preview"
|
||||
>
|
||||
<div class="script-preview-header">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
<span>讲解脚本已生成</span>
|
||||
<span class="storyboard-count" v-if="storyboard.length"
|
||||
>{{ storyboard.length }} 个分镜</span
|
||||
>
|
||||
</div>
|
||||
<p class="script-preview-text">
|
||||
{{ scriptContent.slice(0, 120)
|
||||
}}{{ scriptContent.length > 120 ? "..." : "" }}
|
||||
</p>
|
||||
|
||||
<!-- 图片预览(首尾帧生成阶段) -->
|
||||
<div
|
||||
v-if="
|
||||
(phase === 'firstFrame' || phase === 'lastFrame') &&
|
||||
storyboardImages.length
|
||||
"
|
||||
class="image-preview-list"
|
||||
>
|
||||
<div
|
||||
v-for="imgData in storyboardImages.slice(0, 3)"
|
||||
:key="imgData.index"
|
||||
class="image-preview-item"
|
||||
>
|
||||
<img
|
||||
:src="imgData.firstFrameUrl"
|
||||
alt="首帧"
|
||||
class="preview-thumb"
|
||||
/>
|
||||
<img
|
||||
:src="imgData.lastFrameUrl"
|
||||
alt="尾帧"
|
||||
class="preview-thumb"
|
||||
/>
|
||||
<span class="shot-index">{{ imgData.index }}</span>
|
||||
</div>
|
||||
<div v-if="storyboardImages.length > 3" class="storyboard-more">
|
||||
+{{ storyboardImages.length - 3 }} 个分镜...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分镜列表(视频阶段) -->
|
||||
<div
|
||||
v-if="phase === 'video' && storyboard.length"
|
||||
class="storyboard-list"
|
||||
>
|
||||
<div
|
||||
v-for="shot in storyboard.slice(0, 3)"
|
||||
:key="shot.index"
|
||||
class="storyboard-item"
|
||||
>
|
||||
<span class="shot-index">{{ shot.index }}</span>
|
||||
<div class="shot-info">
|
||||
<span class="shot-desc">{{ shot.desc }}</span>
|
||||
<span class="shot-visual">{{ shot.visual }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="storyboard.length > 3" class="storyboard-more">
|
||||
+{{ storyboard.length - 3 }} 个分镜...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Done State -->
|
||||
<div v-else-if="status === 'done'" class="video-done">
|
||||
<div class="video-wrapper">
|
||||
<video :src="videoUrl" controls autoplay class="video-player">
|
||||
<video ref="videoPlayerRef" controls autoplay class="video-player">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
|
|
@ -371,9 +820,24 @@ onUnmounted(() => {
|
|||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
<span>视频生成完成</span>
|
||||
<span
|
||||
>视频生成完成 · {{ videoClips.length }} 段 · 约
|
||||
{{ videoClips.length * CLIP_DURATION }}s</span
|
||||
>
|
||||
</div>
|
||||
<p class="video-hint">
|
||||
多段视频已自动拼接播放,总计约
|
||||
{{ ((videoClips.length * CLIP_DURATION) / 60).toFixed(1) }} 分钟
|
||||
</p>
|
||||
<div class="clip-timeline">
|
||||
<div
|
||||
v-for="(_clip, idx) in videoClips"
|
||||
:key="idx"
|
||||
class="clip-chip"
|
||||
>
|
||||
片段 {{ idx + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
<p class="video-hint">视频已准备就绪,可以播放查看讲解效果</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -803,6 +1267,224 @@ onUnmounted(() => {
|
|||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
.clip-timeline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.clip-chip {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.65rem;
|
||||
border-radius: 50px;
|
||||
background: rgba(244, 114, 182, 0.1);
|
||||
border: 1px solid rgba(244, 114, 182, 0.2);
|
||||
color: #f472b6;
|
||||
}
|
||||
|
||||
/* Phase Steps 阶段步骤 */
|
||||
.phase-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
.phase-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.phase-step.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.phase-dot {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 2px solid rgba(255, 255, 255, 0.15);
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.phase-step.active .phase-dot {
|
||||
background: linear-gradient(135deg, #f472b6, #db2777);
|
||||
border-color: #f472b6;
|
||||
color: white;
|
||||
box-shadow: 0 0 12px rgba(244, 114, 182, 0.4);
|
||||
}
|
||||
.phase-step.done .phase-dot {
|
||||
background: #14b8a6;
|
||||
border-color: #14b8a6;
|
||||
color: white;
|
||||
box-shadow: 0 0 12px rgba(20, 184, 166, 0.3);
|
||||
}
|
||||
.phase-label {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.phase-step.active .phase-label {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.phase-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
margin: 0 0.5rem;
|
||||
border-radius: 1px;
|
||||
transition: background 0.4s;
|
||||
min-width: 24px;
|
||||
}
|
||||
.phase-line.done {
|
||||
background: linear-gradient(90deg, #14b8a6, rgba(20, 184, 166, 0.3));
|
||||
}
|
||||
|
||||
/* Script Preview 脚本预览 */
|
||||
.script-preview {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 14px;
|
||||
padding: 1rem 1.25rem;
|
||||
text-align: left;
|
||||
animation: fadeInUp 0.3s ease both;
|
||||
}
|
||||
.script-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: #14b8a6;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.script-preview-header svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.storyboard-count {
|
||||
margin-left: auto;
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.7;
|
||||
background: rgba(20, 184, 166, 0.12);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 50px;
|
||||
}
|
||||
.script-preview-text {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
margin: 0 0 0.75rem 0;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.storyboard-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.storyboard-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.shot-index {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
background: rgba(244, 114, 182, 0.15);
|
||||
color: #f472b6;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.shot-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.shot-desc {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.shot-visual {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.7;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.storyboard-more {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.5;
|
||||
text-align: center;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
/* 图片预览 */
|
||||
.image-preview-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.image-preview-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
padding: 0.4rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.image-preview-item .shot-index {
|
||||
position: absolute;
|
||||
left: 0.4rem;
|
||||
top: 0.4rem;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
background: rgba(244, 114, 182, 0.15);
|
||||
color: #f472b6;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.preview-thumb {
|
||||
width: 80px;
|
||||
height: 45px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Generating Card 适配 */
|
||||
.generating-card {
|
||||
gap: 1.25rem;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Error Banner */
|
||||
.error-banner {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ export default defineConfig({
|
|||
target: 'https://ark.cn-beijing.volces.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/ark-api/, '')
|
||||
},
|
||||
'/dashscope-api': {
|
||||
target: 'https://dashscope.aliyuncs.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/dashscope-api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue