1.引入依赖
<!--微软的tts服务-->
<dependency>
<groupId>com.microsoft.cognitiveservices.speech</groupId>
<artifactId>client-sdk</artifactId>
<version>1.44.0</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.13.1</version>
</dependency>
2.英文情绪到中文的映射表
package com.ruoyi.image.utils;
import java.util.HashMap;
import java.util.Map;
public class AzureTTSStyleTranslator {
// 英文风格到中文的映射表
public static final Map<String, String> STYLE_TRANSLATIONS = new HashMap<String, String>() {{
put("assistant", "助理");
put("chat", "聊天");
put("customerservice", "客户服务");
put("newscast", "新闻");
put("affectionate", "撒娇");
put("angry", "愤怒");
put("calm", "平静");
put("cheerful", "愉悦");
put("disgruntled", "不满");
put("fearful", "害怕");
put("gentle", "温柔");
put("lyrical", "抒情");
put("sad", "悲伤");
put("serious", "严厉");
put("poetry-reading", "诗歌朗诵");
put("friendly", "友好");
put("chat-casual", "聊天 - 休闲");
put("whispering", "低语");
put("sorry", "抱歉");
put("excited", "兴奋");
put("narration-relaxed", "旁白-放松");
put("embarrassed", "尴尬");
put("depressed", "沮丧");
put("sports-commentary", "体育解说");
put("sports-commentary-excited", "体育解说-兴奋");
put("documentary-narration", "纪录片-旁白");
put("narration-professional", "旁白 - 专业");
put("newscast-casual", "新闻 - 休闲");
put("livecommercial", "实时广告");
put("envious", "羡慕");
put("empathetic", "同理心");
put("story", "故事");
put("advertisement-upbeat", "广告-欢快");
}};
}
3.工具类
package com.ruoyi.image.utils;
import cn.hutool.core.io.FileUtil;
import com.google.gson.Gson;
import com.microsoft.cognitiveservices.speech.*;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.uuid.UUID;
import org.bouncycastle.tsp.TSPUtil;
import org.springframework.stereotype.Service;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
@Service
public class AzureTtsUtil {
//加微信号 JIkyDy 试用Azure云Tts服务,或者搜索 创云,全云 联系Azure云中国代理
private static final String speechSubscriptionKey = "4BJpOHyvlhiN";
private static final String endpointUrl = "https://germanywestcentral.api.cognitive.microsoft.com/";
/**
* 测试合成语音
* @param text
*/
public static void tts(String text){
try (SpeechConfig config = SpeechConfig.fromEndpoint(new java.net.URI(endpointUrl), speechSubscriptionKey)) {
// Set the voice name, refer to https://aka.ms/speech/voices/neural for full
// list.
//config.setSpeechSynthesisVoiceName("zh-CN-XiaoxiaoMultilingualNeural");
config.setSpeechSynthesisVoiceName("zh-CN-YunjianNeural");
config.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Audio16Khz128KBitRateMonoMp3);
try (SpeechSynthesizer synth = new SpeechSynthesizer(config)) {
assert (config != null);
assert (synth != null);
Future<SpeechSynthesisResult> task = synth.SpeakTextAsync(text);
assert (task != null);
SpeechSynthesisResult result = task.get();
assert (result != null);
if (result.getReason() == ResultReason.SynthesizingAudioCompleted) {
System.out.println("Speech synthesized to speaker for text [" + text + "]");
byte[] audioData = result.getAudioData();
File file = new File("F:\\微软语音合成\\test.mp3");
// 使用 Hutool 的 FileUtil.writeBytes 方法将 byte[] 写入文件
FileUtil.writeBytes(audioData, file);
} else if (result.getReason() == ResultReason.Canceled) {
SpeechSynthesisCancellationDetails cancellation = SpeechSynthesisCancellationDetails
.fromResult(result);
System.out.println("CANCELED: Reason=" + cancellation.getReason());
if (cancellation.getReason() == CancellationReason.Error) {
System.out.println("CANCELED: ErrorCode=" + cancellation.getErrorCode());
System.out.println("CANCELED: ErrorDetails=" + cancellation.getErrorDetails());
System.out.println("CANCELED: Did you update the subscription info?");
}
}
}
} catch (Exception ex) {
System.out.println("Unexpected exception: " + ex.getMessage());
}
}
/**
* 获得朗读人列表
*/
public static Map<String,Object> getVoices(){
try {
//三个儿童数字人 晓双 晓悠 云夏
List<String> list1 = Arrays.asList("zh-CN-XiaoshuangNeural", "zh-CN-XiaoyouNeural", "zh-CN-YunxiaNeural");
//有些角色的localName是英文,在页面上显示效果不好,需要过滤掉 比如:Yunxiao Multilingual
List<String> list = Arrays.asList("Yunxiao Multilingual");
SpeechConfig speechConfig = SpeechConfig.fromEndpoint(new URI(endpointUrl), speechSubscriptionKey);
SpeechSynthesizer synthesizer = new SpeechSynthesizer(speechConfig);
SynthesisVoicesResult voicesResult = synthesizer.getVoicesAsync("zh-CN").get();
Map<String,Object> rmap=new HashMap<>();
List<VoiceInfo> voices = voicesResult.getVoices();
//儿童
List<VoiceInfo> collect = voices.stream().filter(i -> !list.contains(i.getLocalName())).filter(i -> list1.contains(i.getShortName())).collect(Collectors.toList());
List<Map<String,Object>> childList=new ArrayList<>();
for(VoiceInfo info:collect){
Map<String,Object> infoMap=new HashMap<>();
infoMap.put("locale",info.getLocale());
infoMap.put("shortName",info.getShortName());
infoMap.put("gender",info.getGender().name());
infoMap.put("localName",info.getLocalName());
List<Map<String,String>> list12=new ArrayList<>();
List<String> styleList = info.getStyleList();
for(String style:styleList){
Map<String,String> styleMap=new HashMap<>();
styleMap.put("style",style);
styleMap.put("styleName",AzureTTSStyleTranslator.STYLE_TRANSLATIONS.get(style));
list12.add(styleMap);
}
infoMap.put("styleList",list12);
childList.add(infoMap);
}
rmap.put("child",childList);
//男人
List<VoiceInfo> collect1 = voices.stream().filter(i -> !list.contains(i.getLocalName())).filter(i -> !list1.contains(i.getShortName()) && StringUtils.equals(i.getGender().name(),"Male")).collect(Collectors.toList());
List<Map<String,Object>> maleList=new ArrayList<>();
for(VoiceInfo info:collect1){
Map<String,Object> infoMap=new HashMap<>();
infoMap.put("locale",info.getLocale());
infoMap.put("shortName",info.getShortName());
infoMap.put("gender",info.getGender().name());
infoMap.put("localName",info.getLocalName());
List<Map<String,String>> list12=new ArrayList<>();
List<String> styleList = info.getStyleList();
for(String style:styleList){
Map<String,String> styleMap=new HashMap<>();
styleMap.put("style",style);
styleMap.put("styleName",AzureTTSStyleTranslator.STYLE_TRANSLATIONS.get(style));
list12.add(styleMap);
}
infoMap.put("styleList",list12);
maleList.add(infoMap);
}
rmap.put("male",maleList);
//女人
List<VoiceInfo> collect2 = voices.stream().filter(i -> !list.contains(i.getLocalName())).filter(i -> !list1.contains(i.getShortName()) && StringUtils.equals(i.getGender().name(),"Female")).collect(Collectors.toList());
List<Map<String,Object>> femaleList=new ArrayList<>();
for(VoiceInfo info:collect2){
Map<String,Object> infoMap=new HashMap<>();
infoMap.put("locale",info.getLocale());
infoMap.put("shortName",info.getShortName());
infoMap.put("gender",info.getGender().name());
infoMap.put("localName",info.getLocalName());
List<Map<String,String>> list12=new ArrayList<>();
List<String> styleList = info.getStyleList();
for(String style:styleList){
Map<String,String> styleMap=new HashMap<>();
styleMap.put("style",style);
styleMap.put("styleName",AzureTTSStyleTranslator.STYLE_TRANSLATIONS.get(style));
list12.add(styleMap);
}
infoMap.put("styleList",list12);
femaleList.add(infoMap);
}
rmap.put("female",femaleList);
/* Gson gson= new Gson();
String json = gson.toJson(rmap);
System.out.println(json);*/
return rmap;
} catch (URISyntaxException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* 对制定文本,指定朗读人,指定风格进行朗读
* @param text
* @param voice
* @param style
*/
public static File ttsVoiceStyle(String text,String voice,String style){
try (SpeechConfig config = SpeechConfig.fromEndpoint(new java.net.URI(endpointUrl), speechSubscriptionKey)) {
// Set the voice name, refer to https://aka.ms/speech/voices/neural for full
// list.
//config.setSpeechSynthesisVoiceName("zh-CN-XiaoxiaoMultilingualNeural");
//config.setSpeechSynthesisVoiceName("zh-CN-YunjianNeural");
config.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Audio16Khz128KBitRateMonoMp3);
try (SpeechSynthesizer synth = new SpeechSynthesizer(config)) {
assert (config != null);
assert (synth != null);
// styledegree:强度 (0.01-2)
/**
* rate:语速 ("x-slow"~"x-fast" 或百分比) slow:慢 ,不支持百分比,但支持小数
* 值 描述 等效百分比(近似)
* x-slow 极慢速 ~50%
* slow 慢速 ~75%
* medium 中速(默认值) 100%
* fast 快速 ~150%
* x-fast 极快速 ~200%
*
* pitch:音调 ("x-low"~"x-high" 或 +/-n%)
*
* volume:音量 ("silent"~"x-loud" 或 +/-ndB)
*/
// 4. 使用 SSML 设置语音风格
String ssml = "<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='zh-CN'>"
+ "<voice name='"+voice+"'>"
+ "<mstts:express-as style='"+style+"' styledegree='1.5' >"
+ "<prosody rate='0.8'>"
+ text
+ "</prosody>"
+ "</mstts:express-as>"
+ "</voice>"
+ "</speak>";
Future<SpeechSynthesisResult> task = synth.SpeakSsmlAsync(ssml);
assert (task != null);
SpeechSynthesisResult result = task.get();
assert (result != null);
if (result.getReason() == ResultReason.SynthesizingAudioCompleted) {
System.out.println("Speech synthesized to speaker for text [" + text + "]");
byte[] audioData = result.getAudioData();
com.ruoyi.common.core.utils.uuid.UUID uuid = UUID.randomUUID();
String uuidString = uuid.toString().replace("-", "");
//输出文件绝对路径
String tempDir = System.getProperty("java.io.tmpdir");
if (org.apache.commons.lang3.StringUtils.isNotBlank(tempDir) && !tempDir.endsWith(File.separator)) {
tempDir += File.separator;
}
String out=tempDir+uuidString+".mp3";
File file = new File(out);
// 使用 Hutool 的 FileUtil.writeBytes 方法将 byte[] 写入文件
FileUtil.writeBytes(audioData, file);
return file;
} else if (result.getReason() == ResultReason.Canceled) {
SpeechSynthesisCancellationDetails cancellation = SpeechSynthesisCancellationDetails
.fromResult(result);
System.out.println("CANCELED: Reason=" + cancellation.getReason());
if (cancellation.getReason() == CancellationReason.Error) {
System.out.println("CANCELED: ErrorCode=" + cancellation.getErrorCode());
System.out.println("CANCELED: ErrorDetails=" + cancellation.getErrorDetails());
System.out.println("CANCELED: Did you update the subscription info?");
}
}
}
} catch (Exception ex) {
System.out.println("Unexpected exception: " + ex.getMessage());
}
return null;
}
public static void main(String[] args) {
ttsVoiceStyle("沃伦·巴菲特1930年8月30日出生于美国内布拉斯加州奥马哈,是全球投资界传奇人物,被誉为"奥马哈的先知",也是杰出投资者、企业家和慈善家。他出生于普通中产家庭,父亲是股票经纪人和共和党国会议员。巴菲特从小对数字和商业感兴趣,11岁开始投资生涯,购买了三股城市服务优先股。","zh-CN-YunzeNeural","angry");
//getVoices();
}
}
4.接口调用工具类
package com.ruoyi.image.apicontroller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.enums.TempFileModuleEnum;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.file.FileUtils;
import com.ruoyi.common.core.utils.hwcloud.obs.ObsUtil;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.image.apicontroller.req.CopywritingToSpeechByAzureTTSReq;
import com.ruoyi.image.apicontroller.req.MingmenGenerateVideoReq;
import com.ruoyi.image.apicontroller.vo.GenerateAudioVo;
import com.ruoyi.image.utils.AzureTtsUtil;
import com.ruoyi.image.utils.TextKeyGenerator;
import com.ruoyi.image.utils.alitts.AliTtsRes;
import com.ruoyi.image.utils.ffmpeg.FfmpegUtil;
import com.ruoyi.user.api.RemoteTempFileService;
import com.ruoyi.user.api.model.MmTempFile;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.ruoyi.image.utils.ffmpeg.FfmpegUtil.getAudioDuration;
@Slf4j
@RestController
@RequestMapping("/api/azureTts")
public class AzureTtsController {
public static final String SPLIT = ":";
public static final String AUDIO_KEY_PRE = "MMWZ:MMWZ_IMAGE:GENERATE_VIDEO:";
public static final String EXPIRED = "_expired";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RemoteTempFileService remoteTempFileService;
/**
* 获取所有朗读人
* @return
*/
@GetMapping("/getVoices")
public AjaxResult getVoices(){
Map<String, Object> voices = AzureTtsUtil.getVoices();
return AjaxResult.success("获取成功",voices);
}
/**
* 微软朗读
* @param
* @return
*/
@PostMapping("/copywritingToSpeechByAzureTTS")
public AjaxResult copywritingToSpeechByAzureTTS(@RequestBody CopywritingToSpeechByAzureTTSReq req) {
if (StringUtils.isBlank(req.getCopywriting())) {
return AjaxResult.error("请写下文本");
}
try {
// redis key
String encrypt = this.encryptCopywriting(req.getCopywriting(), req.getVoice(), req.getEmotion());
String key = AUDIO_KEY_PRE + encrypt;
// redis里面有 直接取出
String value = null;
if (stringRedisTemplate.hasKey(key + EXPIRED)) {
value = stringRedisTemplate.opsForValue().get(key);
}
if (StringUtils.isNotBlank(value)) {
return AjaxResult.success(JSON.parseObject(value, GenerateAudioVo.class));
}
// 生成语音
Object[] objects = this.textToSpeech(req.getCopywriting(), req.getVoice(), req.getEmotion());
if(objects == null || objects.length != 2){
return AjaxResult.error("生成失败");
}
Map<String,Object> map=new HashMap<>();
map.put("url",objects[0]);
map.put("audioDuration",objects[1]);
// 放redis中
// 带有过期时间的
stringRedisTemplate.opsForValue().set(key + EXPIRED, "1", 1, TimeUnit.DAYS);
// 真正存储数据
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(map));
//加入临时文件,定期删除
MmTempFile mmTempFile = new MmTempFile();
mmTempFile.setUrl(String.valueOf(objects[0]));
mmTempFile.setFileName(FileUtil.getName(String.valueOf(objects[0])));
mmTempFile.setFileType(ObsUtil.FILE_TYPE_AUDIO);
mmTempFile.setModule(TempFileModuleEnum.TUWEN_VOICE_LISTENING.getModule());
mmTempFile.setModuleDesc(com.ruoyi.common.core.utils.StringUtils.join(Arrays.asList(req.getVoice(), req.getEmotion()),"-"));
mmTempFile.setExpireTime(DateUtil.offsetDay(DateUtil.date(), 2));//两天之后删除
R<MmTempFile> booleanR = remoteTempFileService.addTempFile(mmTempFile, SecurityConstants.INNER);
return AjaxResult.success(map);
} catch (Exception e) {
return AjaxResult.error("文案转语音失败: " + e.getMessage());
}
}
/**
* 冒号拼接 并特殊编码
* 用于判断是否相同
**/
private String encryptCopywriting(String copywriting, String voice, String emotion, String... extension) {
StringBuilder content = new StringBuilder(voice + SPLIT + emotion);
if (extension != null) {
for (String s : extension) {
content.append(SPLIT).append(s);
}
}
content.append(SPLIT).append(copywriting);
return TextKeyGenerator.generateKey(content.toString());
}
private Object[] textToSpeech(String copywriting,String voice,String style) throws IOException {
// 校验文案是否为空
if (StringUtils.isBlank(copywriting)) {
throw new ServiceException("文案内容不能为空", Constants.FAIL);
}
List<String> textList = this.split4000(copywriting);
// 保存阿里云返回的音频文件
List<File> aliTtsResList = new ArrayList<>();
// 1、 调用阿里云 语音合成接口 生成语音文件
for (String text : textList) {
// TTS
File audioFile = AzureTtsUtil.ttsVoiceStyle(text, voice,style);
if (audioFile == null || !audioFile.exists()) {
throw new ServiceException("语音生成失败", Constants.FAIL);
}
aliTtsResList.add(audioFile);
}
// 把aliTtsResList中的多个音频文件合成一个
File audioFile = FfmpegUtil.mergeAudioFiles(aliTtsResList.stream().collect(Collectors.toList()));
if (audioFile == null || !audioFile.exists()) {
throw new ServiceException("语音生成失败", Constants.FAIL);
}
/**
* 主要限制
* 标准限制:
*
* 单次请求最大字符数:10,000 个字符(包括 SSML 标签)
*
* 中文文本限制:大约 3,000-5,000 个汉字(因SSML标签占用部分字符)
*
* 实际可用字符:
*
* 纯中文文本(不含SSML标签)通常可处理约 4,500-5,000 个汉字
*
* 含复杂SSML标签的文本可能只能处理 3,000-4,000 个汉字
*/
//File audioFile = AzureTtsUtil.ttsVoiceStyle(audioVo.getCopywriting(), audioVo.getVoice(), audioVo.getEmotion());
// 2. 获取音频时长
int audioDuration = getAudioDuration(audioFile);
//audioVo.setCopywritingAudioDuration(String.valueOf(audioDuration));
// 3. 将语音文件转换为MultipartFile
MultipartFile multipartFile = FileUtils.convertToMultipartFile(audioFile);
// 4. 上传语音文件到OBS
Map<String, String> uploadResult = ObsUtil.upload(multipartFile);
if (!uploadResult.containsKey("url")) {
throw new ServiceException("语音上传失败", Constants.FAIL);
}
return new Object[]{uploadResult.get("url"),audioDuration};
}
/**
* 截取4000以内字符
* 截取时按照标点符号截,保证句子完整。
**/
private List<String> split4000(String text) {
List<String> textList = new ArrayList<>();
if (text.length() >= 4000) {
String copywriting = text;
// 定义句子结束的标点符号
String[] sentenceEndings = {"。", "!", "?", ";", ".", "!", "?", ";", ",", ",", "...", " "};
int startIndex = 0;
while (startIndex < copywriting.length()) {
// 计算当前段的结束位置(不超过300字)
int endIndex = Math.min(startIndex + 4000, copywriting.length());
// 如果不是最后一段,需要找到合适的句子结束位置
if (endIndex < copywriting.length()) {
// 从300字位置向前查找最近的句子结束符
boolean foundEnding = false;
for (int i = endIndex; i > startIndex; i--) {
String currentChar = String.valueOf(copywriting.charAt(i - 1));
for (String ending : sentenceEndings) {
if (ending.equals(currentChar)) {
endIndex = i;
foundEnding = true;
break;
}
}
if (foundEnding) {
break;
}
}
// 如果没找到句子结束符,就按300字截取
if (!foundEnding) {
endIndex = startIndex + 4000;
}
}
// 截取当前段并添加到列表
String segment = copywriting.substring(startIndex, endIndex);
textList.add(segment);
// 更新起始位置
startIndex = endIndex;
}
// 打印分段结果,方便调试
log.info("文案分段结果:");
for (int i = 0; i < textList.size(); i++) {
log.info("第{}段:{}", i + 1, textList.get(i));
}
} else {
textList.add(text);
}
return textList;
}
}
5.如果需要Docker部署,需要构建镜像时加入依赖
#FROM ibm-semeru-runtimes:open-17-jre
#FROM ibm-semeru-runtimes:open-8-jre
FROM openjdk:8-jdk
VOLUME /tmp
WORKDIR /opt/app
ARG JAR_FILE=*.jar
COPY ${JAR_FILE} app.jar
COPY bootstrap.yml bootstrap.yml
#RUN mkdir /opt/arthas
#COPY /opt/arthas/arthas-boot.jar /opt/arthas/arthas-boot.jar
#安装ffmpeg
RUN mkdir /opt/ffmpeg
COPY ffmpeg/ /opt/ffmpeg/
RUN chmod +x /opt/ffmpeg/bin/*
ENV FFMPEG_PATH=/opt/ffmpeg/bin/ffmpeg
#复制字体(一键成片字幕字体)
COPY font/SIMKAI.TTF /usr/share/fonts/
RUN chmod 644 /usr/share/fonts/SIMKAI.TTF
#微软tts依赖
# 安装必要依赖
RUN apt-get update && \
apt-get install -y \
libasound2 \
libpulse-dev \
libssl-dev \
ca-certificates && \
rm -rf /var/lib/apt/lists/*
# 设置环境变量(关键!)
ENV LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH
ENV JAVA_OPTS=""
EXPOSE 9205
#ENTRYPOINT java ${JAVA_OPTS} --add-opens=java.base/java.lang=ALL-UNNAMED -Djava.security.egd=file:/dev/./urandom -jar /opt/app/app.jar
ENTRYPOINT java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /opt/app/app.jar
6.补充工具方法
/**
* 合并多个音频文件
* @param fileList 音频文件列表
* @return 合并后的音频文件
*/
public static File mergeAudioFiles(List<File> fileList) throws IOException {
if (CollectionUtils.isEmpty(fileList)) {
return null;
}
// 如果只有一个文件,直接返回
if (fileList.size() == 1) {
return fileList.get(0);
}
// 创建临时文件列表
List<String> tempFileList = new ArrayList<>();
for (File file : fileList) {
tempFileList.add(file.getAbsolutePath());
}
// 创建合并后的临时文件
String mergedAudioPath = fileList.get(0).getAbsolutePath().replace(".mp3", "_merged.mp3");
File mergedFile = new File(mergedAudioPath);
try {
// 构建FFmpeg命令
List<String> command = new ArrayList<>();
command.add(ffmpegPath);
command.add("-y"); // 覆盖已存在的文件
command.add("-i");
command.add("concat:" + String.join("|", tempFileList));
command.add("-c:a");
command.add("copy");
command.add(mergedAudioPath);
// 执行FFmpeg命令
ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
log.error("音频合并失败,退出码:{}", exitCode);
return null;
}
// 删除原始临时文件
for (File file : fileList) {
if (file != null && file.exists()) {
file.delete();
}
}
return mergedFile;
} catch (Exception e) {
log.error("音频合并失败", e);
// 清理临时文件
if (mergedFile.exists()) {
mergedFile.delete();
}
return null;
}
}