|
|
@@ -2,7 +2,7 @@
|
|
|
<template>
|
|
|
<div class="self-directed-learning">
|
|
|
<div class="title-section">
|
|
|
- <h3>和AI一起学你想学!</h3>
|
|
|
+ <h3>和AI一起,学你想学!</h3>
|
|
|
</div>
|
|
|
<div class="input-section">
|
|
|
<!-- 发送消息输入框 -->
|
|
|
@@ -15,8 +15,8 @@
|
|
|
@keyup.enter.exact="doSendMessage(prompt.trim())"
|
|
|
></textarea>
|
|
|
<div class="dropdowns-container">
|
|
|
- <el-dropdown
|
|
|
- v-for="dropdown in dropdowns"
|
|
|
+ <el-dropdown
|
|
|
+ v-for="dropdown in dropdowns"
|
|
|
:key="dropdown.key"
|
|
|
v-model="dropdown.value"
|
|
|
@command="(command) => handleSelect(dropdown.key, command)"
|
|
|
@@ -80,21 +80,687 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <el-divider></el-divider>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import {ref, onMounted, computed} from "vue";
|
|
|
+import {ref, onMounted, computed, reactive, defineEmits} from "vue";
|
|
|
+
|
|
|
+// 定义事件
|
|
|
+const emit = defineEmits(['refreshData']);
|
|
|
import { Top, Search, User,Memo,ArrowUpBold,ArrowDownBold } from '@element-plus/icons-vue'
|
|
|
+import {
|
|
|
+ aIGenerateCourse,
|
|
|
+ AiImageStatusEnum,
|
|
|
+ CreateDialogue,
|
|
|
+ CreatePainting,
|
|
|
+ PaintingGetMys,
|
|
|
+ sendChatMessageStream, textToSpeech
|
|
|
+} from "@/api/questions.js";
|
|
|
+import {Message} from "@/utils/message/Message.js";
|
|
|
|
|
|
// 发送消息输入框
|
|
|
-const prompt = ref(""); // prompt
|
|
|
+const prompt = ref("给我一首李白的静夜思"); // prompt
|
|
|
+
|
|
|
+// 对话相关
|
|
|
+const activeConversationId = ref(null) // 选中的对话编号
|
|
|
+const conversationInAbortController = ref() // 对话进行中 abort 控制器(控制 stream 对话)
|
|
|
+const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
|
|
|
+const enableContext = ref(true) // 是否开启上下文
|
|
|
+
|
|
|
+// 接收 Stream 消息
|
|
|
+const receiveMessageFullText = ref('')
|
|
|
+const scriptDataTemp = ref(null)
|
|
|
+
|
|
|
+// 生成状态
|
|
|
+const isGenerating = ref(false)// 是否正在生成脚本
|
|
|
+const isGeneratingImages = ref(false)// 是否正在生成图片
|
|
|
+const isGeneratingVoiceovers = ref(false)// 是否正在生成语音
|
|
|
+const generatingImageUrl = ref('')
|
|
|
+
|
|
|
+// 图片生成相关状态
|
|
|
+const inProgressImageMap = ref({}) // 监听的图片映射,key 为 image 编号,value 为 { sectionIndex, type: 'image'|'audio' }
|
|
|
+const inProgressTimer = ref(null) // 生成中的图片定时器,轮询生成进展
|
|
|
+
|
|
|
+// 小节列表
|
|
|
+const lessonList = ref([
|
|
|
+ {id: '1', name: '课程引入'},
|
|
|
+ {id: '2', name: '知识讲解'},
|
|
|
+ {id: '3', name: '课程总结'}
|
|
|
+])
|
|
|
+// 数字人列表
|
|
|
+const digitalHumans = ref([
|
|
|
+ { id: 18, name: '李白', description: '你是诗仙李白' },
|
|
|
+ { id: 10, name: '小智', description: '你是智能机器人小智' }
|
|
|
+])
|
|
|
+//主讲人、助讲人
|
|
|
+const selectedMainTeacher = ref('李白')
|
|
|
+const selectedAssistants = ref(['小智'])
|
|
|
+
|
|
|
+//脚本数据
|
|
|
+const scriptData = reactive(
|
|
|
+ {
|
|
|
+ courseName: '', // 课程名称
|
|
|
+ coverImage: { // 封面图
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ },
|
|
|
+ lessons: [ // 课程小节
|
|
|
+ {
|
|
|
+ lessonName: '课程引入', // 小节课程名称
|
|
|
+ sections: [ // 环节
|
|
|
+ {
|
|
|
+ sectionName: '环节一', // 环节名称
|
|
|
+ backgroundImage: {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ },
|
|
|
+ backgroundAudio: {
|
|
|
+ type: '',
|
|
|
+ url: ''
|
|
|
+ },
|
|
|
+ dialogues: [
|
|
|
+ {
|
|
|
+ type: 'digital',
|
|
|
+ roleName: '',
|
|
|
+ content: '',
|
|
|
+ voiceoverUrl: '',
|
|
|
+ generatingVoiceover: false
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'user',
|
|
|
+ roleName: '',
|
|
|
+ content: ''
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ lessonName: '知识讲解', // 小节课程名称
|
|
|
+ sections: [ // 环节
|
|
|
+ {
|
|
|
+ sectionName: '环节一', // 环节名称
|
|
|
+ backgroundImage: {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ },
|
|
|
+ backgroundAudio: {
|
|
|
+ type: '',
|
|
|
+ url: ''
|
|
|
+ },
|
|
|
+ dialogues: [
|
|
|
+ {
|
|
|
+ type: 'digital',
|
|
|
+ roleName: '',
|
|
|
+ content: '',
|
|
|
+ voiceoverUrl: '',
|
|
|
+ generatingVoiceover: false
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'user',
|
|
|
+ roleName: '',
|
|
|
+ content: ''
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ lessonName: '课程总结', // 小节课程名称
|
|
|
+ sections: [ // 环节
|
|
|
+ {
|
|
|
+ sectionName: '环节一', // 环节名称
|
|
|
+ backgroundImage: {
|
|
|
+ prompt: '',
|
|
|
+ url: '',
|
|
|
+ generating: false
|
|
|
+ },
|
|
|
+ backgroundAudio: {
|
|
|
+ type: '',
|
|
|
+ url: ''
|
|
|
+ },
|
|
|
+ dialogues: [
|
|
|
+ {
|
|
|
+ type: 'digital',
|
|
|
+ roleName: '',
|
|
|
+ content: '',
|
|
|
+ voiceoverUrl: '',
|
|
|
+ generatingVoiceover: false
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'user',
|
|
|
+ roleName: '',
|
|
|
+ content: ''
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+// 生成脚本
|
|
|
+const generateScript = async () => {
|
|
|
+ try {
|
|
|
+ isGenerating.value = true
|
|
|
+
|
|
|
+ const role = digitalHumans.value.find((r) => r.name === selectedMainTeacher.value)
|
|
|
+ let content =
|
|
|
+ prompt.value +
|
|
|
+ '(主讲人:' + role.name +
|
|
|
+ ',主讲人角色定位:' + role.description +
|
|
|
+ ';助讲有:'
|
|
|
+ let zhujiang = []
|
|
|
+
|
|
|
+ selectedAssistants.value.forEach((rName) => {
|
|
|
+ const assistantRole = digitalHumans.value.find((r) => r.name === rName)
|
|
|
+ zhujiang.push(assistantRole.name + '[' + assistantRole.description + ']')
|
|
|
+ })
|
|
|
+ content += zhujiang.join(',') + ')'
|
|
|
+
|
|
|
+ // 1. 创建对话
|
|
|
+ await createAiRoleIdConversation(13)
|
|
|
+
|
|
|
+ //2、生成课程信息
|
|
|
+ content += "注意:我需要让你生成"+lessonList.value.length+"节课程,";
|
|
|
+ for (let i = 1; i <= lessonList.value.length; i++) {
|
|
|
+ content+="第"+i+"节课程是"+lessonList.value[i-1].name+";"
|
|
|
+ }
|
|
|
+
|
|
|
+ //3、生成课程小节
|
|
|
+ progressSteps.value[0].visible = true;
|
|
|
+ progressSteps.value[0].active = true;
|
|
|
+ let courseLessons = [];//存储课程小节信息
|
|
|
+ scriptData.lessons = []
|
|
|
+ for (let i = 0; i < lessonList.value.length; i++) {
|
|
|
+ let lesson = lessonList.value[i]
|
|
|
+ progressSteps.value[0].next = "生成课程小节【" + lesson.name + "】..."
|
|
|
+ content += "现在开始生成第"+(i+1)+"节课:" + lesson.name
|
|
|
+ let lessonInfo = {courseName: lesson.name, courseLabel: lesson.id};
|
|
|
+ await doSendMessageStream(activeConversationId.value, content, lessonInfo)
|
|
|
+ console.log("生成课程小节信息:", lessonInfo)
|
|
|
+ courseLessons.push(lessonInfo)
|
|
|
+ }
|
|
|
+ scriptData.lessons = courseLessons;
|
|
|
+ progressSteps.value[0].completed = true;
|
|
|
+ progressSteps.value[0].active = false;
|
|
|
+
|
|
|
+ content = "最后请给我围绕着这几节课的课程封面图描述词(coverImage)和课程名称(courseName)。"
|
|
|
+ progressSteps.value[1].visible = true;
|
|
|
+ progressSteps.value[1].active = true;
|
|
|
+ let courseInfo = {}//存储课程信息
|
|
|
+ await doSendMessageStream(activeConversationId.value, content, courseInfo)
|
|
|
+ console.log("生成课程信息:", courseInfo)
|
|
|
+ scriptData.courseName = courseInfo.courseName
|
|
|
+ scriptData.coverImage.prompt = courseInfo.coverImage
|
|
|
+ progressSteps.value[1].completed = true;
|
|
|
+ progressSteps.value[1].active = false;
|
|
|
+
|
|
|
+ //4、生成全部图片
|
|
|
+ progressSteps.value[2].visible = true;
|
|
|
+ progressSteps.value[2].active = true;
|
|
|
+ //取courseInfo.coverImage、取courseLessons[*].sections[*].backgroundImage课程小节背景图描述词;生成图片
|
|
|
+ await generateAllImages()
|
|
|
+ progressSteps.value[2].completed = true;
|
|
|
+ progressSteps.value[2].active = false;
|
|
|
+
|
|
|
+ //5、生成全部配音
|
|
|
+ progressSteps.value[3].visible = true;
|
|
|
+ progressSteps.value[3].active = true;
|
|
|
+ //取courseLessons[*].sections[*].dialogues[*].voiceoverUrl课程小节课程内对话文本生成配音
|
|
|
+ await generateAllVoiceovers()
|
|
|
+ progressSteps.value[3].completed = true;
|
|
|
+ progressSteps.value[3].active = false;
|
|
|
+
|
|
|
+ localStorage.setItem('scriptData', JSON.stringify(scriptData))
|
|
|
+ console.log("【最终数据:】", scriptData)
|
|
|
+
|
|
|
+ // 调用完成方法,封装数据并存储到后台
|
|
|
+ await completeGenerate()
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成脚本失败:', error)
|
|
|
+ } finally {
|
|
|
+ isGenerating.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 完成生成,封装数据并存储到后台
|
|
|
+const completeGenerate = async () => {
|
|
|
+ try {
|
|
|
+ console.log('开始封装数据并存储到后台...')
|
|
|
+
|
|
|
+ const courses = scriptData.lessons.map(lesson => ({
|
|
|
+ courseName: lesson.courseName,
|
|
|
+ courseLabel: lesson.courseLabel,
|
|
|
+ courseContent: JSON.stringify(lesson)
|
|
|
+ }))
|
|
|
+
|
|
|
+ // 封装数据
|
|
|
+ const dataToStore = {
|
|
|
+ grade: localStorage.getItem('selectedGradeId'),
|
|
|
+ courseName: scriptData.courseName,
|
|
|
+ coverImage: scriptData.coverImage.url,
|
|
|
+ courseList: courses
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('封装的数据:', dataToStore)
|
|
|
+
|
|
|
+ // 调用后端API将文本转成语音
|
|
|
+ const res = await aIGenerateCourse(dataToStore)
|
|
|
+ if (res.code !== 0) {
|
|
|
+ console.error('生成课程失败:', res.msg)
|
|
|
+ Message().error('生成课程失败:' + res.msg)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 所有步骤完成后,3秒后隐藏容器
|
|
|
+ setTimeout(() => {
|
|
|
+ showNewContainer.value = false;
|
|
|
+ // 通知父组件刷新数据
|
|
|
+ emit('refreshData');
|
|
|
+ }, 2000);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('存储数据失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 新建聊天对话 */
|
|
|
+const createAiRoleIdConversation = async (roleId) => {
|
|
|
+ // 智能问答
|
|
|
+ await CreateDialogue({ roleId: roleId })
|
|
|
+ .then((res) => {
|
|
|
+ console.log("创建会话:", res.data);
|
|
|
+ activeConversationId.value = res.data;
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ console.error("请求出错:", error);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/** 调取数字人生成脚本 */
|
|
|
+const doSendMessageStream = async (conversationId, content, outputContent) => {
|
|
|
+ conversationInAbortController.value = new AbortController()
|
|
|
+ conversationInProgress.value = true
|
|
|
+ receiveMessageFullText.value = '' // 清空之前的文本
|
|
|
+
|
|
|
+ try {
|
|
|
+ let isFirstChunk = true
|
|
|
+ await sendChatMessageStream(
|
|
|
+ conversationId,
|
|
|
+ content,
|
|
|
+ undefined, // contentAnswer 参数
|
|
|
+ conversationInAbortController.value,
|
|
|
+ enableContext.value,
|
|
|
+ async (res) => {
|
|
|
+ const { code, data, msg } = JSON.parse(res.data)
|
|
|
+ if (code !== 0) {
|
|
|
+ console.error(`对话异常! ${msg}`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data.eventType === 'TEXT') {
|
|
|
+ if (data.receive?.content === '') {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isFirstChunk) {
|
|
|
+ isFirstChunk = false
|
|
|
+ }
|
|
|
+
|
|
|
+ receiveMessageFullText.value += data.receive.content
|
|
|
+ console.log('数据加载中..')
|
|
|
+ try {
|
|
|
+ const parsedData = JSON.parse(receiveMessageFullText.value)
|
|
|
+ // 将解析后的数据填充到outputContent对象中
|
|
|
+ console.log('zuizhon..', parsedData)
|
|
|
+ Object.assign(outputContent, parsedData)
|
|
|
+ // updateScriptData(parsedData)
|
|
|
+ } catch (e) {
|
|
|
+ // 解析失败,说明数据还不完整,继续等待
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ console.error(`对话异常! ${error}`)
|
|
|
+ throw error
|
|
|
+ },
|
|
|
+ () => {
|
|
|
+ try {
|
|
|
+ if (receiveMessageFullText.value) {
|
|
|
+ console.log('最终数据:', receiveMessageFullText.value)
|
|
|
+ const parsedData = JSON.parse(receiveMessageFullText.value)
|
|
|
+ console.log('最终数据json:', parsedData)
|
|
|
+ // 将解析后的数据填充到outputContent对象中
|
|
|
+ // updateScriptData(parsedData)
|
|
|
+ console.log('zuizhon..', parsedData)
|
|
|
+ Object.assign(outputContent, parsedData)
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('最终数据解析失败:', e)
|
|
|
+
|
|
|
+ // 清洗规则:移除```json、```、** 等非JSON标记,只保留中间的JSON内容
|
|
|
+ try {
|
|
|
+ let cleanJsonStr = receiveMessageFullText.value
|
|
|
+ const parsedData = JSON.parse(
|
|
|
+ cleanJsonStr
|
|
|
+ .replace(/^```json\s*/, '')
|
|
|
+ .replace(/\s*```$/, '')
|
|
|
+ .replace(/\*\*/g, '')
|
|
|
+ .trim()
|
|
|
+ )
|
|
|
+ console.log('最终清洗后数据json:', parsedData)
|
|
|
+ // 将解析后的数据填充到outputContent对象中
|
|
|
+ console.log('zuizhon..', parsedData)
|
|
|
+ Object.assign(outputContent, parsedData)
|
|
|
+ // updateScriptData(parsedData)
|
|
|
+ } catch (cleanError) {
|
|
|
+ console.error('清洗后数据解析失败:', cleanError)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送消息流失败:', error)
|
|
|
+ } finally {
|
|
|
+ conversationInProgress.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生成所有背景图
|
|
|
+const generateAllImages = async () => {
|
|
|
+ return new Promise(async (resolve, reject) => {
|
|
|
+ isGeneratingImages.value = true
|
|
|
+ try {
|
|
|
+ // 确保scriptData和coverImage存在
|
|
|
+ if (!scriptData.coverImage) {
|
|
|
+ scriptData.coverImage = { prompt: '', url: '', generating: false }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成封面图
|
|
|
+ if (scriptData.coverImage.prompt && !scriptData.coverImage.url) {
|
|
|
+ await generateCoverImage()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成所有环节的背景图
|
|
|
+ if (scriptData.lessons && scriptData.lessons.length > 0) {
|
|
|
+ for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
|
|
|
+ const lesson = scriptData.lessons[lessonIndex]
|
|
|
+ if (lesson && lesson.sections) {
|
|
|
+ for (let sectionIndex = 0; sectionIndex < lesson.sections.length; sectionIndex++) {
|
|
|
+ const section = lesson.sections[sectionIndex]
|
|
|
+ // 确保section和backgroundImage存在
|
|
|
+ if (section) {
|
|
|
+ if (!section.backgroundImage) {
|
|
|
+ section.backgroundImage = { prompt: '', url: '', generating: false }
|
|
|
+ }
|
|
|
+ // 只处理没有背景图URL的环节
|
|
|
+ if (section.backgroundImage.prompt && !section.backgroundImage.url) {
|
|
|
+ await generateImage(lessonIndex, sectionIndex)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否有图片正在生成
|
|
|
+ if (Object.keys(inProgressImageMap.value).length === 0) {
|
|
|
+ // 没有图片需要生成,直接resolve
|
|
|
+ isGeneratingImages.value = false
|
|
|
+ resolve()
|
|
|
+ } else {
|
|
|
+ // 启动图片轮询
|
|
|
+ startImagePolling()
|
|
|
+
|
|
|
+ // 检查是否有图片正在生成
|
|
|
+ const checkImagesComplete = setInterval(() => {
|
|
|
+ if (Object.keys(inProgressImageMap.value).length === 0) {
|
|
|
+ isGeneratingImages.value = false
|
|
|
+ clearInterval(checkImagesComplete)
|
|
|
+ stopImagePolling()
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成所有背景图失败:', error)
|
|
|
+ isGeneratingImages.value = false
|
|
|
+ reject(error)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 生成封面图
|
|
|
+const generateCoverImage = async () => {
|
|
|
+ scriptData.coverImage.generating = true
|
|
|
+ try {
|
|
|
+ const response = await CreatePainting({
|
|
|
+ "modelId": 56,
|
|
|
+ "prompt": scriptData.coverImage.prompt,
|
|
|
+ "width": 1024,
|
|
|
+ "height": 1024
|
|
|
+ })
|
|
|
+ console.log("生成封面图", response)
|
|
|
+ inProgressImageMap.value[response.data] = {
|
|
|
+ type: 'cover'
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成封面图失败:', error)
|
|
|
+ scriptData.coverImage.generating = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生成图片
|
|
|
+const generateImage = async (lessonIndex, sectionIndex) => {
|
|
|
+ const lesson = scriptData.lessons[lessonIndex]
|
|
|
+ if (!lesson) return
|
|
|
+
|
|
|
+ const section = lesson.sections[sectionIndex]
|
|
|
+ if (!section) return
|
|
|
+
|
|
|
+ const media = section.backgroundImage
|
|
|
+
|
|
|
+ media.generating = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await CreatePainting({
|
|
|
+ "modelId": 56,
|
|
|
+ "prompt": media.prompt,
|
|
|
+ "width": 1024,
|
|
|
+ "height": 1024
|
|
|
+ })
|
|
|
+ console.log("生成图片", res)
|
|
|
+ inProgressImageMap.value[res.data] = {
|
|
|
+ lessonIndex: lessonIndex,
|
|
|
+ sectionIndex: sectionIndex,
|
|
|
+ type: 'image'
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成图片失败:', error);
|
|
|
+ media.generating = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 启动图片轮询
|
|
|
+const startImagePolling = () => {
|
|
|
+ if (inProgressTimer.value) {
|
|
|
+ clearInterval(inProgressTimer.value)
|
|
|
+ }
|
|
|
+ inProgressTimer.value = setInterval(refreshWatchImages, 3000)
|
|
|
+}
|
|
|
+
|
|
|
+// 停止图片轮询
|
|
|
+const stopImagePolling = () => {
|
|
|
+ if (inProgressTimer.value) {
|
|
|
+ clearInterval(inProgressTimer.value)
|
|
|
+ inProgressTimer.value = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 轮询生成中的图片列表
|
|
|
+const refreshWatchImages = async () => {
|
|
|
+ const imageIds = Object.keys(inProgressImageMap.value).map(Number)
|
|
|
+ if (imageIds.length === 0) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const list = await PaintingGetMys(imageIds)
|
|
|
+ const newWatchImages = {}
|
|
|
+ // 遍历所有正在生成的图片
|
|
|
+ Object.keys(inProgressImageMap.value).forEach((key) => {
|
|
|
+ const imageId = Number(key)
|
|
|
+ const info = inProgressImageMap.value[key]
|
|
|
+ // 查找对应的图片状态
|
|
|
+ const image = list.data.find(item => item.id === imageId)
|
|
|
+ if (image) {
|
|
|
+ if (image.status === AiImageStatusEnum.IN_PROGRESS) {
|
|
|
+ // 图片正在生成中,保留在map中
|
|
|
+ newWatchImages[key] = info
|
|
|
+ } else if (image.status === AiImageStatusEnum.SUCCESS && image.picUrl) {
|
|
|
+ // 图片生成成功
|
|
|
+ if (info.type === 'cover') {
|
|
|
+ // 处理封面图
|
|
|
+ scriptData.coverImage.url = image.picUrl
|
|
|
+ scriptData.coverImage.generating = false
|
|
|
+ } else {
|
|
|
+ // 处理课程小节背景图
|
|
|
+ const lesson = scriptData.lessons[info.lessonIndex]
|
|
|
+ if (lesson) {
|
|
|
+ const section = lesson.sections[info.sectionIndex]
|
|
|
+ if (section) {
|
|
|
+ section.backgroundImage.url = image.picUrl
|
|
|
+ section.backgroundImage.generating = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 更新生成中的图片预览
|
|
|
+ generatingImageUrl.value = image.picUrl
|
|
|
+
|
|
|
+ localStorage.setItem('scriptData', JSON.stringify(scriptData))
|
|
|
+ console.log("【最终数据,生成图片成功】", scriptData)
|
|
|
+ } else if (image.status === AiImageStatusEnum.FAIL) {
|
|
|
+ // 图片生成失败
|
|
|
+ if (info.type === 'cover') {
|
|
|
+ // 处理封面图失败
|
|
|
+ scriptData.coverImage.generating = false
|
|
|
+ } else {
|
|
|
+ // 处理课程小节背景图失败
|
|
|
+ const lesson = scriptData.lessons[info.lessonIndex]
|
|
|
+ if (lesson) {
|
|
|
+ const section = lesson.sections[info.sectionIndex]
|
|
|
+ if (section) {
|
|
|
+ section.backgroundImage.generating = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 图片不存在于返回列表中,可能已经处理完成或超时,从map中移除
|
|
|
+ if (info.type === 'cover') {
|
|
|
+ scriptData.coverImage.generating = false
|
|
|
+ } else {
|
|
|
+ const lesson = scriptData.lessons[info.lessonIndex]
|
|
|
+ if (lesson) {
|
|
|
+ const section = lesson.sections[info.sectionIndex]
|
|
|
+ if (section) {
|
|
|
+ section.backgroundImage.generating = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ inProgressImageMap.value = newWatchImages
|
|
|
+ } catch (error) {
|
|
|
+ console.error('轮询图片状态失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生成所有语音
|
|
|
+const generateAllVoiceovers = async () => {
|
|
|
+ isGeneratingVoiceovers.value = true
|
|
|
+ try {
|
|
|
+ // 确保scriptData和lessons存在
|
|
|
+ if (scriptData.lessons && scriptData.lessons.length > 0) {
|
|
|
+ for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
|
|
|
+ const lesson = scriptData.lessons[lessonIndex]
|
|
|
+ if (lesson && lesson.sections) {
|
|
|
+ for (let sectionIndex = 0; sectionIndex < lesson.sections.length; sectionIndex++) {
|
|
|
+ const section = lesson.sections[sectionIndex]
|
|
|
+ if (section && section.dialogues) {
|
|
|
+ for (let dialogueIndex = 0; dialogueIndex < section.dialogues.length; dialogueIndex++) {
|
|
|
+ const dialogue = section.dialogues[dialogueIndex]
|
|
|
+ // 只处理没有语音URL且有内容的对话
|
|
|
+ if (dialogue && !dialogue.url && dialogue.content) {
|
|
|
+ await generateVoiceover(lessonIndex, sectionIndex, dialogueIndex)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成所有语音失败:', error)
|
|
|
+ } finally {
|
|
|
+ isGeneratingVoiceovers.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 文本转语音API调用
|
|
|
+// 生成单个语音
|
|
|
+const generateVoiceover = async (lessonIndex, sectionIndex, dialogueIndex) => {
|
|
|
+ const lesson = scriptData.lessons[lessonIndex]
|
|
|
+ if (!lesson) return
|
|
|
+
|
|
|
+ const section = lesson.sections[sectionIndex]
|
|
|
+ if (!section) return
|
|
|
+
|
|
|
+ const dialogue = section.dialogues[dialogueIndex]
|
|
|
+
|
|
|
+ // 只处理数字人对话和提问
|
|
|
+ if (dialogue.type === 'user') {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ dialogue.generatingVoiceover = true
|
|
|
+
|
|
|
+ let role = digitalHumans.value.find((r) => r.name === dialogue.roleName)
|
|
|
+ if (!role) {
|
|
|
+ console.error('未找到角色:', dialogue.roleName)
|
|
|
+ dialogue.generatingVoiceover = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // 调用后端API将文本转成语音
|
|
|
+ const response = await textToSpeech({
|
|
|
+ roleId: role.id,
|
|
|
+ content: dialogue.content
|
|
|
+ })
|
|
|
+
|
|
|
+ // 将返回的URL赋值给对话
|
|
|
+ dialogue.voiceoverUrl = response.data
|
|
|
+ localStorage.setItem('scriptData', JSON.stringify(scriptData))
|
|
|
+ localStorage.setItem('【最终数据,生成语音成功】', scriptData)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成语音失败:', error)
|
|
|
+ } finally {
|
|
|
+ dialogue.generatingVoiceover = false
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
// 控制生成进度显示状态
|
|
|
const showNewContainer = ref(false);
|
|
|
|
|
|
// 生成进度步骤
|
|
|
const progressSteps = ref([
|
|
|
- { text: '生成课程信息...', completed: false, active: false, visible: false },
|
|
|
+ { text: '生成课程脚本...', completed: false, active: false, visible: false },
|
|
|
{ text: '生成课程小节...', completed: false, active: false, visible: false },
|
|
|
{ text: '生成背景图...', completed: false, active: false, visible: false },
|
|
|
{ text: '生成配音...', completed: false, active: false, visible: false }
|
|
|
@@ -104,12 +770,11 @@ const progressSteps = ref([
|
|
|
const dropdowns = ref([
|
|
|
{
|
|
|
key: 'course',
|
|
|
- value: '诗词',
|
|
|
+ value: '生成主题',
|
|
|
visible: false,
|
|
|
icon: Search,
|
|
|
options: [
|
|
|
- { label: '数学课', value: '数学课' },
|
|
|
- { label: '英语课', value: '英语课' }
|
|
|
+ { label: '诗词课', value: '诗词课' }
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
@@ -118,7 +783,7 @@ const dropdowns = ref([
|
|
|
visible: false,
|
|
|
icon: User,
|
|
|
options: [
|
|
|
- { label: '张老师', value: '张老师' },
|
|
|
+ { label: '李白', value: '李白' },
|
|
|
{ label: '李老师', value: '李老师' }
|
|
|
]
|
|
|
},
|
|
|
@@ -128,16 +793,7 @@ const dropdowns = ref([
|
|
|
visible: false,
|
|
|
icon: Memo,
|
|
|
options: [
|
|
|
- { label: '小王', value: '小王' },
|
|
|
- { label: '小李', value: '小李' },
|
|
|
- { label: '小王', value: '小王' },
|
|
|
- { label: '小李', value: '小李' },
|
|
|
- { label: '小王', value: '小王' },
|
|
|
- { label: '小李', value: '小李' },
|
|
|
- { label: '小王', value: '小王' },
|
|
|
- { label: '小李', value: '小李' },
|
|
|
- { label: '小王', value: '小王' },
|
|
|
- { label: '小李', value: '小李' }
|
|
|
+ { label: '小智', value: '小智' },
|
|
|
]
|
|
|
}
|
|
|
]);
|
|
|
@@ -166,12 +822,12 @@ const handleSelect = (key, command) => {
|
|
|
const dropdown = dropdowns.value.find(item => item.key === key);
|
|
|
if (dropdown && dropdownConfig[key]) {
|
|
|
const config = dropdownConfig[key];
|
|
|
-
|
|
|
+
|
|
|
// 生成新内容,仅当选择的值与默认值不同时添加
|
|
|
- const newContent = command === config.default
|
|
|
- ? config.prefix
|
|
|
+ const newContent = command === config.default
|
|
|
+ ? config.prefix
|
|
|
: `${config.prefix}${command}`;
|
|
|
-
|
|
|
+
|
|
|
// 检查输入框中是否已存在该类型的内容
|
|
|
if (config.regex.test(prompt.value)) {
|
|
|
// 替换已存在的内容
|
|
|
@@ -211,7 +867,7 @@ const handleSendByKeydown = async (event) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-/** 真正执行【发送】消息操作 */
|
|
|
+/** 生成操作 */
|
|
|
const doSendMessage = async (content) => {
|
|
|
// 校验
|
|
|
if (content.length < 1) {
|
|
|
@@ -219,55 +875,11 @@ const doSendMessage = async (content) => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 清空输入框
|
|
|
- prompt.value = "";
|
|
|
-
|
|
|
- // 显示new-container
|
|
|
showNewContainer.value = true;
|
|
|
|
|
|
- // 开始模拟生成进度
|
|
|
- simulateProgress();
|
|
|
-};
|
|
|
+ // 调用生成脚本方法
|
|
|
+ await generateScript();
|
|
|
|
|
|
-// 模拟生成进度
|
|
|
-const simulateProgress = () => {
|
|
|
- // 重置所有步骤
|
|
|
- progressSteps.value.forEach(step => {
|
|
|
- step.completed = false;
|
|
|
- step.active = false;
|
|
|
- step.visible = false;
|
|
|
- });
|
|
|
-
|
|
|
- // 逐个处理步骤
|
|
|
- let currentStep = 0;
|
|
|
-
|
|
|
- const processStep = () => {
|
|
|
- if (currentStep < progressSteps.value.length) {
|
|
|
- // 显示当前步骤
|
|
|
- progressSteps.value[currentStep].visible = true;
|
|
|
- // 激活当前步骤
|
|
|
- progressSteps.value[currentStep].active = true;
|
|
|
-
|
|
|
- // 模拟步骤完成
|
|
|
- setTimeout(() => {
|
|
|
- // 标记当前步骤为完成
|
|
|
- progressSteps.value[currentStep].completed = true;
|
|
|
- progressSteps.value[currentStep].active = false;
|
|
|
-
|
|
|
- // 处理下一步
|
|
|
- currentStep++;
|
|
|
- processStep();
|
|
|
- }, 1500); // 每个步骤持续1.5秒
|
|
|
- } else {
|
|
|
- // 所有步骤完成后,3秒后隐藏容器
|
|
|
- setTimeout(() => {
|
|
|
- showNewContainer.value = false;
|
|
|
- }, 2000);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 开始处理第一个步骤
|
|
|
- processStep();
|
|
|
};
|
|
|
|
|
|
// 计算属性:返回可见的进度步骤
|
|
|
@@ -393,7 +1005,7 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
.dropdowns-container .el-button {
|
|
|
- width: rpx(50);
|
|
|
+ width: rpx(60);
|
|
|
height: rpx(18);
|
|
|
background-color: #f1f0ff;
|
|
|
border: rpx(1) solid #a7a4ed;
|