mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-10 03:28:49 +00:00
本地修改
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -18,6 +18,7 @@
|
||||
<module>smartjavaai-all</module>
|
||||
<module>smartjavaai-ocr</module>
|
||||
<module>smartjavaai-bom</module>
|
||||
<module>smartjavaai-speech</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
@@ -25,7 +26,6 @@
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<djl.version>0.32.0</djl.version>
|
||||
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
|
||||
/**
|
||||
* 语言枚举
|
||||
* @author dwj
|
||||
*/
|
||||
public enum Language {
|
||||
|
||||
|
||||
EN("en", "English", "英语"),
|
||||
ZH("zh", "Chinese", "中文"),
|
||||
DE("de", "German", "德语"),
|
||||
ES("es", "Spanish", "西班牙语"),
|
||||
RU("ru", "Russian", "俄语"),
|
||||
KO("ko", "Korean", "韩语"),
|
||||
FR("fr", "French", "法语"),
|
||||
JA("ja", "Japanese", "日语"),
|
||||
PT("pt", "Portuguese", "葡萄牙语"),
|
||||
TR("tr", "Turkish", "土耳其语"),
|
||||
PL("pl", "Polish", "波兰语"),
|
||||
CA("ca", "Catalan", "加泰罗尼亚语"),
|
||||
NL("nl", "Dutch", "荷兰语"),
|
||||
AR("ar", "Arabic", "阿拉伯语"),
|
||||
SV("sv", "Swedish", "瑞典语"),
|
||||
IT("it", "Italian", "意大利语"),
|
||||
ID("id", "Indonesian", "印尼语"),
|
||||
HI("hi", "Hindi", "印地语"),
|
||||
FI("fi", "Finnish", "芬兰语"),
|
||||
VI("vi", "Vietnamese", "越南语"),
|
||||
HE("he", "Hebrew", "希伯来语"),
|
||||
UK("uk", "Ukrainian", "乌克兰语"),
|
||||
EL("el", "Greek", "希腊语"),
|
||||
MS("ms", "Malay", "马来语"),
|
||||
CS("cs", "Czech", "捷克语"),
|
||||
RO("ro", "Romanian", "罗马尼亚语"),
|
||||
DA("da", "Danish", "丹麦语"),
|
||||
HU("hu", "Hungarian", "匈牙利语"),
|
||||
TA("ta", "Tamil", "泰米尔语"),
|
||||
NO("no", "Norwegian", "挪威语"),
|
||||
TH("th", "Thai", "泰语"),
|
||||
UR("ur", "Urdu", "乌尔都语"),
|
||||
HR("hr", "Croatian", "克罗地亚语"),
|
||||
BG("bg", "Bulgarian", "保加利亚语"),
|
||||
LT("lt", "Lithuanian", "立陶宛语"),
|
||||
LA("la", "Latin", "拉丁语"),
|
||||
MI("mi", "Maori", "毛利语"),
|
||||
ML("ml", "Malayalam", "马拉雅拉姆语"),
|
||||
CY("cy", "Welsh", "威尔士语"),
|
||||
SK("sk", "Slovak", "斯洛伐克语"),
|
||||
TE("te", "Telugu", "泰卢固语"),
|
||||
FA("fa", "Persian", "波斯语"),
|
||||
LV("lv", "Latvian", "拉脱维亚语"),
|
||||
BN("bn", "Bengali", "孟加拉语"),
|
||||
SR("sr", "Serbian", "塞尔维亚语"),
|
||||
AZ("az", "Azerbaijani", "阿塞拜疆语"),
|
||||
SL("sl", "Slovenian", "斯洛文尼亚语"),
|
||||
KN("kn", "Kannada", "卡纳达语"),
|
||||
ET("et", "Estonian", "爱沙尼亚语"),
|
||||
MK("mk", "Macedonian", "马其顿语"),
|
||||
BR("br", "Breton", "布列塔尼语"),
|
||||
EU("eu", "Basque", "巴斯克语"),
|
||||
IS("is", "Icelandic", "冰岛语"),
|
||||
HY("hy", "Armenian", "亚美尼亚语"),
|
||||
NE("ne", "Nepali", "尼泊尔语"),
|
||||
MN("mn", "Mongolian", "蒙古语"),
|
||||
BS("bs", "Bosnian", "波斯尼亚语"),
|
||||
KK("kk", "Kazakh", "哈萨克语"),
|
||||
SQ("sq", "Albanian", "阿尔巴尼亚语"),
|
||||
SW("sw", "Swahili", "斯瓦希里语"),
|
||||
GL("gl", "Galician", "加利西亚语"),
|
||||
MR("mr", "Marathi", "马拉地语"),
|
||||
PA("pa", "Punjabi", "旁遮普语"),
|
||||
SI("si", "Sinhala", "僧伽罗语"),
|
||||
KM("km", "Khmer", "高棉语"),
|
||||
SN("sn", "Shona", "修纳语"),
|
||||
YO("yo", "Yoruba", "约鲁巴语"),
|
||||
SO("so", "Somali", "索马里语"),
|
||||
AF("af", "Afrikaans", "南非荷兰语"),
|
||||
OC("oc", "Occitan", "奥克语"),
|
||||
KA("ka", "Georgian", "格鲁吉亚语"),
|
||||
BE("be", "Belarusian", "白俄罗斯语"),
|
||||
TG("tg", "Tajik", "塔吉克语"),
|
||||
SD("sd", "Sindhi", "信德语"),
|
||||
GU("gu", "Gujarati", "古吉拉特语"),
|
||||
AM("am", "Amharic", "阿姆哈拉语"),
|
||||
YI("yi", "Yiddish", "意第绪语"),
|
||||
LO("lo", "Lao", "老挝语"),
|
||||
UZ("uz", "Uzbek", "乌兹别克语"),
|
||||
FO("fo", "Faroese", "法罗语"),
|
||||
HT("ht", "Haitian Creole", "海地克里奥尔语"),
|
||||
PS("ps", "Pashto", "普什图语"),
|
||||
TK("tk", "Turkmen", "土库曼语"),
|
||||
NN("nn", "Nynorsk", "新挪威语"),
|
||||
MT("mt", "Maltese", "马耳他语"),
|
||||
SA("sa", "Sanskrit", "梵语"),
|
||||
LB("lb", "Luxembourgish", "卢森堡语"),
|
||||
MY("my", "Myanmar", "缅甸语"),
|
||||
BO("bo", "Tibetan", "藏语"),
|
||||
TL("tl", "Tagalog", "他加禄语"),
|
||||
MG("mg", "Malagasy", "马尔加什语"),
|
||||
AS("as", "Assamese", "阿萨姆语"),
|
||||
TT("tt", "Tatar", "鞑靼语"),
|
||||
HAW("haw", "Hawaiian", "夏威夷语"),
|
||||
LN("ln", "Lingala", "林加拉语"),
|
||||
HA("ha", "Hausa", "豪萨语"),
|
||||
BA("ba", "Bashkir", "巴什基尔语"),
|
||||
JW("jw", "Javanese", "爪哇语"),
|
||||
SU("su", "Sundanese", "巽他语"),
|
||||
YUE("yue", "Cantonese", "粤语");
|
||||
|
||||
private final String code; // Whisper语言代码
|
||||
private final String englishName; // 英文名称
|
||||
private final String chineseName; // 中文名称
|
||||
|
||||
Language(String code, String englishName, String chineseName) {
|
||||
this.code = code;
|
||||
this.englishName = englishName;
|
||||
this.chineseName = chineseName;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getEnglishName() {
|
||||
return englishName;
|
||||
}
|
||||
|
||||
public String getChineseName() {
|
||||
return chineseName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return code;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -87,7 +87,7 @@ public class Seetaface6QualityModel implements FaceQualityModel {
|
||||
//加载依赖库
|
||||
NativeLoader.loadNativeLibraries(device);
|
||||
this.config = config;
|
||||
int predictorPoolSize = config.getPredictorPoolSize();
|
||||
predictorPoolSize = config.getPredictorPoolSize();
|
||||
if(config.getPredictorPoolSize() <= 0){
|
||||
predictorPoolSize = Runtime.getRuntime().availableProcessors(); // 默认等于CPU核心数
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class DetectorModelConfig extends ModelConfig {
|
||||
private int maxBox;
|
||||
|
||||
/**
|
||||
* 允许的类列表
|
||||
* 允许的分类列表
|
||||
*/
|
||||
private List<String> allowedClasses;
|
||||
|
||||
|
||||
@@ -28,20 +28,6 @@ public class PlateRecModelConfig extends ModelConfig {
|
||||
*/
|
||||
private PlateDetModel plateDetModel;
|
||||
|
||||
/**
|
||||
* 置信度阈值
|
||||
*/
|
||||
private float confidenceThreshold;
|
||||
|
||||
/**
|
||||
* iou阈值
|
||||
*/
|
||||
private float iouThreshold;
|
||||
|
||||
/**
|
||||
* 检测结果数量
|
||||
*/
|
||||
private int topK;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -286,18 +286,8 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
||||
if(Objects.isNull(ocrInfo) || Objects.isNull(ocrInfo.getLineList()) || ocrInfo.getLineList().isEmpty()){
|
||||
throw new OcrException("未检测到文字");
|
||||
}
|
||||
try {
|
||||
OcrUtils.drawRectWithText(sourceImage, ocrInfo, fontSize);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
// 调用 save 方法将 Image 写入字节流
|
||||
img.save(outputStream, "png");
|
||||
// 将字节流转换为 BufferedImage
|
||||
byte[] imageBytes = outputStream.toByteArray();
|
||||
((Mat) img.getWrappedImage()).release();
|
||||
return ImageIO.read(new ByteArrayInputStream(imageBytes));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("导出图片失败", e);
|
||||
}
|
||||
OcrUtils.drawRectWithText(sourceImage, ocrInfo, fontSize);
|
||||
return sourceImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,6 +17,7 @@ import cn.smartjavaai.common.utils.LetterBoxUtils;
|
||||
import cn.smartjavaai.common.utils.NMSUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -92,6 +93,11 @@ public class Yolo5PlateDetectTranslator implements Translator<Image, DetectedObj
|
||||
// 联合过滤
|
||||
NDArray jointMask = jointScore.gt(confThreshold);
|
||||
detsFiltered = detsFiltered.get(jointMask);
|
||||
|
||||
if (detsFiltered.isEmpty()) {
|
||||
return new DetectedObjects(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
clsLogits = clsLogits.get(jointMask);
|
||||
|
||||
|
||||
@@ -113,7 +119,10 @@ public class Yolo5PlateDetectTranslator implements Translator<Image, DetectedObj
|
||||
NDArray output = NDArrays.concat(new NDList(boxes, scores, keyPoints, indices), 1); // (N, 14)
|
||||
|
||||
// NMS 过滤掉重叠框
|
||||
int[] keepIndices = NMSUtils.nms(boxes, scores.squeeze(), iouThreshold); // scores.squeeze() ➝ (N,)
|
||||
int[] keepIndices = NMSUtils.nms(boxes, scores.squeeze(), iouThreshold); // scores.squeeze() ➝ (N,)】
|
||||
if (keepIndices.length == 0) {
|
||||
return new DetectedObjects(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
NDArray kept = output.get(manager.create(keepIndices));
|
||||
// 如果超过 topK,则截断
|
||||
if (keepIndices.length > topK) {
|
||||
|
||||
166
smartjavaai-speech/pom.xml
Normal file
166
smartjavaai-speech/pom.xml
Normal file
@@ -0,0 +1,166 @@
|
||||
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.22</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-speech</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JNA dependency -->
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
<version>5.13.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.givimad</groupId>
|
||||
<artifactId>whisper-jni</artifactId>
|
||||
<version>1.7.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alphacephei</groupId>
|
||||
<artifactId>vosk</artifactId>
|
||||
<version>0.3.45</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ws.schild</groupId>
|
||||
<artifactId>jave-core</artifactId>
|
||||
<version>3.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ws.schild</groupId>
|
||||
<artifactId>jave-nativebin-osxm1</artifactId>
|
||||
<version>3.5.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<version>1.0.22</version>
|
||||
<name>smartjavaai-speech</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://opensource.org/licenses/MIT</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<publishingServerId>dengwenjie</publishingServerId>
|
||||
<tokenAuth>true</tokenAuth>
|
||||
<deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<!-- <javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>-->
|
||||
<doclint>none</doclint>
|
||||
<additionalJOptions>
|
||||
<additionalJOption>-Xdoclint:none</additionalJOption>
|
||||
</additionalJOptions>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<!-- 必须添加:SCM信息 -->
|
||||
<scm>
|
||||
<connection>scm:git:git://github.com/geekwenjie/SmartJavaAI.git</connection>
|
||||
<developerConnection>scm:git:ssh://github.com/geekwenjie/SmartJavaAI.git</developerConnection>
|
||||
<url>http://github.com/geekwenjie/SmartJavaAI/tree/master</url>
|
||||
</scm>
|
||||
|
||||
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>dengwenjie</id>
|
||||
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>dengwenjie</id>
|
||||
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>dengwenjie</name>
|
||||
<email>775747758@qq.com</email>
|
||||
<roles>
|
||||
<role>Project Manager</role>
|
||||
<role>Architect</role>
|
||||
</roles>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,176 @@
|
||||
package cn.smartjavaai.speech.asr.audio;
|
||||
|
||||
import ai.djl.modality.audio.Audio;
|
||||
import ai.djl.modality.audio.AudioFactory;
|
||||
import cn.smartjavaai.speech.asr.exception.AsrException;
|
||||
import cn.smartjavaai.speech.utils.AudioUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import ws.schild.jave.EncoderException;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.UnsupportedAudioFileException;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/8/1
|
||||
*/
|
||||
@Slf4j
|
||||
public class SmartAudioFactory {
|
||||
|
||||
|
||||
private static final SmartAudioFactory INSTANCE = new SmartAudioFactory();
|
||||
|
||||
private SmartAudioFactory() {
|
||||
}
|
||||
|
||||
public static SmartAudioFactory getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Audio fromFile(Path path, AudioFormat targetFormat) throws IOException {
|
||||
try (AudioInputStream ais = AudioSystem.getAudioInputStream(path.toFile())) {
|
||||
if(targetFormat != null){
|
||||
try (AudioInputStream convertedAis = AudioSystem.getAudioInputStream(targetFormat, ais);) {
|
||||
byte[] bytes = read(convertedAis);
|
||||
float[] floats = bytesToFloats(bytes, targetFormat.isBigEndian());
|
||||
return new Audio(floats, targetFormat.getSampleRate(), targetFormat.getChannels());
|
||||
}
|
||||
}else{
|
||||
AudioFormat format = ais.getFormat();
|
||||
byte[] bytes = read(ais);
|
||||
float[] floats = bytesToFloats(bytes, format.isBigEndian());
|
||||
return new Audio(floats, format.getSampleRate(), format.getChannels());
|
||||
}
|
||||
} catch (UnsupportedAudioFileException e) {
|
||||
log.debug("Unsupported Audio file, Conversion to WAV is required");
|
||||
byte[] allBytes = Files.readAllBytes(path);
|
||||
try(AudioInputStream audioInputStream = convertWav(allBytes)){
|
||||
return fromAudioInputStream(audioInputStream, targetFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Audio fromAudioInputStream(AudioInputStream ais, AudioFormat targetFormat) throws IOException {
|
||||
try (AudioInputStream audioInputStream = ais) {
|
||||
if(targetFormat != null){
|
||||
try (AudioInputStream convertedAis = AudioSystem.getAudioInputStream(targetFormat, audioInputStream);) {
|
||||
byte[] bytes = read(convertedAis);
|
||||
float[] floats = bytesToFloats(bytes, targetFormat.isBigEndian());
|
||||
return new Audio(floats, targetFormat.getSampleRate(), targetFormat.getChannels());
|
||||
}
|
||||
}else{
|
||||
AudioFormat format = audioInputStream.getFormat();
|
||||
byte[] bytes = read(audioInputStream);
|
||||
float[] floats = bytesToFloats(bytes, format.isBigEndian());
|
||||
return new Audio(floats, format.getSampleRate(), format.getChannels());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AudioInputStream convertWav(byte[] allBytes) {
|
||||
InputStream conversionStream = new BufferedInputStream(new ByteArrayInputStream(allBytes));
|
||||
File tempFile = null;
|
||||
try {
|
||||
tempFile = AudioUtils.audioFormatConversion(conversionStream, "wav");
|
||||
InputStream fis = new BufferedInputStream(new FileInputStream(tempFile));
|
||||
AudioInputStream ais = AudioSystem.getAudioInputStream(fis);
|
||||
return ais;
|
||||
} catch (EncoderException | IOException | UnsupportedAudioFileException e) {
|
||||
throw new AsrException(e);
|
||||
} finally {
|
||||
if(tempFile != null && tempFile.exists()){
|
||||
tempFile.delete();
|
||||
}
|
||||
if(conversionStream != null){
|
||||
try {
|
||||
conversionStream.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Audio fromInputStream(InputStream is, AudioFormat targetFormat) throws IOException {
|
||||
byte[] allBytes = is.readAllBytes();
|
||||
try (BufferedInputStream tryStream = new BufferedInputStream(new ByteArrayInputStream(allBytes));
|
||||
AudioInputStream ais = AudioSystem.getAudioInputStream(tryStream)) {
|
||||
if(targetFormat != null){
|
||||
try (AudioInputStream convertedAis = AudioSystem.getAudioInputStream(targetFormat, ais);) {
|
||||
byte[] bytes = read(convertedAis);
|
||||
float[] floats = bytesToFloats(bytes, targetFormat.isBigEndian());
|
||||
return new Audio(floats, targetFormat.getSampleRate(), targetFormat.getChannels());
|
||||
}
|
||||
}else{
|
||||
AudioFormat format = ais.getFormat();
|
||||
byte[] bytes = read(ais);
|
||||
float[] floats = bytesToFloats(bytes, format.isBigEndian());
|
||||
return new Audio(floats, format.getSampleRate(), format.getChannels());
|
||||
}
|
||||
} catch (UnsupportedAudioFileException e) {
|
||||
log.debug("Unsupported Audio file, Conversion to WAV is required");
|
||||
try(AudioInputStream audioInputStream = convertWav(allBytes)){
|
||||
return fromAudioInputStream(audioInputStream, targetFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] read(AudioInputStream ais) throws IOException {
|
||||
AudioFormat format = ais.getFormat();
|
||||
int frameSize = format.getFrameSize();
|
||||
|
||||
// Some audio formats may have unspecified frame size
|
||||
if (frameSize == AudioSystem.NOT_SPECIFIED) {
|
||||
frameSize = 1;
|
||||
}
|
||||
|
||||
int size = (int) ais.getFrameLength() * frameSize;
|
||||
|
||||
if (ais.getFrameLength() == AudioSystem.NOT_SPECIFIED || size <= 0){
|
||||
// unknown length, use ByteArrayOutputStream to read all data
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int read;
|
||||
while ((read = ais.read(buf)) != -1) {
|
||||
baos.write(buf, 0, read);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}else{
|
||||
byte[] ret = new byte[size];
|
||||
byte[] buf = new byte[1024];
|
||||
int offset = 0;
|
||||
int read;
|
||||
while ((read = ais.read(buf)) != -1) {
|
||||
System.arraycopy(buf, 0, ret, offset, read);
|
||||
offset += read;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private float[] bytesToFloats(byte[] bytes, boolean isBigEndian) {
|
||||
ByteOrder order = isBigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
|
||||
ShortBuffer buffer = ByteBuffer.wrap(bytes).order(order).asShortBuffer();
|
||||
short[] shorts = new short[buffer.capacity()];
|
||||
buffer.get(shorts);
|
||||
|
||||
// Feed in float values between -1.0f and 1.0f.
|
||||
float[] floats = new float[shorts.length];
|
||||
for (int i = 0; i < shorts.length; i++) {
|
||||
floats[i] = ((float) shorts[i]) / (float) Short.MAX_VALUE;
|
||||
}
|
||||
return floats;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.smartjavaai.speech.asr.config;
|
||||
|
||||
import cn.smartjavaai.common.config.ModelConfig;
|
||||
import cn.smartjavaai.speech.asr.enums.AsrModelEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/7/31
|
||||
*/
|
||||
@Data
|
||||
public class AsrModelConfig extends ModelConfig {
|
||||
|
||||
private AsrModelEnum modelEnum;
|
||||
|
||||
private String modelPath;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.smartjavaai.speech.asr.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 语音识别结果
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public class AsrResult {
|
||||
|
||||
private String text;
|
||||
private List<AsrSegment> segments;
|
||||
|
||||
|
||||
public AsrResult() {
|
||||
}
|
||||
|
||||
public AsrResult(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public AsrResult(String text, List<AsrSegment> segments) {
|
||||
this.text = text;
|
||||
this.segments = segments;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.smartjavaai.speech.asr.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public class AsrSegment {
|
||||
|
||||
private String text;
|
||||
private long startTime;
|
||||
private long endTime;
|
||||
|
||||
public AsrSegment(String text, long startTime, long endTime) {
|
||||
this.text = text;
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public AsrSegment(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public AsrSegment() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.smartjavaai.speech.asr.entity;
|
||||
|
||||
import cn.smartjavaai.common.entity.Language;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 语音识别参数
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public abstract class RecParams {
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
private Language language = Language.ZH;
|
||||
|
||||
public Language getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public void setLanguage(Language language) {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.smartjavaai.speech.asr.entity;
|
||||
|
||||
import cn.smartjavaai.common.entity.Language;
|
||||
import io.github.givimad.whisperjni.WhisperFullParams;
|
||||
import io.github.givimad.whisperjni.WhisperSamplingStrategy;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public class VoskParams extends RecParams{
|
||||
|
||||
/**
|
||||
* 最大候选结果数
|
||||
*/
|
||||
private int maxAlternatives;
|
||||
|
||||
/**
|
||||
* 限定词汇表 例:["yes", "no", "hello"]
|
||||
*/
|
||||
private String grammar;
|
||||
|
||||
/**
|
||||
* 是否返回词级别的识别结果(包含每个词的开始/结束时间和置信度)。
|
||||
*/
|
||||
private boolean words = true;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.smartjavaai.speech.asr.entity;
|
||||
|
||||
import cn.smartjavaai.common.entity.Language;
|
||||
import io.github.givimad.whisperjni.WhisperFullParams;
|
||||
import io.github.givimad.whisperjni.WhisperSamplingStrategy;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public class WhisperParams extends RecParams{
|
||||
|
||||
private WhisperFullParams params;
|
||||
|
||||
public WhisperParams(WhisperFullParams params) {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public WhisperParams() {
|
||||
this.params = new WhisperFullParams(WhisperSamplingStrategy.BEAN_SEARCH);
|
||||
this.params.language = Language.ZH.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLanguage(Language language) {
|
||||
super.setLanguage(language);
|
||||
if (language != null) {
|
||||
this.params.language = language.getCode(); // 关键:设置给底层 whisper 参数
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.smartjavaai.speech.asr.enums;
|
||||
|
||||
/**
|
||||
* 语音识别模型枚举
|
||||
* @author dwj
|
||||
*/
|
||||
public enum AsrModelEnum {
|
||||
|
||||
WHISPER,
|
||||
|
||||
VOSK;
|
||||
|
||||
/**
|
||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||
*/
|
||||
public static AsrModelEnum fromName(String name) {
|
||||
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||
for (AsrModelEnum model : values()) {
|
||||
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知模型名称: " + name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.smartjavaai.speech.asr.exception;
|
||||
|
||||
/**
|
||||
* 语音识别异常
|
||||
* @author dwj
|
||||
*/
|
||||
public class AsrException extends RuntimeException{
|
||||
|
||||
public AsrException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public AsrException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
public AsrException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public AsrException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AsrException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.smartjavaai.speech.asr.model;
|
||||
|
||||
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.exception.AsrException;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* 语音识别
|
||||
* @author dwj
|
||||
*/
|
||||
public interface SpeechRecognizer extends AutoCloseable{
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param config
|
||||
*/
|
||||
void loadModel(AsrModelConfig config); // 加载模型
|
||||
|
||||
|
||||
default R<AsrResult> recognize(String audioPath, RecParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default R<AsrResult> recognize(byte[] audioData, RecParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default R<AsrResult> recognize(InputStream audioStream, RecParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default R<AsrResult> recognize(String audioPath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default R<AsrResult> recognize(byte[] audioData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default R<AsrResult> recognize(InputStream audioStream){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package cn.smartjavaai.speech.asr.model;
|
||||
|
||||
import ai.djl.modality.audio.Audio;
|
||||
import ai.djl.modality.audio.AudioFactory;
|
||||
import ai.djl.util.JsonUtils;
|
||||
import cn.smartjavaai.common.entity.Language;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.speech.asr.audio.SmartAudioFactory;
|
||||
import cn.smartjavaai.speech.asr.config.AsrModelConfig;
|
||||
import cn.smartjavaai.speech.asr.entity.*;
|
||||
import cn.smartjavaai.speech.asr.exception.AsrException;
|
||||
import cn.smartjavaai.speech.asr.pool.WhisperStatePool;
|
||||
import cn.smartjavaai.speech.utils.AudioUtils;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import io.github.givimad.whisperjni.WhisperContext;
|
||||
import io.github.givimad.whisperjni.WhisperJNI;
|
||||
import io.github.givimad.whisperjni.WhisperState;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.vosk.LibVosk;
|
||||
import org.vosk.LogLevel;
|
||||
import org.vosk.Model;
|
||||
import org.vosk.Recognizer;
|
||||
import ws.schild.jave.Encoder;
|
||||
import ws.schild.jave.EncoderException;
|
||||
import ws.schild.jave.info.MultimediaInfo;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.UnsupportedAudioFileException;
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static ai.djl.util.JsonUtils.GSON;
|
||||
|
||||
/**
|
||||
* Vosk 识别器
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class VoskRecognizer implements SpeechRecognizer{
|
||||
|
||||
private Model model;
|
||||
|
||||
@Override
|
||||
public void loadModel(AsrModelConfig config) {
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new AsrException("modelPath is null");
|
||||
}
|
||||
Path testModelPath = Paths.get(config.getModelPath());
|
||||
if(!testModelPath.toFile().exists()){
|
||||
throw new AsrException("Missing model file: " + testModelPath.toAbsolutePath());
|
||||
}
|
||||
try {
|
||||
model = new Model(config.getModelPath());
|
||||
LibVosk.setLogLevel(LogLevel.DEBUG);
|
||||
log.debug("Vosk init success");
|
||||
} catch (IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(String audioPath) {
|
||||
return recognize(audioPath, new VoskParams());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(byte[] audioData) {
|
||||
return recognize(audioData, new VoskParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(InputStream audioStream) {
|
||||
return recognize(audioStream, new VoskParams());
|
||||
}
|
||||
|
||||
private R<AsrResult> recognizeAudioStream(AudioInputStream ais,RecParams params) {
|
||||
try (Recognizer recognizer = buildRecognizer(params, ais.getFormat().getSampleRate())){
|
||||
AudioFormat audioFormat = ais.getFormat();
|
||||
log.debug("sampleRate:{}", audioFormat.getSampleRate());
|
||||
log.debug("channels:{}", audioFormat.getChannels());
|
||||
int nbytes;
|
||||
byte[] b = new byte[4096];
|
||||
List<AsrSegment> segments = new ArrayList<AsrSegment>();
|
||||
StringBuilder text = new StringBuilder();
|
||||
while ((nbytes = ais.read(b)) >= 0) {
|
||||
if (recognizer.acceptWaveForm(b, nbytes)) {
|
||||
String result = recognizer.getResult();
|
||||
AsrSegment segment = parseSegment(result);
|
||||
segments.add(segment);
|
||||
}
|
||||
}
|
||||
return R.ok(new AsrResult(text.toString(), segments));
|
||||
} catch (IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析结果
|
||||
* @param segment
|
||||
* @return
|
||||
*/
|
||||
private AsrSegment parseSegment(String segment) {
|
||||
JsonObject json = GSON.fromJson(segment, JsonObject.class);
|
||||
JsonArray resultArray = json.getAsJsonArray("result");
|
||||
double segmentStart = resultArray.get(0).getAsJsonObject().get("start").getAsDouble();
|
||||
double segmentEnd = resultArray.get(resultArray.size() - 1).getAsJsonObject().get("end").getAsDouble();
|
||||
long startMs = Math.round(segmentStart * 1000);
|
||||
long endMs = Math.round(segmentEnd * 1000);
|
||||
String text = json.get("text").getAsString();
|
||||
String noSpaces = text.replace(" ", "");
|
||||
return new AsrSegment(noSpaces, startMs, endMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建识别器
|
||||
* @param params
|
||||
* @param sampleRate
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private Recognizer buildRecognizer(RecParams params,float sampleRate) throws IOException {
|
||||
if(!(params instanceof VoskParams)){
|
||||
throw new AsrException("params is not VoskParams");
|
||||
}
|
||||
VoskParams voskParams = (VoskParams) params;
|
||||
Recognizer recognizer = new Recognizer(model, sampleRate);
|
||||
if(StringUtils.isNotBlank(voskParams.getGrammar())){
|
||||
recognizer.setGrammar(voskParams.getGrammar());
|
||||
}
|
||||
// if(voskParams.getMaxAlternatives() > 0){
|
||||
// recognizer.setMaxAlternatives(voskParams.getMaxAlternatives());
|
||||
// }
|
||||
//暂时只支持返回一个结果
|
||||
// recognizer.setMaxAlternatives(1);
|
||||
recognizer.setWords(voskParams.isWords());
|
||||
//recognizer.setPartialWords(true);
|
||||
return recognizer;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(String audioPath, RecParams params) {
|
||||
Path audioFilePath = Paths.get(audioPath);
|
||||
if(!audioFilePath.toFile().exists()){
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
try (InputStream is = Files.newInputStream(audioFilePath)){
|
||||
return recognize(is, params);
|
||||
} catch (IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(byte[] audioData, RecParams params) {
|
||||
try (InputStream is = new ByteArrayInputStream(audioData)) {
|
||||
return recognize(is, params);
|
||||
} catch (IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(InputStream audioStream, RecParams params) {
|
||||
AudioInputStream ais = null;
|
||||
InputStream tryStream = null;
|
||||
InputStream conversionStream = null;
|
||||
boolean needConversion = false;
|
||||
try {
|
||||
// 缓存全部字节数据
|
||||
byte[] allBytes = audioStream.readAllBytes();
|
||||
// 创建两个独立流
|
||||
tryStream = new BufferedInputStream(new ByteArrayInputStream(allBytes));
|
||||
conversionStream = new BufferedInputStream(new ByteArrayInputStream(allBytes));
|
||||
ais = AudioSystem.getAudioInputStream(tryStream);
|
||||
} catch (UnsupportedAudioFileException e) {
|
||||
log.debug("Unsupported Audio file, Conversion to WAV is required");
|
||||
needConversion = true;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if(tryStream != null){
|
||||
try {
|
||||
tryStream.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//转换wav格式
|
||||
File tempFile = null;
|
||||
if(needConversion){
|
||||
try {
|
||||
tempFile = AudioUtils.audioFormatConversion(conversionStream, "wav");
|
||||
} catch (EncoderException | IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
try (InputStream fis = new BufferedInputStream(new FileInputStream(tempFile))){
|
||||
ais = AudioSystem.getAudioInputStream(fis);
|
||||
return recognizeAudioStream(ais, params);
|
||||
} catch (IOException | UnsupportedAudioFileException e) {
|
||||
throw new AsrException("音频转换异常", e);
|
||||
} finally {
|
||||
if(tempFile != null && tempFile.exists()){
|
||||
tempFile.delete();
|
||||
}
|
||||
if(conversionStream != null){
|
||||
try {
|
||||
conversionStream.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return recognizeAudioStream(ais, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if(model != null){
|
||||
model.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建高级识别器
|
||||
* @param sampleRate 采样率
|
||||
* @return
|
||||
*/
|
||||
public Recognizer createAdvancedRecognizer(float sampleRate){
|
||||
try {
|
||||
return new Recognizer(model, sampleRate);
|
||||
} catch (IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package cn.smartjavaai.speech.asr.model;
|
||||
|
||||
import ai.djl.modality.audio.Audio;
|
||||
import ai.djl.modality.audio.AudioFactory;
|
||||
import cn.smartjavaai.common.entity.Language;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.speech.asr.audio.SmartAudioFactory;
|
||||
import cn.smartjavaai.speech.asr.config.AsrModelConfig;
|
||||
import cn.smartjavaai.speech.asr.entity.AsrResult;
|
||||
import cn.smartjavaai.speech.asr.entity.AsrSegment;
|
||||
import cn.smartjavaai.speech.asr.entity.RecParams;
|
||||
import cn.smartjavaai.speech.asr.entity.WhisperParams;
|
||||
import cn.smartjavaai.speech.asr.exception.AsrException;
|
||||
import cn.smartjavaai.speech.asr.pool.WhisperStatePool;
|
||||
import io.github.givimad.whisperjni.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.vosk.Model;
|
||||
import org.vosk.Recognizer;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class WhisperRecognizer implements SpeechRecognizer{
|
||||
|
||||
private WhisperJNI whisper;
|
||||
|
||||
private WhisperContext ctx;
|
||||
|
||||
private WhisperStatePool statePool;
|
||||
|
||||
@Override
|
||||
public void loadModel(AsrModelConfig config) {
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new AsrException("modelPath is null");
|
||||
}
|
||||
Path testModelPath = Paths.get(config.getModelPath());
|
||||
if(!testModelPath.toFile().exists()){
|
||||
throw new AsrException("Missing model file: " + testModelPath.toAbsolutePath());
|
||||
}
|
||||
try {
|
||||
WhisperJNI.loadLibrary();
|
||||
WhisperJNI.setLibraryLogger(null);
|
||||
whisper = new WhisperJNI();
|
||||
WhisperJNI.setLibraryLogger(new WhisperJNI.LibraryLogger() {
|
||||
@Override
|
||||
public void log(String s) {
|
||||
log.debug("WhisperJNI {}", s);
|
||||
}
|
||||
});
|
||||
ctx = whisper.initNoState(testModelPath);
|
||||
Boolean initOpenVINO = config.getCustomParam("initOpenVINO", Boolean.class);
|
||||
if(Objects.nonNull(initOpenVINO) && initOpenVINO){
|
||||
String device = Objects.isNull(config.getDevice()) ? DeviceEnum.CPU.name() : config.getDevice().name();
|
||||
whisper.initOpenVINO(ctx, device);
|
||||
log.debug("WhisperJNI initOpenVINO success");
|
||||
}
|
||||
statePool = new WhisperStatePool(whisper, ctx);
|
||||
log.debug("WhisperJNI init success");
|
||||
} catch (IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(String audioPath, RecParams params) {
|
||||
Path audioFilePath = Paths.get(audioPath);
|
||||
if(!audioFilePath.toFile().exists()){
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
Audio audio = null;
|
||||
try {
|
||||
audio = SmartAudioFactory.getInstance().fromFile(audioFilePath, getDefaultAudioFormat());
|
||||
} catch (IOException e) {
|
||||
throw new AsrException("读取音频异常",e);
|
||||
}
|
||||
return recognize(audio, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(byte[] audioData, RecParams params) {
|
||||
try (InputStream is = new ByteArrayInputStream(audioData)) {
|
||||
return recognize(is, params);
|
||||
} catch (IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(InputStream audioStream, RecParams params) {
|
||||
Audio audio = null;
|
||||
try {
|
||||
audio = SmartAudioFactory.getInstance().fromInputStream(audioStream, getDefaultAudioFormat());
|
||||
} catch (IOException e) {
|
||||
throw new AsrException("读取音频异常",e);
|
||||
}
|
||||
return recognize(audio, params);
|
||||
}
|
||||
|
||||
public R<AsrResult> recognize(Audio audio, RecParams params) {
|
||||
WhisperState state = null;
|
||||
StringBuilder text = new StringBuilder();
|
||||
try {
|
||||
WhisperParams whisperParams = (WhisperParams) params;
|
||||
//不是英语,需要检查是否是多语言模型
|
||||
if(!Language.EN.getCode().equals(whisperParams.getParams().language)){
|
||||
if(!whisper.isMultilingual(ctx)){
|
||||
return R.fail(1002, "当前为非多语种模型,仅支持英语识别,暂不支持其他语言,请更换多语种模型");
|
||||
}
|
||||
}
|
||||
state = statePool.borrowObject();
|
||||
int result = whisper.fullWithState(ctx, state, whisperParams.getParams(), audio.getData(), audio.getData().length);
|
||||
if(result != 0) {
|
||||
return R.fail(1000, "Transcription failed with code " + result);
|
||||
}
|
||||
int numSegments = whisper.fullNSegmentsFromState(state);
|
||||
if(numSegments <= 0){
|
||||
return R.fail(1001, "未识别出有效语音");
|
||||
}
|
||||
List<AsrSegment> segments = new ArrayList<AsrSegment>(numSegments);
|
||||
for(int i = 0; i < numSegments; i++){
|
||||
long startTime = whisper.fullGetSegmentTimestamp0FromState(state,i);
|
||||
long endTime = whisper.fullGetSegmentTimestamp1FromState(state,i);
|
||||
String content = whisper.fullGetSegmentTextFromState(state, i);
|
||||
segments.add(new AsrSegment(content, startTime * 10, endTime * 10));
|
||||
text.append(content).append("\n");
|
||||
}
|
||||
return R.ok(new AsrResult(text.toString(), segments));
|
||||
} catch (Exception e) {
|
||||
throw new AsrException(e);
|
||||
} finally {
|
||||
if(state != null){
|
||||
try {
|
||||
statePool.returnObject(state);
|
||||
} catch (Exception e) {
|
||||
log.warn("returnObject失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(String audioPath) {
|
||||
return recognize(audioPath, new WhisperParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(byte[] audioData) {
|
||||
return recognize(audioData, new WhisperParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<AsrResult> recognize(InputStream audioStream) {
|
||||
return recognize(audioStream, new WhisperParams());
|
||||
}
|
||||
|
||||
public WhisperGrammar parseGrammar(String grammarText) {
|
||||
try {
|
||||
return whisper.parseGrammar(grammarText);
|
||||
} catch (IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if(statePool != null){
|
||||
statePool.close();
|
||||
}
|
||||
if(ctx != null){
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个WhisperState对象
|
||||
* @return
|
||||
*/
|
||||
private WhisperState getWhisperState(){
|
||||
try {
|
||||
return statePool.borrowObject();
|
||||
} catch (Exception e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private AudioFormat getDefaultAudioFormat(){
|
||||
return new AudioFormat(
|
||||
AudioFormat.Encoding.PCM_SIGNED,
|
||||
16000,
|
||||
16,
|
||||
1,
|
||||
2,
|
||||
16000,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.smartjavaai.speech.asr.pool;
|
||||
|
||||
import io.github.givimad.whisperjni.WhisperContext;
|
||||
import io.github.givimad.whisperjni.WhisperJNI;
|
||||
import io.github.givimad.whisperjni.WhisperState;
|
||||
import org.apache.commons.pool2.PooledObject;
|
||||
import org.apache.commons.pool2.PooledObjectFactory;
|
||||
import org.apache.commons.pool2.impl.DefaultPooledObject;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
|
||||
/**
|
||||
* WhisperState 实例对象池
|
||||
* @author dwj
|
||||
*/
|
||||
public class WhisperStatePool extends GenericObjectPool<WhisperState> {
|
||||
|
||||
public WhisperStatePool(WhisperJNI whisperInstance, WhisperContext context) {
|
||||
super(new PooledObjectFactory<WhisperState>(){
|
||||
|
||||
@Override
|
||||
public void activateObject(PooledObject<WhisperState> pooledObject) throws Exception {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyObject(PooledObject<WhisperState> pooledObject) throws Exception {
|
||||
pooledObject.getObject().close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PooledObject<WhisperState> makeObject() throws Exception {
|
||||
WhisperState state = whisperInstance.initState(context);
|
||||
return new DefaultPooledObject(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void passivateObject(PooledObject<WhisperState> pooledObject) throws Exception {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateObject(PooledObject<WhisperState> pooledObject) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package cn.smartjavaai.speech.utils;
|
||||
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import cn.smartjavaai.speech.asr.exception.AsrException;
|
||||
import ws.schild.jave.Encoder;
|
||||
import ws.schild.jave.EncoderException;
|
||||
import ws.schild.jave.MultimediaObject;
|
||||
import ws.schild.jave.encode.AudioAttributes;
|
||||
import ws.schild.jave.encode.EncodingAttributes;
|
||||
import ws.schild.jave.info.MultimediaInfo;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
|
||||
/**
|
||||
* 音频工具类
|
||||
* @author dwj
|
||||
*/
|
||||
public class AudioUtils {
|
||||
|
||||
|
||||
public static byte[] read(AudioInputStream ais) throws IOException {
|
||||
AudioFormat format = ais.getFormat();
|
||||
int frameSize = format.getFrameSize();
|
||||
|
||||
// Some audio formats may have unspecified frame size
|
||||
if (frameSize == AudioSystem.NOT_SPECIFIED) {
|
||||
frameSize = 1;
|
||||
}
|
||||
int size = (int) ais.getFrameLength() * frameSize;
|
||||
if (ais.getFrameLength() == AudioSystem.NOT_SPECIFIED || size <= 0){
|
||||
// unknown length, use ByteArrayOutputStream to read all data
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int read;
|
||||
while ((read = ais.read(buf)) != -1) {
|
||||
baos.write(buf, 0, read);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}else{
|
||||
byte[] ret = new byte[size];
|
||||
byte[] buf = new byte[1024];
|
||||
int offset = 0;
|
||||
int read;
|
||||
while ((read = ais.read(buf)) != -1) {
|
||||
System.arraycopy(buf, 0, ret, offset, read);
|
||||
offset += read;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频格式转换
|
||||
*
|
||||
* @param sourceFilePath
|
||||
* @param targetFilePath
|
||||
* @param format wav/mp3/amr
|
||||
* @return
|
||||
*/
|
||||
public static byte[] getAudioFormatConversionBytes(String sourceFilePath,String targetFilePath,String format) {
|
||||
InputStream fis = null;
|
||||
ByteArrayOutputStream bos = null;
|
||||
byte[] bytes = null;
|
||||
try {
|
||||
File sourceFile = new File(sourceFilePath);
|
||||
if (sourceFile.isFile()) {
|
||||
File targetFile = new File(targetFilePath);
|
||||
|
||||
// 音频格式转换
|
||||
audioFormatConversion(sourceFile, targetFile, format);
|
||||
|
||||
fis = new FileInputStream(targetFile);
|
||||
bos = new ByteArrayOutputStream();
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
bytes = bos.toByteArray();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new AsrException("音频格式转换异常:" + e.getMessage(), e);
|
||||
} finally {
|
||||
try {
|
||||
if (fis != null) {
|
||||
fis.close();
|
||||
}
|
||||
if (bos != null) {
|
||||
bos.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AsrException("音频格式转换资源关闭异常:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频格式转换
|
||||
*
|
||||
* @param sourceFilePath
|
||||
* @param targetFilePath
|
||||
* @param format wav/mp3/amr
|
||||
* @return
|
||||
*/
|
||||
public static InputStream getAudioFormatConversionIns(String sourceFilePath, String targetFilePath, String format) throws EncoderException, FileNotFoundException {
|
||||
File sourceFile = new File(sourceFilePath);
|
||||
if (sourceFile.isFile()) {
|
||||
File targetFile = new File(targetFilePath);
|
||||
// 音频格式转换
|
||||
audioFormatConversion(sourceFile, targetFile, format);
|
||||
return new FileInputStream(targetFile);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频格式转换
|
||||
* @param source 源音频文件
|
||||
* @param target 输出的音频文件
|
||||
* @param format wav/mp3/amr
|
||||
*/
|
||||
public static void audioFormatConversion(File source,File target,String format) throws EncoderException {
|
||||
//Audio Attributes
|
||||
AudioAttributes audio = new AudioAttributes();
|
||||
switch (format) {
|
||||
case "wav":
|
||||
audio.setCodec("pcm_s16le");
|
||||
break;
|
||||
case "mp3":
|
||||
audio.setCodec("libmp3lame");
|
||||
break;
|
||||
case "amr":
|
||||
audio.setCodec("libvo_amrwbenc");
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的音频格式:" + format);
|
||||
}
|
||||
audio.setBitRate(16000);
|
||||
audio.setChannels(1);
|
||||
audio.setSamplingRate(16000);
|
||||
//Encoding attributes
|
||||
EncodingAttributes attrs = new EncodingAttributes();
|
||||
attrs.setOutputFormat(format);
|
||||
attrs.setAudioAttributes(audio);
|
||||
//Encode
|
||||
Encoder encoder = new Encoder();
|
||||
encoder.encode(new MultimediaObject(source), target, attrs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 音频格式转换
|
||||
* @param sourceStream 源音频流
|
||||
* @param format wav/mp3/amr
|
||||
* @return
|
||||
*/
|
||||
public static File audioFormatConversion(InputStream sourceStream, String format) throws EncoderException, IOException {
|
||||
// 1. 写入临时源文件
|
||||
File sourceFile = Files.createTempFile("source-", ".tmp").toFile();
|
||||
try (OutputStream os = new FileOutputStream(sourceFile)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int len;
|
||||
while ((len = sourceStream.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
// 2. 创建临时目标文件
|
||||
File targetFile = Files.createTempFile("target-", "." + format).toFile();
|
||||
// 3. 调用已有方法进行格式转换
|
||||
audioFormatConversion(sourceFile, targetFile, format);
|
||||
// 4. 删除源文件(可选)
|
||||
sourceFile.delete();
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频信息
|
||||
* @param source
|
||||
* @return
|
||||
*/
|
||||
public static MultimediaInfo getAudioInfo(File source) throws EncoderException {
|
||||
MultimediaObject mo = new MultimediaObject(source);
|
||||
return mo.getInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频信息
|
||||
* @param ais
|
||||
* @return
|
||||
*/
|
||||
public static MultimediaInfo getAudioInfo(AudioInputStream ais) throws EncoderException, IOException {
|
||||
File tempFile = Files.createTempFile("audio_" + UUID.fastUUID().toString(), null).toFile();
|
||||
tempFile.deleteOnExit(); // JVM退出时自动删除
|
||||
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile))) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = ais.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
return getAudioInfo(tempFile);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user