diff --git a/src/main/java/com/xly/tts/bean/TTSResponseDTO.java b/src/main/java/com/xly/tts/bean/TTSResponseDTO.java index 25ca39a..02f980f 100644 --- a/src/main/java/com/xly/tts/bean/TTSResponseDTO.java +++ b/src/main/java/com/xly/tts/bean/TTSResponseDTO.java @@ -8,7 +8,6 @@ import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; -import java.util.Map; /** * TTS响应数据传输对象 @@ -27,6 +26,11 @@ public class TTSResponseDTO implements Serializable { private String requestId; /** + * 【新加】缓存唯一KEY,用于多用户不冲突 + */ + private String cacheKey; + + /** * 状态码:200成功,其他失败 */ @Builder.Default @@ -65,8 +69,6 @@ public class TTSResponseDTO implements Serializable { private String sReturnType = ReturnTypeCode.MAKEDOWN.getCode(); - - /** * 创建失败响应 */ @@ -97,5 +99,4 @@ public class TTSResponseDTO implements Serializable { .timestamp(System.currentTimeMillis()) .build(); } - } \ No newline at end of file diff --git a/src/main/java/com/xly/tts/service/LocalAudioCache.java b/src/main/java/com/xly/tts/service/LocalAudioCache.java new file mode 100644 index 0000000..417dbfa --- /dev/null +++ b/src/main/java/com/xly/tts/service/LocalAudioCache.java @@ -0,0 +1,24 @@ +package com.xly.tts.service; + +import com.xly.tts.bean.TTSResponseDTO; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class LocalAudioCache { + private static final Map CACHE = new ConcurrentHashMap<>(); + + public static void put(String text, TTSResponseDTO dto) { + CACHE.put(text, dto); + // 5分钟后自动清理 + new Thread(() -> { + try { + Thread.sleep(5 * 60 * 1000); + CACHE.remove(text); + } catch (Exception ignored) {} + }).start(); + } + + public static TTSResponseDTO get(String text) { + return CACHE.get(text); + } +} \ No newline at end of file diff --git a/src/main/java/com/xly/tts/service/PythonTtsProxyService.java b/src/main/java/com/xly/tts/service/PythonTtsProxyService.java index 4657e24..05b1209 100644 --- a/src/main/java/com/xly/tts/service/PythonTtsProxyService.java +++ b/src/main/java/com/xly/tts/service/PythonTtsProxyService.java @@ -24,6 +24,10 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.net.URL; +import java.net.HttpURLConnection; +import java.io.OutputStream; +import java.io.InputStream; @Slf4j @Service @@ -41,7 +45,6 @@ public class PythonTtsProxyService { private ExecutorService executorService; private final XlyErpService xlyErpService; - private final UserSceneSessionService userSceneSessionService; @PostConstruct @@ -62,14 +65,13 @@ public class PythonTtsProxyService { * 流式合成语音 - 代理到Python服务 */ public ResponseEntity synthesizeStream(TTSRequestDTO request) { - return getVoiceResult(request); + return getVoiceResult(request); } /** - * 流式合成语音 - 代理到Python服务 + * 【保持原有返回类型】AI对话 + 流式TTS */ public ResponseEntity synthesizeStreamAi(TTSRequestDTO request) { - //调用AI返回请求内容 String userInput = request.getText(); String sUserId = request.getUserid(); String sUserName = request.getUsername(); @@ -77,43 +79,37 @@ public class PythonTtsProxyService { String sSubsidiaryId = request.getSubsidiaryid(); String sUserType = request.getUsertype(); String authorization = request.getAuthorization(); - AiResponseDTO voiceText = xlyErpService.erpUserInput(userInput,sUserId,sUserName,sBrandsId,sSubsidiaryId,sUserType, authorization); - return synthesizeStreamAi(request,voiceText); + AiResponseDTO voiceText = xlyErpService.erpUserInput(userInput, sUserId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization); + return synthesizeStreamAi(request, voiceText); } public ResponseEntity cleanMemory(TTSRequestDTO request) { - //调用AI返回请求内容 String sUserId = request.getUserid(); String sUserName = request.getUsername(); String sBrandsId = request.getBrandsid(); String sSubsidiaryId = request.getSubsidiaryid(); String sUserType = request.getUsertype(); String authorization = request.getAuthorization(); - AiResponseDTO aiResponseDTO = xlyErpService.cleanMemory(sUserId,sUserName,sBrandsId,sSubsidiaryId,sUserType, authorization); + AiResponseDTO aiResponseDTO = xlyErpService.cleanMemory(sUserId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization); return ResponseEntity.ok(TTSResponseDTO.builder() .code(200) .message("success") - .originalText(request.getText()) // 原始文本 - .processedText(aiResponseDTO.getAiText()) // AI提示语 - .systemText(aiResponseDTO.getSystemText()) // 系统提示语言 + .originalText(request.getText()) + .processedText(aiResponseDTO.getAiText()) + .systemText(aiResponseDTO.getSystemText()) .voice(request.getVoice()) .sSceneName(aiResponseDTO.getSSceneName()) - .sMethodName (aiResponseDTO.getSMethodName()) - .sReturnType (aiResponseDTO.getSReturnType()) + .sMethodName(aiResponseDTO.getSMethodName()) + .sReturnType(aiResponseDTO.getSReturnType()) .timestamp(System.currentTimeMillis()) .textLength(request.getText().length()) .build()); } - /*** - * @Author 钱豹 - * @Date 11:16 2026/2/8 - * @Param [request] - * @return org.springframework.http.ResponseEntity - * @Description 初始化加载方法 - **/ + /** + * 【保持原有返回类型】不动!!! + */ public ResponseEntity init(TTSRequestDTO request) { - //调用AI返回请求内容 String sUserId = request.getUserid(); String sUserName = request.getUsername(); String sBrandsId = request.getBrandsid(); @@ -121,154 +117,127 @@ public class PythonTtsProxyService { String sUserType = request.getUsertype(); String authorization = request.getAuthorization(); - //清空记忆 + // 清空记忆 userSceneSessionService.cleanUserSession(sUserId); -// xlyErpService.initSceneGuide(sUserId,sUserType,StrUtil.EMPTY) - AiResponseDTO voiceText = xlyErpService.initSceneGuide(StrUtil.EMPTY,sUserId,sUserName,sBrandsId,sSubsidiaryId,sUserType, authorization); + AiResponseDTO voiceText = xlyErpService.initSceneGuide(StrUtil.EMPTY, sUserId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization); voiceText.setSReturnType(ReturnTypeCode.HTML.getCode()); - return synthesizeStreamAi(request,voiceText); + return synthesizeStreamAi(request, voiceText); } - public ResponseEntity synthesizeStreamAi(TTSRequestDTO request,AiResponseDTO aiResponseDTO) { + /** + * 【保持原有返回类型】不动!内部流式请求Python + */ + public ResponseEntity synthesizeStreamAi(TTSRequestDTO request, AiResponseDTO aiResponseDTO) { String aiText = aiResponseDTO.getAiText(); String systemText = aiResponseDTO.getSystemText(); - if(ObjectUtil.isEmpty(systemText)){ + if (ObjectUtil.isEmpty(systemText)) { systemText = StrUtil.EMPTY; } - //移除html String voiceTextNew = AdvancedSymbolRemover.removePunctuationHtml(aiText); - try { - //如果没有语音直接返回 - if(!request.getVoiceless() || ObjectUtil.isEmpty(voiceTextNew)){ - return ResponseEntity.ok(TTSResponseDTO.builder() - .code(200) - .message("success") - .originalText(request.getText()) // 原始文本 - .processedText(aiText) // AI提示语 - .systemText(systemText) // 系统提示语言 - .voice(request.getVoice()) - .sSceneName(aiResponseDTO.getSSceneName()) - .sMethodName (aiResponseDTO.getSMethodName()) - .sReturnType (aiResponseDTO.getSReturnType()) - .sCommonts(BusinessCode.COMMONTS.getMessage()) - .timestamp(System.currentTimeMillis()) - .textLength(request.getText().length()) - .build()); - } - // 构建Python服务请求 - Map pythonRequest = new HashMap<>(); - pythonRequest.put("text", voiceTextNew); - pythonRequest.put("voice", request.getVoice()); - pythonRequest.put("rate", request.getRate() != null ? request.getRate() : "+10%"); - pythonRequest.put("volume", request.getVolume() != null ? request.getVolume() : "+0%"); - // 发送请求到Python服务 - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); - HttpEntity> entity = new HttpEntity<>(pythonRequest, headers); - ResponseEntity response = restTemplate.exchange( - pythonServiceUrl + "/stream-synthesize", - HttpMethod.POST, - entity, - byte[].class - ); + TTSResponseDTO dto = TTSResponseDTO.builder() + .code(200) + .message("success") + .originalText(request.getText()) + .processedText(aiText) + .systemText(systemText) + .voice(request.getVoice()) + .sSceneName(aiResponseDTO.getSSceneName()) + .sMethodName(aiResponseDTO.getSMethodName()) + .sReturnType(aiResponseDTO.getSReturnType()) + .sCommonts(BusinessCode.COMMONTS.getMessage()) + .timestamp(System.currentTimeMillis()) + .textLength((aiText + systemText).length()) + .build(); - if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { - // 将音频数据转为Base64 - String audioBase64 = Base64.getEncoder().encodeToString(response.getBody()); - // 构建完整的响应DTO - TTSResponseDTO ttsResponse = TTSResponseDTO.builder() - .code(200) - .message("success") - .originalText(request.getText()) // 原始文本 - .processedText(aiText) // AI提示语 - .systemText(systemText) // 系统提示语言 - .voice(request.getVoice()) - .timestamp(System.currentTimeMillis()) - .textLength((aiText+systemText).length()) - .audioBase64(audioBase64) // Base64编码的音频 - .audioSize(response.getBody().length) - .sSceneName(aiResponseDTO.getSSceneName()) - .sMethodName(aiResponseDTO.getSMethodName()) - .sReturnType(aiResponseDTO.getSReturnType()) - .sCommonts(BusinessCode.COMMONTS.getMessage()) - .audioFormat("audio/mpeg") - .build(); - return ResponseEntity.ok(ttsResponse); - } else { - return ResponseEntity.status(response.getStatusCode()) - .body(TTSResponseDTO.error("python_service_error", 500, - "Python服务响应失败: " + response.getStatusCode())); + boolean voiceless = Boolean.TRUE.equals(request.getVoiceless()); + if (!voiceless || ObjectUtil.isEmpty(voiceTextNew)) { + return ResponseEntity.ok(dto); + } + + // ============================================== + // 👇 【关键】生成 全局唯一的 key(多用户不冲突) + // ============================================== + String cacheKey = request.getUserid() + "_" + System.currentTimeMillis() + "_" + request.getText(); + + CompletableFuture.runAsync(() -> { + try { + Map params = new HashMap<>(); + params.put("text", voiceTextNew); + params.put("voice", request.getVoice()); + params.put("rate", request.getRate() != null ? request.getRate() : "+10%"); + params.put("volume", request.getVolume() != null ? request.getVolume() : "+0%"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM)); + HttpEntity> entity = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.exchange( + pythonServiceUrl + "/stream-synthesize", + HttpMethod.POST, entity, byte[].class + ); + + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + dto.setAudioBase64(Base64.getEncoder().encodeToString(response.getBody())); + dto.setAudioSize(response.getBody().length); + dto.setAudioFormat("audio/mpeg"); + + // ============================================== + // 👇 用唯一key存(不覆盖别人) + // ============================================== + LocalAudioCache.put(cacheKey, dto); + } + } catch (Exception e) { + log.warn("语音合成忽略:{}", e.getMessage()); } + }, executorService); - } catch (Exception e) { -// e.printStackTrace(); - TTSResponseDTO ttsResponse = TTSResponseDTO.builder() - .code(200) - .message("success") - .originalText(request.getText()) // 原始文本 - .voice(request.getVoice()) - .timestamp(System.currentTimeMillis()) - .processedText(aiText) // AI提示语 - .systemText(systemText) // 系统提示语言 - .textLength((aiText+systemText).length()) - .sSceneName(aiResponseDTO.getSSceneName()) - .sMethodName (aiResponseDTO.getSMethodName()) - .sReturnType (aiResponseDTO.getSReturnType()) - .sCommonts(BusinessCode.COMMONTS.getMessage()) - .build(); - return ResponseEntity.ok(ttsResponse); - } + // ============================================== + // 👇 把 cacheKey 返回给前端(前端靠它取音频) + // ============================================== + dto.setCacheKey(cacheKey); + + return ResponseEntity.ok(dto); } public ResponseEntity getVoiceResult(TTSRequestDTO request) { try { - - String voiceText = request.getText(); - //移除html - voiceText = AdvancedSymbolRemover.removePunctuationHtml( voiceText); - // 构建Python服务请求 + String voiceText = AdvancedSymbolRemover.removePunctuationHtml(request.getText()); Map pythonRequest = new HashMap<>(); pythonRequest.put("text", voiceText); pythonRequest.put("voice", request.getVoice()); pythonRequest.put("rate", request.getRate() != null ? request.getRate() : "+0%"); pythonRequest.put("volume", request.getVolume() != null ? request.getVolume() : "+0%"); - // 发送请求到Python服务 + HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); + HttpEntity> entity = new HttpEntity<>(pythonRequest, headers); + ResponseEntity response = restTemplate.exchange( pythonServiceUrl + "/stream-synthesize", HttpMethod.POST, entity, byte[].class ); + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { InputStream inputStream = new ByteArrayInputStream(response.getBody()); InputStreamResource resource = new InputStreamResource(inputStream); - // 构建响应头 HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setContentType(MediaType.parseMediaType("audio/mpeg")); responseHeaders.setContentLength(response.getBody().length); responseHeaders.set("Content-Disposition", "inline; filename=\"speech.mp3\""); - responseHeaders.set("X-TTS-Source", "python-service"); - responseHeaders.set("X-TTS-Voice", request.getVoice()); - return ResponseEntity.ok() - .headers(responseHeaders) - .body(resource); - } else { - return ResponseEntity.status(response.getStatusCode()).build(); + return ResponseEntity.ok().headers(responseHeaders).body(resource); } } catch (Exception e) { return fallbackResponse(request); } + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } - /** - * 快速合成接口 - */ public ResponseEntity quickSynthesize(String text, String voice) { TTSRequestDTO request = new TTSRequestDTO(); request.setText(text); @@ -276,157 +245,81 @@ public class PythonTtsProxyService { return synthesizeStream(request); } - /** - * 异步流式合成 - */ public CompletableFuture> synthesizeStreamAsync(TTSRequestDTO request) { return CompletableFuture.supplyAsync(() -> synthesizeStream(request), executorService); } - /** - * 获取可用语音列表 - */ public List getAvailableVoices() { try { - log.info("从Python服务获取语音列表"); - - ResponseEntity response = restTemplate.getForEntity( - pythonServiceUrl + "/voices", - Map.class - ); - + ResponseEntity response = restTemplate.getForEntity(pythonServiceUrl + "/voices", Map.class); if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { - Map responseBody = response.getBody(); - List> voicesData = (List>) responseBody.get("voices"); - + List> voicesData = (List>) response.getBody().get("voices"); List voices = new ArrayList<>(); - for (Map voiceData : voicesData) { - VoiceInfoDTO voice = new VoiceInfoDTO(); - voice.setName(voiceData.get("name")); - voice.setLocale(voiceData.get("locale")); - voice.setGender(voiceData.get("gender")); - voice.setDisplayName(voiceData.get("displayName")); - voices.add(voice); + for (Map vd : voicesData) { + VoiceInfoDTO vo = new VoiceInfoDTO(); + vo.setName(vd.get("name")); + vo.setLocale(vd.get("locale")); + vo.setGender(vd.get("gender")); + vo.setDisplayName(vd.get("displayName")); + voices.add(vo); } - - log.info("从Python服务获取到 {} 个语音", voices.size()); return voices; } } catch (Exception e) { - log.error("获取Python服务语音列表失败: {}", e.getMessage()); + log.error("获取语音列表失败", e); } - - // 返回默认语音列表作为降级 return getDefaultVoices(); } - /** - * 获取语音详情 - */ public VoiceInfoDTO getVoiceDetail(String name) { - List voices = getAvailableVoices(); - return voices.stream() - .filter(v -> v.getName().equals(name)) - .findFirst() - .orElse(null); + return getAvailableVoices().stream().filter(v -> v.getName().equals(name)).findFirst().orElse(null); } - /** - * 健康检查 - */ public boolean healthCheck() { try { - ResponseEntity response = restTemplate.getForEntity( - pythonServiceUrl + "/health", - Map.class - ); - - boolean healthy = response.getStatusCode() == HttpStatus.OK && - "healthy".equals(response.getBody().get("status")); - - log.info("Python服务健康状态: {}", healthy ? "健康" : "异常"); - return healthy; - + ResponseEntity res = restTemplate.getForEntity(pythonServiceUrl + "/health", Map.class); + return res.getStatusCode() == HttpStatus.OK && "healthy".equals(res.getBody().get("status")); } catch (Exception e) { - log.error("Python服务健康检查失败: {}", e.getMessage()); return false; } } - /** - * 批量合成 - */ public List> batchSynthesize(List requests) { - List> results = new ArrayList<>(); - - for (TTSRequestDTO request : requests) { - results.add(synthesizeStream(request)); - } - - return results; + List> list = new ArrayList<>(); + for (TTSRequestDTO req : requests) list.add(synthesizeStream(req)); + return list; } - /** - * 直接合成(用于测试) - */ public ResponseEntity synthesizeDirect(TTSRequestDTO request) { return synthesizeStream(request); } - /** - * 关闭服务 - */ public void shutdown() { - if (executorService != null) { - executorService.shutdown(); - } - log.info("Python TTS代理服务已关闭"); + if (executorService != null) executorService.shutdown(); + log.info("Python TTS服务已关闭"); } - /** - * 降级响应 - */ private ResponseEntity fallbackResponse(TTSRequestDTO request) { try { - // 可以返回一个默认的音频文件 - String fallbackText = "对不起,语音合成服务暂时不可用,请稍后重试。"; - TTSRequestDTO fallbackRequest = new TTSRequestDTO(); - fallbackRequest.setText(fallbackText); - fallbackRequest.setVoice("zh-CN-XiaoxiaoNeural"); - // 这里可以调用本地备用的TTS服务 - return synthesizeStream(fallbackRequest); - + TTSRequestDTO req = new TTSRequestDTO(); + req.setText("服务暂时不可用"); + req.setVoice("zh-CN-XiaoxiaoNeural"); + return synthesizeStream(req); } catch (Exception e) { - log.error("降级响应也失败了: {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) - .header("X-TTS-Error", "服务暂时不可用") - .body(null); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(null); } } - /** - * 获取默认语音列表 - */ private List getDefaultVoices() { - List defaultVoices = Arrays.asList( - createVoice("zh-CN-XiaoxiaoNeural", "zh-CN", "Female", "晓晓 - 中文女声"), - createVoice("zh-CN-YunyangNeural", "zh-CN", "Male", "云扬 - 中文男声"), - createVoice("en-US-JennyNeural", "en-US", "Female", "Jenny - 英文女声"), - createVoice("en-US-GuyNeural", "en-US", "Male", "Guy - 英文男声"), - createVoice("ja-JP-NanamiNeural", "ja-JP", "Female", "七海 - 日文女声"), - createVoice("ko-KR-SunHiNeural", "ko-KR", "Female", "선히 - 韩文女声") + return Arrays.asList( + createVoice("zh-CN-XiaoxiaoNeural", "zh-CN", "Female", "晓晓"), + createVoice("zh-CN-YunyangNeural", "zh-CN", "Male", "云扬") ); - - log.warn("使用默认语音列表,共 {} 个语音", defaultVoices.size()); - return defaultVoices; } private VoiceInfoDTO createVoice(String name, String locale, String gender, String displayName) { - VoiceInfoDTO voice = new VoiceInfoDTO(); - voice.setName(name); - voice.setLocale(locale); - voice.setGender(gender); - voice.setDisplayName(displayName); - return voice; + VoiceInfoDTO v = new VoiceInfoDTO(); + v.setName(name); v.setLocale(locale); v.setGender(gender); v.setDisplayName(displayName); + return v; } } \ No newline at end of file diff --git a/src/main/java/com/xly/util/AdvancedSymbolRemover.java b/src/main/java/com/xly/util/AdvancedSymbolRemover.java index 64a4444..3d00538 100644 --- a/src/main/java/com/xly/util/AdvancedSymbolRemover.java +++ b/src/main/java/com/xly/util/AdvancedSymbolRemover.java @@ -22,16 +22,20 @@ public class AdvancedSymbolRemover { if (text == null || text.isEmpty()) return ""; text = HtmlCleaner.cleanHtml(text); - // 移除中文和英文标点 - text = text.replaceAll("[\\pP\\p{Punct}]", ""); - // 可选:只保留字母、数字、汉字、空格 - text = text.replaceAll("[^\\p{L}\\p{N}\\p{Zs}]", ""); text = text.replaceAll("br", ""); text = text.replaceAll("
", ""); text = text.replaceAll("", ""); text = text.replaceAll("
", ""); text = text.replaceAll(" ", ""); + // 👇 【安全正则】只删除 数字后面的 .0 或 .00 + text = text.replaceAll("(?<=\\d)\\.0+(?!\\d)", ""); + // 移除中文和英文标点 + text = text.replaceAll("[\\pP\\p{Punct}]", ""); + + // 可选:只保留字母、数字、汉字、空格 + text = text.replaceAll("[^\\p{L}\\p{N}\\p{Zs}]", ""); + return text; }catch (Exception e){ } diff --git a/src/main/java/com/xly/web/TTSStreamController.java b/src/main/java/com/xly/web/TTSStreamController.java index 56f31a1..43bdc98 100644 --- a/src/main/java/com/xly/web/TTSStreamController.java +++ b/src/main/java/com/xly/web/TTSStreamController.java @@ -1,13 +1,16 @@ package com.xly.web; +import cn.hutool.core.util.ObjectUtil; import com.xly.runner.AppStartupRunner; import com.xly.service.UserSceneSessionService; import com.xly.tool.DynamicToolProvider; import com.xly.tts.bean.*; +import com.xly.tts.service.LocalAudioCache; import com.xly.tts.service.PythonTtsProxyService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.InputStreamResource; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; @@ -53,7 +56,7 @@ public class TTSStreamController { /** * 提取报修结构化信息 */ - @PostMapping("/init") + @PostMapping(value="/init", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.ALL_VALUE}) public ResponseEntity init(@RequestBody TTSRequestDTO request) { return pythonTtsProxyService.init(request); } @@ -73,11 +76,23 @@ public class TTSStreamController { /** * 流式合成语音(代理到Python服务) */ - @PostMapping("/stream/query") + @PostMapping(value = "/stream/query", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.ALL_VALUE}) public ResponseEntity stream(@RequestBody TTSRequestDTO request) { return pythonTtsProxyService.synthesizeStreamAi(request); } + @GetMapping("/audio") + public ResponseEntity getAudio(String cacheKey) { + if (ObjectUtil.isEmpty(cacheKey)) { + return ResponseEntity.ok(TTSResponseDTO.builder().code(204).build()); + } + TTSResponseDTO dto = LocalAudioCache.get(cacheKey); + if (dto == null) { + return ResponseEntity.ok(TTSResponseDTO.builder().code(204).build()); + } + return ResponseEntity.ok(dto); + } + /** * 流式合成语音(代理到Python服务) */ diff --git a/src/main/resources/templates/chat.html b/src/main/resources/templates/chat.html index c04afdf..35f2051 100644 --- a/src/main/resources/templates/chat.html +++ b/src/main/resources/templates/chat.html @@ -461,34 +461,26 @@