Qwen3-Omni-Captioner:通义千问 3-Omni 基座的智能音频描述开源模型

一、概论

Qwen3-Omni-Captioner是以通义千问3-Omni为基座的开源模型,无需任何提示,自动为复杂语音、环境声、音乐、影视声效等生成精准、全面的描述,能识别说话人的情绪、音乐元素(如风格、乐器)、敏感信息等,适用于音频内容分析、安全审核、意图识别、音频剪辑等多个领域。

简单来说:阿里开源的智能音频理解模型,无需提示即可自动为语音、环境声、音乐等生成精准描述,识别情绪、音乐元素与敏感信息。

核心特点

  • 无提示自动描述:输入音频直接输出详细文本,无需任何提示词
  • 多场景全覆盖:支持复杂语音、环境声、音乐、影视声效的精细化解析
  • 深度语义理解:识别说话人情绪、多语言混合 (支持 19 种语言)、音乐风格 / 乐器
  • 敏感信息检测:自动识别并标记音频中的敏感内容
  • 端到端架构:基于 Qwen3-Omni-30B-A3B-Instruct 基座模型,30 秒内音频精准分析

技术亮点

  • 原生多模态融合:非 "文本模型 + 外挂",而是从底层设计实现音频与文本语义深度融合
  • 低幻觉率:较前代模型幻觉率降低 72%,细节还原度达行业新高
  • 商业友好许可:Apache 2.0 协议,允许商业使用且无需开源修改代码
  • 成本优势:API 调用成本较 GPT-4o 降低 70% 以上

典型应用

  • 内容审核:自动识别音频中的违规内容,保障平台安全
  • 媒体内容分析:生成音频摘要、字幕,提升内容检索效率
  • 智能导览:博物馆、景区等场所的自动语音解说
  • 无障碍服务:为视障人士提供音频内容 "可视化" 描述
  • 音频编辑辅助:快速了解音频内容结构,提升剪辑效率

二、代码实现

1、Request 实体类(AudioCaptionRequest)

java 复制代码
package gzj.spring.ai.Request;

import lombok.Data;

/**
 * 音频识别请求参数实体类
 * @author DELL
 */
@Data
public class AudioCaptionRequest {
    /**
     * 远程音频链接(HTTP/HTTPS)
     */
    private String audioUrl;

    /**
     * 音频类型(可选:remote-远程音频,local-本地音频)
     */
    private String audioType;

    /**
     * 地域配置(可选:cn-北京,sg-新加坡)
     */
    private String region = "cn";
}

2、Service 接口(AudioCaptionService)

java 复制代码
package gzj.spring.ai.Service;

import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import gzj.spring.ai.Request.AudioCaptionRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

/**
 * 音频识别服务接口
 * @author DELL
 */
public interface AudioCaptionService {

    /**
     * 非流式调用(同步返回音频识别结果)
     */
    String operation(AudioCaptionRequest request) throws ApiException, NoApiKeyException, UploadFileException, IOException;

    /**
     * SSE流式调用(实时推送音频识别结果)
     */
    SseEmitter streamOperation(AudioCaptionRequest request);

    /**
     * 本地音频文件识别(非流式)
     */
    String recognizeLocalFile(MultipartFile file, String region) throws ApiException, NoApiKeyException, UploadFileException, IOException;
}

3、Controller 控制器(AudioCaptionController)

java 复制代码
package gzj.spring.ai.Controller;

import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import gzj.spring.ai.Request.AudioCaptionRequest;
import gzj.spring.ai.Service.AudioCaptionService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.awt.*;
import java.io.IOException;

import static com.alibaba.cloud.ai.graph.utils.TryConsumer.log;

/**
 * 音频识别控制器
 * @author DELL
 */
@RestController
@RequestMapping("/api/AudioCaption")
@CrossOrigin // 跨域支持
public class AudioCaptionController {

    private final AudioCaptionService audioCaptionService;

