mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-11 20:28:56 +00:00
【通用视觉】集成 OpenAI CLIP 模型,支持以图搜图、以文搜图、以图搜文等功能
【通用视觉】新增 YOLO 图像分类模型支持 【ASR/TTS】集成 Sherpa TTS(语音合成)与 ASR(语音识别)模块,支持中文、粤语、方言、英文等多种语言 【目标检测】优化视频目标检测功能
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.25</version>
|
||||
<version>1.0.26</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>speech</artifactId>
|
||||
@@ -48,10 +48,16 @@
|
||||
<artifactId>jave-core</artifactId>
|
||||
<version>3.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.gitee.dengwenjie</groupId>
|
||||
<artifactId>sherpa-onnx-java-api</artifactId>
|
||||
<version>1.12.14</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<version>1.0.25</version>
|
||||
<version>1.0.26</version>
|
||||
<name>speech</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -22,4 +22,6 @@ public class AsrModelConfig extends ModelConfig {
|
||||
* 依赖库目录
|
||||
*/
|
||||
private Path libPath;
|
||||
|
||||
private String modelName;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,31 @@ public enum AsrModelEnum {
|
||||
|
||||
WHISPER,
|
||||
|
||||
VOSK;
|
||||
VOSK,
|
||||
|
||||
SHERPA_PARAFORMER,
|
||||
|
||||
SHERPA_TRANSDUCER,
|
||||
|
||||
SHERPA_WHISPER,
|
||||
|
||||
SHERPA_FIREREDASR,
|
||||
|
||||
SHERPA_MOONSHINE,
|
||||
|
||||
SHERPA_NEMO,
|
||||
|
||||
SHERPA_SENSEVOICE,
|
||||
|
||||
SHERPA_DOLPHIN,
|
||||
|
||||
SHERPA_ZIPFORMERCTC,
|
||||
|
||||
SHERPA_WENETCTC,
|
||||
|
||||
SHERPA_CANARY,
|
||||
|
||||
SHERPA_TELESPEECH;
|
||||
|
||||
/**
|
||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
package cn.smartjavaai.speech.asr.factory;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.speech.asr.config.AsrModelConfig;
|
||||
import cn.smartjavaai.speech.asr.exception.AsrException;
|
||||
import cn.smartjavaai.speech.tts.config.TtsModelConfig;
|
||||
import cn.smartjavaai.speech.tts.exception.TtsException;
|
||||
import com.k2fsa.sherpa.onnx.*;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
public class SherpaOfflineAsrModelConfigFactory {
|
||||
|
||||
public static OfflineDolphinModelConfig createDolphinConfig(AsrModelConfig config) {
|
||||
if(StringUtils.isBlank(config.getModelName())){
|
||||
throw new AsrException("modelName is null");
|
||||
}
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();
|
||||
OfflineDolphinModelConfig dolphin = OfflineDolphinModelConfig.builder().setModel(model).build();
|
||||
return dolphin;
|
||||
}
|
||||
|
||||
public static OfflineFireRedAsrModelConfig createFireRedConfig(AsrModelConfig config) {
|
||||
List<File> decoderFiles = FileUtils.searchFiles(config.getModelPath(), "decoder", ".onnx",false);
|
||||
List<File> encoderFiles = FileUtils.searchFiles(config.getModelPath(), "encoder", ".onnx",false);
|
||||
if (CollectionUtils.isEmpty(decoderFiles)){
|
||||
throw new AsrException("decoder onnx not found");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(encoderFiles)){
|
||||
throw new AsrException("encoder onnx not found");
|
||||
}
|
||||
String encoder = encoderFiles.get(0).getAbsolutePath();
|
||||
String decoder = decoderFiles.get(0).getAbsolutePath();
|
||||
OfflineFireRedAsrModelConfig fireRedAsr =
|
||||
OfflineFireRedAsrModelConfig.builder().setEncoder(encoder).setDecoder(decoder).build();
|
||||
return fireRedAsr;
|
||||
}
|
||||
|
||||
public static OfflineMoonshineModelConfig createMoonshineConfig(AsrModelConfig config) {
|
||||
List<File> preprocessorFiles = FileUtils.searchFiles(config.getModelPath(), "preprocess", ".onnx",false);
|
||||
List<File> uncachedDecoderFiles = FileUtils.searchFiles(config.getModelPath(), "uncached_decode", ".onnx",false);
|
||||
List<File> cachedDecoderFiles = FileUtils.searchFiles(config.getModelPath(), "cached_decode", ".onnx",false);
|
||||
List<File> encoderFiles = FileUtils.searchFiles(config.getModelPath(), "encode", ".onnx",false);
|
||||
if (CollectionUtils.isEmpty(uncachedDecoderFiles)){
|
||||
throw new AsrException("uncached_decode onnx not found");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(cachedDecoderFiles)){
|
||||
throw new AsrException("cached_decode onnx not found");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(encoderFiles)){
|
||||
throw new AsrException("encoder onnx not found");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(preprocessorFiles)){
|
||||
throw new AsrException("preprocess onnx not found");
|
||||
}
|
||||
String encoder = encoderFiles.get(0).getAbsolutePath();
|
||||
String preprocessor = preprocessorFiles.get(0).getAbsolutePath();
|
||||
String cachedDecoder = cachedDecoderFiles.get(0).getAbsolutePath();
|
||||
String uncachedDecoder = uncachedDecoderFiles.get(0).getAbsolutePath();
|
||||
OfflineMoonshineModelConfig moonshine =
|
||||
OfflineMoonshineModelConfig.builder()
|
||||
.setPreprocessor(preprocessor)
|
||||
.setEncoder(encoder)
|
||||
.setUncachedDecoder(uncachedDecoder)
|
||||
.setCachedDecoder(cachedDecoder)
|
||||
.build();
|
||||
return moonshine;
|
||||
}
|
||||
|
||||
public static OfflineNemoEncDecCtcModelConfig createNemoConfig(AsrModelConfig config) {
|
||||
if(StringUtils.isBlank(config.getModelName())){
|
||||
throw new AsrException("modelName is null");
|
||||
}
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();
|
||||
OfflineNemoEncDecCtcModelConfig modelConfig = OfflineNemoEncDecCtcModelConfig.builder().setModel(model).build();
|
||||
return modelConfig;
|
||||
}
|
||||
|
||||
public static OfflineSenseVoiceModelConfig createSenseVoiceConfig(AsrModelConfig config) {
|
||||
if(StringUtils.isBlank(config.getModelName())){
|
||||
throw new AsrException("modelName is null");
|
||||
}
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();
|
||||
OfflineSenseVoiceModelConfig senseVoice =
|
||||
OfflineSenseVoiceModelConfig.builder().setModel(model).build();
|
||||
return senseVoice;
|
||||
}
|
||||
|
||||
public static OfflineTransducerModelConfig createTransducerConfig(AsrModelConfig config) {
|
||||
List<File> decoderFiles = FileUtils.searchFiles(config.getModelPath(), "decoder", ".onnx",false);
|
||||
List<File> encoderFiles = FileUtils.searchFiles(config.getModelPath(), "encoder", "int8.onnx",false);
|
||||
List<File> joinerFiles = FileUtils.searchFiles(config.getModelPath(), "joiner", ".onnx",false);
|
||||
if (CollectionUtils.isEmpty(decoderFiles)){
|
||||
throw new AsrException("decoder onnx not found");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(encoderFiles)){
|
||||
throw new AsrException("encoder onnx not found");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(joinerFiles)){
|
||||
throw new AsrException("joiner onnx not found");
|
||||
}
|
||||
String encoder = encoderFiles.get(0).getAbsolutePath();
|
||||
String decoder = decoderFiles.get(0).getAbsolutePath();
|
||||
String joiner = joinerFiles.get(0).getAbsolutePath();
|
||||
OfflineTransducerModelConfig transducer =
|
||||
OfflineTransducerModelConfig.builder()
|
||||
.setEncoder(encoder)
|
||||
.setDecoder(decoder)
|
||||
.setJoiner(joiner)
|
||||
.build();
|
||||
return transducer;
|
||||
}
|
||||
|
||||
public static OfflineParaformerModelConfig createParaformerConfig(AsrModelConfig config) {
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();
|
||||
OfflineParaformerModelConfig modelConfig = OfflineParaformerModelConfig.builder().setModel(model).build();
|
||||
return modelConfig;
|
||||
}
|
||||
|
||||
public static OfflineWenetCtcModelConfig createWenetCtcConfig(AsrModelConfig config) {
|
||||
if(StringUtils.isBlank(config.getModelName())){
|
||||
throw new AsrException("modelName is null");
|
||||
}
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();
|
||||
OfflineWenetCtcModelConfig wenetCtc =
|
||||
OfflineWenetCtcModelConfig.builder().setModel(model).build();
|
||||
return wenetCtc;
|
||||
}
|
||||
|
||||
public static OfflineCanaryModelConfig createCanaryConfig(AsrModelConfig config) {
|
||||
List<File> decoderFiles = FileUtils.searchFiles(config.getModelPath(), "decoder", ".onnx",false);
|
||||
List<File> encoderFiles = FileUtils.searchFiles(config.getModelPath(), "encoder", ".onnx",false);
|
||||
if (CollectionUtils.isEmpty(decoderFiles)){
|
||||
throw new AsrException("decoder onnx not found");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(encoderFiles)){
|
||||
throw new AsrException("encoder onnx not found");
|
||||
}
|
||||
String encoder = encoderFiles.get(0).getAbsolutePath();
|
||||
String decoder = decoderFiles.get(0).getAbsolutePath();
|
||||
OfflineCanaryModelConfig canary =
|
||||
OfflineCanaryModelConfig.builder()
|
||||
.setEncoder(encoder)
|
||||
.setDecoder(decoder)
|
||||
.setSrcLang("en")
|
||||
.setTgtLang("en")
|
||||
.setUsePnc(true)
|
||||
.build();
|
||||
return canary;
|
||||
}
|
||||
|
||||
public static OfflineWhisperModelConfig createWhisperConfig(AsrModelConfig config) {
|
||||
//是否使用量化模型
|
||||
boolean useInt8 = config.getCustomParam("useInt8", Boolean.class, false);
|
||||
String extension = useInt8 ? "int8.onnx" : "onnx";
|
||||
List<File> decoderFiles = FileUtils.searchFiles(config.getModelPath(), "decoder", extension,false);
|
||||
List<File> encoderFiles = FileUtils.searchFiles(config.getModelPath(), "encoder", extension,false);
|
||||
if (CollectionUtils.isEmpty(decoderFiles)){
|
||||
throw new AsrException("decoder onnx not found");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(encoderFiles)){
|
||||
throw new AsrException("encoder onnx not found");
|
||||
}
|
||||
String encoder = encoderFiles.get(0).getAbsolutePath();
|
||||
String decoder = decoderFiles.get(0).getAbsolutePath();
|
||||
OfflineWhisperModelConfig fireRedAsr =
|
||||
OfflineWhisperModelConfig.builder().setEncoder(encoder).setDecoder(decoder).build();
|
||||
return fireRedAsr;
|
||||
}
|
||||
|
||||
public static OfflineZipformerCtcModelConfig createZipformerCtcConfig(AsrModelConfig config) {
|
||||
if(StringUtils.isBlank(config.getModelName())){
|
||||
throw new AsrException("modelName is null");
|
||||
}
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();
|
||||
OfflineZipformerCtcModelConfig zipformerCtc =
|
||||
OfflineZipformerCtcModelConfig.builder().setModel(model).build();
|
||||
return zipformerCtc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模型配置
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public static OfflineRecognizerConfig createConfig(AsrModelConfig config) {
|
||||
OfflineModelConfig modelConfig = null;
|
||||
int numThreads = config.getCustomParam("numThreads", Integer.class, 1);
|
||||
boolean debug = config.getCustomParam("debug", Boolean.class, true);
|
||||
List<File> tokensFiles = FileUtils.findFilesWithSuffix(new File(config.getModelPath()), "tokens.txt", false);
|
||||
String tokens = CollectionUtils.isEmpty(tokensFiles) ? "" : tokensFiles.get(0).getAbsolutePath();
|
||||
String provider = config.getCustomParam("provider", String.class, "cpu");
|
||||
switch (config.getModelEnum()){
|
||||
case SHERPA_PARAFORMER:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setParaformer(createParaformerConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_TRANSDUCER:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setTransducer(createTransducerConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_WHISPER:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setWhisper(createWhisperConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_FIREREDASR:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setFireRedAsr(createFireRedConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_MOONSHINE:
|
||||
numThreads = config.getCustomParam("numThreads", Integer.class, 2);
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setMoonshine(createMoonshineConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_NEMO:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setNemo(createNemoConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setProvider(provider)
|
||||
.setDebug(debug)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_SENSEVOICE:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setSenseVoice(createSenseVoiceConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_DOLPHIN:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setDolphin(createDolphinConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_ZIPFORMERCTC:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setZipformerCtc(createZipformerCtcConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_WENETCTC:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setWenetCtc(createWenetCtcConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_CANARY:
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setCanary(createCanaryConfig(config))
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_TELESPEECH:
|
||||
if(StringUtils.isBlank(config.getModelName())){
|
||||
throw new AsrException("modelName is null");
|
||||
}
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();
|
||||
modelConfig =
|
||||
OfflineModelConfig.builder()
|
||||
.setTeleSpeech(model)
|
||||
.setTokens(tokens)
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setModelType("telespeech_ctc")
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
}
|
||||
OfflineRecognizerConfig offlineRecognizerConfig = OfflineRecognizerConfig.builder()
|
||||
.setOfflineModelConfig(modelConfig)
|
||||
.setDecodingMethod("greedy_search")
|
||||
.build();
|
||||
return offlineRecognizerConfig;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.speech.asr.config.AsrModelConfig;
|
||||
import cn.smartjavaai.speech.asr.enums.AsrModelEnum;
|
||||
import cn.smartjavaai.speech.asr.exception.AsrException;
|
||||
import cn.smartjavaai.speech.asr.model.SherpaRecognizer;
|
||||
import cn.smartjavaai.speech.asr.model.SpeechRecognizer;
|
||||
import cn.smartjavaai.speech.asr.model.VoskRecognizer;
|
||||
import cn.smartjavaai.speech.asr.model.WhisperRecognizer;
|
||||
@@ -68,7 +69,7 @@ public class SpeechRecognizerFactory {
|
||||
throw new AsrException("未配置语音识别模型枚举");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum(), k -> {
|
||||
return createFaceModel(config);
|
||||
return createModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ public class SpeechRecognizerFactory {
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private SpeechRecognizer createFaceModel(AsrModelConfig config) {
|
||||
private SpeechRecognizer createModel(AsrModelConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum());
|
||||
if(clazz == null){
|
||||
throw new AsrException("Unsupported model");
|
||||
@@ -98,6 +99,18 @@ public class SpeechRecognizerFactory {
|
||||
static {
|
||||
registerModel(AsrModelEnum.WHISPER, WhisperRecognizer.class);
|
||||
registerModel(AsrModelEnum.VOSK, VoskRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_PARAFORMER, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_TRANSDUCER, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_WHISPER, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_FIREREDASR, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_MOONSHINE, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_NEMO, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_SENSEVOICE, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_DOLPHIN, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_ZIPFORMERCTC, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_WENETCTC, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_CANARY, SherpaRecognizer.class);
|
||||
registerModel(AsrModelEnum.SHERPA_TELESPEECH, SherpaRecognizer.class);
|
||||
log.debug("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package cn.smartjavaai.speech.asr.model;
|
||||
|
||||
import ai.djl.modality.audio.Audio;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.speech.asr.config.AsrModelConfig;
|
||||
import cn.smartjavaai.speech.asr.entity.AsrResult;
|
||||
import cn.smartjavaai.speech.asr.entity.RecParams;
|
||||
import cn.smartjavaai.speech.asr.factory.SherpaOfflineAsrModelConfigFactory;
|
||||
import cn.smartjavaai.speech.tts.exception.TtsException;
|
||||
import cn.smartjavaai.speech.tts.factory.SherpaOfflineTtsModelConfigFactory;
|
||||
import com.k2fsa.sherpa.onnx.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class SherpaRecognizer implements SpeechRecognizer{
|
||||
|
||||
private AsrModelConfig config;
|
||||
private OfflineRecognizer recognizer;
|
||||
|
||||
@Override
|
||||
public void loadModel(AsrModelConfig config) {
|
||||
this.config = config;
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new TtsException("modelPath is null");
|
||||
}
|
||||
Path testModelPath = Paths.get(config.getModelPath());
|
||||
if(!testModelPath.toFile().exists()){
|
||||
throw new TtsException("modelPath does not exist: " + testModelPath.toAbsolutePath());
|
||||
}
|
||||
if(Objects.isNull(config.getLibPath())){
|
||||
throw new TtsException("libPath is null");
|
||||
}
|
||||
if(!config.getLibPath().toFile().exists()){
|
||||
throw new TtsException("libPath does not exist: " + testModelPath.toAbsolutePath());
|
||||
}
|
||||
try {
|
||||
//加载依赖库
|
||||
System.setProperty("sherpa_onnx.native.path",config.getLibPath().toAbsolutePath().toString());
|
||||
OfflineRecognizerConfig offlineRecognizerConfig = SherpaOfflineAsrModelConfigFactory.createConfig(config);
|
||||
recognizer = new OfflineRecognizer(offlineRecognizerConfig);
|
||||
log.debug("Sherpa tts init success");
|
||||
} catch (Exception e) {
|
||||
throw new TtsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(Audio audio) {
|
||||
if(recognizer == null){
|
||||
throw new TtsException("模型未初始化");
|
||||
}
|
||||
OfflineStream stream = recognizer.createStream();
|
||||
stream.acceptWaveform(audio.getData(), (int)audio.getSampleRate());
|
||||
recognizer.decode(stream);
|
||||
String text = recognizer.getResult(stream).getText();
|
||||
stream.release();
|
||||
return R.ok(new AsrResult(text));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(String audioPath) {
|
||||
if(recognizer == null){
|
||||
throw new TtsException("模型未初始化");
|
||||
}
|
||||
OfflineStream stream = recognizer.createStream();
|
||||
WaveReader reader = new WaveReader(audioPath);
|
||||
stream.acceptWaveform(reader.getSamples(), reader.getSampleRate());
|
||||
recognizer.decode(stream);
|
||||
String text = recognizer.getResult(stream).getText();
|
||||
stream.release();
|
||||
return R.ok(new AsrResult(text));
|
||||
}
|
||||
|
||||
private boolean fromFactory = false;
|
||||
|
||||
@Override
|
||||
public void setFromFactory(boolean fromFactory) {
|
||||
this.fromFactory = fromFactory;
|
||||
}
|
||||
public boolean isFromFactory() {
|
||||
return fromFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if(recognizer != null){
|
||||
recognizer.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.smartjavaai.speech.asr.model;
|
||||
|
||||
import ai.djl.modality.audio.Audio;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.speech.asr.config.AsrModelConfig;
|
||||
import cn.smartjavaai.speech.asr.entity.AsrResult;
|
||||
@@ -45,6 +46,14 @@ public interface SpeechRecognizer extends AutoCloseable{
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default R<AsrResult> recognize(Audio audio){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default R<AsrResult> recognize(Audio audio, RecParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default void setFromFactory(boolean fromFactory){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.smartjavaai.speech.tts.config;
|
||||
|
||||
import cn.smartjavaai.common.config.ModelConfig;
|
||||
import cn.smartjavaai.speech.tts.enums.TtsModelEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/10/14
|
||||
*/
|
||||
@Data
|
||||
public class TtsModelConfig extends ModelConfig {
|
||||
|
||||
private TtsModelEnum modelEnum;
|
||||
|
||||
private String modelPath;
|
||||
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 依赖库目录
|
||||
*/
|
||||
private Path libPath;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.smartjavaai.speech.tts.entity;
|
||||
|
||||
import com.k2fsa.sherpa.onnx.OfflineTtsCallback;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public class SherpaTtsParams extends TtsParams{
|
||||
|
||||
private OfflineTtsCallback callback;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.smartjavaai.speech.tts.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* tts参数
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public abstract class TtsParams {
|
||||
|
||||
/**
|
||||
* 发音人
|
||||
*/
|
||||
private int speakerId;
|
||||
|
||||
/**
|
||||
* 语速
|
||||
*/
|
||||
private float speed;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cn.smartjavaai.speech.tts.enums;
|
||||
|
||||
/**
|
||||
* tts模型枚举
|
||||
* @author dwj
|
||||
*/
|
||||
public enum TtsModelEnum {
|
||||
|
||||
SHERPA_VITS("vits"),
|
||||
SHERPA_MATCHA("matcha"),
|
||||
SHERPA_KOKORO("kokoro"),
|
||||
SHERPA_KITTEN("kitten");
|
||||
|
||||
TtsModelEnum(String engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型引擎
|
||||
*/
|
||||
private final String engine;
|
||||
|
||||
public String getEngine() {
|
||||
return engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||
*/
|
||||
public static TtsModelEnum fromName(String name) {
|
||||
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||
for (TtsModelEnum model : values()) {
|
||||
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知模型名称: " + name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.smartjavaai.speech.tts.exception;
|
||||
|
||||
/**
|
||||
* tts异常
|
||||
* @author dwj
|
||||
*/
|
||||
public class TtsException extends RuntimeException{
|
||||
|
||||
public TtsException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public TtsException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
public TtsException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public TtsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public TtsException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package cn.smartjavaai.speech.tts.factory;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.speech.tts.exception.TtsException;
|
||||
import com.k2fsa.sherpa.onnx.*;
|
||||
import cn.smartjavaai.speech.tts.config.TtsModelConfig;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/10/14
|
||||
*/
|
||||
public class SherpaOfflineTtsModelConfigFactory {
|
||||
|
||||
/**
|
||||
* 创建Vits模型配置
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public static OfflineTtsVitsModelConfig createVitsConfig(TtsModelConfig config) {
|
||||
List<File> tokensFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "tokens.txt", false);
|
||||
List<File> lexiconFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "lexicon.txt", false);
|
||||
List<File> dictFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "dict", false);
|
||||
List<File> dataDirFiles = FileUtils.findFilesWithSuffix(new File(config.getModelPath()), "-data", false);
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();;
|
||||
String tokens = CollectionUtils.isEmpty(tokensFiles) ? "" : tokensFiles.get(0).getAbsolutePath();
|
||||
String lexicon = CollectionUtils.isEmpty(lexiconFiles) ? "" : lexiconFiles.get(0).getAbsolutePath();
|
||||
String dictPath = CollectionUtils.isEmpty(dictFiles) ? "" : dictFiles.get(0).getAbsolutePath();
|
||||
String dataDir = CollectionUtils.isEmpty(dataDirFiles) ? "" : dataDirFiles.get(0).getAbsolutePath();
|
||||
float lengthScale = config.getCustomParam("lengthScale", Float.class, 1f);
|
||||
float noiseScale = config.getCustomParam("noiseScale", Float.class, 0.667F);
|
||||
float noiseScaleW = config.getCustomParam("noiseScaleW", Float.class, 0.8f);
|
||||
OfflineTtsVitsModelConfig vitsModelConfig =
|
||||
OfflineTtsVitsModelConfig.builder()
|
||||
.setModel(model)
|
||||
.setTokens(tokens)
|
||||
.setLexicon(lexicon)
|
||||
.setDictDir(dictPath)
|
||||
.setDataDir(dataDir)
|
||||
.setLengthScale(lengthScale)
|
||||
.setNoiseScale(noiseScale)
|
||||
.setNoiseScaleW(noiseScaleW)
|
||||
.build();
|
||||
return vitsModelConfig;
|
||||
}
|
||||
|
||||
public static OfflineTtsMatchaModelConfig createMatchaConfig(TtsModelConfig config) {
|
||||
String vocoder = config.getCustomParam("vocoder", String.class);
|
||||
if(StringUtils.isBlank(vocoder)){
|
||||
throw new TtsException("vocoder is null");
|
||||
}
|
||||
List<File> tokensFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "tokens.txt", false);
|
||||
List<File> lexiconFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "lexicon.txt", false);
|
||||
List<File> dictFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "dict", false);
|
||||
List<File> dataDirFiles = FileUtils.findFilesWithSuffix(new File(config.getModelPath()), "-data", false);
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();;
|
||||
String tokens = CollectionUtils.isEmpty(tokensFiles) ? "" : tokensFiles.get(0).getAbsolutePath();
|
||||
String lexicon = CollectionUtils.isEmpty(lexiconFiles) ? "" : lexiconFiles.get(0).getAbsolutePath();
|
||||
String dictPath = CollectionUtils.isEmpty(dictFiles) ? "" : dictFiles.get(0).getAbsolutePath();
|
||||
String dataDir = CollectionUtils.isEmpty(dataDirFiles) ? "" : dataDirFiles.get(0).getAbsolutePath();
|
||||
float lengthScale = config.getCustomParam("lengthScale", Float.class, 1f);
|
||||
float noiseScale = config.getCustomParam("noiseScale", Float.class, 1f);
|
||||
OfflineTtsMatchaModelConfig vitsModelConfig =
|
||||
OfflineTtsMatchaModelConfig.builder()
|
||||
.setAcousticModel(model)
|
||||
.setTokens(tokens)
|
||||
.setLexicon(lexicon)
|
||||
.setDictDir(dictPath)
|
||||
.setDataDir(dataDir)
|
||||
.setVocoder(vocoder)
|
||||
.setLengthScale(lengthScale)
|
||||
.setNoiseScale(noiseScale)
|
||||
.build();
|
||||
return vitsModelConfig;
|
||||
}
|
||||
|
||||
public static OfflineTtsKittenModelConfig createKittenConfig(TtsModelConfig config) {
|
||||
List<File> tokensFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "tokens.txt", false);
|
||||
List<File> voicesFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "voices.bin", false);
|
||||
List<File> dataDirFiles = FileUtils.findFilesWithSuffix(new File(config.getModelPath()), "-data", false);
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();;
|
||||
String tokens = CollectionUtils.isEmpty(tokensFiles) ? "" : tokensFiles.get(0).getAbsolutePath();
|
||||
String voices = CollectionUtils.isEmpty(voicesFiles) ? "" : voicesFiles.get(0).getAbsolutePath();
|
||||
String dataDir = CollectionUtils.isEmpty(dataDirFiles) ? "" : dataDirFiles.get(0).getAbsolutePath();
|
||||
float lengthScale = config.getCustomParam("lengthScale", Float.class, 1f);
|
||||
OfflineTtsKittenModelConfig vitsModelConfig =
|
||||
OfflineTtsKittenModelConfig.builder()
|
||||
.setModel(model)
|
||||
.setTokens(tokens)
|
||||
.setVoices(voices)
|
||||
.setDataDir(dataDir)
|
||||
.setLengthScale(lengthScale)
|
||||
.build();
|
||||
return vitsModelConfig;
|
||||
}
|
||||
|
||||
public static OfflineTtsKokoroModelConfig createKokoroConfig(TtsModelConfig config) {
|
||||
List<File> tokensFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "tokens.txt", false);
|
||||
List<File> lexiconFiles = FileUtils.searchFiles(config.getModelPath(), "lexicon", ".txt",false);
|
||||
List<File> dictFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "dict", false);
|
||||
List<File> dataDirFiles = FileUtils.findFilesWithSuffix(new File(config.getModelPath()), "-data", false);
|
||||
List<File> voicesFiles = FileUtils.findFilesByName(new File(config.getModelPath()), "voices.bin", false);
|
||||
String model = config.getModelPath() + File.separator + config.getModelName();;
|
||||
String tokens = CollectionUtils.isEmpty(tokensFiles) ? "" : tokensFiles.get(0).getAbsolutePath();
|
||||
String lexicon = CollectionUtils.isEmpty(lexiconFiles) ? "" : FileUtils.joinAbsolutePaths(lexiconFiles);
|
||||
String dictPath = CollectionUtils.isEmpty(dictFiles) ? "" : dictFiles.get(0).getAbsolutePath();
|
||||
String dataDir = CollectionUtils.isEmpty(dataDirFiles) ? "" : dataDirFiles.get(0).getAbsolutePath();
|
||||
String voices = CollectionUtils.isEmpty(voicesFiles) ? "" : voicesFiles.get(0).getAbsolutePath();
|
||||
float lengthScale = config.getCustomParam("lengthScale", Float.class, 1f);
|
||||
OfflineTtsKokoroModelConfig vitsModelConfig =
|
||||
OfflineTtsKokoroModelConfig.builder()
|
||||
.setModel(model)
|
||||
.setTokens(tokens)
|
||||
.setLexicon(lexicon)
|
||||
.setDictDir(dictPath)
|
||||
.setDataDir(dataDir)
|
||||
.setVoices(voices)
|
||||
.setLengthScale(lengthScale)
|
||||
.build();
|
||||
return vitsModelConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模型配置
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public static OfflineTtsConfig createConfig(TtsModelConfig config) {
|
||||
OfflineTtsModelConfig modelConfig = null;
|
||||
int numThreads = config.getCustomParam("numThreads", Integer.class, 1);
|
||||
boolean debug = config.getCustomParam("debug", Boolean.class, true);
|
||||
String provider = config.getCustomParam("provider", String.class, "cpu");
|
||||
String ruleFsts = "";
|
||||
switch (config.getModelEnum()){
|
||||
case SHERPA_VITS:
|
||||
modelConfig =
|
||||
OfflineTtsModelConfig.builder()
|
||||
.setVits(createVitsConfig(config))
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_MATCHA:
|
||||
modelConfig =
|
||||
OfflineTtsModelConfig.builder()
|
||||
.setMatcha(createMatchaConfig(config))
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_KITTEN:
|
||||
modelConfig =
|
||||
OfflineTtsModelConfig.builder()
|
||||
.setKitten(createKittenConfig(config))
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
case SHERPA_KOKORO:
|
||||
numThreads = config.getCustomParam("numThreads", Integer.class, 2);
|
||||
modelConfig =
|
||||
OfflineTtsModelConfig.builder()
|
||||
.setKokoro(createKokoroConfig(config))
|
||||
.setNumThreads(numThreads)
|
||||
.setDebug(debug)
|
||||
.setProvider(provider)
|
||||
.build();
|
||||
break;
|
||||
}
|
||||
List<File> ruleFstFiles = FileUtils.findFilesWithSuffix(new File(config.getModelPath()), ".fst", false);
|
||||
ruleFsts = CollectionUtils.isEmpty(ruleFstFiles) ? "" : FileUtils.joinAbsolutePaths(ruleFstFiles);
|
||||
OfflineTtsConfig offlineTtsConfig =
|
||||
OfflineTtsConfig.builder().setModel(modelConfig).setRuleFsts(ruleFsts).build();
|
||||
return offlineTtsConfig;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package cn.smartjavaai.speech.tts.factory;
|
||||
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.speech.asr.exception.AsrException;
|
||||
import cn.smartjavaai.speech.asr.model.VoskRecognizer;
|
||||
import cn.smartjavaai.speech.asr.model.WhisperRecognizer;
|
||||
import cn.smartjavaai.speech.tts.config.TtsModelConfig;
|
||||
import cn.smartjavaai.speech.tts.enums.TtsModelEnum;
|
||||
import cn.smartjavaai.speech.tts.model.SherpaTtsModel;
|
||||
import cn.smartjavaai.speech.tts.model.TtsModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 语音合成模型工厂
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class TtsModelFactory {
|
||||
|
||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||
private static volatile TtsModelFactory instance;
|
||||
|
||||
/**
|
||||
* 模型缓存
|
||||
*/
|
||||
private static final ConcurrentHashMap<TtsModelEnum, TtsModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 模型注册表
|
||||
*/
|
||||
private static final Map<TtsModelEnum, Class<? extends TtsModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static TtsModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (TtsModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new TtsModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册模型
|
||||
* @param expressionModelEnum
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerModel(TtsModelEnum expressionModelEnum, Class<? extends TtsModel> clazz) {
|
||||
registry.put(expressionModelEnum, clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public TtsModel getModel(TtsModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new AsrException("未配置语音识别模型枚举");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum(), k -> {
|
||||
return createModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private TtsModel createModel(TtsModelConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum());
|
||||
if(clazz == null){
|
||||
throw new AsrException("Unsupported model");
|
||||
}
|
||||
TtsModel model = null;
|
||||
try {
|
||||
model = (TtsModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
model.setFromFactory(true);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerModel(TtsModelEnum.SHERPA_KOKORO, SherpaTtsModel.class);
|
||||
registerModel(TtsModelEnum.SHERPA_KITTEN, SherpaTtsModel.class);
|
||||
registerModel(TtsModelEnum.SHERPA_MATCHA, SherpaTtsModel.class);
|
||||
registerModel(TtsModelEnum.SHERPA_VITS, SherpaTtsModel.class);
|
||||
log.debug("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有已加载的模型
|
||||
*/
|
||||
public void closeAll() {
|
||||
modelMap.values().forEach(model -> {
|
||||
try {
|
||||
model.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
modelMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除缓存的模型
|
||||
* @param modelEnum
|
||||
*/
|
||||
public static void removeFromCache(TtsModelEnum modelEnum) {
|
||||
modelMap.remove(modelEnum);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package cn.smartjavaai.speech.tts.model;
|
||||
|
||||
import ai.djl.modality.audio.Audio;
|
||||
import ai.djl.modality.audio.AudioFactory;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.speech.asr.exception.AsrException;
|
||||
import cn.smartjavaai.speech.asr.pool.WhisperStatePool;
|
||||
import cn.smartjavaai.speech.tts.config.TtsModelConfig;
|
||||
import cn.smartjavaai.speech.tts.entity.SherpaTtsParams;
|
||||
import cn.smartjavaai.speech.tts.entity.TtsParams;
|
||||
import cn.smartjavaai.speech.tts.exception.TtsException;
|
||||
import cn.smartjavaai.speech.tts.factory.SherpaOfflineTtsModelConfigFactory;
|
||||
import com.k2fsa.sherpa.onnx.GeneratedAudio;
|
||||
import com.k2fsa.sherpa.onnx.OfflineTts;
|
||||
import com.k2fsa.sherpa.onnx.OfflineTtsCallback;
|
||||
import com.k2fsa.sherpa.onnx.OfflineTtsConfig;
|
||||
import io.github.givimad.whisperjni.WhisperJNI;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class SherpaTtsModel implements TtsModel{
|
||||
|
||||
private TtsModelConfig config;
|
||||
|
||||
private OfflineTts offlineTts;
|
||||
|
||||
|
||||
@Override
|
||||
public void loadModel(TtsModelConfig config) {
|
||||
this.config = config;
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new TtsException("modelPath is null");
|
||||
}
|
||||
if(StringUtils.isBlank(config.getModelName())){
|
||||
throw new TtsException("modelName is null");
|
||||
}
|
||||
Path testModelPath = Paths.get(config.getModelPath());
|
||||
if(!testModelPath.toFile().exists()){
|
||||
throw new TtsException("modelPath does not exist: " + testModelPath.toAbsolutePath());
|
||||
}
|
||||
if(Objects.isNull(config.getLibPath())){
|
||||
throw new TtsException("libPath is null");
|
||||
}
|
||||
if(!config.getLibPath().toFile().exists()){
|
||||
throw new TtsException("libPath does not exist: " + testModelPath.toAbsolutePath());
|
||||
}
|
||||
try {
|
||||
//加载依赖库
|
||||
System.setProperty("sherpa_onnx.native.path",config.getLibPath().toAbsolutePath().toString());
|
||||
OfflineTtsConfig offlineTtsConfig = SherpaOfflineTtsModelConfigFactory.createConfig(config);
|
||||
offlineTts = new OfflineTts(offlineTtsConfig);
|
||||
log.debug("Sherpa tts init success");
|
||||
} catch (Exception e) {
|
||||
throw new TtsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private GeneratedAudio generateCore(String text, TtsParams params) {
|
||||
if(offlineTts == null){
|
||||
throw new TtsException("模型未初始化");
|
||||
}
|
||||
SherpaTtsParams sherpaTtsParams =null;
|
||||
if(params == null){
|
||||
sherpaTtsParams = new SherpaTtsParams();
|
||||
}else{
|
||||
if(params instanceof SherpaTtsParams){
|
||||
sherpaTtsParams = (SherpaTtsParams) params;
|
||||
}else{
|
||||
throw new TtsException("params参数类型不是 SherpaTtsParams");
|
||||
}
|
||||
}
|
||||
try {
|
||||
int sid = 100;
|
||||
float speed = 1.0f;
|
||||
if(params != null){
|
||||
sid = sherpaTtsParams.getSpeakerId() > 0 ? sherpaTtsParams.getSpeakerId() : 100;
|
||||
speed = sherpaTtsParams.getSpeed() > 0.0f ? sherpaTtsParams.getSpeed() : 1.0f;
|
||||
}
|
||||
GeneratedAudio audio = null;
|
||||
if(sherpaTtsParams.getCallback() != null){
|
||||
audio = offlineTts.generateWithCallback(text, sid, speed, sherpaTtsParams.getCallback());
|
||||
}else{
|
||||
audio = offlineTts.generate(text, sid, speed);
|
||||
}
|
||||
float audioDuration = audio.getSamples().length / (float) audio.getSampleRate();
|
||||
log.debug("-- audio duration: {} seconds", String.format("%.3f", audioDuration));
|
||||
return audio;
|
||||
} catch (Exception e) {
|
||||
throw new TtsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<Audio> generate(String text, TtsParams params) {
|
||||
GeneratedAudio audio = generateCore(text, params);
|
||||
return R.ok(new Audio(audio.getSamples()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generate(String text, TtsParams params, String savePath) {
|
||||
GeneratedAudio audio = generateCore(text, params);
|
||||
audio.save(savePath);
|
||||
}
|
||||
|
||||
private boolean fromFactory = false;
|
||||
|
||||
@Override
|
||||
public void setFromFactory(boolean fromFactory) {
|
||||
this.fromFactory = fromFactory;
|
||||
}
|
||||
public boolean isFromFactory() {
|
||||
return fromFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if(offlineTts != null){
|
||||
offlineTts.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.smartjavaai.speech.tts.model;
|
||||
|
||||
import ai.djl.modality.audio.Audio;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.speech.tts.config.TtsModelConfig;
|
||||
import cn.smartjavaai.speech.tts.entity.TtsParams;
|
||||
|
||||
/**
|
||||
* tts模型
|
||||
* @author dwj
|
||||
*/
|
||||
public interface TtsModel extends AutoCloseable{
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param config
|
||||
*/
|
||||
void loadModel(TtsModelConfig config);
|
||||
|
||||
default R<Audio> generate(String text, TtsParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default void generate(String text, TtsParams params, String savePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default void setFromFactory(boolean fromFactory){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.smartjavaai.speech.utils;
|
||||
|
||||
import ai.djl.modality.audio.Audio;
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import cn.smartjavaai.speech.asr.exception.AsrException;
|
||||
import ws.schild.jave.Encoder;
|
||||
@@ -9,6 +10,7 @@ import ws.schild.jave.encode.AudioAttributes;
|
||||
import ws.schild.jave.encode.EncodingAttributes;
|
||||
import ws.schild.jave.info.MultimediaInfo;
|
||||
|
||||
import javax.sound.sampled.AudioFileFormat;
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
@@ -208,5 +210,62 @@ public class AudioUtils {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将 float[] 音频数据保存为 WAV 文件
|
||||
*/
|
||||
public static void saveToWav(float[] floats, AudioFormat format, String savePath) throws IOException {
|
||||
// 1. 转换为 16-bit PCM
|
||||
byte[] bytes = floatsToPCM16(floats);
|
||||
// 2. 使用 ByteArrayInputStream 封装为音频流
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
|
||||
AudioInputStream ais = new AudioInputStream(bais, format, floats.length)) {
|
||||
// 3. 保存到本地文件
|
||||
AudioSystem.write(ais, AudioFileFormat.Type.WAVE, new File(savePath));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 float[] 音频数据保存为 WAV 文件
|
||||
*/
|
||||
public static void saveToWav(float[] floats, String savePath) throws IOException {
|
||||
// 1. 转换为 16-bit PCM
|
||||
byte[] bytes = floatsToPCM16(floats);
|
||||
// 2. 使用 ByteArrayInputStream 封装为音频流
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
|
||||
AudioInputStream ais = new AudioInputStream(bais, getDefaultAudioFormat(), floats.length)) {
|
||||
// 3. 保存到本地文件
|
||||
AudioSystem.write(ais, AudioFileFormat.Type.WAVE, new File(savePath));
|
||||
}
|
||||
}
|
||||
|
||||
public static AudioFormat getDefaultAudioFormat(){
|
||||
return new AudioFormat(
|
||||
AudioFormat.Encoding.PCM_SIGNED,
|
||||
16000,
|
||||
16,
|
||||
1,
|
||||
2,
|
||||
16000,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 float[] 转为 16bit PCM (little endian)
|
||||
*/
|
||||
private static byte[] floatsToPCM16(float[] floats) {
|
||||
byte[] bytes = new byte[floats.length * 2];
|
||||
int i = 0;
|
||||
for (float sample : floats) {
|
||||
// 裁剪范围 [-1, 1]
|
||||
sample = Math.max(-1.0f, Math.min(1.0f, sample));
|
||||
short s = (short) (sample * 32767);
|
||||
bytes[i++] = (byte) (s & 0xFF);
|
||||
bytes[i++] = (byte) ((s >> 8) & 0xFF);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user