跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaAIjava算法

Java 接入本地 TTS 模型 Sherpa-ONNX 实现离线文本转语音

综述由AI生成在 Java 项目中集成 Sherpa-ONNX 实现离线文本转语音(TTS)的方案。针对断网环境无法使用云端服务的需求,对比了 Piper、PaddleSpeech 等模型后,选定轻量且提供完整 Java API 的 Sherpa-ONNX。教程涵盖核心 JAR 包下载、Kokoro 多语言模型配置、音频格式转换工具类编写及 Spring Boot 接口实现。通过补充官方示例缺失的字典目录配置,解决了加载失败问题,并提供了完整的 Maven 依赖配置与前端测试页面,实现了高质量的本地化语音合成。

NodeJser发布于 2026/3/28更新于 2026/6/132 浏览
Java 接入本地 TTS 模型 Sherpa-ONNX 实现离线文本转语音

选择合适的 TTS 大模型

最近负责的一项业务中,需要实现'文字实时转语音'的功能。一开始使用的是阿里云智能语音合成服务,API 简单易用,接入后很快就能跑通。

然而,随着需求推进,业务要求在断网环境中也必须正常使用。一些业务现场无法连接外网,而云端 TTS 显然无法满足此要求。

于是开始调研可离线部署的 TTS 大模型。测试了多个可本地运行的模型,包括:

  • Piper
  • 百度飞桨 PaddleSpeech
  • Sherpa-ONNX TTS

最终的选型经历如下:

  • Piper:多次尝试仍无法成功运行,兼容问题较多
  • PaddleSpeech:模型体积较大,部署复杂,与 Java 生态结合不友好
  • Sherpa-ONNX:模型轻量、性能不错、部署简单,尤其是提供完整的 Java API,对 Java 开发者极度友好

综合评估后,Sherpa-ONNX 成为最佳选择。特别是在 Java 项目中,无需额外的 Python 服务,也无需多语言混合部署,直接在 Java 中调用即可实现高质量离线 TTS。

实战教程

官方文档参考: https://k2-fsa.github.io/sherpa/onnx/java-api/non-android-java.html

要跑通 Sherpa-ONNX 的 Java TTS 功能,需要下载两个核心 JAR 包:

  1. 纯 Java 实现的 jar(跨平台通用)
  2. 包含 C++ 底层 JNI 的 jar(按平台区分,如 win-x64、linux-x64 等)

选用的是 v1.12.10 版本,也可以根据需要选择更新版本。

下载好 JAR 后,就可以参考官网示例代码: https://github.com/k2-fsa/sherpa-onnx/blob/master/java-api-examples/NonStreamingTtsKokoroZhEn.java

这里需要特别注意一点:

官方示例中缺少一个必要的配置项:

.setDictDir(dictDir)

由于模型使用字典文件,因此必须手动补上,否则会出现加载失败的问题。

选择的预训练模型是:

kokoro-multi-lang-v1_0

模型介绍与下载地址可参考: https://k2-fsa.github.io/sherpa/onnx/tts/all/Chinese-English/kokoro-multi-lang-v1_0.html

下载预训练模型后,准备工作就完成了。接下来即可运行示例代码,实现离线文本转语音。

核心代码

项目结构

下面贴出基于官方示例改造后的核心代码:

