package com.vetti.socket; import com.vetti.common.ai.elevenLabs.ElevenLabsClient; import com.vetti.common.ai.gpt.ChatGPTClient; import com.vetti.common.ai.whisper.WhisperClient; import com.vetti.common.config.RuoYiConfig; import com.vetti.common.utils.spring.SpringUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * 语音面试 web处理器 */ @Slf4j @ServerEndpoint("/voice-websocket/{clientId}") @Component public class ChatWebSocketHandler { // 语音文件保存目录 private static final String VOICE_STORAGE_DIR = "/voice_files/"; // 语音结果文件保存目录 private static final String VOICE_STORAGE_RESULT_DIR = "/voice_result_files/"; public ChatWebSocketHandler() { // 初始化存储目录 File dir = new File(RuoYiConfig.getProfile() + VOICE_STORAGE_DIR); if (!dir.exists()) { dir.mkdirs(); } File resultDir = new File(RuoYiConfig.getProfile() + VOICE_STORAGE_RESULT_DIR); if (!resultDir.exists()) { resultDir.mkdirs(); } } // 连接建立时调用 @OnOpen public void onOpen(Session session, @PathParam("clientId") String clientId) { log.info("WebSocket 链接已建立:{}", clientId); //创建会话 } // 接收文本消息 @OnMessage public void onTextMessage(Session session, String message) { System.out.println("接收到文本消息: " + message); // 可以在这里处理文本流数据 try { // 发送响应 session.getBasicRemote().sendText("已收到文本: " + message); } catch (IOException e) { e.printStackTrace(); } } // 接收二进制消息(流数据) @OnMessage public void onBinaryMessage(Session session, @PathParam("clientId") String clientId, ByteBuffer byteBuffer) { try { log.info("客户端ID为:{}", clientId); // 处理二进制流数据 byte[] bytes = new byte[byteBuffer.remaining()]; //从缓冲区中读取数据并存储到指定的字节数组中 byteBuffer.get(bytes); // 生成唯一文件名 String fileName = clientId + "_" + System.currentTimeMillis() + ".webm"; String pathUrl = RuoYiConfig.getProfile()+VOICE_STORAGE_DIR + fileName; // String pathUrl = "/Users/wangxiangshun/Desktop/0.8733346782733291.webm"; log.info("文件路径为:{}", pathUrl); saveAsWebM(bytes, pathUrl); //拿到文件进行文字转换 WhisperClient whisperClient = SpringUtils.getBean(WhisperClient.class); String resultText = whisperClient.handleVoiceToText(pathUrl); //把提问的文字发送给CPT ChatGPTClient chatGPTClient = SpringUtils.getBean(ChatGPTClient.class); String resultMsg = chatGPTClient.handleAiChat(resultText); //把结果文字转成语音文件 //生成文件 // 生成唯一文件名 String resultFileName = clientId + "_" + System.currentTimeMillis() + ".webm"; String resultPathUrl = RuoYiConfig.getProfile() + VOICE_STORAGE_RESULT_DIR + resultFileName; ElevenLabsClient elevenLabsClient = SpringUtils.getBean(ElevenLabsClient.class); elevenLabsClient.handleTextToVoice(resultMsg, resultPathUrl); //把语音文件转换成流,发送给前端 System.out.println("接收到二进制数据,长度: " + bytes.length + " bytes"); try { //文件转换成文件流 ByteBuffer outByteBuffer = convertFileToByteBuffer(resultPathUrl); session.getBasicRemote().sendBinary(outByteBuffer); // 发送响应确认 session.getBasicRemote().sendText("已收到二进制数据,长度: " + bytes.length); } catch (IOException e) { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } } // 连接关闭时调用 @OnClose public void onClose(Session session, CloseReason reason) { System.out.println("WebSocket连接已关闭: " + session.getId() + ", 原因: " + reason.getReasonPhrase()); } // 发生错误时调用 @OnError public void onError(Session session, Throwable throwable) { System.err.println("WebSocket错误发生: " + throwable.getMessage()); throwable.printStackTrace(); } /** * 将字节数组保存为WebM文件 * * @param byteData 包含WebM数据的字节数组 * @param filePath 目标文件路径 * @return 操作是否成功 */ private boolean saveAsWebM(byte[] byteData, String filePath) { // 检查输入参数 if (byteData == null || byteData.length == 0) { System.err.println("字节数组为空,无法生成WebM文件"); return false; } if (filePath == null || filePath.trim().isEmpty()) { System.err.println("文件路径不能为空"); return false; } // 确保文件以.webm结尾 if (!filePath.toLowerCase().endsWith(".webm")) { filePath += ".webm"; } FileOutputStream fos = null; try { fos = new FileOutputStream(filePath); fos.write(byteData); fos.flush(); System.out.println("WebM文件已成功生成: " + filePath); return true; } catch (IOException e) { System.err.println("写入文件时发生错误: " + e.getMessage()); e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } return false; } /** * File 转换成 ByteBuffer * * @param fileUrl 文件路径 * @return */ private ByteBuffer convertFileToByteBuffer(String fileUrl) { File file = new File(fileUrl); // 使用RandomAccessFile获取FileChannel try (RandomAccessFile raf = new RandomAccessFile(file, "r"); FileChannel channel = raf.getChannel()) { // 分配与文件大小相同的ByteBuffer ByteBuffer buffer = ByteBuffer.allocate((int) channel.size()); // 从通道读取数据到缓冲区 channel.read(buffer); // 切换为读模式 buffer.flip(); return buffer; } catch (Exception e) { e.printStackTrace(); } return null; } }