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