Просмотр исходного кода

给tts都加入语音指令参数(可空)
生成语音将诗词同步生成语音

liyanbo 3 недель назад
Родитель
Сommit
c090b8d3e1
1 измененных файлов с 122 добавлено и 101 удалено
  1. 122 101
      src/views/bjdx/course/aiGenerate/aiGengrate.vue

+ 122 - 101
src/views/bjdx/course/aiGenerate/aiGengrate.vue

@@ -103,8 +103,8 @@
                     placeholder="请选择主题类型"
                     placeholder="请选择主题类型"
                     style="width: 300px"
                     style="width: 300px"
                   >
                   >
-                    <el-option label="课程通用" value=13 />
-                    <el-option label="诗词课" value=256 />
+                    <el-option label="课程通用" value="13" />
+                    <el-option label="诗词课" value="256" />
                   </el-select>
                   </el-select>
                 </div>
                 </div>
               </div>
               </div>
@@ -112,7 +112,9 @@
               <div class="button-group">
               <div class="button-group">
                 <button
                 <button
                   class="generate-btn primary"
                   class="generate-btn primary"
-                  :disabled="!scriptPrompt || !selectedMainTeacher || !selectedThemeType || isGenerating"
+                  :disabled="
+                    !scriptPrompt || !selectedMainTeacher || !selectedThemeType || isGenerating
+                  "
                   @click="generateScript"
                   @click="generateScript"
                 >
                 >
                   {{ isGenerating ? '生成中...' : '生成课程脚本' }}
                   {{ isGenerating ? '生成中...' : '生成课程脚本' }}
@@ -241,11 +243,11 @@
                     v-for="(dialogue, dialogueIndex) in section.dialogues"
                     v-for="(dialogue, dialogueIndex) in section.dialogues"
                     :key="dialogueIndex"
                     :key="dialogueIndex"
                     class="dialogue-item"
                     class="dialogue-item"
-                    :class="[dialogue.type, { 'hidden': dialogue.type === 'user' }]"
+                    :class="[dialogue.type, { hidden: dialogue.type === 'user' }]"
                   >
                   >
                     <div class="dialogue-header">
                     <div class="dialogue-header">
                       <div class="dialogue-type-tag" :class="dialogue.type">
                       <div class="dialogue-type-tag" :class="dialogue.type">
