|  | @@ -1,9 +1,11 @@
 | 
	
		
			
				|  |  |  package com.fs.app.service;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import com.alibaba.fastjson.JSON;
 | 
	
		
			
				|  |  | +import com.fs.common.core.redis.RedisCache;
 | 
	
		
			
				|  |  |  import com.fs.common.utils.DateUtils;
 | 
	
		
			
				|  |  |  import com.fs.company.service.ICompanyConfigService;
 | 
	
		
			
				|  |  |  import com.fs.qw.domain.*;
 | 
	
		
			
				|  |  | +import com.fs.qw.mapper.QwCompanyMapper;
 | 
	
		
			
				|  |  |  import com.fs.qw.mapper.QwUserMapper;
 | 
	
		
			
				|  |  |  import com.fs.qw.param.QwAutoRulesTagsParams;
 | 
	
		
			
				|  |  |  import com.fs.qw.service.*;
 | 
	
	
		
			
				|  | @@ -12,17 +14,33 @@ import com.fs.qwApi.Result.QwOpenidResult;
 | 
	
		
			
				|  |  |  import com.fs.qwApi.domain.QwResult;
 | 
	
		
			
				|  |  |  import com.fs.qwApi.param.QwEditUserTagParam;
 | 
	
		
			
				|  |  |  import com.fs.qwApi.param.QwOpenidByUserParams;
 | 
	
		
			
				|  |  | +import com.google.gson.JsonObject;
 | 
	
		
			
				|  |  | +import com.google.gson.JsonParser;
 | 
	
		
			
				|  |  | +import com.tencent.wework.Finance;
 | 
	
		
			
				|  |  | +import lombok.extern.slf4j.Slf4j;
 | 
	
		
			
				|  |  | +import org.json.JSONObject;
 | 
	
		
			
				|  |  |  import org.slf4j.LoggerFactory;
 | 
	
		
			
				|  |  |  import org.springframework.beans.factory.annotation.Autowired;
 | 
	
		
			
				|  |  |  import org.springframework.scheduling.annotation.Async;
 | 
	
		
			
				|  |  |  import org.springframework.stereotype.Service;
 | 
	
		
			
				|  |  | +import org.vosk.LibVosk;
 | 
	
		
			
				|  |  | +import org.vosk.LogLevel;
 | 
	
		
			
				|  |  | +import org.vosk.Model;
 | 
	
		
			
				|  |  | +import org.vosk.Recognizer;
 | 
	
		
			
				|  |  |  import org.w3c.dom.Document;
 | 
	
		
			
				|  |  |  import org.w3c.dom.Element;
 | 
	
		
			
				|  |  |  import org.w3c.dom.NodeList;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +import javax.crypto.Cipher;
 | 
	
		
			
				|  |  | +import java.io.*;
 | 
	
		
			
				|  |  | +import java.nio.charset.StandardCharsets;
 | 
	
		
			
				|  |  | +import java.security.KeyFactory;
 | 
	
		
			
				|  |  | +import java.security.PrivateKey;
 | 
	
		
			
				|  |  | +import java.security.spec.PKCS8EncodedKeySpec;
 | 
	
		
			
				|  |  |  import java.text.SimpleDateFormat;
 | 
	
		
			
				|  |  |  import java.util.*;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +@Slf4j
 | 
	
		
			
				|  |  |  @Service
 | 
	
		
			
				|  |  |  public class QwDataCallbackService {
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -55,6 +73,10 @@ public class QwDataCallbackService {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      @Autowired
 | 
	
		
			
				|  |  |      private IQwDeptService qwDeptService;
 | 
	
		
			
				|  |  | +    @Autowired
 | 
	
		
			
				|  |  | +    private RedisCache redisCache;
 | 
	
		
			
				|  |  | +    @Autowired
 | 
	
		
			
				|  |  | +    private QwCompanyMapper qwCompanyMapper;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -476,6 +498,288 @@ public class QwDataCallbackService {
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * 拉取存档内容
 | 
	
		
			
				|  |  | +     * @param
 | 
	
		
			
				|  |  | +     * @return 聊天内容列表
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    public List<String> getChatData(String corpId) {
 | 
	
		
			
				|  |  | +        QwCompany qwCompany= qwCompanyMapper.selectQwCompanyByCorpId(corpId);
 | 
	
		
			
				|  |  | +        // 初始化 SDK
 | 
	
		
			
				|  |  | +        long sdk = Finance.NewSdk();
 | 
	
		
			
				|  |  | +        long slice = Finance.NewSlice();
 | 
	
		
			
				|  |  | +        List<String> chatContents = new ArrayList<>();
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +            long seq = 0L;
 | 
	
		
			
				|  |  | +            if (redisCache.redisTemplate.hasKey("seq:"+corpId+":")) {
 | 
	
		
			
				|  |  | +                seq  = redisCache.getCacheObject("seq:"+corpId+":");
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            System.out.println("seq:"+seq);
 | 
	
		
			
				|  |  | +            // SDK 初始化参数
 | 
	
		
			
				|  |  | +            String secret = qwCompany.getMsgSecret();
 | 
	
		
			
				|  |  | +            long limit = 1000L;  // 一次最多拉取 1000 条
 | 
	
		
			
				|  |  | +            String proxy = "";
 | 
	
		
			
				|  |  | +            String passwd = "";
 | 
	
		
			
				|  |  | +            long timeout = 15L;
 | 
	
		
			
				|  |  | +            long ret = Finance.Init(sdk, corpId, secret);
 | 
	
		
			
				|  |  | +            if (ret != 0) {
 | 
	
		
			
				|  |  | +                Finance.DestroySdk(sdk);
 | 
	
		
			
				|  |  | +                System.out.println("初始化 SDK 失败,错误码1:" + ret);
 | 
	
		
			
				|  |  | +                return chatContents;
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            // 拉取会话存档数据
 | 
	
		
			
				|  |  | +            ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
 | 
	
		
			
				|  |  | +            if (ret != 0) {
 | 
	
		
			
				|  |  | +                System.out.println("拉取会话存档失败,错误码:" + ret);
 | 
	
		
			
				|  |  | +                return chatContents;
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            // 获取 JSON 内容
 | 
	
		
			
				|  |  | +            String jsonContent = Finance.GetContentFromSlice(slice);
 | 
	
		
			
				|  |  | +            if (jsonContent == null || jsonContent.isEmpty()) {
 | 
	
		
			
				|  |  | +                System.out.println("没有获取到有效的会话存档数据");
 | 
	
		
			
				|  |  | +                return chatContents;
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            // 解析 JSON 内容
 | 
	
		
			
				|  |  | +            JSONObject json = new JSONObject(jsonContent);
 | 
	
		
			
				|  |  | +            System.out.println("解析的内容"+json.toString());
 | 
	
		
			
				|  |  | +            if (json.has("chatdata")) {
 | 
	
		
			
				|  |  | +                for (Object item : json.getJSONArray("chatdata")) {
 | 
	
		
			
				|  |  | +                    JSONObject chatItem = (JSONObject) item;
 | 
	
		
			
				|  |  | +                    String encrypt_random_key = chatItem.getString("encrypt_random_key"); // 获取加密的随机密钥
 | 
	
		
			
				|  |  | +                    String encrypt_chat_msg = chatItem.getString("encrypt_chat_msg"); // 获取加密消息
 | 
	
		
			
				|  |  | +                    // 更新下一次拉取的 seq
 | 
	
		
			
				|  |  | +                    if (chatItem.has("seq")) {
 | 
	
		
			
				|  |  | +                        long nextSeq = chatItem.getLong("seq");
 | 
	
		
			
				|  |  | +                        System.out.println("将 seq 存入 Redis,key: seq:, value: " + nextSeq);
 | 
	
		
			
				|  |  | +                        redisCache.setCacheObject("seq:"+corpId+":", nextSeq); // 确保 `seq` 更新到缓存
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    // 使用私钥解密 encrypt_random_key 获取 encrypt_key
 | 
	
		
			
				|  |  | +                    String encrypt_key = decryptRandomKey(encrypt_random_key,qwCompany.getMsgPrivateKey());
 | 
	
		
			
				|  |  | +                    if (encrypt_key != null) {
 | 
	
		
			
				|  |  | +                        // 解密加密消息
 | 
	
		
			
				|  |  | +                        String decryptedMsg = decryptChatMessage(sdk, encrypt_key, encrypt_chat_msg);
 | 
	
		
			
				|  |  | +                        // 解析 JSON 字符串
 | 
	
		
			
				|  |  | +                        JsonObject jsonObject = JsonParser.parseString(decryptedMsg).getAsJsonObject();
 | 
	
		
			
				|  |  | +                        String plaintext = Finance.GetContentFromSlice(slice);
 | 
	
		
			
				|  |  | +                        // 获取 msgtype 的值
 | 
	
		
			
				|  |  | +                        String msgtype = jsonObject.get("msgtype").getAsString();
 | 
	
		
			
				|  |  | +                        // 处理媒体消息
 | 
	
		
			
				|  |  | +                        if (msgtype.equals("voice")) {
 | 
	
		
			
				|  |  | +                            getMedia(sdk,decryptedMsg,corpId,secret);
 | 
	
		
			
				|  |  | +                        }
 | 
	
		
			
				|  |  | +                        System.out.println("解密后的消息:" + decryptedMsg);
 | 
	
		
			
				|  |  | +                        chatContents.add(decryptedMsg);
 | 
	
		
			
				|  |  | +                    } else {
 | 
	
		
			
				|  |  | +                        System.out.println("解密随机密钥失败");
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        } catch (Exception e) {
 | 
	
		
			
				|  |  | +            e.printStackTrace();
 | 
	
		
			
				|  |  | +        } finally {
 | 
	
		
			
				|  |  | +            Finance.FreeSlice(slice);
 | 
	
		
			
				|  |  | +            Finance.DestroySdk(sdk);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        return chatContents;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * 解密加密的随机密钥
 | 
	
		
			
				|  |  | +     * @param encryptedRandomKey 加密的随机密钥
 | 
	
		
			
				|  |  | +     * @return 解密后的随机密钥
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    public String decryptRandomKey(String encryptedRandomKey,String msgPrivateKey) {
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +            // 加载私钥
 | 
	
		
			
				|  |  | +            PrivateKey privateKey = loadPrivateKey(msgPrivateKey);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            // 使用 RSA 解密
 | 
	
		
			
				|  |  | +            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
 | 
	
		
			
				|  |  | +            cipher.init(Cipher.DECRYPT_MODE, privateKey);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            // 解密数据
 | 
	
		
			
				|  |  | +            byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedRandomKey));
 | 
	
		
			
				|  |  | +            return new String(decryptedBytes, StandardCharsets.UTF_8);
 | 
	
		
			
				|  |  | +        } catch (Exception e) {
 | 
	
		
			
				|  |  | +            e.printStackTrace();
 | 
	
		
			
				|  |  | +            return null;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * 获取私钥
 | 
	
		
			
				|  |  | +     * @return 私钥
 | 
	
		
			
				|  |  | +     * @throws Exception 异常
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    public PrivateKey loadPrivateKey(String privateKey) throws Exception {
 | 
	
		
			
				|  |  | +        // 解析 PEM 格式的私钥
 | 
	
		
			
				|  |  | +        String privateKeyPEM = privateKey
 | 
	
		
			
				|  |  | +                .replace("-----BEGIN PRIVATE KEY-----", "")
 | 
	
		
			
				|  |  | +                .replace("-----END PRIVATE KEY-----", "")
 | 
	
		
			
				|  |  | +                .replaceAll("\\s", "");
 | 
	
		
			
				|  |  | +        byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM);
 | 
	
		
			
				|  |  | +        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
 | 
	
		
			
				|  |  | +        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
 | 
	
		
			
				|  |  | +        return keyFactory.generatePrivate(keySpec);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * 解密会话消息
 | 
	
		
			
				|  |  | +     * @param sdk SDK
 | 
	
		
			
				|  |  | +     * @param encrypt_key 解密的密钥
 | 
	
		
			
				|  |  | +     * @param encrypt_chat_msg 加密的聊天消息
 | 
	
		
			
				|  |  | +     * @return 解密后的消息内容
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    public String decryptChatMessage(long sdk, String encrypt_key, String encrypt_chat_msg) {
 | 
	
		
			
				|  |  | +        String decryptedMsg = "";
 | 
	
		
			
				|  |  | +        long msg = Finance.NewSlice();
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +            // 解密消息
 | 
	
		
			
				|  |  | +            int ret = Finance.DecryptData(sdk, encrypt_key, encrypt_chat_msg, msg);
 | 
	
		
			
				|  |  | +            if (ret == 0) {
 | 
	
		
			
				|  |  | +                decryptedMsg = Finance.GetContentFromSlice(msg); // 获取解密后的消息内容
 | 
	
		
			
				|  |  | +                decryptedMsg = new String(decryptedMsg.getBytes(), StandardCharsets.UTF_8);
 | 
	
		
			
				|  |  | +            } else {
 | 
	
		
			
				|  |  | +                System.out.println("解密失败,错误码:" + ret);
 | 
	
		
			
				|  |  | +                return null;  // 返回 null,避免上游加入错误数据
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        } catch (Exception e) {
 | 
	
		
			
				|  |  | +            e.printStackTrace();
 | 
	
		
			
				|  |  | +            return null;
 | 
	
		
			
				|  |  | +        } finally {
 | 
	
		
			
				|  |  | +            Finance.FreeSlice(msg);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        return decryptedMsg;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * 下载语音文件
 | 
	
		
			
				|  |  | +     * @param sdk SDK实例
 | 
	
		
			
				|  |  | +     * @param decryptedMsg 解密后的JSON字符串
 | 
	
		
			
				|  |  | +     * @return 转换以后的文字数据
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    public static String getMedia(long sdk, String decryptedMsg,String coreId,String secret) {
 | 
	
		
			
				|  |  | +        JsonObject jsonObject = JsonParser.parseString(decryptedMsg).getAsJsonObject();
 | 
	
		
			
				|  |  | +        if (!jsonObject.has("voice")) {
 | 
	
		
			
				|  |  | +            log.error("JSON 数据中缺少 voice 字段");
 | 
	
		
			
				|  |  | +            return null;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        JsonObject voiceObj = jsonObject.getAsJsonObject("voice");
 | 
	
		
			
				|  |  | +        String sdkFileId = voiceObj.get("sdkfileid").getAsString();
 | 
	
		
			
				|  |  | +        long mediaData = Finance.NewMediaData();
 | 
	
		
			
				|  |  | +        byte[] amrData = downloadVoice(sdk, sdkFileId, "", mediaData,coreId,secret);
 | 
	
		
			
				|  |  | +        Finance.FreeMediaData(mediaData);
 | 
	
		
			
				|  |  | +        if (amrData == null) {
 | 
	
		
			
				|  |  | +            return null;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        // 直接转换 AMR 数据到文本
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +            String s = convertAmrToText(amrData);
 | 
	
		
			
				|  |  | +            log.info("转换成功的文字{}",s);
 | 
	
		
			
				|  |  | +            return s;
 | 
	
		
			
				|  |  | +        } catch (IOException e) {
 | 
	
		
			
				|  |  | +            log.error("语音转换失败: {}", e.getMessage(), e);
 | 
	
		
			
				|  |  | +            return null;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * 递归下载语音文件
 | 
	
		
			
				|  |  | +     * @param sdk SDK实例
 | 
	
		
			
				|  |  | +     * @param sdkFileId 语音文件ID
 | 
	
		
			
				|  |  | +     * @param indexbuf 获取分片数据时的索引
 | 
	
		
			
				|  |  | +     * @param mediaData 存储语音数据的结构体
 | 
	
		
			
				|  |  | +     * @return 下载的语音文件字节
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private static byte[] downloadVoice(long sdk, String sdkFileId, String indexbuf, long mediaData, String corpId, String secret) {
 | 
	
		
			
				|  |  | +        long ret = Finance.Init(sdk, corpId, secret);
 | 
	
		
			
				|  |  | +        if (ret != 0) {
 | 
	
		
			
				|  |  | +            Finance.DestroySdk(sdk);
 | 
	
		
			
				|  |  | +            System.out.println("初始化 SDK 失败,错误码:" + ret);
 | 
	
		
			
				|  |  | +            return null;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        ret = Finance.GetMediaData(sdk, indexbuf, sdkFileId, "", "", 15L, mediaData);
 | 
	
		
			
				|  |  | +        if (ret != 0) {
 | 
	
		
			
				|  |  | +            log.error("获取语音数据失败: ret={}, sdkFileId={}", ret, sdkFileId);
 | 
	
		
			
				|  |  | +            return null;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        boolean isFinish = Finance.IsMediaDataFinish(mediaData) == 1;
 | 
	
		
			
				|  |  | +        String nextIndexBuf = Finance.GetOutIndexBuf(mediaData);
 | 
	
		
			
				|  |  | +        byte[] data = Finance.GetData(mediaData);
 | 
	
		
			
				|  |  | +        if (!isFinish) {
 | 
	
		
			
				|  |  | +            byte[] nextData = downloadVoice(sdk, sdkFileId, nextIndexBuf, mediaData,corpId,secret);
 | 
	
		
			
				|  |  | +            if (nextData != null) {
 | 
	
		
			
				|  |  | +                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 | 
	
		
			
				|  |  | +                try {
 | 
	
		
			
				|  |  | +                    outputStream.write(data);
 | 
	
		
			
				|  |  | +                    outputStream.write(nextData);
 | 
	
		
			
				|  |  | +                    return outputStream.toByteArray();
 | 
	
		
			
				|  |  | +                } catch (IOException e) {
 | 
	
		
			
				|  |  | +                    log.error("合并语音数据失败: {}", e.getMessage(), e);
 | 
	
		
			
				|  |  | +                    return null;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        return data;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * 使用 ffmpeg 将 AMR 文件转换为 WAV 格式
 | 
	
		
			
				|  |  | +     * @return WAV 格式的音频数据
 | 
	
		
			
				|  |  | +     * @throws IOException
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private static String convertAmrToText(byte[] amrData) throws IOException {
 | 
	
		
			
				|  |  | +        // 启动 ffmpeg 进行转换
 | 
	
		
			
				|  |  | +        ProcessBuilder processBuilder = new ProcessBuilder(
 | 
	
		
			
				|  |  | +                "ffmpeg",
 | 
	
		
			
				|  |  | +                "-i", "pipe:0",  // 从标准输入读取 AMR 数据
 | 
	
		
			
				|  |  | +                "-ac", "1",
 | 
	
		
			
				|  |  | +                "-ar", "16000",
 | 
	
		
			
				|  |  | +                "-sample_fmt", "s16",
 | 
	
		
			
				|  |  | +                "-f", "wav",
 | 
	
		
			
				|  |  | +                "pipe:1"
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +        processBuilder.redirectErrorStream(true);
 | 
	
		
			
				|  |  | +        Process process = processBuilder.start();
 | 
	
		
			
				|  |  | +        // 向 ffmpeg 传输 AMR 数据
 | 
	
		
			
				|  |  | +        try (OutputStream outputStream = process.getOutputStream()) {
 | 
	
		
			
				|  |  | +            outputStream.write(amrData);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        // 读取 ffmpeg 转换后的 WAV 数据
 | 
	
		
			
				|  |  | +        ByteArrayOutputStream wavOutput = new ByteArrayOutputStream();
 | 
	
		
			
				|  |  | +        try (InputStream inputStream = process.getInputStream()) {
 | 
	
		
			
				|  |  | +            byte[] buffer = new byte[4096];
 | 
	
		
			
				|  |  | +            int bytesRead;
 | 
	
		
			
				|  |  | +            while ((bytesRead = inputStream.read(buffer)) != -1) {
 | 
	
		
			
				|  |  | +                wavOutput.write(buffer, 0, bytesRead);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            int exitCode = process.waitFor();
 | 
	
		
			
				|  |  | +            if (exitCode != 0) {
 | 
	
		
			
				|  |  | +                throw new IOException("ffmpeg 转换失败,退出码:" + exitCode);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        } catch (InterruptedException e) {
 | 
	
		
			
				|  |  | +            throw new IOException("ffmpeg 转换过程中被中断", e);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        return recognizeSpeech(wavOutput.toByteArray());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * 使用 Vosk 识别音频
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private static String recognizeSpeech(byte[] wavData) throws IOException {
 | 
	
		
			
				|  |  | +        LibVosk.setLogLevel(LogLevel.DEBUG);
 | 
	
		
			
				|  |  | +        try (Model model = new Model("C:/vosk-model-small-cn-0.22");
 | 
	
		
			
				|  |  | +             //try (Model model = new Model("E:/vosk-model/vosk-model-small-cn-0.22");
 | 
	
		
			
				|  |  | +             InputStream ais = new ByteArrayInputStream(wavData);
 | 
	
		
			
				|  |  | +             Recognizer recognizer = new Recognizer(model, 16000)) {
 | 
	
		
			
				|  |  | +            byte[] buffer = new byte[4096];
 | 
	
		
			
				|  |  | +            int bytesRead;
 | 
	
		
			
				|  |  | +            while ((bytesRead = ais.read(buffer)) >= 0) {
 | 
	
		
			
				|  |  | +                recognizer.acceptWaveForm(buffer, bytesRead);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            return recognizer.getFinalResult();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  }
 |