shenrongliang
2025-03-31 0343d31837e1436ff775facd25c0e4e8c334fb2a
easegen-front/pnpm-lock.yaml
ÎļþÌ«´ó
easegen-front/src/components/UploadFile/src/UploadFile.vue
@@ -87,7 +87,7 @@
const props = defineProps({
  modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
  fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // æ–‡ä»¶ç±»åž‹, ä¾‹å¦‚['png', 'jpg', 'jpeg']
  fileSize: propTypes.number.def(5), // å¤§å°é™åˆ¶(MB)
  fileSize: propTypes.number.def(500), // å¤§å°é™åˆ¶(MB)
  limit: propTypes.number.def(5), // æ•°é‡é™åˆ¶
  autoUpload: propTypes.bool.def(true), // è‡ªåŠ¨ä¸Šä¼ 
  drag: propTypes.bool.def(false), // æ‹–拽上传
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/controller/admin/coursemedia/vo/CourseMediaMegerVO.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.module.digitalcourse.controller.admin.coursescenes.vo.AppCourseScenesMegerReqVO;
import cn.iocoder.yudao.module.digitalcourse.controller.admin.coursescenes.vo.AppCourseScenesSaveReqVO;
import cn.iocoder.yudao.module.digitalcourse.dal.dataobject.voices.AuditionVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -52,6 +53,8 @@
    private List<AppCourseScenesMegerReqVO> scenes;
    private AuditionVO auditionVo;
    private List<String> ppt;
    @Schema(description = "前端传的预估时间 ç§’", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/dal/dataobject/coursemedia/CourseMediaDO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.digitalcourse.dal.dataobject.coursemedia;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.digitalcourse.dal.dataobject.voices.AuditionVO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -87,6 +88,7 @@
    private String thumbnail;
//    private AuditionVO auditVo;
    /*
    * é¢„估所需扣除的积分
    * */
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/dal/mysql/digitalhumans/DigitalHumansMapper.java
@@ -40,4 +40,6 @@
    Integer auditing(@Param("creator") Long creator);
    DigitalHumansDO selectByCode(@Param("code") String code);
}
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/coursemedia/CourseMediaServiceImpl.java
@@ -106,6 +106,7 @@
            courseMediaDO.setMediaType(1);
            courseMediaDO.setName(updateReqVO.getName());
            courseMediaDO.setCourseName(updateReqVO.getName());
//            courseMediaDO.setAuditVo(updateReqVO.getAuditionVo());
            //将updateReqVO è½¬æ¢ä¸ºjson字符串
            courseMediaDO.setReqJson(JSON.toJSONString(updateReqVO));
            courseMediaMapper.insert(courseMediaDO);
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/coursemedia/CourseMediaServiceUtil.java
@@ -1,12 +1,19 @@
package cn.iocoder.yudao.module.digitalcourse.service.coursemedia;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.digitalcourse.controller.admin.coursemedia.vo.CourseMediaMegerVO;
import cn.iocoder.yudao.module.digitalcourse.controller.admin.coursescenecomponents.vo.AppCourseSceneComponentsMegerReqVO;
import cn.iocoder.yudao.module.digitalcourse.controller.admin.coursescenes.vo.AppCourseScenesMegerReqVO;
import cn.iocoder.yudao.module.digitalcourse.dal.dataobject.coursemedia.CourseMediaDO;
import cn.iocoder.yudao.module.digitalcourse.dal.dataobject.digitalhumans.DigitalHumansDO;
import cn.iocoder.yudao.module.digitalcourse.dal.dataobject.voices.AuditionVO;
import cn.iocoder.yudao.module.digitalcourse.dal.mysql.coursemedia.CourseMediaMapper;
import cn.iocoder.yudao.module.digitalcourse.dal.mysql.digitalhumans.DigitalHumansMapper;
import cn.iocoder.yudao.module.digitalcourse.service.voices.VoicesServiceImpl;
import cn.iocoder.yudao.module.digitalcourse.util.SrtToVttUtil;
import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
import com.alibaba.fastjson.JSON;
@@ -18,10 +25,15 @@
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.io.IOException;
import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -45,84 +57,232 @@
    @Resource
    private SrtToVttUtil srtToVttUtil;
    @Resource
    private DigitalHumansMapper digitalcourseDigitalHumansMapper;
    @Resource
    private VoicesServiceImpl serviceImpl;
    /**
     * è¿œç¨‹åˆå¹¶è§†é¢‘
     *
     * @param updateReqVO
     * @return
     */
    @Async
    public void remoteMegerMedia(CourseMediaMegerVO updateReqVO) {
        CourseMediaDO courseMediaDO = courseMediaMapper.selectById(updateReqVO.getCourseMediaId());
        List<AppCourseScenesMegerReqVO> scenes = updateReqVO.getScenes();
        //获取数字人素材(声音、视频)
        String entityId = null;
        if (scenes != null) {
            //获取scenes中的第一个元素的entityId
            List<AppCourseSceneComponentsMegerReqVO> components = scenes.get(0).getComponents();
            if (components != null) {
                entityId = components.get(0).getEntityId();
            }
        }
        DigitalHumansDO digitalHumansDO = digitalcourseDigitalHumansMapper.selectByCode(entityId);
        //获取数字人素材(声音、视频)
        if (digitalHumansDO == null) {
            // å¦‚果找不到对应的课程媒体记录,直接返回或记录错误日志
            return;
        }
        if (courseMediaDO == null) {
            // å¦‚果找不到对应的课程媒体记录,直接返回或记录错误日志
            return;
        }
        boolean success;
        List<String> videoUrls = new ArrayList<>();
        AuditionVO auditionVO = new AuditionVO();
        auditionVO.setHumanId(String.valueOf(digitalHumansDO.getId()));
        for (AppCourseScenesMegerReqVO scene : scenes) {
        int maxRetries = 3; // æœ€å¤§é‡è¯•次数
        int retryCount = 0;  // å½“前重试次数
        boolean success = false;
        while (retryCount < maxRetries && !success) {
            auditionVO.setText(scene.getBackground().getPptRemark());
            String audition = serviceImpl.audition(auditionVO);
            // æå–音频文件名(路径的最后一部分)
            String substring = configApi.getConfigValueByKey("easegen.url") + audition.substring(audition.lastIndexOf("/"));
            String fileName = audition.substring(audition.lastIndexOf('/') + 1);
            String newFileName = "D:/heygem_data/face2face/temp/" + fileName;
            // æå–视频文件名(路径的最后一部分)
            String substring1 = configApi.getConfigValueByKey("easegen.url") + digitalHumansDO.getFixVideoUrl().substring(digitalHumansDO.getFixVideoUrl().lastIndexOf("/"));
            String fileName1 = digitalHumansDO.getFixVideoUrl().substring(digitalHumansDO.getFixVideoUrl().lastIndexOf('/') + 1);
            String newFileName1 = "D:/heygem_data/face2face/temp/" + fileName1;
            //获取PPT内容
            String cover = scene.getComponents().get(1).getCover();
            //获取背景
            String cover1 = scene.getBackground().getCover();
            // åˆæˆppt背景,视频,模板
            //ffmpeg -i 2.png -i 1.mp4 -filter_complex "[0:v]scale=w=ceil(iw/2)*2:h=ceil(ih/2)*2[bg];[1:v]scale=iw/2:ih/2[v1];[bg][v1]overlay=x=0:y=H-h" output.mp4
            ProcessBuilder builder = new ProcessBuilder(
                    "ffmpeg", "-i", cover1,"-i",cover,"-i",substring1,"-filter_complex","[0:v]scale=",scene.getBackground().getWidth().toString(),":",scene.getBackground().getHeight().toString(),"[bg];[1:v]scale=",scene.getComponents().get(1).getWidth().toString(),":",scene.getComponents().get(1).getHeight().toString(),"[v1];[bg][v1]overlay=x=",scene.getComponents().get(1).getMarginLeft().toString(),":y=",scene.getComponents().get(1).getTop().toString(),"[img];[2:v]scale=",scene.getComponents().get(0).getWidth().toString(),":",scene.getComponents().get(0).getHeight().toString(),"[v2];[img][v2]overlay=x=",scene.getComponents().get(0).getMarginLeft().toString(),":y=",scene.getComponents().get(0).getTop().toString(),newFileName1
            );
            builder.redirectErrorStream(true);
            Process process = null;
            try {
                // å‘送POST请求
                HttpResponse execute = HttpRequest.post(configApi.getConfigValueByKey(EASEGEN_CORE_URL) + "/api/mergemedia")
                        .header("X-API-Key", configApi.getConfigValueByKey(EASEGEN_CORE_KEY))
                        .body(JSON.toJSONString(updateReqVO))
                        .execute();
                String body = execute.body();
                process = builder.start();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
                // æ£€æŸ¥å“åº”状态码是否成功
                if (execute.getStatus() != 200) {
                    retryCount++;
                    if (retryCount >= maxRetries) {
                        // è¶…过重试次数,更新状态和错误信息
                        courseMediaDO.setStatus(3); // 3 è¡¨ç¤ºåˆæˆå¤±è´¥
                        courseMediaDO.setErrorReason(truncateErrorMsg("HTTP è¯·æ±‚报错: " + execute.getStatus()));
                        courseMediaMapper.updateById(courseMediaDO);
                        return;
                    }
                    continue; // é‡æ–°å°è¯•
                }
            // è¯»å– FFmpeg è¾“出(可选)
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            try {
                Files.copy(Path.of(substring), Path.of(newFileName), StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
                // è§£æžå“åº”,检查是否有错误信息
                JSONObject responseJson = JSON.parseObject(body);
                if (!responseJson.getBoolean("success")) {
                    // å¤„理业务逻辑错误,更新状态和错误信息
                    String errorDetail = responseJson.getString("detail");
                    retryCount++;
                    if (retryCount >= maxRetries) {
                        courseMediaDO.setStatus(3); // 3 è¡¨ç¤ºåˆæˆå¤±è´¥
                        courseMediaDO.setErrorReason(truncateErrorMsg("API æŽ¥å£å¼‚常: " + errorDetail));
                        courseMediaMapper.updateById(courseMediaDO);
                        return;
                    }
                    continue; // é‡æ–°å°è¯•
                }
            //最大重试次数
            int maxRetries = 3;
            // å½“前重试次数
            int retryCount = 0;
            success = false;
                // å¦‚果成功,更新状态为1(成功)
                courseMediaDO.setStatus(1); // 1 è¡¨ç¤ºåˆæˆæˆåŠŸ
                courseMediaMapper.updateById(courseMediaDO);
                success = true;
            } catch (Exception e) {
                retryCount++;
                if (retryCount >= maxRetries) {
                    // æ•获异常,记录错误原因并更新状态
                    courseMediaDO.setStatus(3); // 3 è¡¨ç¤ºåˆæˆå¤±è´¥
                    courseMediaDO.setErrorReason(truncateErrorMsg("视频合成任务失败,请联系管理员,错误信息: " + e.getMessage()));
                    courseMediaMapper.updateById(courseMediaDO);
                    return;
                }
            while (retryCount < maxRetries && !success) {
                try {
                    // é‡è¯•前等待一段时间,避免频繁请求
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt(); // å¤„理中断异常
                    break;
                    // å‘送POST请求
                    HashMap<String, Object> objectObjectHashMap = new HashMap<>();
                    //音频路径
                    objectObjectHashMap.put("audio_url", fileName);
                    //视频路径(无声)
                    objectObjectHashMap.put("video_url", fileName1);
                    //唯一key(用于查询)
                    String code = RandomUtil.randomString(32);
                    //固定值
                    objectObjectHashMap.put("code", code);
                    objectObjectHashMap.put("chaofen", 0);
                    objectObjectHashMap.put("watermark_switch", 0);
                    objectObjectHashMap.put("pn", 1);
                    HttpResponse execute = HttpRequest.post("http://192.168.3.161:8383/easy/submit")
                            .body(JSON.toJSONString(objectObjectHashMap))
                            .execute();
                    String body = execute.body();
                    // æ£€æŸ¥å“åº”状态码是否成功
                    if (execute.getStatus() != 200) {
                        retryCount++;
                        if (retryCount >= maxRetries) {
                            // è¶…过重试次数,更新状态和错误信息
                            courseMediaDO.setStatus(3); // 3 è¡¨ç¤ºåˆæˆå¤±è´¥
                            courseMediaDO.setErrorReason(truncateErrorMsg("HTTP è¯·æ±‚报错: " + execute.getStatus()));
                            courseMediaMapper.updateById(courseMediaDO);
                            return;
                        }
                        continue; // é‡æ–°å°è¯•
                    }
                    // è§£æžå“åº”,检查是否有错误信息
                    JSONObject responseJson = JSON.parseObject(body);
                    if (!responseJson.getBoolean("success")) {
                        // å¤„理业务逻辑错误,更新状态和错误信息
                        String errorDetail = responseJson.getString("detail");
                        retryCount++;
                        if (retryCount >= maxRetries) {
                            courseMediaDO.setStatus(3); // 3 è¡¨ç¤ºåˆæˆå¤±è´¥
                            courseMediaDO.setErrorReason(truncateErrorMsg("API æŽ¥å£å¼‚常: " + errorDetail));
                            courseMediaMapper.updateById(courseMediaDO);
                            return;
                        }
                        continue; // é‡æ–°å°è¯•
                    }
                    //调用查询视频结果
                    String result = getResult(code);
                    result = "D:/heygem_data/face2face/temp" + result;
                    videoUrls.add(result);
                    // å¦‚果成功,更新状态为1(成功)
                    courseMediaDO.setStatus(1);
                    courseMediaMapper.updateById(courseMediaDO);
                    success = true;
                } catch (Exception e) {
                    retryCount++;
                    if (retryCount >= maxRetries) {
                        // æ•获异常,记录错误原因并更新状态
                        courseMediaDO.setStatus(3); // 3 è¡¨ç¤ºåˆæˆå¤±è´¥
                        courseMediaDO.setErrorReason(truncateErrorMsg("视频合成任务失败,请联系管理员,错误信息: " + e.getMessage()));
                        courseMediaMapper.updateById(courseMediaDO);
                        return;
                    }
                    try {
                        // é‡è¯•前等待一段时间,避免频繁请求
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt(); // å¤„理中断异常
                        break;
                    }
                }
            }
        }
        String fileListPath = "D:/heygem_data/face2face/temp/filelist.txt";
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileListPath))) {
            for (String path : videoUrls) {
                writer.write("file '" + path + "'\n");
            }
            System.out.println("文件列表已生成:" + fileListPath);
        } catch (IOException e) {
            e.printStackTrace();
        }
        ProcessBuilder builder = new ProcessBuilder(
                "ffmpeg", "-f", "concat", "-safe","0", "-i", "DD:/heygem_data/face2face/temp/filelist.txt", "-c", "copy", "D:/heygem_data/face2face/temp/生成视频.mp4"
        );
        builder.redirectErrorStream(true);
        Process process = null;
        try {
            process = builder.start();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public String getResult(String taskCode) {
        // ä½¿ç”¨ do-while å¾ªçŽ¯è½®è¯¢ä»»åŠ¡çŠ¶æ€
        // å®šä¹‰å˜é‡å­˜å‚¨è¿”回结果
        String result = null;
        // åˆå§‹åŒ–状态为未完成
        int status = -1;
        do {
            try {
                // è°ƒç”¨æŽ¥å£èŽ·å–ä»»åŠ¡çŠ¶æ€ï¼ˆå‡è®¾ä½¿ç”¨ Hutool çš„ HttpRequest)
                String body = HttpRequest.get("http://192.168.3.161:8383/easy/query?code=" + taskCode)
                        .execute()
                        .body();
                System.out.println("接口返回数据: " + body);
                // ä½¿ç”¨ fastjson è§£æž JSON æ•°æ®
                JSONObject jsonObject = JSON.parseObject(body);
                JSONObject data = jsonObject.getJSONObject("data");
                // æå–任务状态和结果
                status = data.getIntValue("status");
                // å¦‚果任务完成
                if (status == 2) {
                    result = data.getString("result");
                    System.out.println("任务已完成,结果文件路径: " + result);
                } else {
                    System.out.println("任务尚未完成,当前进度: " + data.getIntValue("progress") + "%");
                }
                // ç­‰å¾…一段时间再进行下一次轮询(避免频繁请求)
                // æ¯éš” 5 ç§’轮询一次
                Thread.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("调用接口失败,稍后重试...");
                try {
                    // å‡ºçŽ°å¼‚å¸¸æ—¶ä¹Ÿç­‰å¾… 5 ç§’
                    Thread.sleep(5000);
                } catch (InterruptedException interruptedException) {
                    interruptedException.printStackTrace();
                }
            }
            // å½“ status ä¸ç­‰äºŽ 2 æ—¶ç»§ç»­å¾ªçޝ
        } while (status != 2);
        return result;
    }
    public Boolean reMegerMedia(CourseMediaDO courseMediaDO) {
@@ -152,7 +312,7 @@
                    if (retryCount >= maxRetries) {
                        // è¶…过重试次数,更新状态和错误信息
                        courseMediaDO.setStatus(3); // 3 è¡¨ç¤ºåˆæˆå¤±è´¥
                        courseMediaDO.setErrorReason(truncateErrorMsg("HTTP è¯·æ±‚报错: " + execute.getStatus()+", æŠ¥é”™å†…容: " + body));
                        courseMediaDO.setErrorReason(truncateErrorMsg("HTTP è¯·æ±‚报错: " + execute.getStatus() + ", æŠ¥é”™å†…容: " + body));
                        courseMediaMapper.updateById(courseMediaDO);
                        return false;
                    }
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/util/PPTUtil.java
@@ -192,7 +192,7 @@
        File pdfFile = File.createTempFile("ppt_to_pdf_"+tempFileName, ".pdf");
        String command;
        if (isWindows()) {
            command = String.format("\"C:\\Program Files\\LibreOffice\\program\\soffice.com\" --headless --convert-to pdf --outdir %s %s", pdfFile.getParent(), pptFile.getAbsolutePath());
            command = String.format("\"E:\\LibreOffice\\LibreOffice\\program\\soffice.exe\" --headless --convert-to pdf --outdir %s %s", pdfFile.getParent(), pptFile.getAbsolutePath());
        } else {
            command = String.format("libreoffice --headless --convert-to pdf --outdir %s %s", pdfFile.getParent(), pptFile.getAbsolutePath());
        }
yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/resources/mapper/digitalhumans/DigitalHumansMapper.xml
@@ -12,4 +12,8 @@
    <select id="auditing">
        select count(1) from digitalcourse_digital_humans where deleted = 0 and creator = #{creator} and status in (1,2,4)
    </select>
    <select id="selectByCode"
            resultType="cn.iocoder.yudao.module.digitalcourse.dal.dataobject.digitalhumans.DigitalHumansDO">
        select * from digitalcourse_digital_humans where code = #{code} and deleted = 0
    </select>
</mapper>
yudao-server/src/main/resources/application.yaml
@@ -12,8 +12,8 @@
  servlet:
    # æ–‡ä»¶ä¸Šä¼ ç›¸å…³é…ç½®é¡¹
    multipart:
      max-file-size: 50MB # å•个文件大小
      max-request-size: 50MB # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
      max-file-size: 500MB # å•个文件大小
      max-request-size: 500MB # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
  # Jackson é…ç½®é¡¹
  jackson: