文章目录
- [Spring AI Alibaba 视频模型实战(文生视频 / 图生视频)](#Spring AI Alibaba 视频模型实战(文生视频 / 图生视频))
-
- 版本
- 一、依赖引入
- [二、yml 配置](#二、yml 配置)
- 三、代码案例:文生视频
- 四、代码案例:多图参考合成
- [五、代码案例:本地图片 → 图生视频](#五、代码案例:本地图片 → 图生视频)
- [六、临时 URL 立刻落盘](#六、临时 URL 立刻落盘)
Spring AI Alibaba 视频模型实战(文生视频 / 图生视频)
用 Spring AI Alibaba 的 DashScopeVideoModel 完成文生视频、多图参考合成、本地图生视频。对应模块 spring-video-demo。
版本
- Spring Boot:
3.4.5 - spring-ai-alibaba-starter-dashscope:
1.1.2.1 - Java:
17
Spring-ai更新速度快,请以官方为准
可参考网址:
一、依赖引入
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>1.1.2.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.22.18</version>
</dependency>
二、yml 配置
yaml
spring:
application:
name: spring-video-demo
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
video:
options:
# 默认文生视频;接口里可覆盖为 wanx2.1-vace-plus / wanx2.1-i2v-turbo 等
model: wan2.6-t2v
三、代码案例:文生视频
java
@RestController
@RequestMapping("/ai/video")
public class Text2Video {
private final DashScopeVideoModel videoModel;
public Text2Video(DashScopeVideoModel videoModel) {
this.videoModel = videoModel;
}
/**
* GET /ai/video/text2video?prompt=...
* call() 内部会提交异步任务并轮询,直到成功或失败。
*/
@GetMapping("/text2video")
public String text2Video(@RequestParam(defaultValue = "一只猫在草地上奔跑") String prompt) {
// 1)组装模型、输入、输出参数
DashScopeVideoOptions options = DashScopeVideoOptions.builder()
.model(DashScopeModel.VideoModel.WAN26_T2V.getName()) // wan2.6-t2v
.input(DashScopeVideoOptions.InputOptions.builder()
.prompt(prompt) // 正向提示词(可写分镜)
.negativePrompt("") // 反向提示词:不想出现的内容
.build())
.parameters(DashScopeVideoOptions.ParametersOptions.builder()
.size("1280*720") // 分辨率 宽*高
.shotType("multi") // 多镜头
.duration(10) // 时长(秒)
.promptExtend(true) // 提示词智能扩写
.seed(12345L) // 随机种子,便于复现
.build())
.build();
// 2)Prompt = 正文 + Options
VideoPrompt videoPrompt = VideoPrompt.builder()
.content(prompt)
.options(options)
.build();
// 3)同步等待生成完成
VideoResponse response = videoModel.call(videoPrompt);
DashScopeVideoResponse.VideoOutput output = response.getResult().getOutput();
if (output == null) {
return "生成失败: 无 output";
}
if (output.videoUrl() != null && !output.videoUrl().isBlank()) {
// 立刻下载到本地 video/,避免 OSS 签名 URL 过期后 AccessDenied
return VideoFileHelper.successMessage(output.videoUrl(), output.taskId(), output.taskStatus());
}
return "生成未完成或失败: status=" + output.taskStatus()
+ ", taskId=" + output.taskId();
}
}
四、代码案例:多图参考合成
java
@RestController
@RequestMapping("/ai/video")
public class VideoSynthesis {
private final DashScopeVideoModel videoModel;
// 官方示例参考图(公网可访问)
private static final List<String> DEFAULT_REF_IMAGES = List.of(
"http://wanx.alicdn.com/material/20250318/image_reference_2_5_16.png",
"http://wanx.alicdn.com/material/20250318/image_reference_1_5_16.png"
);
public VideoSynthesis(DashScopeVideoModel videoModel) {
this.videoModel = videoModel;
}
/**
* GET /ai/video/synthesis?prompt=...
* 功能:image_reference(多图参考 → 视频)
*/
@GetMapping("/synthesis")
public String synthesis(@RequestParam(defaultValue = "女孩从雾气森林中走出") String prompt) {
DashScopeVideoOptions options = DashScopeVideoOptions.builder()
.model(DashScopeModel.VideoModel.WANX21_VACE_PLUS.getName()) // wanx2.1-vace-plus
.input(DashScopeVideoOptions.InputOptions.builder()
.function("image_reference") // 功能类型:多图参考
.prompt(prompt)
.refImagesUrl(DEFAULT_REF_IMAGES) // 参考图 URL 列表
.build())
.parameters(DashScopeVideoOptions.ParametersOptions.builder()
.promptExtend(true)
// 与参考图顺序对应:第一张主体,第二张背景
.objOrBg(List.of("obj", "bg"))
.size("1280*720")
.build())
.build();
VideoPrompt videoPrompt = VideoPrompt.builder()
.content(prompt)
.options(options)
.build();
VideoResponse response = videoModel.call(videoPrompt);
DashScopeVideoResponse.VideoOutput output = response.getResult().getOutput();
if (output != null && output.videoUrl() != null && !output.videoUrl().isBlank()) {
return VideoFileHelper.successMessage(output.videoUrl(), output.taskId(), output.taskStatus());
}
return "生成失败或未完成";
}
}
五、代码案例:本地图片 → 图生视频
云端读不到你电脑上的路径,所以把 MultipartFile 转成 Base64 data URI。
java
/**
* POST /ai/video/image2video
* form-data:file=本地图片,prompt=可选运动描述
*/
@PostMapping("/image2video")
public String image2Video(
@RequestParam("file") MultipartFile file,
@RequestParam(defaultValue = "图片中的主体轻轻走动,镜头缓慢推进") String prompt)
throws IOException {
if (file == null || file.isEmpty()) {
return "请上传图片,表单字段名: file";
}
// MultipartFile → data:image/png;base64,xxxx
String imgUrl = toImageDataUri(file);
DashScopeVideoOptions options = DashScopeVideoOptions.builder()
.model(DashScopeModel.VideoModel.WANX21_I2V_TURBO.getName()) // wanx2.1-i2v-turbo
.input(DashScopeVideoOptions.InputOptions.builder()
.prompt(prompt)
.imgUrl(imgUrl) // 首帧:公网 URL 或 data URI
.build())
.parameters(DashScopeVideoOptions.ParametersOptions.builder()
.size("1280*720")
.promptExtend(true)
.duration(5)
.build())
.build();
VideoResponse response = videoModel.call(
VideoPrompt.builder().content(prompt).options(options).build());
DashScopeVideoResponse.VideoOutput output = response.getResult().getOutput();
if (output != null && output.videoUrl() != null && !output.videoUrl().isBlank()) {
return VideoFileHelper.successMessage(output.videoUrl(), output.taskId(), output.taskStatus());
}
return "生成失败或未完成";
}
/** 上传图片 → data:{mime};base64,{data} */
private static String toImageDataUri(MultipartFile file) throws IOException {
String mime = file.getContentType();
if (mime == null || !mime.startsWith("image/")) {
mime = "image/png";
}
String base64 = Base64.getEncoder().encodeToString(file.getBytes());
return "data:" + mime + ";base64," + base64;
}
六、临时 URL 立刻落盘
返回的 OSS 地址带 Expires / Signature,过期或截断后会出现 AccessDenied。生成成功后立刻下载:
java
public final class VideoFileHelper {
/** 运行目录下的 video/ */
public static final Path VIDEO_DIR = Paths.get("video");
/** 从签名 URL 下载 mp4;成功返回本地绝对路径 */
public static String download(String videoUrl, String taskId) {
try {
Files.createDirectories(VIDEO_DIR);
Path path = VIDEO_DIR.resolve("video-" + taskId + ".mp4");
try (InputStream in = URI.create(videoUrl).toURL().openStream()) {
Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
}
return path.toAbsolutePath().toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String successMessage(String videoUrl, String taskId, String status) {
String local = download(videoUrl, taskId);
return "视频 URL: " + videoUrl
+ "\ntaskId: " + taskId
+ "\nstatus: " + status
+ (local != null ? "\n本地文件: " + local : "\n本地下载失败");
}
}