AudioProcessingUtil
package com.example.demo;

 com.k2fsa.sherpa.onnx.GeneratedAudio;
 com.k2fsa.sherpa.onnx.OfflineTts;
 javax.sound.sampled.*;
 java.io.File;
 java.io.IOException;
 java.security.MessageDigest;
 java.security.NoSuchAlgorithmException;

   {
          ;

     {
        
            (OUTPUT_DIR);
         (!dir.exists()) {
            dir.mkdirs();
        }
    }

    
      String   Exception {
        
           md5(text);
        
           System.currentTimeMillis();
           tts.generate(text, speakerId, speed);
           System.currentTimeMillis();
        System.out.printf(, (stop - start) / );

        
           OUTPUT_DIR + textMd5 + ;
        audio.save(tempWaveFilename);

        
           convertAudioFormat( (tempWaveFilename), textMd5);

        
         (tempWaveFilename).delete();

         (convertedFile == ) {
              ();
        }
         convertedFile.getAbsolutePath();
    }

    
      File  {
           ;
           ;
           ;
         {
            
            sourceStream = AudioSystem.getAudioInputStream(sourceFile);
               sourceStream.getFormat();
            System.out.println(String.format(,
                    sourceFormat.getSampleRate(), sourceFormat.getChannels(), sourceFormat.getSampleSizeInBits()));

            
                (AudioFormat.Encoding.PCM_SIGNED,
                    , 
                    ,       
                    ,        
                    ,        
                    , 
                    );   

            
             (!AudioSystem.isConversionSupported(targetFormat, sourceFormat)) {
                System.out.println();
                
                    (AudioFormat.Encoding.PCM_SIGNED,
                        , sourceFormat.getSampleSizeInBits(), , 
                        sourceFormat.getSampleSizeInBits() /  * ,
                        , sourceFormat.isBigEndian());
                convertedStream = AudioSystem.getAudioInputStream(intermediateFormat, sourceStream);

                
                 (!AudioSystem.isConversionSupported(targetFormat, intermediateFormat)) {
                    System.err.println();
                     ;
                }
                convertedStream = AudioSystem.getAudioInputStream(targetFormat, convertedStream);
            }  {
                
                convertedStream = AudioSystem.getAudioInputStream(targetFormat, sourceStream);
            }

            
            outputFile =  (OUTPUT_DIR + textMd5 + );
            
            AudioSystem.write(convertedStream, AudioFileFormat.Type.WAVE, outputFile);
            System.out.println(String.format(, outputFile.getName()));
             outputFile;
        }  (UnsupportedAudioFileException e) {
            System.err.println( + e.getMessage());
             ;
        }  (IOException e) {
            System.err.println( + e.getMessage());
             ;
        }  (Exception e) {
            System.err.println( + e.getMessage());
             ;
        }  {
            
             {
                 (convertedStream != ) convertedStream.close();
                 (sourceStream != ) sourceStream.close();
            }  (IOException e) {
                System.err.println( + e.getMessage());
            }
        }
    }

    
      String  {
         {
               MessageDigest.getInstance();
            [] messageDigest = md.digest(input.getBytes());
                ();
             ( b : messageDigest) {
                   Integer.toHexString( & b);
                 (hex.length() == ) {
                    hexString.append();
                }
                hexString.append(hex);
            }
             hexString.toString();
        }  (NoSuchAlgorithmException e) {
              (e);
        }
    }
}

目录

  1. 选择合适的 TTS 大模型
  2. 实战教程
  3. 核心代码
  4. 项目结构
  5. AudioProcessingUtil
  6. DemoApplication
  7. TtsConfig
  8. TtsController
  9. TtsRequest
  10. TtsResponse
  11. index.html
  12. pom.xml
  • 💰 8折买阿里云服务器限时8折了解详情
import
import
import
import
import
import
import
public
class
AudioProcessingUtil
private
static
final
String
OUTPUT_DIR
=
"./tts-output/"
static
// 确保输出目录存在
File
dir
=
new
File
if
/** * 生成 TTS 音频并转换格式 * * @param tts TTS 引擎实例 * @param text 要转换的文本 * @param speakerId 说话人 ID * @param speed 语速 * @return 转换后的音频文件路径 * @throws Exception 处理过程中可能抛出的异常 */
public
static
generateAndConvertAudio
(OfflineTts tts, String text, int speakerId, float speed)
throws
// 生成文本的 MD5 作为文件名
String
textMd5
=
// 生成音频
long
start
=
GeneratedAudio
audio
=
long
stop
=
"-- elapsed : %.3f seconds\n"
1000.0f
// 保存为临时 WAV 文件
String
tempWaveFilename
=
".wav"
// 转换音频格式为 48kHz 双声道
File
convertedFile
=
new
File
// 删除临时文件
new
File
if
null
throw
new
RuntimeException
"音频转换失败"
return
/** * 转换音频格式为 48kHz 双声道 * * @param sourceFile 源文件 * @param textMd5 文本 MD5 值(用于生成输出文件名) * @return 转换后的文件 */
private
static
convertAudioFormat
(File sourceFile, String textMd5)
AudioInputStream
sourceStream
=
null
AudioInputStream
convertedStream
=
null
File
outputFile
=
null
try
// 读取源音频文件
AudioFormat
sourceFormat
=
"源音频格式 - 采样率:%sHz, 声道数:%s, 位深:%sbit"
// 定义目标格式:48000Hz, 双声道 (2), 16bit, signed, little-endian
AudioFormat
targetFormat
=
new
AudioFormat
48000.0F
// 采样率:48kHz
16
// 位深:16bit
2
// 声道数:2(立体声)
4
// 帧大小:2 声道 * 2 字节 (16bit) = 4 字节
48000.0F
// 帧率等于采样率
false
// little-endian
// 检查是否支持转换
if
"不支持直接转换,尝试分步转换"
// 先转换采样率和声道数
AudioFormat
intermediateFormat
=
new
AudioFormat
48000.0F
2
// 先转为双声道
8
2
48000.0F
// 再转换其他属性
if
"无法转换音频格式,使用原始文件"
return
null
else
// 直接转换
// 生成输出文件
new
File
"_48k.wav"
// 写入转换后的音频
"音频转换成功 - 采样率:48000Hz, 声道数:2, 文件:%s"
return
catch
"不支持的音频文件格式:"
return
null
catch
"音频文件读写失败:"
return
null
catch
"音频格式转换失败:"
return
null
finally
// 关闭流
try
if
null
if
null
catch
"关闭音频流失败:"
/** * 计算字符串的 MD5 值 */
private
static
md5
(String input)
try
MessageDigest
md
=
"MD5"
byte
StringBuilder
hexString
=
new
StringBuilder
for
byte
String
hex
=
0xff
if
1
'0'
return
catch
throw
new
RuntimeException
DemoApplication
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addResourceHandlers(ResourceHandlerRegistry registry) {
                // 确保静态资源能被正确访问
                registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
            }
        };
    }
}
TtsConfig
package com.example.demo;

