easegen-front/src/views/Login/Login.vue
@@ -1,28 +1,28 @@ <template> <div class="bei"> <div class="BeiArea"> <!-- 左侧图片 --> <div class="Left-img"> <div class="TitleText" > <text> 数字人 </text>智能交互平台 </div> </div> <!-- 右边的登录界面 --> <Transition appear enter-active-class="animate__animated animate__bounceInRight"> <div class="form-box"> <!-- 账号登录 --> <LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> <!-- 手机登录 --> <MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> <!-- 二维码登录 --> <QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> <!-- 注册 --> <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> <!-- 三方登录 --> <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> </div> </Transition> <div class="BeiArea"> <!-- 左侧图片 --> <div class="Left-Area"> <div class="TitleText"> <h1>数字人智能交互平台</h1> </div> </div> <!-- 右边的登录界面 --> <Transition appear enter-active-class="animate__animated animate__bounceInRight"> <div class="form-box"> <!-- 账号登录 --> <LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> <!-- 手机登录 --> <MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> <!-- 二维码登录 --> <QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> <!-- 注册 --> <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> <!-- 三方登录 --> <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white) bai" /> </div> </Transition> </div> </div> </template> <script lang="ts" setup> @@ -33,7 +33,7 @@ import { LocaleDropdown } from '@/layout/components/LocaleDropdown' import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components' import * as ConfigApi from "@/api/infra/config"; import * as ConfigApi from '@/api/infra/config' defineOptions({ name: 'Login' }) @@ -43,89 +43,113 @@ const prefixCls = getPrefixCls('login') const passwordLoginSwitch = ref(undefined) onMounted(async ()=>{ const data = await ConfigApi.getConfigKey('password-login-switch') if (data && data.length > 0) { passwordLoginSwitch.value = data } onMounted(async () => { const data = await ConfigApi.getConfigKey('password-login-switch') if (data && data.length > 0) { passwordLoginSwitch.value = data } }) </script> <style scoped> *{ <style> .bei .BeiArea .form-box > .el-form { height: 100%; } .bei .BeiArea .form-box > .el-form > div { height: 100%; display: flex; justify-content: space-between; align-items: center; } </style> <style scoped> * { margin: 0; padding: 0; } .bei{ .bei { width: 100%; height: 100%; background-color: #000; min-height: 100%; display: flex; justify-content: center; align-items: center; } .bei .BeiArea{ width: 86%; height: 95%; .bei .BeiArea { width: auto; max-width: 100%; height: auto; display: flex; justify-content: center; align-items: center; background-color: #000a25; border-radius: 8px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; overflow: hidden; } .bei .BeiArea .form-box{ /* width: 25%; */ width: 400px; margin-top: -36px; padding: 70px 30px; .bei .BeiArea .Left-Area { width: 500px; height: 450px; display: flex; justify-content: center; align-content: center; background: linear-gradient(to bottom right, #1b6ac2, #57b5f2); } .bei .BeiArea .Left-Area .TitleText { display: flex; justify-content: center; align-items: center; } .bei .BeiArea .Left-Area .TitleText h1 { color: #fff; font-size: 2.6rem; } .bei .BeiArea .form-box { box-sizing: border-box; background: #fff; margin-left: 300px; box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; } .bei .BeiArea .Left-img{ width: 729px; height: 655px; margin-top: 138px; margin-left: -74px; background-image:url( "@/assets/imgs/bei3-1.png" ); /* background-size: 100%; */ background-size: contain; background-repeat: no-repeat; background-position: center center; display: flex; justify-content: center; align-items: center; } .bei .BeiArea .Left-img .TitleText{ margin-top: calc( -85% ); margin-left: -60px; color: #fff; width: 100%; text-align: left; font-size: 48px; padding-left: 10%; box-sizing: border-box; } .bei .BeiArea .Left-img .TitleText text{ color: #2d84fa; /* width: calc(400px + (100vw - 1900px) * 0.5); */ width: 500px; height: 450px; padding: 20px 40px; } @media screen and ( max-width: 1300px ) { .form-box{ width: 50% !important; margin: 0 auto !important; /* @media ( max-width:1300px ) and (min-width:1000px) { .bei .BeiArea .form-box{ width: 45%; height: 90vh; } .Left-img{ display: none !important; .bei .BeiArea .Left-Area{ width: 45%; height: 90vh; } } */ /* @media ( max-width:1000px ){ .bei .BeiArea .form-box{ width: 50%; height: 100vh; } .bei .BeiArea .Left-Area{ width: 50%; height: 100vh; } } */ @media ( max-width:1550px ){ .bei .BeiArea .Left-Area .TitleText h1{ font-size: 2.4rem !important; } } @media (max-width:1650px) and ( min-width: 1300px ) { .bei .BeiArea .Left-img{ margin-left: 0; @media ( max-width:1050px ){ .bei .BeiArea .Left-Area .TitleText h1{ font-size: 2rem !important; } } easegen-front/src/views/Login/components/LoginForm.vue
@@ -358,14 +358,11 @@ .NewClass { ::v-deep(.el-input__inner) { font-size: 20px; line-height: 40px; height: 60px; } ::v-deep(.el-button--large) { padding: 20px; box-sizing: border-box; font-size: 20px; height: 60px; margin-top: 20px; } } @@ -374,8 +371,6 @@ .NewClass { ::v-deep(.el-input__inner) { font-size: 20px; line-height: 40px; height: 50px; } ::v-deep(.el-form-item--large){ margin-bottom: 40px; @@ -387,7 +382,6 @@ padding: 20px; box-sizing: border-box; font-size: 20px; height: 60px; margin-top: 10px; } } easegen-front/src/views/Login/components/LoginFormTitle.vue
@@ -1,7 +1,8 @@ <style> .c717a8a{ color: #717a8a color: #717a8a; text-align: center; } </style> easegen-front/src/views/chooseTemplate/index.vue
@@ -586,36 +586,44 @@ </div> </div> <div class="SoundModelArea" v-loading="soundLoading"> <div class="ModealBox" v-for="(item, index) in audioList" :key="index" @click="handleSelect(item)" @mouseenter="handleMouseenter(item)" @mouseleave="handleMouseleave(item)" :class="item.isSelect ? 'slectModel' : ''" > <div class="ImgBox"> <img :src="item.avatarUrl" alt="" /> <div class="SoundModelAreaBox" v-for="(value, key, index) in audioList" :key="index"> <div class="SoundClassTit"> <el-divider content-position="center"> {{ languageClass(key) }} </el-divider> <!-- <el-divider content-position="center"> 123 </el-divider> --> </div> <div class="TextArea"> <p> {{ item.name }} </p> <p> {{ item.introduction }} </p> <div class="SoundClassContent"> <div class="ModealBox" v-for="(item, index) in value" :key="index" @click="handleSelect(key, item)" @mouseenter="handleMouseenter(key, item)" @mouseleave="handleMouseleave(key, item)" :class="item.isSelect ? 'slectModel' : ''" > <div class="ImgBox"> <img :src="item.avatarUrl" alt="" /> </div> <div class="TextArea"> <p> {{ item.name }} </p> <p> {{ item.introduction }} </p> </div> <img class="play-img" v-if="item.isHover && !item.isPlay" src="@/assets/imgs/play.png" alt="" @click.stop="playAudio(item)" /> <img class="play-img" v-if="item.isHover && item.isPlay" src="@/assets/imgs/pause.png" alt="" @click.stop="SoundpauseAudio(item)" /> </div> </div> <img class="play-img" v-if="item.isHover && !item.isPlay" src="@/assets/imgs/play.png" alt="" @click.stop="playAudio(item)" /> <img class="play-img" v-if="item.isHover && item.isPlay" src="@/assets/imgs/pause.png" alt="" @click.stop="SoundpauseAudio(item)" /> </div> </div> <div class="ButtonArea"> @@ -740,7 +748,6 @@ import { useEditorHtml } from '@/hooks/web/useEditorHtml' import { ElMessage, ElMessageBox } from 'element-plus' import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict' import { measureMemory } from 'vm' import { any } from 'vue-types' const editorHtml = useEditorHtml() @@ -773,7 +780,6 @@ hostValue.value = item chooseHost1(2) // dialogVisible2.value = true } const chooseHost1 = (index) => { if (index == 1) { @@ -1012,11 +1018,11 @@ // 获取声音类别 getVoiceType() } if( selectList.value === undefined ){ if (selectList.value === undefined) { // 获取模型列表 getSoundModelList() } if( ChangeSoundTypeList.value === undefined ){ if (ChangeSoundTypeList.value === undefined) { // 获取可选的声音类型列表 GetSoundTypeList() } @@ -1043,7 +1049,7 @@ const changeAudio = ref<any>() //获取性别字典 const getAudioType = () => { const list = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX) let list = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX) audioType.value = list changeAudio.value = list[0].value } @@ -1088,8 +1094,11 @@ soundLoading.value = true // 语言类型 soundQueryParams.language = selectLanguage?.value.value ?? '' soundQueryParams.language = soundQueryParams.language === 'all_Language' ? '' : soundQueryParams.language // 性别 soundQueryParams.gender = changeAudio?.value ?? '' soundQueryParams.gender = Number(soundQueryParams.gender) === 3 ? '' : soundQueryParams.gender // 声音类型 soundQueryParams.voiceType = activeSoundType?.value.value ?? '' const data = await pptTemplateApi.videlPageList(soundQueryParams) @@ -1098,11 +1107,45 @@ item.isPlay = false item.isSelect = false }) audioList.value = data.list let LanguageArr = {} data.list.forEach((item) => { if (LanguageArr?.[item.language] !== undefined) { LanguageArr[item.language].push(item) } else { LanguageArr = { ...LanguageArr, [item.language]: [{ ...item }] } } }) console.log(LanguageArr) audioList.value = LanguageArr total.value = data.total if (selectList.value !== undefined && selectList.value !== null) { selectList.value = null //初始化 } // 停止当前播放的音频 if (SoundcurrentAudio.value) { SoundcurrentAudio.value.pause() SoundcurrentAudio.value = null } // 重置当前播放状态 if (SoundcurrentlyPlaying.value) { SoundcurrentlyPlaying.value.isPlay = false SoundcurrentlyPlaying.value = null } } finally { soundLoading.value = false } } // 当前语种显示 const languageClass = (language) => { let text = '' languageList.value.forEach((element) => { if (element.value === language) { text = element.label } }) return text } // 语种选择 const LanguageChange = (event) => { @@ -1122,15 +1165,30 @@ SoundTypeList.value.forEach((element) => { if (element.value === event) { ChangeSoundTypeList.value = { ...element } if (selectList.value !== undefined && selectList.value !== null) { selectList.value = null //初始化 } // 停止当前播放的音频 if (SoundcurrentAudio.value) { SoundcurrentAudio.value.pause() SoundcurrentAudio.value = null } // 重置当前播放状态 if (SoundcurrentlyPlaying.value) { SoundcurrentlyPlaying.value.isPlay = false SoundcurrentlyPlaying.value = null } } }) getSoundModelList() } //选择声音模型 const selectList = ref() const handleSelect = (item) => { const handleSelect = (key, item) => { selectList.value = [item] audioList.value.forEach((child) => { audioList.value[key].forEach((child) => { if (child.id == item.id) { child.isSelect = true } else { @@ -1147,11 +1205,14 @@ }) // 确定按钮点击处理函数 const submitForm = () => { console.log(ChangeSoundTypeList.value.value) console.log(selectLanguage.value) if (selectLanguage.value.value === 'all_Language') { message.warning('请将语种按钮由全部语种修改为您需要生成的声音的文本的语种类型') return false } if (ChangeSoundTypeList.value.value === 2) { //此时为通用 console.log(selectList.value) if (selectList.value === undefined) { if (selectList.value === undefined || selectList.value === null) { message.warning('请选择声音模型') return false } @@ -1189,23 +1250,24 @@ model: selectList.value !== undefined ? selectList.value : '' //声音模型 } SoundTool.value = false rightTools.forEach((child) => { if (child.name == '声音' || child.name == 'sound') { child.isActive = false } }) // SoundTool.value = false // rightTools.forEach((child) => { // if (child.name == '声音' || child.name == 'sound') { // child.isActive = false // } // }) } // 鼠标移入与移出 const handleMouseenter = (item) => { audioList.value.forEach((child) => { const handleMouseenter = (key, item) => { audioList.value[key].forEach((child) => { if (child.id == item.id) { child.isHover = true } }) } const handleMouseleave = (item) => { audioList.value.forEach((child) => { const handleMouseleave = (key, item) => { audioList.value[key].forEach((child) => { if (child.id == item.id) { child.isHover = false } @@ -1431,7 +1493,7 @@ } const chooseTemplate = (currTemplate) => { console.log( "currTemplate", currTemplate) console.log('currTemplate', currTemplate) selectTemplate.value = cloneDeep(currTemplate) templates.value.forEach((item) => { item.isActive = false @@ -2078,6 +2140,13 @@ language: selectLanguage.value?.value } if (ChangeSoundTypeList.value.value === 2) { //此时选取了声音模型 params.humanId = null } else if (ChangeSoundTypeList.value.value === 1) { params.voiceId = null } try { showAudioPlay1.value = true const res = await pptTemplateApi.createAudio(params) @@ -2142,14 +2211,13 @@ } const goBack = () => { if (PPTArr.value.length==0) { if (PPTArr.value.length == 0) { pptTemplateApi.coursesDelete(courseInfo.value.id).then((res) => { router.go(-1) }) }else { } else { router.go(-1) } } const editorRef = shallowRef() @@ -2878,60 +2946,71 @@ height: 86%; margin: 10px 0; overflow-y: scroll; display: flex; flex-wrap: wrap; align-content: flex-start; > .ModealBox { width: 47%; margin: 10px 1%; display: flex; justify-content: space-around; align-items: center; position: relative; > .ImgBox { width: 26%; .SoundModelAreaBox { width: 100%; .SoundClassTit { width: 80%; margin: 0 auto; img { width: 100%; } .SoundClassContent { width: 100%; display: flex; flex-wrap: wrap; align-content: flex-start; > .ModealBox { width: 47%; margin: 10px 1%; display: flex; justify-content: space-around; align-items: center; position: relative; > .ImgBox { width: 26%; margin: 0 auto; img { width: 100%; } } > .TextArea { width: 48%; p { font-size: 12px; margin: 4px 0; padding-left: 6px; box-sizing: border-box; text-align: left; word-wrap: break-word; } } > .play-img { width: 32px; height: 32px; cursor: pointer; position: absolute; top: 0; right: 0; left: 0; bottom: 0; margin: auto; z-index: +10; } } .ModealBox:hover { background-color: #000; opacity: 0.5; border: 2px solid #0183f4; > .TextArea { p { color: #fff; } } } > .slectModel { border: 2px solid #1989fa; border-radius: 6px; } } > .TextArea { width: 48%; p { font-size: 12px; margin: 4px 0; padding-left: 6px; box-sizing: border-box; text-align: left; word-wrap: break-word; } } > .play-img { width: 32px; height: 32px; cursor: pointer; position: absolute; top: 0; right: 0; left: 0; bottom: 0; margin: auto; z-index: +10; } } .ModealBox:hover { background-color: #000; opacity: 0.5; border: 2px solid #0183f4; > .TextArea { p { color: #fff; } } } > .slectModel { border: 2px solid #1989fa; border-radius: 6px; } } .ButtonArea { yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/controller/admin/coursescenevoices/vo/AppCourseSceneVoicesMegerReqVO.java
@@ -36,4 +36,5 @@ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private Integer status; } private String language; } yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/dal/dataobject/voices/AuditionVO.java
@@ -15,4 +15,5 @@ //声音模型ID private String voiceId; private String language; } yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/coursemedia/CourseMediaServiceUtil.java
@@ -119,6 +119,7 @@ for (AppCourseScenesMegerReqVO scene : scenes) { //TODO 先判断是否有备注内容 auditionVO.setText(scene.getBackground().getPptRemark()); auditionVO.setLanguage(scene.getVoice().getLanguage().toLowerCase()); if (scene.getVoice().getVoiceId() == null){ auditionVO.setHumanId(String.valueOf(digitalHumansDO.getId())); }else{ yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/voices/VoicesServiceImpl.java
@@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.digitalcourse.service.voices; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.UUID; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; @@ -16,11 +17,17 @@ import cn.iocoder.yudao.module.digitalcourse.dal.mysql.voices.VoicesMapper; import cn.iocoder.yudao.module.infra.api.config.ConfigApi; import cn.iocoder.yudao.module.infra.api.file.FileApi; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.HashMap; import java.util.Map; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.digitalcourse.enums.ErrorCodeConstants.VOICES_NOT_EXISTS; @@ -135,47 +142,136 @@ private DigitalHumansMapper digitalHumansMapper; private static final String EASEGEN_URL = "easegen.url"; private static final String HEYGEM_CORE_URL = "heygem.core.url"; private static final String HEYGEM_VOICE_DATA = "heygem.voice.data"; public static final Set<String> SUPPORTED_LANGUAGES = Set.of( "en", "es", "fr", "de", "it", "pt", "pl", "tr", "ru", "nl", "cs", "ar", "zh-cn", "hu", "ko", "ja", "hi" ); // 中英文专用模型支持的语言 private static final Set<String> CN_EN_LANGUAGES = Set.of("zh-cn", "en"); @Override public String audition(AuditionVO auditionVO) { String language = auditionVO.getLanguage().toLowerCase(); // 判断是否是支持的语言 if (!SUPPORTED_LANGUAGES.contains(language)) { throw new IllegalArgumentException("不支持的语言类型: " + language); } // 构建参数 InvokeVO invokeVO = new InvokeVO(); invokeVO.setSpeaker(InvokeVO.generateUUID()); invokeVO.setText(auditionVO.getText()); if (auditionVO.getVoiceId() == null) { DigitalHumansDO digitalHumansDO = digitalHumansMapper.selectById(auditionVO.getHumanId()); invokeVO.setReferenceText(digitalHumansDO.getReferenceAudioText()); invokeVO.setReferenceAudio(digitalHumansDO.getAsrFormatAudioUrl()); }else if (auditionVO.getHumanId() == null){ } else if (auditionVO.getHumanId() == null) { VoicesDO voicesDO = voicesMapper.selectById(auditionVO.getVoiceId()); invokeVO.setReferenceText(voicesDO.getReferenceAudioText()); invokeVO.setReferenceAudio(voicesDO.getAsrFormatAudioUrl()); } ObjectMapper objectMapper = new ObjectMapper(); String jsonString = null; try { jsonString = objectMapper.writeValueAsString(invokeVO); } catch (JsonProcessingException e) { throw new RuntimeException(e); } String configValueByKey = configApi.getConfigValueByKey(HEYGEM_CORE_URL); String url = configValueByKey + "/v1/invoke"; HttpResponse execute = HttpRequest.post(url) .body(jsonString) .execute(); if (execute.getStatus() != 200) { return null; } String fileName = UUID.randomUUID().toString() + ".wav"; byte[] content; // 获取音频文件的二进制数据 byte[] content = execute.bodyBytes(); try { if (CN_EN_LANGUAGES.contains(language)) { // 使用中英文模型 String jsonString = new ObjectMapper().writeValueAsString(invokeVO); String coreUrl = configApi.getConfigValueByKey(HEYGEM_CORE_URL) + "/v1/invoke"; HttpResponse response = HttpRequest.post(coreUrl) .body(jsonString) .execute(); // 使用 `createFile` 方法存储文件,并获取 URL String fileUrl = fileApi.createFile(fileName, null, content); if (response.getStatus() != 200) { return null; } content = response.bodyBytes(); return fileUrl; // 返回存储的文件 URL // 返回音频文件路径 } else { // 使用其他语言模型,如 http://127.0.0.1:5002/synthesize String referenceAudio = invokeVO.getReferenceAudio(); String resultName = ""; if (referenceAudio != null) { if (referenceAudio.startsWith("/code/sessions/") || referenceAudio.startsWith("/code/data/")) { System.out.println("路径属于 /code/sessions/ 或 /code/data/"); // 只取第一个路径(以|||分割) String firstPath = referenceAudio.split("\\|\\|\\|")[0]; // 取最后一级文件名 String fileName1 = firstPath.substring(firstPath.lastIndexOf('/') + 1); String coreName; if (referenceAudio.startsWith("/code/sessions/")) { // sessions路径可能有 _partN,去除 _partN 及后面部分 int partIndex = fileName1.indexOf("_part"); if (partIndex != -1) { coreName = fileName1.substring(0, partIndex); } else { // 没有_part,去掉扩展名 int dotIndex = fileName1.lastIndexOf('.'); coreName = (dotIndex != -1) ? fileName1.substring(0, dotIndex) : fileName1; } } else { // data路径直接取完整文件名(即格式名+后缀) coreName = fileName1.substring(0, fileName1.lastIndexOf('.')); } // 获取后缀 int dotIndex = fileName1.lastIndexOf('.'); String suffix = (dotIndex != -1) ? fileName1.substring(dotIndex) : ""; // 最终结果 resultName = coreName + suffix; System.out.println("提取的格式名:" + resultName); } else { // 其他路径 System.out.println("未知路径类型"); throw new IllegalArgumentException("声音模型异常,请联系管理员"); } } //resultName String resultVoiceUrl = configApi.getConfigValueByKey(HEYGEM_VOICE_DATA)+"/origin_audio/" + resultName; Map<String, Object> params = new HashMap<>(); params.put("text", auditionVO.getText()); params.put("speaker_wav", resultVoiceUrl); params.put("language", language); HttpResponse response = HttpRequest.post("http://127.0.0.1:5002/synthesize") .contentType("application/json") .body(new ObjectMapper().writeValueAsString(params)) .execute(); if (response.getStatus() != 200) { return null; } String body = response.body(); JSONObject json = JSON.parseObject(body); Integer code = json.getInteger("code"); String message = json.getString("message"); if (code == null || code != 200) { throw new RuntimeException("语音合成失败:" + message); } JSONObject outputPath = json.getJSONObject("output_path"); String diskPath = outputPath.getString("disk_path"); String url = outputPath.getString("url"); // 使用 diskPath 和 url content = FileUtil.readBytes(diskPath); } // 保存音频文件并返回地址 return fileApi.createFile(fileName, null, content); } catch (Exception e) { throw new RuntimeException("语音合成失败", e); } } yudao-module-digitalcourse/yudao-module-digitalcourse-biz/src/main/java/cn/iocoder/yudao/module/digitalcourse/service/voices/VoicesServiceUtil.java
@@ -26,10 +26,7 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Map; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -60,28 +57,65 @@ private static final String HEYGEM_CORE_URL = "heygem.core.url"; @Async public void remoteTrain(VoicesTrailVO trailVO){ // 创建目标目录 String origin_audio = configApi.getConfigValueByKey(HEYGEM_VOICE_DATA) + "/origin_audio"; //训练前校验 try { Files.createDirectories(Path.of(origin_audio)); } catch (IOException e) { throw new RuntimeException(e); throw new RuntimeException("创建目录失败: " + origin_audio, e); } String extname = trailVO.getFixAuditionUrl().substring(trailVO.getFixAuditionUrl().lastIndexOf(".")); // 获取源文件的扩展名(如 .mp3 或 .wav) String fixAuditionUrl = trailVO.getFixAuditionUrl(); String extname = fixAuditionUrl.substring(fixAuditionUrl.lastIndexOf(".")); // 生成目标文件名(初始为原始扩展名) String modelFileName = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + extname; String modelFilePath = Paths.get(origin_audio, modelFileName).toString(); Path modelFilePath = Paths.get(origin_audio, modelFileName); String substring = configApi.getConfigValueByKey(EASEGEN_URL)+trailVO.getFixAuditionUrl().substring(trailVO.getFixAuditionUrl().lastIndexOf("/")); // 获取本地源文件路径(拼接 EASEGEN_URL 和文件名) String substring = configApi.getConfigValueByKey(EASEGEN_URL) + fixAuditionUrl.substring(fixAuditionUrl.lastIndexOf("/")); Path sourcePath = Path.of(substring); try { Files.copy(Path.of(substring), Path.of(modelFilePath), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new RuntimeException(e); // 如果不是 .wav,就转码 if (!substring.toLowerCase(Locale.ROOT).endsWith(".wav")) { // 构造 wav 路径(与原路径同目录) String filename = sourcePath.getFileName().toString(); String nameWithoutExt = filename.substring(0, filename.lastIndexOf(".")); Path wavPath = sourcePath.resolveSibling(nameWithoutExt + ".wav"); // 执行 FFmpeg 命令 String ffmpegCmd = String.format("ffmpeg -y -i \"%s\" \"%s\"", sourcePath, wavPath); try { Process process = Runtime.getRuntime().exec(ffmpegCmd); int exitCode = process.waitFor(); if (exitCode != 0) { throw new RuntimeException("FFmpeg 转换失败,返回码:" + exitCode); } // 转换成功后使用新的 .wav 路径作为最终文件路径(目标名也改为 .wav) modelFileName = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + ".wav"; modelFilePath = Paths.get(origin_audio, modelFileName); Files.copy(wavPath, modelFilePath, StandardCopyOption.REPLACE_EXISTING); } catch (IOException | InterruptedException e) { throw new RuntimeException("执行 FFmpeg 转换失败", e); } } else { // 已是 .wav,直接复制 try { Files.copy(sourcePath, modelFilePath, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new RuntimeException("复制 .wav 文件失败", e); } } String configValueByKey = configApi.getConfigValueByKey(HEYGEM_VOICE_DATA); // 计算相对路径 Path relativeAudioPath = Path.of(configValueByKey).relativize(Path.of(modelFilePath)); Path relativeAudioPath = Path.of(configValueByKey).relativize(modelFilePath); Map<String, Object> map = Map.of( "format", "wav", "reference_audio", relativeAudioPath.toString().replace("\\", "/"), @@ -100,6 +134,7 @@ .body(JSON.toJSONString(map)) .execute(); String body = execute.body(); // 检查响应状态码是否成功 if (execute.getStatus() != 200) { @@ -123,13 +158,35 @@ // 处理业务逻辑错误,更新状态和错误信息 String referenceAudioText = responseJson.getString("reference_audio_text"); String asrFormatAudioUrl = responseJson.getString("asr_format_audio_url"); if (referenceAudioText == null || asrFormatAudioUrl == null) { if (body.equals("{\"code\":-1,\"msg\":\"asr failed\"}")) { String a = origin_audio+"/format_denoise_"+modelFileName; String b = origin_audio+"/format_"+modelFileName; Path pathA = Path.of(a); Path pathB = Path.of(b); ///code/data/origin_audio/format_denoise_20250609090124273.wav if (Files.exists(pathA)) { asrFormatAudioUrl = "/code/data/origin_audio/format_denoise_" + modelFileName; referenceAudioText = "123"; }else if (Files.exists(pathB)) { asrFormatAudioUrl = "/code/data/origin_audio/format_" + modelFileName; referenceAudioText = "123"; } }else{ // 如果没有返回预期的字段,认为是错误 voicesMapper.update(new UpdateWrapper<VoicesDO>().lambda().eq(VoicesDO::getCode, trailVO.getCode()).set(VoicesDO::getStatus, ERROR_STATUS)); log.error("训练失败:->>>>>>>>> 未返回预期的字段"); return; } } voicesMapper.update( new UpdateWrapper<VoicesDO>() .lambda() .eq(VoicesDO::getCode, trailVO.getCode()) // 条件:code 等于传入的值 .set(VoicesDO::getStatus, 0) // 更新字段 status 为 0 .set(VoicesDO::getStatus, COMPLETE_STATUS) // 更新字段 status 为 0 .set(VoicesDO::getAsrFormatAudioUrl,asrFormatAudioUrl) .set(VoicesDO::getReferenceAudioText,referenceAudioText) );