SenseVoice-Small语音模型Java集成实战:SpringBoot服务端调用ONNX Runtime

📅 发布时间:2026/7/7 12:26:14 👁️ 浏览次数:
SenseVoice-Small语音模型Java集成实战:SpringBoot服务端调用ONNX Runtime
SenseVoice-Small语音模型Java集成实战SpringBoot服务端调用ONNX Runtime语音识别正在从消费级应用快速渗透到企业级服务中。无论是客服中心的通话质检、在线会议的实时字幕还是智能硬件中的语音指令都需要一个稳定、高效且易于集成的后端方案。对于Java技术栈的团队来说直接在熟悉的SpringBoot环境中集成AI模型往往比引入一套全新的Python服务更实际。SenseVoice-Small作为一个轻量级、高性能的语音识别模型其ONNX格式为跨语言部署提供了便利。今天我们就来聊聊如何在一个标准的SpringBoot微服务里把它跑起来并封装成一套能扛住压力的RESTful API。整个过程我会尽量避开那些让人头疼的配置用最直白的方式讲清楚。1. 项目准备与环境搭建在开始写代码之前我们需要把“舞台”搭好。这里没有复杂的Python虚拟环境核心就是让Java能通过ONNX Runtime去调用那个模型文件。首先创建一个最普通的SpringBoot项目用Maven或者Gradle都可以。我习惯用Maven所以在pom.xml里关键是要引入ONNX Runtime的Java依赖。dependencies !-- SpringBoot基础依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- ONNX Runtime for Java -- dependency groupIdcom.microsoft.onnxruntime/groupId artifactIdonnxruntime/artifactId version1.16.3/version !-- 请使用最新稳定版 -- /dependency !-- 数据库相关用于结果持久化 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- 音频处理用于解码MP3/WAV等 -- dependency groupIdorg.apache.tika/groupId artifactIdtika-core/artifactId version2.9.1/version /dependency /dependencies这里有个小坑需要注意onnxruntime这个JAR包本身不包含本地库Native Library。所以你需要根据你的部署环境Linux/Windows/macOS去ONNX Runtime的GitHub Release页面下载对应版本的预构建包把里面的动态链接库比如Linux的libonnxruntime.so放到Java库路径下或者通过启动参数-Djava.library.path指定其位置。这是Java通过JNI调用C库的标准操作。接下来去SenseVoice的官方仓库找到SenseVoice-Small模型的ONNX格式文件通常以.onnx结尾把它放到项目的资源目录里比如src/main/resources/models/sensevoice-small.onnx。这样打包后模型文件就在Jar包内部了。2. 核心推理服务封装环境准备好后我们来创建最核心的类——语音识别服务。这个服务要干几件事加载模型、预处理音频、执行推理、后处理文本。2.1 模型加载与会话管理我们先创建一个VoiceRecognitionService在Spring容器启动时就把模型加载好。import ai.onnxruntime.*; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.nio.file.Path; import java.nio.file.Paths; Service Slf4j public class VoiceRecognitionService { private OrtEnvironment environment; private OrtSession session; PostConstruct public void init() throws OrtException { log.info(正在初始化ONNX Runtime环境...); environment OrtEnvironment.getEnvironment(); // 从classpath加载模型文件 ClassPathResource modelResource new ClassPathResource(models/sensevoice-small.onnx); try { Path modelPath Paths.get(modelResource.getURI()); OrtSession.SessionOptions sessionOptions new OrtSession.SessionOptions(); // 根据需求设置选项比如使用CPU还是GPU // sessionOptions.addCUDA(0); // 如果启用GPU session environment.createSession(modelPath.toString(), sessionOptions); log.info(SenseVoice-Small ONNX模型加载成功。输入信息: {}, session.getInputInfo()); } catch (Exception e) { log.error(模型加载失败, e); throw new RuntimeException(初始化语音识别模型失败, e); } } // 提供一个简单的识别方法后续会完善 public String recognize(byte[] audioData) { // 这里先留空后面填充具体逻辑 return 识别结果待实现; } PreDestroy public void cleanup() { try { if (session ! null) { session.close(); } if (environment ! null) { environment.close(); } log.info(ONNX Runtime资源已释放。); } catch (Exception e) { log.error(资源释放异常, e); } } }这段代码在服务启动时自动加载模型关闭时清理资源符合Spring Bean的生命周期管理。OrtSession就是模型在内存中的代表后续的推理都通过它进行。2.2 音频预处理与推理执行SenseVoice模型对输入有特定要求通常是特定采样率如16kHz的单声道浮点数组。我们需要将上传的音频文件如MP3、WAV转换成模型期望的格式。import javax.sound.sampled.*; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class AudioPreprocessor { /** * 将音频字节数据转换为模型需要的浮点数组。 * 这是一个简化示例实际处理可能需要更复杂的重采样和归一化。 */ public static float[] preprocessAudio(byte[] audioBytes, String contentType) throws IOException, UnsupportedAudioFileException { // 使用Java Sound API或第三方库如Tika解码音频 AudioInputStream audioStream AudioSystem.getAudioInputStream(new ByteArrayInputStream(audioBytes)); AudioFormat format audioStream.getFormat(); // 检查并转换为目标格式16kHz, 单声道, PCM_SIGNED AudioFormat targetFormat new AudioFormat(16000, 16, 1, true, false); if (!format.matches(targetFormat)) { audioStream AudioSystem.getAudioInputStream(targetFormat, audioStream); } // 读取音频数据到字节数组 byte[] rawData audioStream.readAllBytes(); // 将16位PCM字节转换为浮点数组-1.0 到 1.0 float[] floatSamples new float[rawData.length / 2]; for (int i 0; i floatSamples.length; i) { short sample (short) ((rawData[2*i1] 8) | (rawData[2*i] 0xFF)); floatSamples[i] sample / 32768.0f; } audioStream.close(); return floatSamples; } }有了预处理后的浮点数组就可以填充到VoiceRecognitionService的recognize方法中了。// 在VoiceRecognitionService类中完善recognize方法 public String recognize(float[] audioSamples) throws OrtException { // 1. 将浮点数组包装成Tensor long[] shape {1, audioSamples.length}; // 假设模型输入shape为 [batch, sequence] OnnxTensor audioTensor OnnxTensor.createTensor(environment, FloatBuffer.wrap(audioSamples), shape); // 2. 准备输入Map键名需要与模型输入节点名称匹配 MapString, OnnxTensor inputs new HashMap(); inputs.put(audio_input, audioTensor); // audio_input需根据实际模型修改 // 3. 执行推理 try (OrtSession.Result results session.run(inputs)) { // 4. 获取输出输出节点名称也需根据模型修改 OnnxTensor outputTensor (OnnxTensor) results.get(text_output); float[][] outputArray (float[][]) outputTensor.getValue(); // 5. 后处理将模型输出的logits或token ids转换为字符串 // 这里需要根据SenseVoice的具体输出格式编写解码逻辑 String recognizedText decodeOutput(outputArray[0]); // 假设batch1 return recognizedText; } finally { audioTensor.close(); } } // 一个简单的占位解码函数实际需要词汇表映射 private String decodeOutput(float[] tokens) { // 简化处理这里应该是将token ids转换为汉字/单词的过程 // 实际项目中你需要加载模型的词汇表文件来实现 StringBuilder sb new StringBuilder(); // ... 解码逻辑 ... return sb.toString(); }音频预处理和模型推理的流程就走通了。当然实际的解码decodeOutput部分需要结合SenseVoice模型的词表vocab.txt来实现将模型输出的ID序列转换成可读的文本。3. 构建高并发RESTful API单个请求处理没问题了但企业级应用面临的是并发请求。我们不能让每个HTTP请求都同步等待漫长的语音识别过程那样会很快耗尽Tomcat线程。3.1 异步处理与线程池设计我们的策略是API接口快速接收请求将耗时的识别任务提交给后台线程池处理并通过异步机制返回结果。import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.stereotype.Component; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; EnableAsync Component public class AsyncRecognitionHandler { private final VoiceRecognitionService recognitionService; private final Executor taskExecutor; // 自定义线程池 public AsyncRecognitionHandler(VoiceRecognitionService recognitionService) { this.recognitionService recognitionService; // 配置一个专用于语音识别的线程池 this.taskExecutor new ThreadPoolTaskExecutor(); // 实际使用需配置参数 } Async(taskExecutor) public CompletableFutureString handleRecognitionAsync(float[] audioSamples) { try { String text recognitionService.recognize(audioSamples); return CompletableFuture.completedFuture(text); } catch (Exception e) { CompletableFutureString future new CompletableFuture(); future.completeExceptionally(e); return future; } } }然后我们创建一个ThreadPoolTaskExecutor的配置Bean隔离语音识别任务避免影响Web容器的其他业务。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; Configuration public class AsyncConfig { Bean(name recognitionTaskExecutor) public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); // 核心线程数根据CPU核心数调整 executor.setMaxPoolSize(10); // 最大线程数 executor.setQueueCapacity(100); // 队列容量 executor.setThreadNamePrefix(voice-recog-); executor.initialize(); return executor; } }3.2 控制器与文件上传现在创建REST控制器来暴露API。我们设计两个端点一个用于同步测试一个用于异步提交任务并返回任务ID。import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; RestController RequestMapping(/api/v1/voice) public class VoiceRecognitionController { private final AsyncRecognitionHandler asyncHandler; private final VoiceRecognitionService syncService; // 用于简单同步测试 private final ConcurrentHashMapString, CompletableFutureString taskMap new ConcurrentHashMap(); public VoiceRecognitionController(AsyncRecognitionHandler asyncHandler, VoiceRecognitionService syncService) { this.asyncHandler asyncHandler; this.syncService syncService; } /** * 同步识别接口仅用于测试或小文件 */ PostMapping(/recognize/sync) public ApiResponseString recognizeSync(RequestParam(file) MultipartFile file) { try { float[] samples AudioPreprocessor.preprocessAudio(file.getBytes(), file.getContentType()); String text syncService.recognize(samples); return ApiResponse.success(text); } catch (Exception e) { return ApiResponse.error(识别失败: e.getMessage()); } } /** * 异步识别接口生产环境推荐 */ PostMapping(/recognize/async) public ApiResponseString recognizeAsync(RequestParam(file) MultipartFile file) { try { float[] samples AudioPreprocessor.preprocessAudio(file.getBytes(), file.getContentType()); String taskId UUID.randomUUID().toString(); CompletableFutureString future asyncHandler.handleRecognitionAsync(samples); taskMap.put(taskId, future); // 立即返回任务ID客户端可凭此查询结果 return ApiResponse.success(taskId); } catch (Exception e) { return ApiResponse.error(任务提交失败: e.getMessage()); } } /** * 查询异步任务结果 */ GetMapping(/task/{taskId}) public ApiResponseString getTaskResult(PathVariable String taskId) { CompletableFutureString future taskMap.get(taskId); if (future null) { return ApiResponse.error(任务不存在或已过期); } if (future.isDone()) { try { String result future.get(); taskMap.remove(taskId); // 清理已完成任务 return ApiResponse.success(result); } catch (Exception e) { return ApiResponse.error(任务执行出错: e.getMessage()); } } else { return ApiResponse.success(null, 任务处理中); } } } // 简单的统一响应封装 class ApiResponseT { private int code; private String msg; private T data; // 构造方法、getter/setter、success/error静态方法省略... }这样一个支持文件上传、异步处理、结果查询的语音识别API骨架就完成了。前端可以先调用/api/v1/voice/recognize/async提交音频拿到taskId然后轮询/api/v1/voice/task/{taskId}获取最终结果。4. 识别结果持久化与业务集成识别出来的文本通常需要存下来供后续分析、检索或展示。我们结合Spring Data JPA将结果存入MySQL。4.1 数据实体与存储定义一个实体类记录每次识别的元数据和结果。import javax.persistence.*; import java.time.LocalDateTime; Entity Table(name voice_recognition_task) public class RecognitionTask { Id private String taskId; Lob Column(columnDefinition TEXT) private String originalFileName; Lob Column(columnDefinition TEXT) private String recognizedText; private String status; // PENDING, PROCESSING, SUCCESS, FAILED private LocalDateTime submitTime; private LocalDateTime finishTime; private Long durationMs; // 处理耗时 // 省略构造方法、getter/setter... }创建对应的Repository接口。import org.springframework.data.jpa.repository.JpaRepository; public interface RecognitionTaskRepository extends JpaRepositoryRecognitionTask, String { }然后修改我们的异步处理器在任务开始、成功、失败时更新数据库状态。// 在AsyncRecognitionHandler中注入Repository private final RecognitionTaskRepository taskRepository; Async(recognitionTaskExecutor) public CompletableFutureString handleRecognitionAsync(String taskId, float[] audioSamples) { // 1. 更新任务状态为PROCESSING RecognitionTask task taskRepository.findById(taskId).orElseThrow(); task.setStatus(PROCESSING); taskRepository.save(task); long startTime System.currentTimeMillis(); try { // 2. 执行识别 String text recognitionService.recognize(audioSamples); long endTime System.currentTimeMillis(); // 3. 更新任务为SUCCESS保存结果 task.setStatus(SUCCESS); task.setRecognizedText(text); task.setFinishTime(LocalDateTime.now()); task.setDurationMs(endTime - startTime); taskRepository.save(task); return CompletableFuture.completedFuture(text); } catch (Exception e) { // 4. 更新任务为FAILED task.setStatus(FAILED); taskRepository.save(task); CompletableFutureString future new CompletableFuture(); future.completeExceptionally(e); return future; } }控制器在提交异步任务时需要先在数据库创建一条PENDING状态的记录再将taskId返回给客户端。4.2 扩展思考性能与优化在实际高并发场景下还有几个点可以考虑音频文件存储上传的音频文件本身可能很大不应直接进入数据库。可以存到对象存储如MinIO、S3或文件服务器数据库中只存URL。模型批处理ONNX Runtime支持批量推理。可以设计一个队列累积一定数量的音频样本后一次性送入模型能显著提升GPU利用率如果使用GPU。结果缓存对于相同的音频文件可通过MD5判断可以直接返回缓存中的识别结果避免重复计算。监控与告警集成Micrometer等监控工具收集识别耗时、成功率、线程池状态等指标。5. 总结走完这一趟你会发现在SpringBoot里集成一个ONNX格式的AI模型并没有想象中那么复杂。核心就是三件事用正确的姿势加载模型和本地库、把业务数据音频转换成模型能吃的格式、设计好异步流程别让服务被堵死。这套方案的好处是整个技术栈是统一的Java生态你的团队不需要额外维护Python服务调试、部署、监控都走现有的成熟流程。SenseVoice-Small模型本身比较轻量在CPU上也能跑出不错的速度对于很多企业级并发量来说已经够用。当然每个实际项目都有自己的特殊需求。你可能需要处理更复杂的音频流、集成更专业的解码器、或者对识别结果进行二次加工。但有了这个基础框架后续的扩展就都有了落脚点。建议你先跑通这个最小可行版本感受一下从上传音频到看到文字结果的完整链路然后再根据实际情况一步步添砖加瓦。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。