199 lines
6.9 KiB
Java
199 lines
6.9 KiB
Java
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;
|
||
}
|
||
|
||
}
|
||
|