socket 逻辑初始化
This commit is contained in:
3
.idea/compiler.xml
generated
3
.idea/compiler.xml
generated
@@ -11,8 +11,9 @@
|
||||
<module name="vetti-generator" />
|
||||
<module name="vetti-quartz" />
|
||||
<module name="vetti-system" />
|
||||
<module name="vetti-framework" />
|
||||
<module name="vetti-common" />
|
||||
<module name="vetti-framework" />
|
||||
<module name="vetti-ai" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
|
||||
2
.idea/encodings.xml
generated
2
.idea/encodings.xml
generated
@@ -5,6 +5,8 @@
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/vetti-admin/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/vetti-admin/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/vetti-ai/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/vetti-ai/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/vetti-common/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/vetti-common/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/vetti-framework/src/main/java" charset="UTF-8" />
|
||||
|
||||
15
pom.xml
15
pom.xml
@@ -80,6 +80,13 @@
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot WebSocket -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
<version>2.5.15</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 覆盖logback的依赖配置-->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
@@ -254,6 +261,13 @@
|
||||
<version>${vetti.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- AI 聊天-->
|
||||
<dependency>
|
||||
<groupId>com.vetti</groupId>
|
||||
<artifactId>vetti-ai</artifactId>
|
||||
<version>${vetti.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 4.x 版本,兼容 MinIO 8.x -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
@@ -300,6 +314,7 @@
|
||||
<module>vetti-quartz</module>
|
||||
<module>vetti-generator</module>
|
||||
<module>vetti-common</module>
|
||||
<module>vetti-ai</module>
|
||||
</modules>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot WebSocket -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- swagger3-->
|
||||
<dependency>
|
||||
<groupId>io.springfox</groupId>
|
||||
@@ -69,7 +75,15 @@
|
||||
<artifactId>vetti-generator</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- AI 业务逻辑处理-->
|
||||
<dependency>
|
||||
<groupId>com.vetti</groupId>
|
||||
<artifactId>vetti-ai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.vetti.socket;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class VoiceHandshakeInterceptor implements HandshakeInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
||||
// 从请求参数中获取客户端ID
|
||||
if (request instanceof ServletServerHttpRequest) {
|
||||
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
|
||||
String clientId = servletRequest.getServletRequest().getParameter("clientId");
|
||||
if (clientId != null && !clientId.isEmpty()) {
|
||||
attributes.put("clientId", clientId);
|
||||
System.out.println("客户端连接: " + clientId);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler wsHandler, Exception exception) {
|
||||
// 握手后操作,可留空
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.vetti.socket;
|
||||
|
||||
import com.vetti.socket.vo.VoicePartMessage;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.Base64;
|
||||
|
||||
@Component
|
||||
public class VoiceWebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
// 存储每个客户端的语音分片,key: clientId, value: 分片映射
|
||||
private final Map<String, Map<Integer, byte[]>> clientVoiceParts = new ConcurrentHashMap<>();
|
||||
// 存储每个客户端的总分片数,key: clientId
|
||||
private final Map<String, Integer> clientTotalParts = new ConcurrentHashMap<>();
|
||||
// 用于并发控制的锁
|
||||
private final Map<String, ReentrantLock> clientLocks = new ConcurrentHashMap<>();
|
||||
// JSON序列化工具
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
// 语音文件保存目录
|
||||
private static final String VOICE_STORAGE_DIR = "voice_files/";
|
||||
|
||||
public VoiceWebSocketHandler() {
|
||||
// 初始化存储目录
|
||||
File dir = new File(VOICE_STORAGE_DIR);
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
String clientId = getClientId(session);
|
||||
if (clientId != null) {
|
||||
// 初始化客户端数据结构
|
||||
clientVoiceParts.put(clientId, new TreeMap<>()); // TreeMap保证分片有序
|
||||
clientLocks.putIfAbsent(clientId, new ReentrantLock());
|
||||
System.out.println("客户端连接建立: " + clientId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
String clientId = getClientId(session);
|
||||
if (clientId == null) {
|
||||
System.err.println("无法获取客户端ID");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析前端发送的JSON消息
|
||||
VoicePartMessage voiceMessage = objectMapper.readValue(message.getPayload(), VoicePartMessage.class);
|
||||
|
||||
// 处理语音分片
|
||||
if ("voice_part".equals(voiceMessage.getType())) {
|
||||
processVoicePart(clientId, voiceMessage, session);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("处理消息出错: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||
String clientId = getClientId(session);
|
||||
if (clientId != null) {
|
||||
// 清理客户端资源
|
||||
clientVoiceParts.remove(clientId);
|
||||
clientTotalParts.remove(clientId);
|
||||
clientLocks.remove(clientId);
|
||||
System.out.println("客户端连接关闭: " + clientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理语音分片
|
||||
*/
|
||||
private void processVoicePart(String clientId, VoicePartMessage message, WebSocketSession session) throws Exception {
|
||||
ReentrantLock lock = clientLocks.get(clientId);
|
||||
lock.lock(); // 加锁确保线程安全
|
||||
try {
|
||||
// 保存总分片数
|
||||
clientTotalParts.put(clientId, message.getTotalParts());
|
||||
|
||||
// 解码Base64数据并存储分片
|
||||
byte[] voiceData = Base64.getDecoder().decode(message.getData());
|
||||
clientVoiceParts.get(clientId).put(message.getPartNumber(), voiceData);
|
||||
|
||||
System.out.printf("接收客户端 %s 的分片 %d/%d%n",
|
||||
clientId, message.getPartNumber() + 1, message.getTotalParts());
|
||||
|
||||
// 检查是否所有分片都已接收
|
||||
checkAndMergeParts(clientId, session);
|
||||
} finally {
|
||||
lock.unlock(); // 释放锁
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否所有分片都已接收,如果是则合并
|
||||
*/
|
||||
private void checkAndMergeParts(String clientId, WebSocketSession session) throws Exception {
|
||||
Map<Integer, byte[]> parts = clientVoiceParts.get(clientId);
|
||||
Integer totalParts = clientTotalParts.get(clientId);
|
||||
|
||||
if (parts == null || totalParts == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 所有分片都已接收
|
||||
if (parts.size() == totalParts) {
|
||||
System.out.println("所有分片接收完成,开始合并: " + clientId);
|
||||
|
||||
// 生成唯一文件名
|
||||
String fileName = clientId + "_" + System.currentTimeMillis() + ".wav";
|
||||
Path outputPath = Paths.get(VOICE_STORAGE_DIR + fileName);
|
||||
|
||||
// 合并分片
|
||||
try (FileOutputStream fos = new FileOutputStream(outputPath.toFile())) {
|
||||
for (byte[] part : parts.values()) {
|
||||
fos.write(part);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("语音文件合并完成,保存路径: " + outputPath);
|
||||
|
||||
// 向客户端发送处理完成消息
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("type", "complete");
|
||||
response.put("message", "语音接收完成");
|
||||
response.put("fileName", fileName);
|
||||
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
|
||||
|
||||
// 清理已合并的分片数据
|
||||
clientVoiceParts.get(clientId).clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从会话中获取客户端ID
|
||||
*/
|
||||
private String getClientId(WebSocketSession session) {
|
||||
return (String) session.getAttributes().get("clientId");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.vetti.socket;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
private final VoiceWebSocketHandler voiceWebSocketHandler;
|
||||
|
||||
private final VoiceHandshakeInterceptor voiceHandshakeInterceptor;
|
||||
|
||||
// 构造函数注入
|
||||
public WebSocketConfig(VoiceWebSocketHandler voiceWebSocketHandler,
|
||||
VoiceHandshakeInterceptor interceptor) {
|
||||
this.voiceWebSocketHandler = voiceWebSocketHandler;
|
||||
this.voiceHandshakeInterceptor = interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
// 注册WebSocket处理器,设置路径和允许跨域
|
||||
registry.addHandler(voiceWebSocketHandler, "/voice-websocket")
|
||||
.addInterceptors(voiceHandshakeInterceptor)
|
||||
.setAllowedOrigins("*"); // 生产环境应指定具体域名而非*
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.vetti.socket.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* 语音分片消息实体类,对应前端发送的JSON结构
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class VoicePartMessage {
|
||||
private String type; // 消息类型,如"voice_part"
|
||||
private int partNumber; // 分片编号,从0开始
|
||||
private int totalParts; // 总分片数
|
||||
private String data; // Base64编码的分片数据
|
||||
|
||||
// getter和setter
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getPartNumber() {
|
||||
return partNumber;
|
||||
}
|
||||
|
||||
public void setPartNumber(int partNumber) {
|
||||
this.partNumber = partNumber;
|
||||
}
|
||||
|
||||
public int getTotalParts() {
|
||||
return totalParts;
|
||||
}
|
||||
|
||||
public void setTotalParts(int totalParts) {
|
||||
this.totalParts = totalParts;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
@@ -142,11 +142,13 @@ verification:
|
||||
length: 5
|
||||
# 验证码过期时间(分钟)
|
||||
expiration-minutes: 10
|
||||
|
||||
# 文本转语音
|
||||
elevenLabs:
|
||||
baseUrl: https://api.elevenlabs.io/v1
|
||||
apiKey: sk_5240d8f56cb1eb5225fffcf903f62479884d1af5b3de6812
|
||||
modelId: eleven_monolingual_v1
|
||||
|
||||
# 语音转文本
|
||||
whisper:
|
||||
apiUrl: https://api.openai.com/v1/audio/transcriptions
|
||||
|
||||
37
vetti-ai/pom.xml
Normal file
37
vetti-ai/pom.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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">
|
||||
|
||||
<parent>
|
||||
<artifactId>vetti-service</artifactId>
|
||||
<groupId>com.vetti</groupId>
|
||||
<version>3.9.0</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>vetti-ai</artifactId>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
|
||||
<dependency>
|
||||
<groupId>io.swagger</groupId>
|
||||
<artifactId>swagger-models</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>com.vetti</groupId>
|
||||
<artifactId>vetti-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.vetti.ai.service;
|
||||
|
||||
/**
|
||||
* 面试聊天共通 服务层
|
||||
*/
|
||||
public interface ChatCommonService {
|
||||
|
||||
|
||||
/**
|
||||
* 处理面试聊天语音结果数据
|
||||
*/
|
||||
public void handleChatVoiceData();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.vetti.ai.service.impl;
|
||||
|
||||
import com.vetti.ai.service.ChatCommonService;
|
||||
import com.vetti.common.ai.elevenLabs.ElevenLabsClient;
|
||||
import com.vetti.common.ai.gpt.ChatGPTClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
/**
|
||||
* 聊天面试共通 服务层实现
|
||||
*/
|
||||
@Service
|
||||
public class ChatCommonServiceImpl implements ChatCommonService {
|
||||
|
||||
@Autowired
|
||||
private ElevenLabsClient elevenLabsClient;
|
||||
|
||||
@Autowired
|
||||
private ChatGPTClient chatGPTClient;
|
||||
|
||||
@Override
|
||||
public void handleChatVoiceData() {
|
||||
//1、获取面试传输的语音文件
|
||||
|
||||
//2、语音文件转换成文本字符串
|
||||
|
||||
//3、把文本传输到GPT中,等待回复
|
||||
|
||||
//4、GPT返回的结果,文本转成语音文件
|
||||
|
||||
//5、返回最终的语音文件
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user