import com.k2fsa.sherpa.onnx.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TtsConfig {
    @Bean
    public OfflineTts offlineTts() {
        String model = "./kokoro-multi-lang-v1_0/model.onnx";
        String voices = "./kokoro-multi-lang-v1_0/voices.bin";
        String tokens = "./kokoro-multi-lang-v1_0/tokens.txt";
        String dataDir = "./kokoro-multi-lang-v1_0/espeak-ng-data";
        String dictDir = "./kokoro-multi-lang-v1_0/dict";
        String lexicon = "./kokoro-multi-lang-v1_0/lexicon-us-en.txt,./kokoro-multi-lang-v1_0/lexicon-zh.txt";

        OfflineTtsKokoroModelConfig kokoroModelConfig = OfflineTtsKokoroModelConfig.builder()
                .setModel(model)
                .setVoices(voices)
                .setTokens(tokens)
                .setDataDir(dataDir)
                .setLexicon(lexicon)
                .setDictDir(dictDir)
                .build();

        OfflineTtsModelConfig modelConfig = OfflineTtsModelConfig.builder()
                .setKokoro(kokoroModelConfig)
                .setNumThreads(2)
                .setDebug(true)
                .build();

        OfflineTtsConfig config = OfflineTtsConfig.builder().setModel(modelConfig).build();
        return new OfflineTts(config);
    }
}
TtsController
package com.example.demo;

import com.k2fsa.sherpa.onnx.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/tts")
public class TtsController {
    @Autowired
    private OfflineTts tts;

    @PostMapping("/generate")
    public ResponseEntity<TtsResponse> generateTts(@RequestBody TtsRequest request) {
        try {
            String text = request.getText();
            int speakerId = request.getSpeakerId() != null ? request.getSpeakerId() : 47;
            float speed = request.getSpeed() != null ? request.getSpeed() : 1.0f;

            // 使用工具类处理 TTS 生成和转换
            String filePath = AudioProcessingUtil.generateAndConvertAudio(tts, text, speakerId, speed);
            return ResponseEntity.ok(new TtsResponse(filePath, "success"));
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.internalServerError().body(new TtsResponse(null, "处理失败:" + e.getMessage()));
        }
    }
}
TtsRequest
package com.example.demo;

public class TtsRequest {
    private String text;
    private Integer speakerId;
    private Float speed;

    // Getters and setters
    public String getText() { return text; }
    public void setText(String text) { this.text = text; }
    public Integer getSpeakerId() { return speakerId; }
    public void setSpeakerId(Integer speakerId) { this.speakerId = speakerId; }
    public Float getSpeed() { return speed; }
    public void setSpeed(Float speed) { this.speed = speed; }
}
TtsResponse
package com.example.demo;

public class TtsResponse {
    private String filePath;
    private String message;

    public TtsResponse(String filePath, String message) {
        this.filePath = filePath;
        this.message = message;
    }

