|
|
@@ -0,0 +1,548 @@
|
|
|
+package com.fs.aiSoundReplication.service.impl;
|
|
|
+
|
|
|
+
|
|
|
+import cn.hutool.core.bean.BeanUtil;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.fs.aiSoundReplication.config.TtsConfig;
|
|
|
+import com.fs.aiSoundReplication.config.VoiceCloneConfig;
|
|
|
+import com.fs.aiSoundReplication.exception.VoiceCloneException;
|
|
|
+import com.fs.aiSoundReplication.mapper.VoiceCloneMapper;
|
|
|
+import com.fs.aiSoundReplication.param.TtsChargeParam;
|
|
|
+import com.fs.aiSoundReplication.param.TtsRequest;
|
|
|
+import com.fs.aiSoundReplication.param.TtsResponse;
|
|
|
+import com.fs.aiSoundReplication.service.TtsService;
|
|
|
+import com.fs.company.param.VcCompanyUser;
|
|
|
+import com.fs.fastGpt.domain.FastgptEventLogTotal;
|
|
|
+import com.fs.fastGpt.mapper.FastGptRoleMapper;
|
|
|
+import com.fs.fastGpt.service.impl.FastgptEventLogTotalServiceImpl;
|
|
|
+import com.fs.fastgptApi.vo.AudioVO;
|
|
|
+import com.fs.qw.domain.QwUser;
|
|
|
+import com.fs.system.oss.CloudStorageService;
|
|
|
+import com.fs.system.oss.OSSFactory;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import okhttp3.*;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileInputStream;
|
|
|
+import java.io.FileOutputStream;
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.Executors;
|
|
|
+
|
|
|
+import static com.fs.fastgptApi.util.AudioUtils.getDurations;
|
|
|
+import static com.fs.fastgptApi.util.AudioUtils.transferAudioSilk;
|
|
|
+
|
|
|
+@Service("ttsService")
|
|
|
+@Slf4j
|
|
|
+public class TtsServiceImpl implements TtsService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TtsConfig ttsConfig;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private VoiceCloneConfig voiceCloneConfig;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private OkHttpClient okHttpClient;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ObjectMapper objectMapper;
|
|
|
+
|
|
|
+ private final ExecutorService executorService = Executors.newFixedThreadPool(5);
|
|
|
+
|
|
|
+ private static final String AUTHORIZATION_HEADER = "Authorization";
|
|
|
+ private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
|
|
+ @Autowired
|
|
|
+ private VoiceCloneMapper voiceCloneMapper;
|
|
|
+ @Autowired
|
|
|
+ private FastGptRoleMapper fastGptRoleMapper;
|
|
|
+ @Autowired
|
|
|
+ private FastgptEventLogTotalServiceImpl fastgptEventLogTotalServiceImpl;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public AudioVO textToSpeech(TtsRequest request) {
|
|
|
+ try {
|
|
|
+ // 1. 参数校验
|
|
|
+ validateTtsRequest(request);
|
|
|
+
|
|
|
+ // 2. 设置默认值
|
|
|
+ setDefaultValues(request);
|
|
|
+
|
|
|
+ // 3. 构建请求体
|
|
|
+ String requestBody = buildRequestBody(request);
|
|
|
+
|
|
|
+ // 4. 构建HTTP请求
|
|
|
+ Request httpRequest = buildHttpRequest(requestBody);
|
|
|
+
|
|
|
+ // 5. 发送请求(带重试)
|
|
|
+ byte[] bytes = executeTtsRequest(httpRequest);
|
|
|
+
|
|
|
+ // 6. 检查音频数据
|
|
|
+ if (bytes == null || bytes.length == 0) {
|
|
|
+ log.error("音频数据为空,VoiceType: {}", request.getVoiceType());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 自动保存音频文件
|
|
|
+ // 创建临时文件
|
|
|
+ File tempFile = File.createTempFile("tts_", ".wav");
|
|
|
+ try (FileOutputStream fos = new FileOutputStream(tempFile)) {
|
|
|
+ fos.write(bytes);
|
|
|
+ }
|
|
|
+ // 上传到OSS
|
|
|
+ try (FileInputStream fileInputStream = new FileInputStream(tempFile)) {
|
|
|
+ //直接转silk然后传桶,返回url 优化-需要wav格式
|
|
|
+ CloudStorageService storage = OSSFactory.build();
|
|
|
+ String wavUrl = storage.uploadSuffix(fileInputStream, ".wav");
|
|
|
+// AudioVO audioVO = AudioUtils.transferAudioSilkFromUrl(wavUrl, false);
|
|
|
+ Integer durations = getDurations(tempFile.getParent()+"\\"+tempFile.getName());
|
|
|
+ String silkUrl = transferAudioSilk(tempFile.getParent()+"\\", tempFile.getName(), false);
|
|
|
+ AudioVO audioVO = new AudioVO();
|
|
|
+ audioVO.setDuration(durations);
|
|
|
+ audioVO.setUrl(silkUrl);
|
|
|
+ audioVO.setWavUrl(wavUrl);
|
|
|
+ audioVO.setVoiceTxt(request.getText());
|
|
|
+ log.info("音频文件上传OSS成功: {}", audioVO.getUrl());
|
|
|
+ return audioVO;
|
|
|
+ } finally {
|
|
|
+ // 删除临时文件
|
|
|
+ tempFile.delete();
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("TTS合成失败,reqId: {}, 错误: {}",
|
|
|
+ request.getReqId(), e.getMessage());
|
|
|
+ throw e instanceof VoiceCloneException ?
|
|
|
+ (VoiceCloneException) e :
|
|
|
+ new VoiceCloneException("TTS合成失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void ttsChargeByCount(VcCompanyUser vcCompanyUser, AudioVO audioVO, QwUser user) {
|
|
|
+ try {
|
|
|
+ // BigDecimal ttsCharge = getTtsCharge(texts,unitPrice); // 算钱 暂时没有确认价格,只是统计字数
|
|
|
+ TtsChargeParam ttsChargeParam = new TtsChargeParam();
|
|
|
+ ttsChargeParam.setText(audioVO.getVoiceTxt()).setTextLength(audioVO.getVoiceTxt().length()).setCreateTime(LocalDateTime.now())
|
|
|
+// .setTotalPrice(ttsCharge).setUnitPrice(unitPrice)
|
|
|
+ .setCompanyUserId(Long.valueOf(vcCompanyUser.getId())).setVoiceUrl(audioVO.getUrl()).setDuration(audioVO.getDuration())
|
|
|
+ .setVersionId(vcCompanyUser.getVersionId());
|
|
|
+ if (user.getCompanyId()!=null){
|
|
|
+ ttsChargeParam.setCompanyId(user.getCompanyId()).setQwUserId(user.getId());
|
|
|
+ if (user.getFastGptRoleId()!=null){
|
|
|
+ ttsChargeParam.setFastgptRoleId(user.getFastGptRoleId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ voiceCloneMapper.insert(ttsChargeParam);
|
|
|
+ }catch (Exception e){
|
|
|
+ log.error("豆包声音复刻计数异常",e);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void countDailyTtsWords() {
|
|
|
+ List<TtsChargeParam> ttsChargeParams = voiceCloneMapper.countDailyTtsWords();
|
|
|
+ if (ttsChargeParams.isEmpty()){
|
|
|
+ log.info("每日统计声音复刻昨日无数据");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ log.info("每日统计声音复刻:"+ttsChargeParams);
|
|
|
+
|
|
|
+ ArrayList<FastgptEventLogTotal> Totals = new ArrayList<>();
|
|
|
+ ttsChargeParams.forEach(o->{
|
|
|
+ FastgptEventLogTotal bean = BeanUtil.toBean(o, FastgptEventLogTotal.class);
|
|
|
+ bean.setRoleId(o.getFastgptRoleId());
|
|
|
+ bean.setType(14);
|
|
|
+ bean.setCount(o.getTotalTextLength() * 450);
|
|
|
+ bean.setStatTime(o.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
|
|
|
+ Totals.add(bean);
|
|
|
+ });
|
|
|
+ fastgptEventLogTotalServiceImpl.getBaseMapper().insertFastgptEventLogTotalBatch(Totals);
|
|
|
+ }
|
|
|
+// @Override
|
|
|
+// public String textToSpeech(String text, String voiceType) {
|
|
|
+// // 创建简化版请求
|
|
|
+// TtsRequest request = new TtsRequest(
|
|
|
+// voiceCloneConfig.getAppId(),
|
|
|
+// voiceCloneConfig.getAccessToken(),
|
|
|
+// voiceType,
|
|
|
+// text
|
|
|
+// );
|
|
|
+//
|
|
|
+// // 设置默认参数
|
|
|
+// request.setFormat(ttsConfig.getDefaultFormat());
|
|
|
+// request.setSampleRate(ttsConfig.getDefaultSampleRate());
|
|
|
+// request.setCluster(ttsConfig.getDefaultCluster());
|
|
|
+//
|
|
|
+// return textToSpeech(request);
|
|
|
+// }
|
|
|
+
|
|
|
+// @Override
|
|
|
+// public File textToSpeechAndSave(TtsRequest request, String savePath) {
|
|
|
+// try {
|
|
|
+// // 1. 执行TTS合成
|
|
|
+// TtsResponse response = textToSpeech(request);
|
|
|
+//
|
|
|
+// if (response.getData() == null || response.getData().getAudio() == null) {
|
|
|
+// throw new VoiceCloneException(-1, "音频数据为空");
|
|
|
+// }
|
|
|
+//
|
|
|
+// // 2. 解码Base64音频数据
|
|
|
+// byte[] audioBytes = Base64.getDecoder().decode(response.getData().getAudio());
|
|
|
+//
|
|
|
+// // 3. 确定保存路径
|
|
|
+// String finalSavePath = savePath != null ? savePath : ttsConfig.getAudioSavePath();
|
|
|
+//
|
|
|
+// // 创建目录
|
|
|
+// Path directory = Paths.get(finalSavePath);
|
|
|
+// if (!Files.exists(directory)) {
|
|
|
+// Files.createDirectories(directory);
|
|
|
+// }
|
|
|
+//
|
|
|
+// // 4. 生成文件名
|
|
|
+// String fileName = String.format("%s_%s.%s",
|
|
|
+// request.getVoiceType(),
|
|
|
+// request.getReqId().substring(0, 8),
|
|
|
+// request.getFormat());
|
|
|
+//
|
|
|
+// File audioFile = new File(finalSavePath, fileName);
|
|
|
+//
|
|
|
+// // 5. 保存文件
|
|
|
+// try (FileOutputStream fos = new FileOutputStream(audioFile)) {
|
|
|
+// fos.write(audioBytes);
|
|
|
+// }
|
|
|
+//
|
|
|
+// log.info("音频文件保存成功: {}, 大小: {}KB",
|
|
|
+// audioFile.getAbsolutePath(), audioBytes.length / 1024);
|
|
|
+//
|
|
|
+// return audioFile;
|
|
|
+//
|
|
|
+// } catch (IOException e) {
|
|
|
+// log.error("保存音频文件失败", e);
|
|
|
+// throw new VoiceCloneException("保存音频文件失败", e);
|
|
|
+// }
|
|
|
+// }
|
|
|
+
|
|
|
+// @Override
|
|
|
+// public byte[] textToSpeechBytes(TtsRequest request) {
|
|
|
+// textToSpeech(request);
|
|
|
+// }
|
|
|
+
|
|
|
+// @Override
|
|
|
+// public String textToSpeechStream(TtsRequest request) {
|
|
|
+// byte[] audioBytes = textToSpeechBytes(request);
|
|
|
+//
|
|
|
+// }
|
|
|
+
|
|
|
+// @Override
|
|
|
+// public List<File> batchTextToSpeech(List<String> texts, String voiceType) {
|
|
|
+// List<CompletableFuture<File>> futures = new ArrayList<>();
|
|
|
+// List<File> results = new ArrayList<>();
|
|
|
+//
|
|
|
+// for (int i = 0; i < texts.size(); i++) {
|
|
|
+// final String text = texts.get(i);
|
|
|
+// final int index = i;
|
|
|
+//
|
|
|
+// CompletableFuture<File> future = CompletableFuture.supplyAsync(() -> {
|
|
|
+// try {
|
|
|
+// TtsRequest request = new TtsRequest(
|
|
|
+// voiceCloneConfig.getAppId(),
|
|
|
+// voiceCloneConfig.getAccessToken(),
|
|
|
+// voiceType,
|
|
|
+// text
|
|
|
+// );
|
|
|
+// request.setReqId(String.format("batch_%s_%d",
|
|
|
+// UUID.randomUUID().toString().substring(0, 8), index));
|
|
|
+//
|
|
|
+// return textToSpeechAndSave(request, null);
|
|
|
+// } catch (Exception e) {
|
|
|
+// log.error("批量TTS处理失败,文本索引: {}, 错误: {}", index, e.getMessage());
|
|
|
+// return null;
|
|
|
+// }
|
|
|
+// }, executorService);
|
|
|
+//
|
|
|
+// futures.add(future);
|
|
|
+// }
|
|
|
+//
|
|
|
+// // 等待所有任务完成
|
|
|
+// CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
|
+//
|
|
|
+// // 收集结果
|
|
|
+// for (CompletableFuture<File> future : futures) {
|
|
|
+// try {
|
|
|
+// File file = future.get();
|
|
|
+// if (file != null) {
|
|
|
+// results.add(file);
|
|
|
+// }
|
|
|
+// } catch (Exception e) {
|
|
|
+// log.error("获取批量处理结果失败", e);
|
|
|
+// }
|
|
|
+// }
|
|
|
+//
|
|
|
+// log.info("批量TTS处理完成,成功: {}/{}", results.size(), texts.size());
|
|
|
+// return results;
|
|
|
+// }
|
|
|
+
|
|
|
+ // ============ 私有方法 ============
|
|
|
+
|
|
|
+ private void validateTtsRequest(TtsRequest request) {
|
|
|
+ if (request == null) {
|
|
|
+ throw new VoiceCloneException(1001, "请求参数不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getText() == null || request.getText().trim().isEmpty()) {
|
|
|
+ throw new VoiceCloneException(1001, "文本内容不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getText().length() > ttsConfig.getMaxTextLength()) {
|
|
|
+ throw new VoiceCloneException(1001,
|
|
|
+ String.format("文本长度超过限制(%d字符)", ttsConfig.getMaxTextLength()));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getVoiceType() == null || request.getVoiceType().trim().isEmpty()) {
|
|
|
+ throw new VoiceCloneException(1001, "音色ID不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getReqId() == null || request.getReqId().trim().isEmpty()) {
|
|
|
+ request.setReqId(UUID.randomUUID().toString());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void setDefaultValues(TtsRequest request) {
|
|
|
+ if (request.getAppId() == null || request.getAppId().equals("")) {
|
|
|
+ request.setAppId(voiceCloneConfig.getAppId());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getToken() == null || request.getToken().equals("")) {
|
|
|
+ request.setToken(voiceCloneConfig.getAccessToken());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getCluster() == null) {
|
|
|
+ request.setCluster(ttsConfig.getDefaultCluster());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getFormat() == null) {
|
|
|
+ request.setFormat(ttsConfig.getDefaultFormat());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getSampleRate() == null) {
|
|
|
+ request.setSampleRate(ttsConfig.getDefaultSampleRate());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getSpeed() == null) {
|
|
|
+ request.setSpeed(ttsConfig.getDefaultSpeed());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getVolume() == null) {
|
|
|
+ request.setVolume(ttsConfig.getDefaultVolume());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (request.getPitch() == null) {
|
|
|
+ request.setPitch(ttsConfig.getDefaultPitch());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildRequestBody(TtsRequest request) throws IOException {
|
|
|
+ Map<String, Object> requestBody = new HashMap<>();
|
|
|
+
|
|
|
+ // 必填参数
|
|
|
+ HashMap<String, Object> app = new HashMap<String, Object>() {{
|
|
|
+ put("appid", request.getAppId());
|
|
|
+ put("token", request.getToken());
|
|
|
+ put("cluster", request.getCluster());
|
|
|
+ }};
|
|
|
+ HashMap<String, Object> user = new HashMap<String, Object>() {{
|
|
|
+ put("uid","01");
|
|
|
+ }};
|
|
|
+ HashMap<String, Object> audio = new HashMap<String, Object>() {{
|
|
|
+ put("voice_type", request.getVoiceType());
|
|
|
+ }};
|
|
|
+ if (request.getFormat() != null)audio.put("encoding", request.getFormat());
|
|
|
+ if (request.getSpeed() != null)audio.put("speed_ratio", request.getSpeed());
|
|
|
+
|
|
|
+ HashMap<String, Object> requestMap = new HashMap<String, Object>() {{
|
|
|
+ put("reqid", request.getReqId());
|
|
|
+ put("text", request.getText());
|
|
|
+ put("operation","query");
|
|
|
+ put("split_sentence", 1);//处理声音复刻语速过快
|
|
|
+ }};
|
|
|
+
|
|
|
+ requestBody.put("app",app);
|
|
|
+ requestBody.put("user", user);
|
|
|
+ requestBody.put("audio", audio);
|
|
|
+ requestBody.put("request", requestMap);
|
|
|
+
|
|
|
+ // 可选参数
|
|
|
+// if (request.getFormat() != null) {
|
|
|
+// requestBody.put("format", request.getFormat());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getSampleRate() != null) {
|
|
|
+// requestBody.put("sample_rate", request.getSampleRate());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getSpeed() != null) {
|
|
|
+// requestBody.put("speed", request.getSpeed());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getVolume() != null) {
|
|
|
+// requestBody.put("volume", request.getVolume());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getPitch() != null) {
|
|
|
+// requestBody.put("pitch", request.getPitch());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getAudioEncodeType() != null) {
|
|
|
+// requestBody.put("audio_encode_type", request.getAudioEncodeType());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getEnableSubtitle() != null) {
|
|
|
+// requestBody.put("enable_subtitle", request.getEnableSubtitle());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getVoiceId() != null) {
|
|
|
+// requestBody.put("voice_id", request.getVoiceId());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getLanguage() != null) {
|
|
|
+// requestBody.put("language", request.getLanguage());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getEmotion() != null) {
|
|
|
+// requestBody.put("emotion", request.getEmotion());
|
|
|
+// }
|
|
|
+
|
|
|
+// if (request.getSpeakingStyle() != null) {
|
|
|
+// requestBody.put("speaking_style", request.getSpeakingStyle());
|
|
|
+// }
|
|
|
+
|
|
|
+ return objectMapper.writeValueAsString(requestBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Request buildHttpRequest(String requestBody) {
|
|
|
+ RequestBody body = RequestBody.create(JSON,requestBody );
|
|
|
+
|
|
|
+ return new Request.Builder()
|
|
|
+ .url(ttsConfig.getHttpUrl())
|
|
|
+ .post(body)
|
|
|
+ .addHeader(AUTHORIZATION_HEADER, "Bearer;" + voiceCloneConfig.getAccessToken())
|
|
|
+ .addHeader("Content-Type", "application/json")
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[] executeTtsRequest(Request httpRequest) {
|
|
|
+ IOException lastException = null;
|
|
|
+
|
|
|
+ for (int i = 0; i < ttsConfig.getMaxRetryTimes(); i++) {
|
|
|
+ try (Response response = okHttpClient.newCall(httpRequest).execute()) {
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
+ throw new IOException("HTTP请求失败,状态码: " + response.code());
|
|
|
+ }
|
|
|
+
|
|
|
+ String responseBody = response.body().string();
|
|
|
+ log.debug("TTS API响应: {}", responseBody);
|
|
|
+
|
|
|
+ Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
|
|
|
+ Integer code = (Integer) responseMap.get("code");
|
|
|
+ if (code != null && code != 3000) {
|
|
|
+ String message = (String) responseMap.get("message");
|
|
|
+ throw new VoiceCloneException(code != null ? code : -1,
|
|
|
+ String.format("TTS合成失败: %s (错误码: %d)", message, code != null ? code : -1));
|
|
|
+ }
|
|
|
+ // 获取data字段(base64编码的音频数据)
|
|
|
+ Object data = responseMap.get("data");
|
|
|
+ if (data == null) {
|
|
|
+ throw new VoiceCloneException(-1, "TTS音频数据为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!(data instanceof String)) {
|
|
|
+ throw new VoiceCloneException(-1, "TTS音频数据格式错误");
|
|
|
+ }
|
|
|
+
|
|
|
+ String base64Audio = (String) data;
|
|
|
+
|
|
|
+ // 解码base64音频数据
|
|
|
+ try {
|
|
|
+ return Base64.getDecoder().decode(base64Audio);
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
+ log.error("Base64解码失败", e);
|
|
|
+ throw new VoiceCloneException("音频数据解码失败", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (IOException e) {
|
|
|
+ lastException = e;
|
|
|
+ log.warn("第{}次TTS请求失败: {}", i + 1, e.getMessage());
|
|
|
+
|
|
|
+ if (i < ttsConfig.getMaxRetryTimes() - 1) {
|
|
|
+ try {
|
|
|
+ Thread.sleep(ttsConfig.getRetryInterval());
|
|
|
+ } catch (InterruptedException ie) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ throw new VoiceCloneException("重试被中断", ie);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new VoiceCloneException("TTS请求失败,达到最大重试次数", lastException);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void checkTtsResponse(TtsResponse response) {
|
|
|
+ if (response == null || response.getBaseResp() == null) {
|
|
|
+ throw new VoiceCloneException(-1, "TTS响应数据异常");
|
|
|
+ }
|
|
|
+
|
|
|
+ Integer statusCode = response.getBaseResp().getStatusCode();
|
|
|
+ if (statusCode != 0) {
|
|
|
+ String errorMessage = response.getBaseResp().getStatusMessage();
|
|
|
+ throw new VoiceCloneException(statusCode,
|
|
|
+ String.format("TTS合成失败: %s (错误码: %d)", errorMessage, statusCode));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (response.getData() == null) {
|
|
|
+ throw new VoiceCloneException(-1, "TTS音频数据为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (response.getData().getAudio() == null) {
|
|
|
+ throw new VoiceCloneException(-1, "Base64音频数据为空");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void autoSaveAudio(String base64Audio, String reqId, String format) {
|
|
|
+ try {
|
|
|
+ // 解码音频
|
|
|
+ byte[] audioBytes = Base64.getDecoder().decode(base64Audio);
|
|
|
+
|
|
|
+ // 创建保存目录
|
|
|
+ Path saveDir = Paths.get(ttsConfig.getAudioSavePath());
|
|
|
+ if (!Files.exists(saveDir)) {
|
|
|
+ Files.createDirectories(saveDir);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成文件名
|
|
|
+ String fileName = String.format("auto_save_%s.%s",
|
|
|
+ reqId.substring(0, 8), format);
|
|
|
+ Path filePath = saveDir.resolve(fileName);
|
|
|
+
|
|
|
+ // 保存文件
|
|
|
+ Files.write(filePath, audioBytes);
|
|
|
+
|
|
|
+ log.debug("音频自动保存成功: {}", filePath);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("音频自动保存失败: {}", e.getMessage());
|
|
|
+ // 不抛出异常,自动保存失败不影响主要功能
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|