大模型通义千问3-VL-Plus - 视觉推理(图像列表)

一、概论

视频抽帧说明

当视频以图像列表(即预先抽取的视频帧)传入时,可通过fps参数告知模型视频帧之间的时间间隔,这能帮助模型更准确地理解事件的顺序、持续时间和动态变化。

  • DashScope SDK:

    支持通过 fps 参数指定原始视频的抽帧率,表示视频帧是每隔fps1​秒从原始视频中抽取的。该参数支持 Qwen2.5-VL、Qwen3-VL模型

  • OpenAI 兼容 SDK:

    不支持 fps 参数。模型将默认视频帧是按照每 0.5 秒一帧的频率抽取的。

简单来说:

就是把提前抽好的视频帧(一张张图片)传给模型时,fps 参数是给模型的「时间标尺」------ 告诉模型这些帧之间的时间间隔,帮模型准确理解视频里事件的先后、持续时长和画面动态:

  1. 用 DashScope SDK(仅 Qwen2.5-VL/Qwen3-VL 支持):可自定义 fps,比如 fps=2,就是告诉模型 "这些帧是每隔 0.5 秒从原视频抽的";
  2. 用 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、≤模型上限)。

核心总结

  1. 最终模型都是基于「视频帧」分析内容,没有本质差异;
  2. 传视频文件:懒人选这个,不用处理抽帧,SDK 自动搞定,适合大部分普通场景;
  3. 传图像列表:精准控选这个,自己决定抽哪些帧(比如只留关键画面),适合需要降低算力、精准分析的场景;
  4. 数量限制对两者都生效:传视频文件时,SDK 抽帧后的总帧数不能超模型上限;传图像列表时,列表长度直接不能超上限(最少都要 4 帧,不然模型没法判断动态)。

简单说:传视频文件是「交钥匙工程」,传图像列表是「定制化工程」,前者省事儿,后者可控。

三、代码实现

以下是基于官方「视频帧列表(图像列表)」示例,新增的完整接口代码(包含实体类、服务接口、实现类、控制器)

第一步:新增视频帧列表请求实体类 VideoFrameListRequest

java 复制代码
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

java 复制代码
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

java 复制代码
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

直接在之前的视频接口里写入接口

java 复制代码
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 个核心模块,保持代码风格、异常处理、流式推送逻辑统一:

  1. 请求实体(VideoFrameListRequest):封装帧列表核心参数(按顺序的图片 URL 列表、fps、提问文本、模型名),强化参数校验(帧列表最少 4 张、fps 范围 0.1~10),贴合模型底层限制;
  2. 服务接口(VideoFrameListService):定义普通 / 流式两种调用方式,与原有视频接口设计一致;
  3. 服务实现(VideoFrameListServiceImpl) :对齐官方示例,将video字段设为图片 URL 列表(而非视频文件 URL),fps 参数仅用于 "告知模型帧的时间间隔",并适配 60 秒流式超时;
  4. 控制器(VideoFrameListController) :暴露/api/multimodal/video-frame接口,支持普通 / 流式调用,加入参数校验和跨域支持。

二、与原有 "视频文件接口" 的核心差异

维度 原有视频文件接口 新增视频帧列表接口
video字段值 单个视频文件 URL(如.mp4) 按播放顺序的图片 URL 列表
fps 参数作用 控制 SDK 自动抽帧频率 仅告知模型 "帧的时间间隔"(无抽帧动作)
预处理要求 无需预处理(SDK 自动抽帧) 需提前抽帧并按顺序整理图片 URL

三、关键注意事项

  1. 帧列表必须严格按视频播放顺序排列,否则模型会误判事件顺序;
  2. fps 仅对 Qwen2.5-VL/Qwen3-VL 系列模型生效,其他模型传入无效;
  3. 帧数量需符合模型上限(qwen3-vl-plus≤2000、Qwen2.5-VL≤512),超量会报参数错误;
  4. 图片 URL 需为公开可访问的 HTTPS 链接,确保模型能读取。

四、核心价值

实现了 "开发者自定义抽帧" 的视频理解能力,相比自动抽帧更灵活(可只传关键帧),适配需要精准控制抽帧的场景(如高速运动视频、长视频关键片段分析)。

四、示例

参数

java 复制代码
{
  "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 接口封装、代码优化的干货技巧,一起解锁更多好用的功能,少踩坑多提效!🥰 你的支持就是我更新的最大动力,咱们下次分享再见呀~🌟

相关推荐
AngelPP1 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年1 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼1 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS1 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区3 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈3 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang3 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx
shengjk14 小时前
NanoClaw 深度剖析:一个"AI 原生"架构的个人助手是如何运转的?
人工智能
西门老铁6 小时前
🦞OpenClaw 让 MacMini 脱销了,而我拿出了6年陈的安卓机
人工智能