    // Getters and setters
    public String getFilePath() { return filePath; }
    public void setFilePath(String filePath) { this.filePath = filePath; }
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
}
index.html
<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<title>TTS 服务测试</title>
<style>
body{font-family: Arial, sans-serif;max-width: 800px;margin: 0 auto;padding: 20px;}
.form-group{margin-bottom: 15px;}
label{display: block;margin-bottom: 5px;font-weight: bold;}
input, textarea, button{width: 100%;padding: 8px;box-sizing: border-box;}
textarea{height: 100px;resize: vertical;}
button{background-color: #007bff;color: white;border: none;cursor: pointer;font-size: 16px;}
button:hover{background-color: #0056b3;}
#result{margin-top: 20px;padding: 10px;background-color: #f8f9fa;border: 1px solid #dee2e6;}
.audio-container{margin-top: 10px;}
</style>
</head>
<body>
<h1>TTS 文本转语音服务</h1>
<div class="form-group">
<label for="text">输入文本:</label>
<textarea id="text" placeholder="请输入要转换为语音的文本">运维人员正在操作,请别操作电脑</textarea>
</div>
<div class="form-group">
<label for="speakerId">说话人 ID (0-52):</label>
<input type="number" id="speakerId" min="0" max="52" value="47">
</div>
<div class="form-group">
<label for="speed">语速 (0.5-2.0):</label>
<input type="number" id="speed" min="0.5" max="2.0" step="0.1" value="1.0">
</div>
<button onclick="generateTts()">生成语音</button>
<div id="result"></div>
<script>
async function generateTts(){
    const text = document.getElementById('text').value;
    const speakerId = parseInt(document.getElementById('speakerId').value);
    const speed = parseFloat(document.getElementById('speed').value);
    if(!text){alert('请输入文本');return;}
    const requestData = { text: text, speakerId: speakerId, speed: speed };
    const resultDiv = document.getElementById('result');
    resultDiv.innerHTML = '<p>正在生成语音...</p>';
    try{
        const response = await fetch('/api/tts/generate', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(requestData)});
        const data = await response.json();
        if(response.ok && data.){
            resultDiv. = ;
        }{
            resultDiv. = ;
        }
    }(error){
        .(, error);
        resultDiv. = ;
    }
}
</script>
</body>
</html>
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.0</version>
        <relativePath/>
    </parent>
    <groupId>com.weixin.wt</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>demo</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 添加 WebStarter 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 添加 Sherpa-onnx 核心依赖 -->
        <dependency>
            <groupId>com.k2fsa.sherpa.onnx</groupId>
            <artifactId>sherpa-onnx</artifactId>
            <version>1.12.10</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/sherpa-onnx-v1.12.10.jar</systemPath>
        </dependency>
        <!-- Windows 平台依赖 -->
        <dependency>
            <groupId>com.k2fsa.sherpa.onnx</groupId>
            <artifactId>sherpa-onnx-native-lib-win</artifactId>
            <version>1.12.10</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/sherpa-onnx-native-lib-win-x64-v1.12.10.jar</systemPath>
        </dependency>
        <!-- Linux 平台依赖 -->
        <dependency>
            <groupId>com.k2fsa.sherpa.onnx</groupId>
            <artifactId>sherpa-onnx-native-lib-linux</artifactId>
            <version>1.12.10</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/sherpa-onnx-native-lib-linux-x64-v1.12.10.jar</systemPath>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

至此,离线 TTS 模型部署成功。

filePath
innerHTML
` <p>语音生成成功!</p> <p>文件路径:${data.filePath}</p> <div> <audio controls> <source src="file://${data.filePath}" type="audio/wav"> 您的浏览器不支持音频播放。 </audio> </div> <p><a href="file://${data.filePath}" download>下载音频文件</a></p> `
else
innerHTML
`<p>语音生成失败:${data.message ||'未知错误'}</p>`
catch
console
error
'Error:'
innerHTML
'<p>发生错误,请查看控制台。</p>'
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • OpenClaw 为何爆火?AI Agent 从技术圈走向大众场景的真相
  • AI 领域最新技术动态与工具更新
  • FPGA 实现 OV5640 摄像头视频图像显示
  • ELF 解析:Linux 程序从编译到运行的全流程
  • Java HashMap 底层原理、源码设计与面试考点解析
  • 基于 FPGA 的 OV5640 摄像头视频采集与 VGA 显示实现
  • FLUX.1 AI 绘画镜像免配置部署:内置中文界面与双语 Web UI
  • 深信服超融合 HCI 核心技术解析:aSV、aSAN 与 aNET 协同架构
  • 【征文计划】玩转 Rokid JSAR:基于 Web 技术栈的 AR 开发环境搭建、核心 API 应用与 3D 时钟等创意项目全流程解析
  • RAG 系统实战:Langchain 框架与纯手搓方案对比
  • OpenClaw vs AutoGPT:AI Agent 核心能力、部署与落地场景实测
  • 前端面试高频场景题汇总:2026 最新风向
  • 从零开始搭建 Trae 的 Java 开发环境
  • C++ unordered_set 和 unordered_map 原理及哈希表模拟实现
  • 2026 年 RAG 技术演进:基于 DeepSeek 与 Neo4j 构建企业智能体系
  • OpenClaw 中构建专业 AI 角色的实战指南
  • 多模态 AI 桌面机器人 Kubee Robot 技术架构与应用解析
  • VRM4U插件在Unreal Engine中实现VRM模型高效集成方案
  • 自然语言处理技术与应用实践
  • AI 应用核心架构解析:从 Prompt 到 Agent 与 MCP

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online