mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-10 03:28:49 +00:00
临时提交
This commit is contained in:
188
speech/pom.xml
Normal file
188
speech/pom.xml
Normal file
@@ -0,0 +1,188 @@
|
||||
<?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.24</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>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>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>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<version>1.0.24</version>
|
||||
<name>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.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.10.1</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<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>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-toolchains-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>toolchain</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<toolchains>
|
||||
<jdk>
|
||||
<version>11</version>
|
||||
</jdk>
|
||||
</toolchains>
|
||||
</configuration>
|
||||
</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,25 @@
|
||||
package cn.smartjavaai.speech.asr.config;
|
||||
|
||||
import cn.smartjavaai.common.config.ModelConfig;
|
||||
import cn.smartjavaai.speech.asr.enums.AsrModelEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Asr模型配置
|
||||
* @author dwj
|
||||
* @date 2025/7/31
|
||||
*/
|
||||
@Data
|
||||
public class AsrModelConfig extends ModelConfig {
|
||||
|
||||
private AsrModelEnum modelEnum;
|
||||
|
||||
private String modelPath;
|
||||
|
||||
/**
|
||||
* 依赖库目录
|
||||
*/
|
||||
private Path libPath;
|
||||
}
|
||||
@@ -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,20 @@
|
||||
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 String grammar;
|
||||
|
||||
|
||||
}
|
||||
@@ -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,103 @@
|
||||
package cn.smartjavaai.speech.asr.factory;
|
||||
|
||||
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.SpeechRecognizer;
|
||||
import cn.smartjavaai.speech.asr.model.VoskRecognizer;
|
||||
import cn.smartjavaai.speech.asr.model.WhisperRecognizer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 语音识别模型工厂
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class SpeechRecognizerFactory {
|
||||
|
||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||
private static volatile SpeechRecognizerFactory instance;
|
||||
|
||||
/**
|
||||
* 模型缓存
|
||||
*/
|
||||
private static final ConcurrentHashMap<AsrModelEnum, SpeechRecognizer> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 模型注册表
|
||||
*/
|
||||
private static final Map<AsrModelEnum, Class<? extends SpeechRecognizer>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static SpeechRecognizerFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (SpeechRecognizerFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new SpeechRecognizerFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册模型
|
||||
* @param expressionModelEnum
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerModel(AsrModelEnum expressionModelEnum, Class<? extends SpeechRecognizer> clazz) {
|
||||
registry.put(expressionModelEnum, clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public SpeechRecognizer getModel(AsrModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new AsrException("未配置语音识别模型枚举");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum(), k -> {
|
||||
return createFaceModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private SpeechRecognizer createFaceModel(AsrModelConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum());
|
||||
if(clazz == null){
|
||||
throw new AsrException("Unsupported model");
|
||||
}
|
||||
SpeechRecognizer model = null;
|
||||
try {
|
||||
model = (SpeechRecognizer) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerModel(AsrModelEnum.WHISPER, WhisperRecognizer.class);
|
||||
registerModel(AsrModelEnum.VOSK, VoskRecognizer.class);
|
||||
log.debug("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,342 @@
|
||||
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 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 java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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 {
|
||||
//加载自定义依赖库
|
||||
if(Objects.nonNull(config.getLibPath())){
|
||||
System.load(config.getLibPath().toAbsolutePath().toString());
|
||||
}
|
||||
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();
|
||||
String temp = "";
|
||||
while ((nbytes = ais.read(b)) >= 0) {
|
||||
if (recognizer.acceptWaveForm(b, nbytes)) {
|
||||
String result = recognizer.getResult();
|
||||
// log.info("result:{}", result);
|
||||
AsrSegment segment = parseSegment(result, params);
|
||||
if(segment != null){
|
||||
segments.add(segment);
|
||||
text.append(segment.getText());
|
||||
}
|
||||
}else{
|
||||
temp = recognizer.getPartialResult();
|
||||
// log.info("temp:{}", temp);
|
||||
}
|
||||
}
|
||||
if(StringUtils.isNotBlank(temp)){
|
||||
AsrSegment segment = parsePartialSegment(temp, params);
|
||||
if(segment != null){
|
||||
segments.add(segment);
|
||||
text.append(segment.getText());
|
||||
}
|
||||
}
|
||||
//补全结果
|
||||
String finalText = recognizer.getFinalResult();
|
||||
if(StringUtils.isNotBlank(text.toString()) && StringUtils.isNotBlank(finalText)){
|
||||
AsrSegment finalSegment = parseSegment(finalText, params);
|
||||
if(finalSegment != null){
|
||||
//需要补全
|
||||
if(!text.toString().endsWith(finalSegment.getText())){
|
||||
AsrSegment alignSegment = VoskRecognizer.alignSegment(segments.get(segments.size() - 1), finalSegment);
|
||||
//如果匹配失败,则直接使用最终片段
|
||||
if(alignSegment != null){
|
||||
segments.set(segments.size() - 1,alignSegment);
|
||||
}else{
|
||||
segments.set(segments.size() - 1,finalSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String result = segments.stream()
|
||||
.map(AsrSegment::getText)
|
||||
.collect(Collectors.joining("\n"));
|
||||
return R.ok(new AsrResult(result, segments));
|
||||
} catch (IOException e) {
|
||||
throw new AsrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析结果
|
||||
* @param segment
|
||||
* @return
|
||||
*/
|
||||
private AsrSegment parseSegment(String segment, RecParams params) {
|
||||
JsonObject json = GSON.fromJson(segment, JsonObject.class);
|
||||
JsonArray resultArray = json.getAsJsonArray("result");
|
||||
if(Objects.isNull(resultArray) || resultArray.size() == 0){
|
||||
return null;
|
||||
}
|
||||
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();
|
||||
if(Objects.nonNull(params.getLanguage()) && params.getLanguage() == Language.ZH){
|
||||
text = text.replace(" ", "");
|
||||
}
|
||||
return new AsrSegment(text, startMs, endMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析结果
|
||||
* @param segment
|
||||
* @return
|
||||
*/
|
||||
private AsrSegment parsePartialSegment(String segment, RecParams params) {
|
||||
JsonObject json = GSON.fromJson(segment, JsonObject.class);
|
||||
JsonArray resultArray = json.getAsJsonArray("partial_result");
|
||||
if(Objects.isNull(resultArray) || resultArray.size() == 0){
|
||||
return null;
|
||||
}
|
||||
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("partial").getAsString();
|
||||
if(Objects.nonNull(params.getLanguage()) && params.getLanguage() == Language.ZH){
|
||||
text = text.replace(" ", "");
|
||||
}
|
||||
return new AsrSegment(text, startMs, endMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 补全
|
||||
* @param shortSeg
|
||||
* @param longSeg
|
||||
* @return
|
||||
*/
|
||||
public static AsrSegment alignSegment(AsrSegment shortSeg, AsrSegment longSeg) {
|
||||
String shortText = shortSeg.getText();
|
||||
String longText = longSeg.getText();
|
||||
|
||||
int index = longText.indexOf(shortText);
|
||||
if (index == -1) {
|
||||
// log.debug("短文本不在长文本中");
|
||||
return null;
|
||||
}
|
||||
String resultText = longText.substring(index);
|
||||
return new AsrSegment(resultText, shortSeg.getStartTime(), longSeg.getEndTime());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建识别器
|
||||
* @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(true);
|
||||
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);
|
||||
return recognizeAudioStream(ais, params);
|
||||
} 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");
|
||||
// log.info("tempFile:{}", tempFile.getAbsolutePath());
|
||||
} 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 R.fail(R.Status.Unknown);
|
||||
}
|
||||
|
||||
@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,219 @@
|
||||
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 {
|
||||
//加载自定义依赖库
|
||||
if(Objects.nonNull(config.getLibPath())){
|
||||
System.setProperty("io.github.givimad.whisperjni.libdir",config.getLibPath().toAbsolutePath().toString());
|
||||
}
|
||||
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(Objects.isNull(whisperParams.getParams().language)){
|
||||
return R.fail(1003, "请指定语言");
|
||||
}
|
||||
//不是英语,需要检查是否是多语言模型
|
||||
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
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
212
speech/src/main/java/cn/smartjavaai/speech/utils/AudioUtils.java
Normal file
212
speech/src/main/java/cn/smartjavaai/speech/utils/AudioUtils.java
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
218
speech/src/test/java/Test.java
Normal file
218
speech/src/test/java/Test.java
Normal file
@@ -0,0 +1,218 @@
|
||||
import ai.djl.util.JsonUtils;
|
||||
import cn.smartjavaai.common.entity.Language;
|
||||
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.AsrSegment;
|
||||
import cn.smartjavaai.speech.asr.entity.RecParams;
|
||||
import cn.smartjavaai.speech.asr.entity.WhisperParams;
|
||||
import cn.smartjavaai.speech.asr.enums.AsrModelEnum;
|
||||
import cn.smartjavaai.speech.asr.model.VoskRecognizer;
|
||||
import cn.smartjavaai.speech.asr.model.WhisperRecognizer;
|
||||
import io.github.givimad.whisperjni.WhisperFullParams;
|
||||
import io.github.givimad.whisperjni.WhisperSamplingStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import ws.schild.jave.Encoder;
|
||||
import ws.schild.jave.EncoderException;
|
||||
import ws.schild.jave.InputFormatException;
|
||||
import ws.schild.jave.MultimediaObject;
|
||||
import ws.schild.jave.encode.AudioAttributes;
|
||||
import ws.schild.jave.encode.EncodingAttributes;
|
||||
import ws.schild.jave.info.AudioInfo;
|
||||
import ws.schild.jave.info.MultimediaInfo;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/8/1
|
||||
*/
|
||||
@Slf4j
|
||||
public class Test {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
// System.out.println("TMPDIR = " + System.getProperty("java.io.tmpdir"));
|
||||
//
|
||||
//// System.setProperty("io.github.givimad.whisperjni.libdir","/Users/wenjie/smartjavaai_cache/whisper");
|
||||
// WhisperRecognizer whisperRecognizer = new WhisperRecognizer();
|
||||
// AsrModelConfig config = new AsrModelConfig();
|
||||
// config.setModelEnum(AsrModelEnum.WHISPER);
|
||||
//// config.setModelPath("/Users/wenjie/Downloads/ggml-medium.bin");
|
||||
// config.setModelPath("/Users/wenjie/Documents/develop/model/speech/ggml-medium.bin");
|
||||
// whisperRecognizer.loadModel(config);
|
||||
// WhisperParams params = new WhisperParams();
|
||||
// WhisperFullParams params1 = new WhisperFullParams(WhisperSamplingStrategy.BEAN_SEARCH);
|
||||
//// params1.detectLanguage = true;
|
||||
// params1.language = Language.ZH.getCode();
|
||||
// //params1.translate = true;
|
||||
// params1.initialPrompt = "语音模型";
|
||||
// params.setParams(params1);
|
||||
//// params1.printTimestamps = true;
|
||||
// params1.printRealtime = true;
|
||||
// //params.setLanguage(Language.ZH);
|
||||
// R<AsrResult> result = whisperRecognizer.recognize("/Users/wenjie/Downloads/友谊大街.m4a",params);
|
||||
// if (result.isSuccess()){
|
||||
// System.out.println("结果:" + JsonUtils.toJson(result.getData()));
|
||||
// }else{
|
||||
// System.out.println(result.getMessage());
|
||||
// }
|
||||
//
|
||||
// while (true){
|
||||
// try {
|
||||
// Thread.sleep(10);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
|
||||
testVosk();
|
||||
|
||||
// testConvert();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void testVosk(){
|
||||
System.load("/Users/wenjie/Downloads/vosk-arrch64-dylib-main/libvosk.dylib");
|
||||
VoskRecognizer voskRecognizer = new VoskRecognizer();
|
||||
AsrModelConfig config = new AsrModelConfig();
|
||||
config.setModelEnum(AsrModelEnum.VOSK);
|
||||
config.setModelPath("/Users/wenjie/Documents/develop/model/speech/vosk-model-cn-0.22");
|
||||
voskRecognizer.loadModel(config);
|
||||
|
||||
R<AsrResult> result = voskRecognizer.recognize("/Users/wenjie/Documents/idea_workplace/SmartJavaAI/examples/speech-examples/src/main/resources/lff_zh.mp3");
|
||||
if (result.isSuccess()){
|
||||
System.out.println("结果:" + JsonUtils.toJson(result.getData()));
|
||||
}else{
|
||||
System.out.println(result.getMessage());
|
||||
}
|
||||
|
||||
// while (true){
|
||||
// try {
|
||||
// Thread.sleep(10);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
public static void testConvert(){
|
||||
getAudioFormatConversionIns("/Users/wenjie/Downloads/中国建设银行(包头当代支行).m4a","/Users/wenjie/Downloads/test1.wav","wav");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 音频格式转换
|
||||
*
|
||||
* @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) {
|
||||
log.error("音频格式转换异常:" + e.getMessage(), e);
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
if (fis != null) {
|
||||
fis.close();
|
||||
}
|
||||
if (bos != null) {
|
||||
bos.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("音频格式转换资源关闭异常:" + 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) {
|
||||
try {
|
||||
File sourceFile = new File(sourceFilePath);
|
||||
if (sourceFile.isFile()) {
|
||||
File targetFile = new File(targetFilePath);
|
||||
|
||||
// 音频格式转换
|
||||
audioFormatConversion(sourceFile, targetFile, format);
|
||||
|
||||
return new FileInputStream(targetFile);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("音频格式转换异常:" + e.getMessage(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频格式转换
|
||||
* @param source 源音频文件
|
||||
* @param target 输出的音频文件
|
||||
* @param format wav/mp3/amr
|
||||
*/
|
||||
public static void audioFormatConversion(File source,File target,String format) {
|
||||
try {
|
||||
//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:
|
||||
log.error("音频格式不合法!");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
} catch (Exception e) {
|
||||
log.error("音频格式转换异常:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user