|
@@ -0,0 +1,177 @@
|
|
|
|
|
+package cn.iocoder.byzs.module.web.service.ai;
|
|
|
|
|
+
|
|
|
|
|
+import cn.hutool.core.util.ObjUtil;
|
|
|
|
|
+import cn.iocoder.byzs.framework.common.pojo.CommonResult;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.dal.dataobject.chat.AiChatConversationDO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.dal.dataobject.model.AiChatRoleDO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.dal.dataobject.tts.AiTtsDO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.dal.mysql.tts.AiTtsMapper;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.enums.ErrorCodeConstants;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.service.chat.AiChatConversationService;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.service.model.AiChatRoleService;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.util.tts.StreamTtsService;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.util.tts.WavHeader;
|
|
|
|
|
+import jakarta.annotation.Resource;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.beans.factory.ObjectProvider;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.validation.annotation.Validated;
|
|
|
|
|
+import reactor.core.publisher.Flux;
|
|
|
|
|
+
|
|
|
|
|
+import java.nio.ByteBuffer;
|
|
|
|
|
+import java.nio.ByteOrder;
|
|
|
|
|
+import java.util.Base64;
|
|
|
|
|
+import java.util.concurrent.Executors;
|
|
|
|
|
+import java.util.concurrent.ScheduledExecutorService;
|
|
|
|
|
+import java.util.concurrent.ScheduledFuture;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
|
+import java.util.concurrent.atomic.AtomicReference;
|
|
|
|
|
+import java.util.regex.Matcher;
|
|
|
|
|
+import java.util.regex.Pattern;
|
|
|
|
|
+
|
|
|
|
|
+import static cn.iocoder.byzs.framework.common.exception.util.ServiceExceptionUtil.exception;
|
|
|
|
|
+import static cn.iocoder.byzs.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
|
|
|
|
|
+import static cn.iocoder.byzs.framework.common.pojo.CommonResult.error;
|
|
|
|
|
+import static cn.iocoder.byzs.framework.common.pojo.CommonResult.success;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * webAi Service 实现类
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author lyb
|
|
|
|
|
+ */
|
|
|
|
|
+@Service
|
|
|
|
|
+@Validated
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+public class WebAiServiceImpl {
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiChatConversationService chatConversationService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiChatRoleService chatRoleService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiTtsMapper ttsMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private ObjectProvider<StreamTtsService> streamTtsServiceProvider;
|
|
|
|
|
+
|
|
|
|
|
+ public Flux<CommonResult<AiChatMessageSendRespVO>> sendSpecifiedAnswerStream(AiChatMessageSendReqVO sendReqVO, Long userId) {
|
|
|
|
|
+ // 1. 校验对话存在
|
|
|
|
|
+ AiChatConversationDO conversation = chatConversationService
|
|
|
|
|
+ .validateChatConversationExists(sendReqVO.getConversationId());
|
|
|
|
|
+ if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
|
|
|
|
|
+ throw exception(CHAT_CONVERSATION_NOT_EXISTS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 获取TTS配置
|
|
|
|
|
+ AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId());
|
|
|
|
|
+ if (chatRole == null || chatRole.getTtsId() == null) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.TTS_NOT_EXISTS);
|
|
|
|
|
+ }
|
|
|
|
|
+ AiTtsDO aiTtsDO = ttsMapper.selectById(chatRole.getTtsId());
|
|
|
|
|
+ if (aiTtsDO == null) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.TTS_NOT_EXISTS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 初始化TTS服务
|
|
|
|
|
+ StreamTtsService streamTtsService = streamTtsServiceProvider.getObject();
|
|
|
|
|
+ streamTtsService.startTts(aiTtsDO);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 处理指定回答内容
|
|
|
|
|
+ String contentAnswer = sendReqVO.getContentAnswer();
|
|
|
|
|
+ StringBuffer contentTTSBuffer = new StringBuffer(contentAnswer);
|
|
|
|
|
+ Pattern sentencePattern = Pattern.compile("[。!?;\n\r]");
|
|
|
|
|
+
|
|
|
|
|
+ ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
|
|
|
|
+ AtomicReference<ScheduledFuture<?>> ttsTask = new AtomicReference<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 创建文本流
|
|
|
|
|
+ Flux<CommonResult<AiChatMessageSendRespVO>> textStream = Flux.just(success(
|
|
|
|
|
+ new AiChatMessageSendRespVO()
|
|
|
|
|
+ .setEventType("TEXT")
|
|
|
|
|
+ .setReceive(new AiChatMessageSendRespVO.Message().setContent(contentAnswer))
|
|
|
|
|
+ )).doOnComplete(() -> {
|
|
|
|
|
+ processRemainingText(streamTtsService, contentTTSBuffer);
|
|
|
|
|
+ if (ttsTask.get() != null) {
|
|
|
|
|
+ ttsTask.get().cancel(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ scheduler.shutdown();
|
|
|
|
|
+ }).doOnError(throwable -> {
|
|
|
|
|
+ streamTtsService.stopTts();
|
|
|
|
|
+ }).doFinally(signalType -> {
|
|
|
|
|
+ streamTtsService.stopTts();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 创建音频流
|
|
|
|
|
+ Flux<CommonResult<AiChatMessageSendRespVO>> audioStream = Flux.create(sink -> {
|
|
|
|
|
+ AtomicBoolean isFirstChunk = new AtomicBoolean(true);
|
|
|
|
|
+ streamTtsService.setAudioDataCallback(audioBytes -> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ byte[] processedAudio;
|
|
|
|
|
+ if (isFirstChunk.getAndSet(false)) {
|
|
|
|
|
+ processedAudio = WavHeader.addWavHeader(audioBytes, 16000, 16, 1);
|
|
|
|
|
+ log.info("首包音频带WAV头,长度={} bytes", processedAudio.length);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ processedAudio = audioBytes;
|
|
|
|
|
+ }
|
|
|
|
|
+ String base64Audio = Base64.getEncoder().encodeToString(processedAudio);
|
|
|
|
|
+ AiChatMessageSendRespVO audioResp = new AiChatMessageSendRespVO();
|
|
|
|
|
+ audioResp.setEventType("AUDIO");
|
|
|
|
|
+ audioResp.setAudioData(base64Audio);
|
|
|
|
|
+ sink.next(success(audioResp));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[TTS处理异常] 音频编码失败", e);
|
|
|
|
|
+ sink.error(new RuntimeException("TTS音频处理失败: " + e.getMessage(), e));
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ streamTtsService.setOnCompleteCallback(sink::complete);
|
|
|
|
|
+
|
|
|
|
|
+ // 立即处理文本
|
|
|
|
|
+ ttsTask.set(scheduler.schedule(() -> {
|
|
|
|
|
+ Matcher matcher = sentencePattern.matcher(contentTTSBuffer);
|
|
|
|
|
+ if (matcher.find()) {
|
|
|
|
|
+ processCompleteSentence(streamTtsService, contentTTSBuffer, matcher);
|
|
|
|
|
+ } else if (!contentTTSBuffer.isEmpty()) {
|
|
|
|
|
+ processCompleteSentence(streamTtsService, contentTTSBuffer, contentTTSBuffer.length());
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 100, TimeUnit.MILLISECONDS));
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 合并流并返回
|
|
|
|
|
+ return Flux.merge(textStream, audioStream)
|
|
|
|
|
+ .doFinally(signalType -> {
|
|
|
|
|
+ streamTtsService.setAudioDataCallback(null);
|
|
|
|
|
+ streamTtsService.setOnCompleteCallback(null);
|
|
|
|
|
+ scheduler.shutdownNow();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理完整句子
|
|
|
|
|
+ private void processCompleteSentence(StreamTtsService streamTtsService, StringBuffer buffer, Matcher matcher) {
|
|
|
|
|
+ String sentence = buffer.substring(0, matcher.end());
|
|
|
|
|
+ streamTtsService.sendText(sentence);
|
|
|
|
|
+ buffer.delete(0, matcher.end());
|
|
|
|
|
+ log.info("TTS合成完整句: {}", sentence);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理指定长度文本
|
|
|
|
|
+ private void processCompleteSentence(StreamTtsService streamTtsService, StringBuffer buffer, int length) {
|
|
|
|
|
+ String sentence = buffer.substring(0, length);
|
|
|
|
|
+ streamTtsService.sendText(sentence);
|
|
|
|
|
+ buffer.delete(0, length);
|
|
|
|
|
+ log.info("TTS合成长文本: {}", sentence);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理剩余文本
|
|
|
|
|
+ private void processRemainingText(StreamTtsService streamTtsService, StringBuffer buffer) {
|
|
|
|
|
+ if (!buffer.isEmpty()) {
|
|
|
|
|
+ streamTtsService.sendText(buffer.toString());
|
|
|
|
|
+ buffer.setLength(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|