大模型通义千问3-VL-Plus - 视觉推理(图像列表)
本文介绍了视频抽帧分析的两种实现方式:1)直接传入视频文件由SDK自动抽帧;2)传入预先抽取的视频帧图像列表。重点阐述了图像列表方式的特点:通过fps参数告知模型帧间时间间隔(仅Qwen2.5-VL/Qwen3-VL支持),实现精准时序理解。提供了完整的Java接口实现,包含请求实体类、服务接口及控制器,支持普通调用和流式响应。两种方式本质都是基于帧分析,但图像列表方式更灵活可控,适合需要精确控制
一、概论
视频抽帧说明
当视频以图像列表(即预先抽取的视频帧)传入时,可通过fps参数告知模型视频帧之间的时间间隔,这能帮助模型更准确地理解事件的顺序、持续时间和动态变化。
-
DashScope SDK:
支持通过
fps参数指定原始视频的抽帧率,表示视频帧是每隔fps1秒从原始视频中抽取的。该参数支持 Qwen2.5-VL、Qwen3-VL模型。 -
OpenAI 兼容 SDK:
不支持
fps参数。模型将默认视频帧是按照每 0.5 秒一帧的频率抽取的。
简单来说:
就是把提前抽好的视频帧(一张张图片)传给模型时,fps 参数是给模型的「时间标尺」—— 告诉模型这些帧之间的时间间隔,帮模型准确理解视频里事件的先后、持续时长和画面动态:
- 用 DashScope SDK(仅 Qwen2.5-VL/Qwen3-VL 支持):可自定义 fps,比如 fps=2,就是告诉模型 “这些帧是每隔 0.5 秒从原视频抽的”;
- 用 OpenAI 兼容 SDK:没法设 fps,模型默认按 “每 0.5 秒抽 1 帧” 来理解这些帧的时间间隔。
二、与视频的区别
你可以把这两种方式理解为「模型帮你拆视频」和「你自己拆好视频给模型」的区别 —— 核心都是基于视频帧分析,但抽帧的主体、参数作用、灵活性、适用场景 完全不同,具体对比和解释如下:
| 维度 | 传入「视频文件」(如.mp4 URL) | 传入「图像列表(视频帧)」 |
|---|---|---|
| 传入形式 | 直接传视频文件的 HTTPS URL / 本地文件 | 传按时间顺序排列的图片列表(帧)+ fps 参数 |
| 抽帧的主体 | 由 DashScope SDK / 模型自动完成抽帧 | 由你(开发者)提前手动 / 代码抽帧 |
| fps 参数的作用 | 控制 SDK 的抽帧频率(比如 fps=2 → SDK 自动每 0.5 秒抽 1 帧) | 告知模型 “这些帧是每隔 1/fps 秒抽的”(仅做时间标注,不控制抽帧) |
| 数量限制的影响 | SDK 抽帧后的总帧数需≤模型上限(如 2000),若超则报错 | 直接受数量限制(4~2000/512/80),传多了直接报错 |
| 灵活性 | 简单省心,但抽帧规则固定(只能按 fps 均匀抽) | 灵活可控(可只抽关键帧,比如跳过无变化画面),但需自己做抽帧预处理 |
| 适用场景 | 快速调用、无需精准控制抽帧的普通场景 | 需精准控制抽帧(如高速运动视频、长视频只分析关键片段) |
通俗举例
比如你要分析一段 10 秒的球赛视频:
- 传视频文件:你只需要传.mp4 的 URL + fps=5(每 0.2 秒抽 1 帧),SDK 会自动抽 50 帧,模型基于这 50 帧分析;如果 50 帧≤模型上限(如 2000)就正常,超了就报错。
- 传图像列表:你先手动抽取出球赛的 10 个关键帧(比如进球、传球的画面),按顺序组成列表,再传 fps=10(告诉模型 “这些帧是每隔 0.1 秒抽的”),模型基于这 10 帧分析(需≥4、≤模型上限)。
核心总结
- 最终模型都是基于「视频帧」分析内容,没有本质差异;
- 传视频文件:懒人选这个,不用处理抽帧,SDK 自动搞定,适合大部分普通场景;
- 传图像列表:精准控选这个,自己决定抽哪些帧(比如只留关键画面),适合需要降低算力、精准分析的场景;
- 数量限制对两者都生效:传视频文件时,SDK 抽帧后的总帧数不能超模型上限;传图像列表时,列表长度直接不能超上限(最少都要 4 帧,不然模型没法判断动态)。
简单说:传视频文件是「交钥匙工程」,传图像列表是「定制化工程」,前者省事儿,后者可控。
三、代码实现
以下是基于官方「视频帧列表(图像列表)」示例,新增的完整接口代码(包含实体类、服务接口、实现类、控制器)
第一步:新增视频帧列表请求实体类 VideoFrameListRequest
import lombok.Data;
import javax.validation.constraints.*;
import java.util.List;
/**
* 视频帧列表(图像列表)理解请求参数(通义千问VL)
* 适用于:预先抽取视频帧为图片列表,传给模型分析
* @author DELL
*/
@Data
public class VideoFrameListRequest {
/** 视频帧图片URL列表(按播放顺序排列)
* 数量限制:最少4张,最多按模型定(qwen3-vl-plus≤2000,Qwen2.5-VL≤512)
*/
@NotEmpty(message = "视频帧图片URL列表不能为空")
@Size(min = 4, message = "视频帧图片URL列表最少需要4张")
private List<String> frameImageUrls;
/** 抽帧频率fps(范围0.1~10,默认2.0)
* 含义:告知模型「这些帧是每隔 1/fps 秒从原视频抽取的」(仅Qwen2.5-VL/Qwen3-VL支持)
*/
@DecimalMin(value = "0.1", message = "fps最小值为0.1")
@DecimalMax(value = "10", message = "fps最大值为10")
private Float fps = 2.0f;
/** 针对视频帧的提问文本 */
@NotBlank(message = "提问文本不能为空")
private String question;
/** 模型名称(默认qwen3-vl-plus,仅Qwen2.5-VL/Qwen3-VL支持fps参数) */
private String modelName = "qwen3-vl-plus";
}
第二步:新增视频帧列表服务接口 VideoFrameListService
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import gzj.spring.ai.Request.VideoFrameListRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* 通义千问VL-视频帧列表(图像列表)理解服务接口
* @author DELL
*/
public interface VideoFrameListService {
/**
* 视频帧列表理解-普通调用(非流式)
* @param request 视频帧列表请求参数
* @return 视频内容理解结果文本
*/
String simpleFrameListCall(VideoFrameListRequest request) throws ApiException, NoApiKeyException, UploadFileException;
/**
* 视频帧列表理解-流式调用(SSE推送)
* @param request 视频帧列表请求参数
* @return SseEmitter 用于前端接收流式结果
*/
SseEmitter streamFrameListCall(VideoFrameListRequest request);
}
第三步:新增视频帧列表服务实现类 VideoFrameListServiceImpl
package gzj.spring.ai.Service.ServiceImpl;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult;
import com.alibaba.dashscope.common.MultiModalMessage;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import gzj.spring.ai.Request.VideoFrameListRequest;
import gzj.spring.ai.Service.VideoFrameListService;
import io.reactivex.Flowable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.*;
/**
* 视频帧列表(图像列表)理解服务实现(通义千问VL)
* 适配官方「预先抽帧为图片列表」的调用方式
* @author DELL
*/
@Slf4j
@Service
public class VideoFrameListServiceImpl implements VideoFrameListService {
@Value("${dashscope.api-key}")
private String apiKey;
/**
* 构建视频帧列表参数(video=图片URL列表 + fps)
*/
private Map<String, Object> buildFrameListParams(VideoFrameListRequest request) {
Map<String, Object> frameParams = new HashMap<>(2);
// video字段传入图片URL列表(核心:和传视频文件的区别)
frameParams.put("video", request.getFrameImageUrls());
// 告知模型帧的时间间隔
frameParams.put("fps", request.getFps());
log.info("视频帧配置:fps={} → 模型将理解为「每隔{}秒抽取一帧」;帧数量={}",
request.getFps(), 1/request.getFps(), request.getFrameImageUrls().size());
return frameParams;
}
/**
* 视频帧列表理解-普通调用(非流式)
*/
@Override
public String simpleFrameListCall(VideoFrameListRequest request) throws ApiException, NoApiKeyException, UploadFileException {
MultiModalConversation conv = new MultiModalConversation();
// 1. 构建用户消息(视频帧列表参数 + 提问文本)
MultiModalMessage userMessage = MultiModalMessage.builder()
.role(Role.USER.getValue())
.content(Arrays.asList(
buildFrameListParams(request), // 视频帧+fps参数
Collections.singletonMap("text", request.getQuestion()) // 提问文本
)).build();
// 2. 构建API请求参数
MultiModalConversationParam param = MultiModalConversationParam.builder()
.apiKey(apiKey)
.model(request.getModelName()) // 支持自定义模型(需为Qwen2.5-VL/Qwen3-VL系列)
.messages(Arrays.asList(userMessage))
.build();
// 3. 同步调用API
MultiModalConversationResult result = conv.call(param);
// 4. 解析返回结果
List<Map<String, Object>> content = result.getOutput().getChoices().get(0).getMessage().getContent();
if (content != null && !content.isEmpty()) {
return content.get(0).get("text").toString();
}
return "未获取到视频帧列表理解结果";
}
/**
* 视频帧列表理解-流式调用(SSE推送)
*/
@Override
public SseEmitter streamFrameListCall(VideoFrameListRequest request) {
// 视频帧分析耗时可能更长,超时设为60秒
SseEmitter emitter = new SseEmitter(60000L);
new Thread(() -> {
MultiModalConversation conv = new MultiModalConversation();
try {
// 1. 构建用户消息
MultiModalMessage userMessage = MultiModalMessage.builder()
.role(Role.USER.getValue())
.content(Arrays.asList(
buildFrameListParams(request),
Collections.singletonMap("text", request.getQuestion())
)).build();
// 2. 构建流式请求参数
MultiModalConversationParam param = MultiModalConversationParam.builder()
.apiKey(apiKey)
.model(request.getModelName())
.messages(Arrays.asList(userMessage))
.incrementalOutput(true) // 增量输出(流式)
.build();
// 3. 流式调用API
Flowable<MultiModalConversationResult> resultFlow = conv.streamCall(param);
resultFlow.blockingForEach(item -> {
try {
List<Map<String, Object>> content = item.getOutput().getChoices().get(0).getMessage().getContent();
if (content != null && !content.isEmpty()) {
String text = content.get(0).get("text").toString();
// 推送流式数据到前端
emitter.send(SseEmitter.event().data(text));
}
} catch (Exception e) {
log.error("视频帧列表流式推送失败", e);
handleEmitterError(emitter, "流式推送失败:" + e.getMessage());
}
});
// 流式结束标记
emitter.send(SseEmitter.event().name("complete").data("视频帧列表理解流结束"));
emitter.complete();
} catch (ApiException | NoApiKeyException | UploadFileException e) {
log.error("视频帧列表流式调用API失败", e);
handleEmitterError(emitter, "API调用失败:" + e.getMessage());
} catch (Exception e) {
log.error("视频帧列表流式调用未知异常", e);
handleEmitterError(emitter, "系统异常:" + e.getMessage());
}
}).start();
return emitter;
}
/**
* 复用:统一处理SSE发射器异常
*/
private void handleEmitterError(SseEmitter emitter, String errorMsg) {
try {
emitter.send(SseEmitter.event().name("error").data(errorMsg));
emitter.completeWithError(new RuntimeException(errorMsg));
} catch (Exception e) {
log.error("处理发射器异常失败", e);
}
}
}
第四步:新增视频帧列表控制器 VideoController
直接在之前的视频接口里写入接口
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import gzj.spring.ai.Request.VideoFrameListRequest;
import gzj.spring.ai.Request.VideoRequest;
import gzj.spring.ai.Service.VideoFrameListService;
import gzj.spring.ai.Service.VideoService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* @author DELL
*/
@RestController
@RequestMapping("/api/multimodal/video")
@RequiredArgsConstructor
@CrossOrigin // 跨域支持(生产环境建议限定域名)
public class VideoController {
private final VideoService videoService;
private final VideoFrameListService videoFrameListService;
@RequestMapping("/simple")
public String simpleVideoCall(@RequestBody VideoRequest request) throws ApiException, NoApiKeyException, UploadFileException {
return videoService.simpleVideoCall(request);
}
/**
* 视频帧列表理解-普通调用(非流式)
*/
@PostMapping("/simpleFrame")
public String simpleFrameListCall(@Validated @RequestBody VideoFrameListRequest request)
throws ApiException, NoApiKeyException, UploadFileException {
return videoFrameListService.simpleFrameListCall(request);
}
/**
* 视频帧列表理解-流式调用(SSE推送)
*/
@PostMapping("/streamFrame")
public SseEmitter streamFrameListCall(@Validated @RequestBody VideoFrameListRequest request) {
return videoFrameListService.streamFrameListCall(request);
}
}
总结
本次新增视频帧列表(图像列表)理解接口的代码核心总结如下:
一、代码结构与设计(延续原有风格)
新增 4 个核心模块,保持代码风格、异常处理、流式推送逻辑统一:
- 请求实体(VideoFrameListRequest):封装帧列表核心参数(按顺序的图片 URL 列表、fps、提问文本、模型名),强化参数校验(帧列表最少 4 张、fps 范围 0.1~10),贴合模型底层限制;
- 服务接口(VideoFrameListService):定义普通 / 流式两种调用方式,与原有视频接口设计一致;
- 服务实现(VideoFrameListServiceImpl):对齐官方示例,将
video字段设为图片 URL 列表(而非视频文件 URL),fps 参数仅用于 “告知模型帧的时间间隔”,并适配 60 秒流式超时; - 控制器(VideoFrameListController):暴露
/api/multimodal/video-frame接口,支持普通 / 流式调用,加入参数校验和跨域支持。
二、与原有 “视频文件接口” 的核心差异
| 维度 | 原有视频文件接口 | 新增视频帧列表接口 |
|---|---|---|
video字段值 |
单个视频文件 URL(如.mp4) | 按播放顺序的图片 URL 列表 |
| fps 参数作用 | 控制 SDK 自动抽帧频率 | 仅告知模型 “帧的时间间隔”(无抽帧动作) |
| 预处理要求 | 无需预处理(SDK 自动抽帧) | 需提前抽帧并按顺序整理图片 URL |
三、关键注意事项
- 帧列表必须严格按视频播放顺序排列,否则模型会误判事件顺序;
- fps 仅对 Qwen2.5-VL/Qwen3-VL 系列模型生效,其他模型传入无效;
- 帧数量需符合模型上限(qwen3-vl-plus≤2000、Qwen2.5-VL≤512),超量会报参数错误;
- 图片 URL 需为公开可访问的 HTTPS 链接,确保模型能读取。
四、核心价值
实现了 “开发者自定义抽帧” 的视频理解能力,相比自动抽帧更灵活(可只传关键帧),适配需要精准控制抽帧的场景(如高速运动视频、长视频关键片段分析)。
四、示例
参数
{
"frameImageUrls": [
"https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/xzsgiz/football1.jpg",
"https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/tdescd/football2.jpg",
"https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/zefdja/football3.jpg",
"https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/aedbqh/football4.jpg"
],
"fps": 2.0,
"question": "描述这个视频的具体过程",
"modelName": "qwen3-vl-plus"
}
结果
如果觉得这份修改实用、总结清晰,别忘了动动小手点个赞👍,再关注一下呀~ 后续还会分享更多 AI 接口封装、代码优化的干货技巧,一起解锁更多好用的功能,少踩坑多提效!🥰 你的支持就是我更新的最大动力,咱们下次分享再见呀~🌟
更多推荐



所有评论(0)