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-generator" />
|
||||||
<module name="vetti-quartz" />
|
<module name="vetti-quartz" />
|
||||||
<module name="vetti-system" />
|
<module name="vetti-system" />
|
||||||
<module name="vetti-framework" />
|
|
||||||
<module name="vetti-common" />
|
<module name="vetti-common" />
|
||||||
|
<module name="vetti-framework" />
|
||||||
|
<module name="vetti-ai" />
|
||||||
</profile>
|
</profile>
|
||||||
</annotationProcessing>
|
</annotationProcessing>
|
||||||
</component>
|
</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$/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/java" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/vetti-admin/src/main/resources" 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/java" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/vetti-common/src/main/resources" 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" />
|
<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>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot WebSocket -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
<version>2.5.15</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 覆盖logback的依赖配置-->
|
<!-- 覆盖logback的依赖配置-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ch.qos.logback</groupId>
|
<groupId>ch.qos.logback</groupId>
|
||||||
@@ -254,6 +261,13 @@
|
|||||||
<version>${vetti.version}</version>
|
<version>${vetti.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- AI 聊天-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.vetti</groupId>
|
||||||
|
<artifactId>vetti-ai</artifactId>
|
||||||
|
<version>${vetti.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 4.x 版本,兼容 MinIO 8.x -->
|
<!-- 4.x 版本,兼容 MinIO 8.x -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.okhttp3</groupId>
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
@@ -300,6 +314,7 @@
|
|||||||
<module>vetti-quartz</module>
|
<module>vetti-quartz</module>
|
||||||
<module>vetti-generator</module>
|
<module>vetti-generator</module>
|
||||||
<module>vetti-common</module>
|
<module>vetti-common</module>
|
||||||
|
<module>vetti-ai</module>
|
||||||
</modules>
|
</modules>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,12 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot WebSocket -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- swagger3-->
|
<!-- swagger3-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.springfox</groupId>
|
<groupId>io.springfox</groupId>
|
||||||
@@ -69,7 +75,15 @@
|
|||||||
<artifactId>vetti-generator</artifactId>
|
<artifactId>vetti-generator</artifactId>
|
||||||
</dependency>
|
</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>
|
</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
|
length: 5
|
||||||
# 验证码过期时间(分钟)
|
# 验证码过期时间(分钟)
|
||||||
expiration-minutes: 10
|
expiration-minutes: 10
|
||||||
|
|
||||||
# 文本转语音
|
# 文本转语音
|
||||||
elevenLabs:
|
elevenLabs:
|
||||||
baseUrl: https://api.elevenlabs.io/v1
|
baseUrl: https://api.elevenlabs.io/v1
|
||||||
apiKey: sk_5240d8f56cb1eb5225fffcf903f62479884d1af5b3de6812
|
apiKey: sk_5240d8f56cb1eb5225fffcf903f62479884d1af5b3de6812
|
||||||
modelId: eleven_monolingual_v1
|
modelId: eleven_monolingual_v1
|
||||||
|
|
||||||
# 语音转文本
|
# 语音转文本
|
||||||
whisper:
|
whisper:
|
||||||
apiUrl: https://api.openai.com/v1/audio/transcriptions
|
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