|
|
@@ -19,6 +19,7 @@ import org.ruoyi.common.chat.entity.chat.Message;
|
|
|
import org.ruoyi.common.chat.entity.chat.SchemaMessage;
|
|
|
import org.ruoyi.common.chat.entity.chat.ThinkProject;
|
|
|
import org.ruoyi.common.chat.entity.files.UploadFileResponse;
|
|
|
+import org.ruoyi.common.chat.entity.whisper.Transcriptions;
|
|
|
import org.ruoyi.common.chat.entity.whisper.WhisperResponse;
|
|
|
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
|
|
|
import org.ruoyi.common.chat.request.ChatRequest;
|
|
|
@@ -28,6 +29,7 @@ import org.ruoyi.common.core.utils.StringUtils;
|
|
|
import org.ruoyi.common.core.utils.file.FileUtils;
|
|
|
import org.ruoyi.common.core.utils.file.MimeTypeUtils;
|
|
|
import org.ruoyi.common.satoken.utils.LoginHelper;
|
|
|
+import org.ruoyi.common.core.service.ConfigService;
|
|
|
import org.ruoyi.core.page.PageQuery;
|
|
|
import org.ruoyi.core.page.TableDataInfo;
|
|
|
import org.ruoyi.domain.ThinkModel;
|
|
|
@@ -44,14 +46,19 @@ import org.ruoyi.service.IChatSessionService;
|
|
|
import org.ruoyi.service.IKnowledgeInfoService;
|
|
|
import org.ruoyi.service.VectorStoreService;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.core.io.ByteArrayResource;
|
|
|
import org.springframework.core.io.InputStreamResource;
|
|
|
import org.springframework.core.io.Resource;
|
|
|
import org.springframework.http.MediaType;
|
|
|
import org.springframework.http.ResponseEntity;
|
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
import java.io.File;
|
|
|
import java.io.FileOutputStream;
|
|
|
@@ -60,6 +67,7 @@ import java.io.InputStream;
|
|
|
import java.nio.file.Files;
|
|
|
import java.nio.file.Path;
|
|
|
import java.util.ArrayList;
|
|
|
+import java.util.HashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
|
|
|
@@ -85,8 +93,12 @@ public class SseServiceImpl implements ISseService {
|
|
|
|
|
|
private final IKnowledgeInfoService knowledgeInfoService;
|
|
|
|
|
|
+ private final RestTemplate restTemplate;
|
|
|
+
|
|
|
private ChatModelVo chatModelVo;
|
|
|
|
|
|
+ private final ConfigService configService;
|
|
|
+
|
|
|
@Autowired
|
|
|
private JdbcTemplate jdbcTemplate;
|
|
|
|
|
|
@@ -99,6 +111,9 @@ public class SseServiceImpl implements ISseService {
|
|
|
|
|
|
private String rerankModelName = "bge-reranker-v2-m3";
|
|
|
|
|
|
+ private static final String DEFAULT_TTS_PROXY_URL = "http://1.13.255.120:7899/v1/audio/speech";
|
|
|
+ private static final String DEFAULT_TTS_VOICE = "zh-CN-XiaoxiaoNeural";
|
|
|
+
|
|
|
@Override
|
|
|
public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) {
|
|
|
SseEmitter sseEmitter = new SseEmitter(0L);
|
|
|
@@ -382,6 +397,47 @@ public class SseServiceImpl implements ISseService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 自定义TTS代理,解决前端跨域
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public ResponseEntity<Resource> proxySpeech(TextToSpeech textToSpeech) {
|
|
|
+ if (textToSpeech == null || StringUtils.isEmpty(textToSpeech.getInput())) {
|
|
|
+ throw new IllegalArgumentException("input 不能为空");
|
|
|
+ }
|
|
|
+ String proxyUrl = resolveTtsProxyUrl();
|
|
|
+ String voice = StringUtils.isNotBlank(textToSpeech.getVoice()) ? textToSpeech.getVoice() : resolveTtsDefaultVoice();
|
|
|
+ String responseFormat = determineResponseFormat(textToSpeech);
|
|
|
+
|
|
|
+ Map<String, Object> payload = new HashMap<>();
|
|
|
+ payload.put("input", textToSpeech.getInput());
|
|
|
+ payload.put("voice", voice);
|
|
|
+ payload.put("response_format", responseFormat);
|
|
|
+ if (textToSpeech.getSpeed() != null) {
|
|
|
+ payload.put("speed", textToSpeech.getSpeed());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(textToSpeech.getModel())) {
|
|
|
+ payload.put("model", textToSpeech.getModel());
|
|
|
+ }
|
|
|
+
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
+ headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
+ HttpEntity<Map<String, Object>> entity = new HttpEntity<>(payload, headers);
|
|
|
+ ResponseEntity<byte[]> response = restTemplate.postForEntity(proxyUrl, entity, byte[].class);
|
|
|
+ if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
|
|
|
+ throw new IllegalStateException("TTS代理服务调用失败:" + response.getStatusCode());
|
|
|
+ }
|
|
|
+ byte[] body = response.getBody();
|
|
|
+ MediaType mediaType = resolveMediaType(responseFormat, response.getHeaders().getContentType());
|
|
|
+ String filename = "speech." + responseFormat.toLowerCase();
|
|
|
+ return ResponseEntity.status(HttpStatus.OK)
|
|
|
+ .contentType(mediaType)
|
|
|
+ .contentLength(body.length)
|
|
|
+ .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=" + filename)
|
|
|
+ .header(HttpHeaders.ACCEPT_RANGES, "bytes")
|
|
|
+ .body(new ByteArrayResource(body));
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 语音转文字
|
|
|
*/
|
|
|
@@ -402,7 +458,23 @@ public class SseServiceImpl implements ISseService {
|
|
|
} catch (IOException e) {
|
|
|
throw new RuntimeException("Failed to convert MultipartFile to File", e);
|
|
|
}
|
|
|
- return openAiStreamClient.speechToTextTranscriptions(fileA);
|
|
|
+ Transcriptions transcriptions = Transcriptions.builder()
|
|
|
+ .model(resolveAudioModel())
|
|
|
+ .build();
|
|
|
+ return openAiStreamClient.speechToTextTranscriptions(fileA, transcriptions);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveAudioModel() {
|
|
|
+ String defaultModel = "whisper-large-v3";
|
|
|
+ try {
|
|
|
+ String audioModel = configService.getConfigValue("chat", "audioModel");
|
|
|
+ if (StringUtils.isNotBlank(audioModel)) {
|
|
|
+ return audioModel;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("音频模型配置获取失败,使用默认模型 {} :{}", defaultModel, e.getMessage());
|
|
|
+ }
|
|
|
+ return defaultModel;
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -452,6 +524,56 @@ public class SseServiceImpl implements ISseService {
|
|
|
return file;
|
|
|
}
|
|
|
|
|
|
+ private String resolveTtsProxyUrl() {
|
|
|
+ try {
|
|
|
+ String configValue = configService.getConfigValue("chat", "ttsProxyUrl");
|
|
|
+ if (StringUtils.isNotBlank(configValue)) {
|
|
|
+ return configValue;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("获取ttsProxyUrl失败,使用默认地址 {} :{}", DEFAULT_TTS_PROXY_URL, e.getMessage());
|
|
|
+ }
|
|
|
+ return DEFAULT_TTS_PROXY_URL;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveTtsDefaultVoice() {
|
|
|
+ try {
|
|
|
+ String configValue = configService.getConfigValue("chat", "ttsDefaultVoice");
|
|
|
+ if (StringUtils.isNotBlank(configValue)) {
|
|
|
+ return configValue;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("获取ttsDefaultVoice失败,使用默认值 {} :{}", DEFAULT_TTS_VOICE, e.getMessage());
|
|
|
+ }
|
|
|
+ return DEFAULT_TTS_VOICE;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String determineResponseFormat(TextToSpeech textToSpeech) {
|
|
|
+ String format = textToSpeech.getResponseFormat();
|
|
|
+ if (StringUtils.isBlank(format)) {
|
|
|
+ format = "mp3";
|
|
|
+ }
|
|
|
+ textToSpeech.setResponseFormat(format);
|
|
|
+ return format;
|
|
|
+ }
|
|
|
+
|
|
|
+ private MediaType resolveMediaType(String responseFormat, MediaType upstreamType) {
|
|
|
+ String format = responseFormat != null ? responseFormat.toLowerCase() : "";
|
|
|
+ if (format.contains("mp3") || format.contains("mpeg")) {
|
|
|
+ return MediaType.parseMediaType("audio/mpeg");
|
|
|
+ }
|
|
|
+ if (format.contains("wav")) {
|
|
|
+ return MediaType.parseMediaType("audio/wav");
|
|
|
+ }
|
|
|
+ if (format.contains("ogg")) {
|
|
|
+ return MediaType.parseMediaType("audio/ogg");
|
|
|
+ }
|
|
|
+ if (upstreamType != null) {
|
|
|
+ return upstreamType;
|
|
|
+ }
|
|
|
+ return MediaType.APPLICATION_OCTET_STREAM;
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
public SchemaMessage getSchema(SchemaRequest schemaRequest, HttpServletRequest request) throws IOException {
|
|
|
SchemaMessage schema = new SchemaMessage();
|