    // 构造器注入Service(与你提供的格式保持一致)
    public AudioCaptionController(AudioCaptionService audioCaptionService) {
        this.audioCaptionService = audioCaptionService;
    }

    /**
     * 非流式调用(远程音频识别)
     */
    @PostMapping("/operation/easy")
    public String operation(@RequestBody AudioCaptionRequest request) throws ApiException, NoApiKeyException, UploadFileException, IOException {
        log.info("接收音频识别非流式请求:{}", request);
        return audioCaptionService.operation(request);
    }

    /**
     * SSE流式调用(远程音频实时识别推送)
     */
    @PostMapping("/operation/stream")
    public SseEmitter streamOperation(@RequestBody AudioCaptionRequest request) {
        log.info("接收音频识别SSE流式调用请求:{}", request);
        return audioCaptionService.streamOperation(request);
    }

    /**
     * 本地音频文件上传识别
     */
    @PostMapping("/operation/local")
    public String recognizeLocalFile(@RequestParam("file") MultipartFile file,
                                     @RequestParam(value = "region", defaultValue = "cn") String region) throws ApiException, NoApiKeyException, UploadFileException, IOException {
        log.info("接收本地音频文件识别请求,文件名称:{},地域:{}", file.getOriginalFilename(), region);
        return audioCaptionService.recognizeLocalFile(file, region);
    }

    /**
     * 全局异常处理(与你提供的格式保持一致)
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> globalExceptionHandler(Exception e) {
        log.error("音频识别接口全局异常", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("服务器内部错误:" + e.getMessage());
    }
}

4、Service 实现类(AudioCaptionServiceImpl)

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.AudioCaptionRequest;
import gzj.spring.ai.Service.AudioCaptionService;
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.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 音频识别服务封装(支持网络音频URL + 本地音频文件)
 * @author DELL
 */
@Slf4j
@Service
public class AudioCaptionServiceImpl implements AudioCaptionService {

    @Value("${spring.ai.dashscope.api-key}")
    private String apiKey;

    @Value("${dashscope.cn-url:https://dashscope.aliyuncs.com/api/v1}")
    private String cnBaseUrl;

    @Value("${dashscope.sg-url:https://dashscope-intl.aliyuncs.com/api/v1}")
    private String sgBaseUrl;

    @Value("${temp.file.path:${user.home}/audio-temp/}")
    private String tempFilePath;

    // 模型名称
    private static final String MODEL_NAME = "qwen3-omni-30b-a3b-captioner";

    /**
     * 工具方法:设置地域对应的基础URL
     */
    private void setRegionBaseUrl(String region) {
        if ("sg".equalsIgnoreCase(region)) {
            com.alibaba.dashscope.utils.Constants.baseHttpApiUrl = sgBaseUrl;
            log.info("切换至新加坡地域,基础URL:{}", sgBaseUrl);
        } else {
            com.alibaba.dashscope.utils.Constants.baseHttpApiUrl = cnBaseUrl;
            log.info("切换至北京地域,基础URL:{}", cnBaseUrl);
        }
    }

    /**
     * 工具方法:保存本地上传文件到临时目录
     */
    private String saveTempFile(MultipartFile file) throws IOException {
        // 创建临时目录
        File tempDir = new File(tempFilePath);
        if (!tempDir.exists()) {
            boolean mkdirs = tempDir.mkdirs();
            log.info("临时目录创建状态:{},路径:{}", mkdirs, tempDir.getAbsolutePath());
        }
        // 生成唯一文件名
        String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
        Path tempPath = Paths.get(tempDir.getAbsolutePath(), fileName);
        // 保存文件
        Files.write(tempPath, file.getBytes());
        String fileUrl = "file://" + tempPath.toAbsolutePath().toString().replace("\\", "/");
        log.info("临时文件保存成功,路径:{}", fileUrl);
        return fileUrl;
    }

