diff --git a/pom.xml b/pom.xml index 4142bfb..2a2b9cb 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ smartjavaai-all smartjavaai-ocr smartjavaai-bom + smartjavaai-speech @@ -25,7 +26,6 @@ 8 UTF-8 0.32.0 - diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Language.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Language.java new file mode 100644 index 0000000..8ee3deb --- /dev/null +++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Language.java @@ -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; + } + +} diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/quality/Seetaface6QualityModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/quality/Seetaface6QualityModel.java index 7dd8469..976b20b 100644 --- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/quality/Seetaface6QualityModel.java +++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/quality/Seetaface6QualityModel.java @@ -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核心数 } diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/config/DetectorModelConfig.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/config/DetectorModelConfig.java index b0b1a24..1d89dcb 100644 --- a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/config/DetectorModelConfig.java +++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/config/DetectorModelConfig.java @@ -43,7 +43,7 @@ public class DetectorModelConfig extends ModelConfig { private int maxBox; /** - * 允许的类列表 + * 允许的分类列表 */ private List allowedClasses; diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/PlateRecModelConfig.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/PlateRecModelConfig.java index a8af403..d5be142 100644 --- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/PlateRecModelConfig.java +++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/PlateRecModelConfig.java @@ -28,20 +28,6 @@ public class PlateRecModelConfig extends ModelConfig { */ private PlateDetModel plateDetModel; - /** - * 置信度阈值 - */ - private float confidenceThreshold; - - /** - * iou阈值 - */ - private float iouThreshold; - - /** - * 检测结果数量 - */ - private int topK; } diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/OcrCommonRecModelImpl.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/OcrCommonRecModelImpl.java index 7fd4806..9f6c6f7 100644 --- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/OcrCommonRecModelImpl.java +++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/OcrCommonRecModelImpl.java @@ -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 diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/plate/translator/Yolo5PlateDetectTranslator.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/plate/translator/Yolo5PlateDetectTranslator.java index 899b964..480a0ae 100644 --- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/plate/translator/Yolo5PlateDetectTranslator.java +++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/plate/translator/Yolo5PlateDetectTranslator.java @@ -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 topK) { diff --git a/smartjavaai-speech/pom.xml b/smartjavaai-speech/pom.xml new file mode 100644 index 0000000..f56f5a1 --- /dev/null +++ b/smartjavaai-speech/pom.xml @@ -0,0 +1,166 @@ + + + 4.0.0 + + cn.smartjavaai + smartjavaai-parent + 1.0.22 + + + smartjavaai-speech + + + 11 + 11 + UTF-8 + + + + + cn.smartjavaai + smartjavaai-common + ${project.version} + + + + + net.java.dev.jna + jna + 5.13.0 + + + + io.github.givimad + whisper-jni + 1.7.1 + + + + com.alphacephei + vosk + 0.3.45 + + + + ws.schild + jave-core + 3.5.0 + + + + ws.schild + jave-nativebin-osxm1 + 3.5.0 + + + + + 1.0.22 + smartjavaai-speech + SmartJavaAI + https://github.com/geekwenjie/SmartJavaAI + + + MIT License + https://opensource.org/licenses/MIT + + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.4.0 + true + + dengwenjie + true + ${project.groupId}:${project.artifactId}:${project.version} + + + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.0 + + + none + + -Xdoclint:none + + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.1.0 + + + sign-artifacts + verify + + sign + + + + + + + + + + scm:git:git://github.com/geekwenjie/SmartJavaAI.git + scm:git:ssh://github.com/geekwenjie/SmartJavaAI.git + http://github.com/geekwenjie/SmartJavaAI/tree/master + + + + + + dengwenjie + https://s01.oss.sonatype.org/content/repositories/snapshots + + + dengwenjie + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + dengwenjie + 775747758@qq.com + + Project Manager + Architect + + + + + + diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/audio/SmartAudioFactory.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/audio/SmartAudioFactory.java new file mode 100644 index 0000000..2312b6e --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/audio/SmartAudioFactory.java @@ -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; + } + +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/config/AsrModelConfig.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/config/AsrModelConfig.java new file mode 100644 index 0000000..9ae36aa --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/config/AsrModelConfig.java @@ -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; +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/AsrResult.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/AsrResult.java new file mode 100644 index 0000000..854319f --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/AsrResult.java @@ -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 segments; + + + public AsrResult() { + } + + public AsrResult(String text) { + this.text = text; + } + + public AsrResult(String text, List segments) { + this.text = text; + this.segments = segments; + } + +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/AsrSegment.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/AsrSegment.java new file mode 100644 index 0000000..05ea172 --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/AsrSegment.java @@ -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() { + } +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/RecParams.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/RecParams.java new file mode 100644 index 0000000..c5706b2 --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/RecParams.java @@ -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; + } + + +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/VoskParams.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/VoskParams.java new file mode 100644 index 0000000..298ce16 --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/VoskParams.java @@ -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; + + +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/WhisperParams.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/WhisperParams.java new file mode 100644 index 0000000..ffb5e22 --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/entity/WhisperParams.java @@ -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 参数 + } + } +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/enums/AsrModelEnum.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/enums/AsrModelEnum.java new file mode 100644 index 0000000..7ce905e --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/enums/AsrModelEnum.java @@ -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); + } + +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/exception/AsrException.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/exception/AsrException.java new file mode 100644 index 0000000..00cf9c1 --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/exception/AsrException.java @@ -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); + } + +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/model/SpeechRecognizer.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/model/SpeechRecognizer.java new file mode 100644 index 0000000..d158610 --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/model/SpeechRecognizer.java @@ -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 recognize(String audioPath, RecParams params){ + throw new UnsupportedOperationException("默认不支持该功能"); + } + + default R recognize(byte[] audioData, RecParams params){ + throw new UnsupportedOperationException("默认不支持该功能"); + } + + default R recognize(InputStream audioStream, RecParams params){ + throw new UnsupportedOperationException("默认不支持该功能"); + } + + default R recognize(String audioPath){ + throw new UnsupportedOperationException("默认不支持该功能"); + } + + default R recognize(byte[] audioData){ + throw new UnsupportedOperationException("默认不支持该功能"); + } + + default R recognize(InputStream audioStream){ + throw new UnsupportedOperationException("默认不支持该功能"); + } + +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/model/VoskRecognizer.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/model/VoskRecognizer.java new file mode 100644 index 0000000..2a94a0f --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/model/VoskRecognizer.java @@ -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 recognize(String audioPath) { + return recognize(audioPath, new VoskParams()); + } + + + @Override + public R recognize(byte[] audioData) { + return recognize(audioData, new VoskParams()); + } + + @Override + public R recognize(InputStream audioStream) { + return recognize(audioStream, new VoskParams()); + } + + private R 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 segments = new ArrayList(); + 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 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 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 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); + } + } + + +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/model/WhisperRecognizer.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/model/WhisperRecognizer.java new file mode 100644 index 0000000..e6eb3e8 --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/model/WhisperRecognizer.java @@ -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 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 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 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 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 segments = new ArrayList(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 recognize(String audioPath) { + return recognize(audioPath, new WhisperParams()); + } + + @Override + public R recognize(byte[] audioData) { + return recognize(audioData, new WhisperParams()); + } + + @Override + public R 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 + ); + } +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/pool/WhisperStatePool.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/pool/WhisperStatePool.java new file mode 100644 index 0000000..ebea0f4 --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/asr/pool/WhisperStatePool.java @@ -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 { + + public WhisperStatePool(WhisperJNI whisperInstance, WhisperContext context) { + super(new PooledObjectFactory(){ + + @Override + public void activateObject(PooledObject pooledObject) throws Exception { + + } + + @Override + public void destroyObject(PooledObject pooledObject) throws Exception { + pooledObject.getObject().close(); + } + + @Override + public PooledObject makeObject() throws Exception { + WhisperState state = whisperInstance.initState(context); + return new DefaultPooledObject(state); + } + + @Override + public void passivateObject(PooledObject pooledObject) throws Exception { + + } + + @Override + public boolean validateObject(PooledObject pooledObject) { + return false; + } + }); + } +} diff --git a/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/utils/AudioUtils.java b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/utils/AudioUtils.java new file mode 100644 index 0000000..8881096 --- /dev/null +++ b/smartjavaai-speech/src/main/java/cn/smartjavaai/speech/utils/AudioUtils.java @@ -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); + } + + + +}