String boot 接入 azure云TTS

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;
        }
    }
相关推荐
斜月12 分钟前
Python Asyncio以及Futures并发编程实践
后端·python
No0d1es17 分钟前
第15届蓝桥杯Pthon青少组_国赛_中/高级组_2024年9月7日真题
python·青少年编程·蓝桥杯·国赛·中高组
talented_pure1 小时前
Python打卡Day30 模块和库的导入
开发语言·python
大虫小呓1 小时前
Python So Easy 大虫小呓三部曲 - 高阶篇
python
王大傻09282 小时前
python匿名函数lambda
python
Ashlee_code2 小时前
关税战火中的技术方舟:新西兰证券交易所的破局之道 ——从15%关税冲击到跨塔斯曼结算联盟,解码下一代交易基础设施
java·python·算法·金融·架构·系统架构·区块链
蓝倾9762 小时前
电商API接口的优势、数据采集方法及功能说明
开发语言·python·api·开放api·电商开放平台
倔强青铜三3 小时前
GIL竟是Python命中注定的解药?统治AI时代的核心秘密!
人工智能·python·ai编程
cliffordl3 小时前
wxPython 实践(二)基础控件
python