    /**
     * 工具方法:构建音频消息体
     */
    private MultiModalMessage buildAudioMessage(String audioUrl) {
        return MultiModalMessage.builder()
                .role(Role.USER.getValue())
                .content(Arrays.asList(
                        Collections.singletonMap("audio", audioUrl)
                )).build();
    }

    /**
     * 工具方法:解析识别结果文本
     */
    private String parseResultText(MultiModalConversationResult result) {
        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发射器异常
     */
    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);
        }
    }

    /**
     * 非流式调用(同步返回结果)
     */
    @Override
    public String operation(AudioCaptionRequest request) throws ApiException, NoApiKeyException, UploadFileException, IOException {
        // 1. 参数校验
        if (request == null || request.getAudioUrl() == null || request.getAudioUrl().isEmpty()) {
            throw new IllegalArgumentException("远程音频URL不能为空");
        }
        // 2. 设置地域URL
        setRegionBaseUrl(request.getRegion());

        MultiModalConversation conv = new MultiModalConversation();
        // 3. 构建音频消息
        MultiModalMessage userMessage = buildAudioMessage(request.getAudioUrl());
        // 4. 构建请求参数(关键:添加enable_omni_output_audio_url参数)
        MultiModalConversationParam param = MultiModalConversationParam.builder()
                .apiKey(apiKey)
                .model(MODEL_NAME)
                .messages(Arrays.asList(userMessage))
                // 核心修复:非流式调用必须设置该参数
                .parameters(Collections.singletonMap("enable_omni_output_audio_url", true))
                .build();

        // 5. 同步调用模型
        MultiModalConversationResult result = conv.call(param);
        // 6. 解析结果
        return parseResultText(result);
    }

    /**
     * SSE流式调用(实时推送结果)
     */
    @Override
    public SseEmitter streamOperation(AudioCaptionRequest request) {
        // 设置超时时间3分钟
        SseEmitter emitter = new SseEmitter(TimeUnit.MINUTES.toMillis(3));

        new Thread(() -> {
            MultiModalConversation conv = new MultiModalConversation();
            try {
                // 1. 参数校验
                if (request == null || request.getAudioUrl() == null || request.getAudioUrl().isEmpty()) {
                    throw new IllegalArgumentException("远程音频URL不能为空");
                }
                // 2. 设置地域URL
                setRegionBaseUrl(request.getRegion());

                // 3. 构建音频消息
                MultiModalMessage userMessage = buildAudioMessage(request.getAudioUrl());
                // 4. 构建请求参数
                MultiModalConversationParam param = MultiModalConversationParam.builder()
                        .apiKey(apiKey)
                        .model(MODEL_NAME)
                        .messages(Arrays.asList(userMessage))
                        .incrementalOutput(true) // 增量输出(流式)
                        .build();

                // 5. 流式调用
                Flowable<MultiModalConversationResult> resultFlow = conv.streamCall(param);
                resultFlow.blockingForEach(item -> {
                    try {
                        String text = parseResultText(item);
                        // 推送流式数据到前端
                        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 (IllegalArgumentException e) {
                log.error("请求参数异常", e);
                handleEmitterError(emitter, "参数错误:" + e.getMessage());
            } 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;
    }

    /**
     * 本地音频文件识别(非流式)
     */
    @Override
    public String recognizeLocalFile(MultipartFile file, String region) throws ApiException, NoApiKeyException, UploadFileException, IOException {
        // 1. 参数校验
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("上传的音频文件不能为空");
        }
        // 2. 设置地域URL
        setRegionBaseUrl(region);

        MultiModalConversation conv = new MultiModalConversation();
        // 3. 保存临时文件并获取file://路径
        String audioUrl = saveTempFile(file);
        // 4. 构建音频消息
        MultiModalMessage userMessage = buildAudioMessage(audioUrl);
        // 5. 构建请求参数(添加强制参数)
        MultiModalConversationParam param = MultiModalConversationParam.builder()
                .apiKey(apiKey)
                .model(MODEL_NAME)
                .messages(Arrays.asList(userMessage))
                .parameters(Collections.singletonMap("enable_omni_output_audio_url", true))
                .build();

        try {
            // 6. 同步调用模型
            MultiModalConversationResult result = conv.call(param);
            return parseResultText(result);
        } finally {
            // 7. 清理临时文件
            File tempFile = new File(audioUrl.replace("file://", ""));
            if (tempFile.exists()) {
                boolean deleted = tempFile.delete();
                log.info("临时文件清理状态:{},路径:{}", deleted, tempFile.getAbsolutePath());
            }
        }
    }
}

三、实现效果

四、总结

1. 功能维度:完整覆盖业务需求

功能模块 实现细节
远程音频识别 支持 HTTP/HTTPS 格式音频 URL,校验链接合法性,适配北京 / 新加坡双地域 URL 配置
本地音频识别 自动创建临时目录、生成唯一文件名保存上传文件,识别完成后自动清理临时文件
非流式同步调用 同步调用模型 API,直接返回音频识别文本结果,添加强制参数解决 400 错误
SSE 流式调用 异步线程处理调用逻辑,设置 3 分钟超时时间,实时推送识别结果,含结束 / 错误标记

2. 技术实现:工程化与健壮性设计

(1)配置层面
  • 多源配置读取 :API Key 支持环境变量(DASHSCOPE_API_KEY)和配置文件双来源,适配不同部署环境;
  • 地域可配置 :北京 / 新加坡地域 URL 通过配置文件管理,通过setRegionBaseUrl方法动态切换;
  • 文件上传限制:设置单文件 / 单次请求最大 100MB,满足音频文件上传需求。
(2)核心流程
TypeScript 复制代码
参数校验 → 地域URL设置 → 音频消息构建 → 请求参数构建(含强制参数)→ 模型调用 → 结果解析 → 资源清理
  • 参数校验:前置校验音频 URL / 文件是否为空,避免无效调用;
  • 消息构建:复用MultiModalMessage.builder()+Collections.singletonMap,与现有多模态服务语法一致;
  • 结果解析:统一封装parseResultText方法,解析模型返回的文本内容,保证格式一致性;
  • 资源清理:本地文件识别后强制清理临时文件,避免磁盘占用。

如果觉得这份修改实用、总结清晰,别忘了动动小手点个赞👍,再关注一下呀~ 后续还会分享更多 AI 接口封装、代码优化的干货技巧,一起解锁更多好用的功能,少踩坑多提效!🥰 你的支持就是我更新的最大动力,咱们下次分享再见呀~🌟

相关推荐
yesyesyoucan2 小时前
AI证件照生成技术全解析:人脸识别、背景分割与格式合规性实现方案
人工智能·考研·高考
FL16238631292 小时前
[C#][winform]基于yolov11的齿轮缺陷检测系统C#源码+onnx模型+评估指标曲线+精美GUI界面
人工智能·yolo
却道天凉_好个秋2 小时前
OpenCV(四十三):分水岭法
人工智能·opencv·计算机视觉·图像分割·分水岭法
爱笑的眼睛112 小时前
TensorFlow Hub:解锁预训练模型的无限可能,超越基础分类任务
java·人工智能·python·ai
GodGump2 小时前
AI 竞争正在进入什么阶段?
人工智能
万俟淋曦2 小时前
【论文速递】2025年第41周(Oct-05-11)(Robotics/Embodied AI/LLM)
人工智能·深度学习·机器人·大模型·论文·robotics·具身智能
落羽的落羽2 小时前
【C++】深入浅出“图”——图的基本概念与存储结构
服务器·开发语言·数据结构·c++·人工智能·机器学习·图搜索算法
DatGuy2 小时前
Week 30: 机器学习补遗:时序信号处理与数学特征工程
人工智能·机器学习·信号处理
摸鱼仙人~2 小时前
大语言模型微调中的数据分布不均与长尾任务优化策略
人工智能·深度学习·机器学习