|
@@ -19,7 +19,7 @@
|
|
|
<el-icon class="arrow-icon"><CaretLeft /></el-icon>
|
|
<el-icon class="arrow-icon"><CaretLeft /></el-icon>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="arrow-icon-circle" @click="togglePlay">
|
|
<div class="arrow-icon-circle" @click="togglePlay">
|
|
|
- <span class="play-text">{{ isPlaying ? '暂停' : '播放' }}</span>
|
|
|
|
|
|
|
+ <span class="play-text">{{ isPlaying ? '暂停' : '自动' }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="arrow-icon-circle" @click="playNext" :class="{ 'disabled': currentSectionIndex === scriptData.sections.length - 1 && currentDialogueIndex === currentSection.dialogues.length - 1 }">
|
|
<div class="arrow-icon-circle" @click="playNext" :class="{ 'disabled': currentSectionIndex === scriptData.sections.length - 1 && currentDialogueIndex === currentSection.dialogues.length - 1 }">
|
|
|
<el-icon class="arrow-icon"><CaretRight /></el-icon>
|
|
<el-icon class="arrow-icon"><CaretRight /></el-icon>
|
|
@@ -105,7 +105,7 @@ import { marked } from 'marked'
|
|
|
import {CreateDialogue, sendChatMessageStream} from "@/api/questions.js";
|
|
import {CreateDialogue, sendChatMessageStream} from "@/api/questions.js";
|
|
|
import {useAudioPlayer} from "@/api/tts/useAudioPlayer.js";
|
|
import {useAudioPlayer} from "@/api/tts/useAudioPlayer.js";
|
|
|
|
|
|
|
|
-const { playAudioChunk , stopPlayback } = useAudioPlayer();
|
|
|
|
|
|
|
+const { playAudioChunk , stopPlayback, setOnPlaybackComplete } = useAudioPlayer();
|
|
|
|
|
|
|
|
// 路由实例
|
|
// 路由实例
|
|
|
const router = useRouter()
|
|
const router = useRouter()
|
|
@@ -135,8 +135,6 @@ const currentSectionIndex = ref(0)
|
|
|
const currentDialogueIndex = ref(0)
|
|
const currentDialogueIndex = ref(0)
|
|
|
// 是否正在播放
|
|
// 是否正在播放
|
|
|
const isPlaying = ref(false)
|
|
const isPlaying = ref(false)
|
|
|
-// 用户输入前是否正在播放(用于AI回答完成后继续播放)
|
|
|
|
|
-const wasPlayingBeforeUserInput = ref(false)
|
|
|
|
|
// 用户输入内容
|
|
// 用户输入内容
|
|
|
const userInput = ref('')
|
|
const userInput = ref('')
|
|
|
// 语音录音状态
|
|
// 语音录音状态
|
|
@@ -299,7 +297,7 @@ const playBackgroundAudio = () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const playDialogueAudio = () => {
|
|
|
|
|
|
|
+const playDialogueAudio = (isAutoPlay = false) => {
|
|
|
// 停止之前的对话语音
|
|
// 停止之前的对话语音
|
|
|
if (dialogueAudio.value) {
|
|
if (dialogueAudio.value) {
|
|
|
dialogueAudio.value.pause()
|
|
dialogueAudio.value.pause()
|
|
@@ -308,41 +306,49 @@ const playDialogueAudio = () => {
|
|
|
|
|
|
|
|
// 播放当前对话的语音
|
|
// 播放当前对话的语音
|
|
|
if (currentDialogue.value?.voiceoverUrl) {
|
|
if (currentDialogue.value?.voiceoverUrl) {
|
|
|
- dialogueAudio.value = new Audio(currentDialogue.value.voiceoverUrl)
|
|
|
|
|
- return new Promise((resolve) => {
|
|
|
|
|
- // 最短3秒跳转
|
|
|
|
|
- const minDelay = 3000
|
|
|
|
|
- let audioEnded = false
|
|
|
|
|
-
|
|
|
|
|
- // 音频结束事件
|
|
|
|
|
- dialogueAudio.value.onended = () => {
|
|
|
|
|
- audioEnded = true
|
|
|
|
|
- resolve()
|
|
|
|
|
|
|
+ const audio = new Audio(currentDialogue.value.voiceoverUrl)
|
|
|
|
|
+ dialogueAudio.value = audio
|
|
|
|
|
+
|
|
|
|
|
+ // 音频结束事件
|
|
|
|
|
+ audio.onended = () => {
|
|
|
|
|
+ // 如果是自动播放状态,继续播放下一条
|
|
|
|
|
+ if (isAutoPlay && isPlaying.value) {
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (!playNext(true)) {
|
|
|
|
|
+ // 播放完毕
|
|
|
|
|
+ isPlaying.value = false
|
|
|
|
|
+ stopAllAudio()
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 100)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 播放音频
|
|
|
|
|
- dialogueAudio.value.play().catch(e => {
|
|
|
|
|
- console.error('对话语音播放失败:', e)
|
|
|
|
|
- // 播放失败时,2秒后跳转
|
|
|
|
|
- setTimeout(resolve, minDelay)
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- // 检查音频时长
|
|
|
|
|
- dialogueAudio.value.addEventListener('loadedmetadata', () => {
|
|
|
|
|
- const duration = dialogueAudio.value.duration
|
|
|
|
|
- // 如果音频时长小于2秒,2秒后跳转
|
|
|
|
|
- if (duration < 2) {
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- if (!audioEnded) {
|
|
|
|
|
- resolve()
|
|
|
|
|
- }
|
|
|
|
|
- }, minDelay)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 播放音频
|
|
|
|
|
+ audio.play().catch(e => {
|
|
|
|
|
+ console.error('对话语音播放失败:', e)
|
|
|
|
|
+ // 播放失败时,2秒后跳转(暂时注销,因为会导致user类型数字人回复播报跳转后语音播报报错,直接2秒跳转)
|
|
|
|
|
+ // if (isAutoPlay && isPlaying.value) {
|
|
|
|
|
+ // setTimeout(() => {
|
|
|
|
|
+ // if (!playNext(true)) {
|
|
|
|
|
+ // // 播放完毕
|
|
|
|
|
+ // isPlaying.value = false
|
|
|
|
|
+ // stopAllAudio()
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }, 2000)
|
|
|
|
|
+ // }
|
|
|
})
|
|
})
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果没有语音文件,2秒后跳转
|
|
|
|
|
+ if (isAutoPlay && isPlaying.value && currentDialogue.value?.type !== 'user') {
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (!playNext(true)) {
|
|
|
|
|
+ // 播放完毕
|
|
|
|
|
+ isPlaying.value = false
|
|
|
|
|
+ stopAllAudio()
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 2000)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- // 如果没有语音文件,2秒后跳转
|
|
|
|
|
- return new Promise(resolve => setTimeout(resolve, 2000))
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const togglePlay = () => {
|
|
const togglePlay = () => {
|
|
@@ -358,27 +364,15 @@ const togglePlay = () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const playSequence = async () => {
|
|
|
|
|
|
|
+// 自动播放序列
|
|
|
|
|
+const playSequence = () => {
|
|
|
if (!isPlaying.value) return
|
|
if (!isPlaying.value) return
|
|
|
|
|
|
|
|
// 如果当前是用户输入卡片,暂停播放等待用户输入
|
|
// 如果当前是用户输入卡片,暂停播放等待用户输入
|
|
|
- if (currentDialogue.value?.type === 'user') {
|
|
|
|
|
- wasPlayingBeforeUserInput.value = true
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 播放当前对话语音
|
|
|
|
|
- await playDialogueAudio()
|
|
|
|
|
|
|
+ if (currentDialogue.value?.type === 'user') return
|
|
|
|
|
|
|
|
- // 播放下一个对话
|
|
|
|
|
- if (!playNext()) {
|
|
|
|
|
- // 播放完毕
|
|
|
|
|
- isPlaying.value = false
|
|
|
|
|
- // 停止所有音频,包括背景音
|
|
|
|
|
- stopAllAudio()
|
|
|
|
|
- } else {
|
|
|
|
|
- playSequence()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 播放当前对话语音,传递isAutoPlay参数
|
|
|
|
|
+ playDialogueAudio(true)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const playPrevious = () => {
|
|
const playPrevious = () => {
|
|
@@ -387,7 +381,7 @@ const playPrevious = () => {
|
|
|
|
|
|
|
|
// 如果正在进行数字人对话,调用stopStream清理
|
|
// 如果正在进行数字人对话,调用stopStream清理
|
|
|
recoverQuestDialogue()
|
|
recoverQuestDialogue()
|
|
|
- stopPlayback()
|
|
|
|
|
|
|
+ stopPlayback(false) // 不触发回调
|
|
|
if (conversationInProgress.value) {
|
|
if (conversationInProgress.value) {
|
|
|
stopStream()
|
|
stopStream()
|
|
|
}
|
|
}
|
|
@@ -403,10 +397,14 @@ const playPrevious = () => {
|
|
|
// 播放背景音
|
|
// 播放背景音
|
|
|
playBackgroundAudio()
|
|
playBackgroundAudio()
|
|
|
// 播放当前对话语音
|
|
// 播放当前对话语音
|
|
|
- playDialogueAudio()
|
|
|
|
|
|
|
+ if (isPlaying.value) {
|
|
|
|
|
+ playDialogueAudio(true)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ playDialogueAudio()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const playNext = () => {
|
|
|
|
|
|
|
+const playNext = (isAutoPlay = false) => {
|
|
|
// 停止当前对话语音
|
|
// 停止当前对话语音
|
|
|
if (dialogueAudio.value) {
|
|
if (dialogueAudio.value) {
|
|
|
dialogueAudio.value.pause()
|
|
dialogueAudio.value.pause()
|
|
@@ -415,23 +413,31 @@ const playNext = () => {
|
|
|
|
|
|
|
|
// 如果正在进行数字人对话,调用stopStream清理
|
|
// 如果正在进行数字人对话,调用stopStream清理
|
|
|
recoverQuestDialogue()
|
|
recoverQuestDialogue()
|
|
|
- stopPlayback()
|
|
|
|
|
|
|
+ stopPlayback(false)
|
|
|
if (conversationInProgress.value) {
|
|
if (conversationInProgress.value) {
|
|
|
stopStream()
|
|
stopStream()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (currentSection.value && currentDialogueIndex.value < currentSection.value.dialogues.length - 1) {
|
|
if (currentSection.value && currentDialogueIndex.value < currentSection.value.dialogues.length - 1) {
|
|
|
currentDialogueIndex.value++
|
|
currentDialogueIndex.value++
|
|
|
- // 播放当前对话语音
|
|
|
|
|
- playDialogueAudio()
|
|
|
|
|
|
|
+ // 根据是否为自动播放状态决定如何播放语音
|
|
|
|
|
+ if (isPlaying.value) {
|
|
|
|
|
+ playDialogueAudio(true)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ playDialogueAudio()
|
|
|
|
|
+ }
|
|
|
return true
|
|
return true
|
|
|
} else if (currentSectionIndex.value < props.scriptData.sections.length - 1) {
|
|
} else if (currentSectionIndex.value < props.scriptData.sections.length - 1) {
|
|
|
currentSectionIndex.value++
|
|
currentSectionIndex.value++
|
|
|
currentDialogueIndex.value = 0
|
|
currentDialogueIndex.value = 0
|
|
|
// 播放背景音
|
|
// 播放背景音
|
|
|
playBackgroundAudio()
|
|
playBackgroundAudio()
|
|
|
- // 播放当前对话语音
|
|
|
|
|
- playDialogueAudio()
|
|
|
|
|
|
|
+ // 根据是否为自动播放状态决定如何播放语音
|
|
|
|
|
+ if (isPlaying.value) {
|
|
|
|
|
+ playDialogueAudio(true)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ playDialogueAudio()
|
|
|
|
|
+ }
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
return false
|
|
return false
|
|
@@ -515,13 +521,12 @@ const doSendMessage = async () => {
|
|
|
})
|
|
})
|
|
|
// 清空输入框
|
|
// 清空输入框
|
|
|
userInput.value = ''
|
|
userInput.value = ''
|
|
|
- recoverQuestDialogue()
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 显示问题回答对话 */
|
|
/** 显示问题回答对话 */
|
|
|
const showQuestAnswerDialogue = () => {
|
|
const showQuestAnswerDialogue = () => {
|
|
|
// 缓存当前对话
|
|
// 缓存当前对话
|
|
|
- currentDialogueCache.value = currentDialogue.value
|
|
|
|
|
|
|
+ currentDialogueCache.value = JSON.parse(JSON.stringify(currentDialogue.value))
|
|
|
|
|
|
|
|
// 将当前对话类型设置为数字人对话
|
|
// 将当前对话类型设置为数字人对话
|
|
|
currentDialogue.value.type = "digital"
|
|
currentDialogue.value.type = "digital"
|
|
@@ -529,11 +534,21 @@ const showQuestAnswerDialogue = () => {
|
|
|
currentDialogue.value.content = "让我思考一下..."
|
|
currentDialogue.value.content = "让我思考一下..."
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
/** 回复对话 */
|
|
/** 回复对话 */
|
|
|
const recoverQuestDialogue = () => {
|
|
const recoverQuestDialogue = () => {
|
|
|
- // 缓存当前对话
|
|
|
|
|
- currentDialogue.value = currentDialogueCache.value
|
|
|
|
|
- currentDialogueCache.value = null
|
|
|
|
|
|
|
+ // 如果有缓存的对话
|
|
|
|
|
+ if (currentDialogueCache.value){
|
|
|
|
|
+ // 恢复当前对话
|
|
|
|
|
+ const currentSection = props.scriptData.sections[currentSectionIndex.value]
|
|
|
|
|
+ if (currentSection) {
|
|
|
|
|
+ currentSection.dialogues[currentDialogueIndex.value] = JSON.parse(JSON.stringify(currentDialogueCache.value))
|
|
|
|
|
+ }
|
|
|
|
|
+ // 清空缓存
|
|
|
|
|
+ currentDialogueCache.value = null
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 真正执行【发送】消息操作 */
|
|
/** 真正执行【发送】消息操作 */
|
|
@@ -561,7 +576,6 @@ const doSendMessageStream = async userMessage => {
|
|
|
const { code, data, msg } = JSON.parse(res.data)
|
|
const { code, data, msg } = JSON.parse(res.data)
|
|
|
if (code !== 0) {
|
|
if (code !== 0) {
|
|
|
console.log(`对话异常! ${msg}`)
|
|
console.log(`对话异常! ${msg}`)
|
|
|
- recoverQuestDialogue();
|
|
|
|
|
stopStream();
|
|
stopStream();
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -590,20 +604,17 @@ const doSendMessageStream = async userMessage => {
|
|
|
},
|
|
},
|
|
|
error => {
|
|
error => {
|
|
|
console.log(`对话异常! ${error}`)
|
|
console.log(`对话异常! ${error}`)
|
|
|
- recoverQuestDialogue();
|
|
|
|
|
stopStream()
|
|
stopStream()
|
|
|
// 需要抛出异常,禁止重试
|
|
// 需要抛出异常,禁止重试
|
|
|
throw error
|
|
throw error
|
|
|
},
|
|
},
|
|
|
() => {
|
|
() => {
|
|
|
console.log(`结束对话! `)
|
|
console.log(`结束对话! `)
|
|
|
- recoverQuestDialogue();
|
|
|
|
|
stopStream()
|
|
stopStream()
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('发送消息失败:', error)
|
|
console.error('发送消息失败:', error)
|
|
|
- recoverQuestDialogue();
|
|
|
|
|
stopStream()
|
|
stopStream()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -620,20 +631,26 @@ const stopStream = async () => {
|
|
|
conversationInProgress.value = false
|
|
conversationInProgress.value = false
|
|
|
|
|
|
|
|
console.log(`结束对话!更改状态: `,conversationInProgress.value)
|
|
console.log(`结束对话!更改状态: `,conversationInProgress.value)
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+// 处理音频播放完成
|
|
|
|
|
+const handleAudioPlaybackComplete = () => {
|
|
|
|
|
+ console.log('智能问答音频播放完成');
|
|
|
// AI回答完成后,如果之前是播放状态,继续播放
|
|
// AI回答完成后,如果之前是播放状态,继续播放
|
|
|
- if (wasPlayingBeforeUserInput.value) {
|
|
|
|
|
- wasPlayingBeforeUserInput.value = false
|
|
|
|
|
- // 跳到下一个对话并继续播放
|
|
|
|
|
|
|
+
|
|
|
|
|
+ stopAllAudio();
|
|
|
|
|
+ // 如果处于自动播放状态,继续播放下一条对话
|
|
|
|
|
+ if (isPlaying.value) {
|
|
|
if (playNext()) {
|
|
if (playNext()) {
|
|
|
- playSequence()
|
|
|
|
|
|
|
+ // 继续自动播放序列
|
|
|
|
|
+ playSequence();
|
|
|
} else {
|
|
} else {
|
|
|
// 播放完毕
|
|
// 播放完毕
|
|
|
- isPlaying.value = false
|
|
|
|
|
- stopAllAudio()
|
|
|
|
|
|
|
+ isPlaying.value = false;
|
|
|
|
|
+ stopAllAudio();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
// 组件挂载时添加键盘事件监听
|
|
// 组件挂载时添加键盘事件监听
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
@@ -642,13 +659,15 @@ onMounted(() => {
|
|
|
playBackgroundAudio()
|
|
playBackgroundAudio()
|
|
|
// 播放当前对话语音
|
|
// 播放当前对话语音
|
|
|
playDialogueAudio()
|
|
playDialogueAudio()
|
|
|
|
|
+ // 设置音频播放完成回调
|
|
|
|
|
+ setOnPlaybackComplete(handleAudioPlaybackComplete)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 组件卸载时移除键盘事件监听和停止音频
|
|
// 组件卸载时移除键盘事件监听和停止音频
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
window.removeEventListener('keydown', handleKeydown)
|
|
window.removeEventListener('keydown', handleKeydown)
|
|
|
stopAllAudio()
|
|
stopAllAudio()
|
|
|
- stopPlayback()
|
|
|
|
|
|
|
+ stopPlayback(false)
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|