-                        {{ 
+                        {{
                           dialogue.type === 'digital'
                           dialogue.type === 'digital'
                             ? '数字人'
                             ? '数字人'
                             : dialogue.type === 'user'
                             : dialogue.type === 'user'
@@ -540,7 +542,9 @@
                     <button class="add-dialogue-btn digital" @click="addDialogue(sectionIndex)"
                     <button class="add-dialogue-btn digital" @click="addDialogue(sectionIndex)"
                       >+ 添加对话</button
                       >+ 添加对话</button
                     >
                     >
-                    <button class="add-dialogue-btn quest-user" @click="addQuestWithUserReply(sectionIndex)"
+                    <button
+                      class="add-dialogue-btn quest-user"
+                      @click="addQuestWithUserReply(sectionIndex)"
                       >+ 添加提问与回复</button
                       >+ 添加提问与回复</button
                     >
                     >
                     <button class="add-dialogue-btn poem" @click="addPoemDialogue(sectionIndex)"
                     <button class="add-dialogue-btn poem" @click="addPoemDialogue(sectionIndex)"
@@ -558,9 +562,7 @@
                 重新生成
                 重新生成
               </button>
               </button>
 
 
-              <button class="primary-btn"  @click="nextStep">
-                预览完整脚本
-              </button>
+              <button class="primary-btn" @click="nextStep"> 预览完整脚本 </button>
             </div>
             </div>
           </div>
           </div>
 
 
@@ -568,7 +570,7 @@
           <div v-else-if="currentStep === 3" class="step-content">
           <div v-else-if="currentStep === 3" class="step-content">
             <div class="preview-container">
             <div class="preview-container">
               <h3>课程脚本预览</h3>
               <h3>课程脚本预览</h3>
-              
+
               <div class="scrollable-content">
               <div class="scrollable-content">
                 <br />
                 <br />
 
 
@@ -576,9 +578,9 @@
                   {{ validationMessage }}
                   {{ validationMessage }}
                 </div>
                 </div>
                 <div v-else>
                 <div v-else>
-                  <div 
-                    v-for="(error, index) in errorMessages" 
-                    :key="index" 
+                  <div
+                    v-for="(error, index) in errorMessages"
+                    :key="index"
                     class="validation-result invalid"
                     class="validation-result invalid"
                   >
                   >
                     {{ error }}
                     {{ error }}
@@ -586,55 +588,57 @@
                 </div>
                 </div>
 
 
                 <div class="preview-content">
                 <div class="preview-content">
-                <div
-                  v-for="(section, sectionIndex) in scriptData.sections"
-                  :key="sectionIndex"
-                  class="preview-section"
-                  :style="{
-                    backgroundImage: section.backgroundImage.url
-                      ? `url(${section.backgroundImage.url})`
-                      : 'none',
-                    backgroundSize: 'cover',
-                    backgroundPosition: 'center',
-                    backgroundRepeat: 'no-repeat'
-                  }"
-                >
-                  <div class="preview-section-content">
-                    <div class="preview-media">
-                      <div class="preview-media-left">
-                        <label class="section-name-label">环节 {{ sectionIndex + 1 }}</label>
-                        <strong>{{ section.name }}</strong>
-                      </div>
-                      <div class="preview-media-right">
-                        <span v-if="section.backgroundAudio.type" class="preview-audio">
-                          背景音:{{ section.backgroundAudio.type }}
-                          <button
-                            class="play-btn small"
-                            @click="playBackgroundAudio(section.backgroundAudio.type)"
-                          >
-                            <span class="play-icon">{{
-                              audioState.isPlaying &&
-                              audioState.currentType === 'background' &&
-                              audioState.currentUrl === section.backgroundAudio.url
-                                ? '⏸'
-                                : '▶'
-                            }}</span>
-                          </button>
-                        </span>
+                  <div
+                    v-for="(section, sectionIndex) in scriptData.sections"
+                    :key="sectionIndex"
+                    class="preview-section"
+                    :style="{
+                      backgroundImage: section.backgroundImage.url
+                        ? `url(${section.backgroundImage.url})`
+                        : 'none',
+                      backgroundSize: 'cover',
+                      backgroundPosition: 'center',
+                      backgroundRepeat: 'no-repeat'
+                    }"
+                  >
+                    <div class="preview-section-content">
+                      <div class="preview-media">
+                        <div class="preview-media-left">
+                          <label class="section-name-label">环节 {{ sectionIndex + 1 }}</label>
+                          <strong>{{ section.name }}</strong>
+                        </div>
+                        <div class="preview-media-right">
+                          <span v-if="section.backgroundAudio.type" class="preview-audio">
+                            背景音:{{ section.backgroundAudio.type }}
+                            <button
+                              class="play-btn small"
+                              @click="playBackgroundAudio(section.backgroundAudio.type)"
+                            >
+                              <span class="play-icon">{{
+                                audioState.isPlaying &&
+                                audioState.currentType === 'background' &&
+                                audioState.currentUrl === section.backgroundAudio.url
+                                  ? '⏸'
+                                  : '▶'
+                              }}</span>
+                            </button>
+                          </span>
+                        </div>
                       </div>
                       </div>
-                    </div>
 
 
-                    <div class="preview-dialogues">
-                      <div
-                        v-for="(dialogue, dialogueIndex) in section.dialogues.filter(d => d.type !== 'user')"
-                        :key="dialogueIndex"
-                        class="preview-dialogue"
-                        :class="dialogue.type"
-                      >
-                        <div class="dialogue-header">
-                          <div class="dialogue-header-left">
-                            <div class="dialogue-type-tag" :class="dialogue.type">
-                              {{ 
+                      <div class="preview-dialogues">
+                        <div
+                          v-for="(dialogue, dialogueIndex) in section.dialogues.filter(
+                            (d) => d.type !== 'user'
+                          )"
+                          :key="dialogueIndex"
+                          class="preview-dialogue"
+                          :class="dialogue.type"
+                        >
+                          <div class="dialogue-header">
+                            <div class="dialogue-header-left">
+                              <div class="dialogue-type-tag" :class="dialogue.type">
+                                {{
                                   dialogue.type === 'digital'
                                   dialogue.type === 'digital'
                                     ? '数字人'
                                     ? '数字人'
                                     : dialogue.type === 'user'
                                     : dialogue.type === 'user'
@@ -643,44 +647,46 @@
                                         ? '提问'
                                         ? '提问'
                                         : '诗词'
                                         : '诗词'
                                 }}
                                 }}
+                              </div>
+                              <div class="dialogue-role">
+                                {{
+                                  dialogue.type !== 'user'
+                                    ? getRoleName(dialogue.roleName)
+                                    : '用户'
+                                }}:
+                              </div>
                             </div>
                             </div>
-                            <div class="dialogue-role">
-                              {{
-                                dialogue.type !== 'user' ? getRoleName(dialogue.roleName) : '用户'
-                              }}:
-                            </div>
+                            <button
+                              v-if="dialogue.voiceoverUrl"
+                              class="play-btn small"
+                              @click="playVoiceover(dialogue.voiceoverUrl)"
+                            >
+                              <span class="play-icon">{{
+                                audioState.isPlaying &&
+                                audioState.currentType === 'voice' &&
+                                audioState.currentUrl === dialogue.voiceoverUrl
+                                  ? '⏸'
+                                  : '▶'
+                              }}</span>
+                            </button>
                           </div>
                           </div>
-                          <button
-                            v-if="dialogue.voiceoverUrl"
-                            class="play-btn small"
-                            @click="playVoiceover(dialogue.voiceoverUrl)"
+                          <div class="dialogue-text" v-html="parseMarkdown(dialogue.content)"></div>
+                          <div
+                            v-if="dialogue.type === 'user' && dialogue.roleName"
+                            class="reply-info"
                           >
                           >
-                            <span class="play-icon">{{
-                              audioState.isPlaying &&
-                              audioState.currentType === 'voice' &&
-                              audioState.currentUrl === dialogue.voiceoverUrl
-                                ? '⏸'
-                                : '▶'
-                            }}</span>
-                          </button>
-                        </div>
-                        <div class="dialogue-text" v-html="parseMarkdown(dialogue.content)"></div>
-                        <div
-                          v-if="dialogue.type === 'user' && dialogue.roleName"
-                          class="reply-info"
-                        >
-                          回复角色:{{ getRoleName(dialogue.roleName) }}
+                            回复角色:{{ getRoleName(dialogue.roleName) }}
+                          </div>
                         </div>
                         </div>
                       </div>
                       </div>
                     </div>
                     </div>
                   </div>
                   </div>
                 </div>
                 </div>
-                </div>
               </div>
               </div>
 
 
               <div class="preview-actions">
               <div class="preview-actions">
                 <button class="secondary-btn" @click="currentStep = 2">返回修改</button>
                 <button class="secondary-btn" @click="currentStep = 2">返回修改</button>
-<!--                <button
+                <!--                <button
                   class="primary-btn"
                   class="primary-btn"
                   :disabled="!isValidationPassed"
                   :disabled="!isValidationPassed"
                   @click="showVideoPreview"
                   @click="showVideoPreview"
@@ -944,10 +950,12 @@ const canProceed = computed(() => {
           section.backgroundImage.url &&
           section.backgroundImage.url &&
           section.dialogues.every(
           section.dialogues.every(
             (dialogue) =>
             (dialogue) =>
-              ((dialogue.type === 'digital' || dialogue.type === 'quest' || dialogue.type === 'poem') &&
-                dialogue.roleName &&
-                dialogue.content.trim() &&
-                dialogue.voiceoverUrl)
+              (dialogue.type === 'digital' ||
+                dialogue.type === 'quest' ||
+                dialogue.type === 'poem') &&
+              dialogue.roleName &&
+              dialogue.content.trim() &&
+              dialogue.voiceoverUrl
           )
           )
       )
       )
     default:
     default:
@@ -1195,9 +1203,13 @@ const addQuestWithUserReply = (sectionIndex) => {
 const removeDialogue = (sectionIndex, dialogueIndex) => {
 const removeDialogue = (sectionIndex, dialogueIndex) => {
   const dialogues = scriptData.sections[sectionIndex].dialogues
   const dialogues = scriptData.sections[sectionIndex].dialogues
   const dialogue = dialogues[dialogueIndex]
   const dialogue = dialogues[dialogueIndex]
-  
+
   // 如果是提问类型,同时删除后面的用户回复
   // 如果是提问类型,同时删除后面的用户回复
-  if (dialogue.type === 'quest' && dialogueIndex < dialogues.length - 1 && dialogues[dialogueIndex + 1].type === 'user') {
+  if (
+    dialogue.type === 'quest' &&
+    dialogueIndex < dialogues.length - 1 &&
+    dialogues[dialogueIndex + 1].type === 'user'
+  ) {
     dialogues.splice(dialogueIndex, 2)
     dialogues.splice(dialogueIndex, 2)
   } else {
   } else {
     dialogues.splice(dialogueIndex, 1)
     dialogues.splice(dialogueIndex, 1)
@@ -1397,14 +1409,17 @@ const generateVoiceover = async (sectionIndex, dialogueIndex) => {
   let role = digitalHumans.value.find((r) => r.name === dialogue.roleName)
   let role = digitalHumans.value.find((r) => r.name === dialogue.roleName)
 
 
   try {
   try {
+    // 根据对话类型设置不同的command
+    const command =
+      dialogue.type === 'poem' ? '要有感情的朗读诗词语句,要符合诗词的节奏和韵律。' : null
+
     // 调用后端API将文本转成语音
     // 调用后端API将文本转成语音
-    const speechUrl = await TtsApi.convert({
+    // 将返回的URL赋值给对话
+    dialogue.voiceoverUrl = await TtsApi.convert({
       roleId: Number(role.id),
       roleId: Number(role.id),
-      content: dialogue.content
+      content: dialogue.content,
+      command: command
     })
     })
-
-    // 将返回的URL赋值给对话
-    dialogue.voiceoverUrl = speechUrl
   } catch (error) {
   } catch (error) {
     console.error('生成语音失败:', error)
     console.error('生成语音失败:', error)
     // 生成失败,从replacedUrls中移除旧URL
     // 生成失败,从replacedUrls中移除旧URL
@@ -1424,7 +1439,7 @@ const generateAllVoiceovers = async () => {
       for (let j = 0; j < scriptData.sections[i].dialogues.length; j++) {
       for (let j = 0; j < scriptData.sections[i].dialogues.length; j++) {
         const dialogue = scriptData.sections[i].dialogues[j]
         const dialogue = scriptData.sections[i].dialogues[j]
         // 只处理没有语音URL的对话
         // 只处理没有语音URL的对话
-        if (!dialogue.voiceoverUrl && dialogue.type !== 'poem') {
+        if (!dialogue.voiceoverUrl) {
           await generateVoiceover(i, j)
           await generateVoiceover(i, j)
         }
         }
       }
       }
@@ -1455,7 +1470,7 @@ const validateScript = () => {
 
 
   // 检查环节名称、背景图
   // 检查环节名称、背景图
   scriptData.sections.forEach((section, sectionIndex) => {
   scriptData.sections.forEach((section, sectionIndex) => {
-    if (!section.name.trim() || !section.backgroundImage.url) {
+    if (!section.name.trim() || !section.backgroundImage.url) {
       errorMessages.value.push(`环节${sectionIndex + 1}:名称或背景图未配置!`)
       errorMessages.value.push(`环节${sectionIndex + 1}:名称或背景图未配置!`)
       isValid = false
       isValid = false
     }
     }
@@ -1464,8 +1479,14 @@ const validateScript = () => {
     section.dialogues.forEach((dialogue, dialogueIndex) => {
     section.dialogues.forEach((dialogue, dialogueIndex) => {
       // 只检查数字人、提问和诗词类型的对话
       // 只检查数字人、提问和诗词类型的对话
       if (dialogue.type === 'digital' || dialogue.type === 'quest' || dialogue.type === 'poem') {
       if (dialogue.type === 'digital' || dialogue.type === 'quest' || dialogue.type === 'poem') {
-        if (!dialogue.roleName || !dialogue.content.trim() || (dialogue.type !== 'poem' && !dialogue.voiceoverUrl)) {
-          errorMessages.value.push(`环节${sectionIndex + 1}:对话${dialogueIndex + 1}:存在完成内容!`)
+        if (
+          !dialogue.roleName ||
+          !dialogue.content.trim() ||
+          (dialogue.type !== 'poem' && !dialogue.voiceoverUrl)
+        ) {
+          errorMessages.value.push(
+            `环节${sectionIndex + 1}:对话${dialogueIndex + 1}:存在完成内容!`
+          )
           isValid = false
           isValid = false
         }
         }
       }
       }