选择合适的 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 包:
- 纯 Java 实现的 jar(跨平台通用)
- 包含 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;
import 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);
}
}
}


