Skip to content

Commit 9a93d2b

Browse files
committed
feat: Support autoplay audio tag
--story=1017623 --user=刘瑞斌 【应用】-语音播放时遇到音频文件直接播放文件内容 https://www.tapd.cn/57709429/s/1642154
1 parent d9df013 commit 9a93d2b

File tree

2 files changed

+91
-34
lines changed

2 files changed

+91
-34
lines changed

ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
</span>
7777
</div>
7878
<!-- 先渲染,不然不能播放 -->
79-
<audio ref="audioPlayer" controls hidden="hidden"></audio>
79+
<audio ref="audioPlayer" v-for="item in audioList" :key="item" controls hidden="hidden"></audio>
8080
</template>
8181
<script setup lang="ts">
8282
import { onMounted, ref } from 'vue'
@@ -110,11 +110,13 @@ const props = withDefaults(
110110
111111
const emit = defineEmits(['update:data', 'regeneration'])
112112
113-
const audioPlayer = ref<HTMLAudioElement | null>(null)
113+
const audioPlayer = ref<HTMLAudioElement[] | null>([])
114114
const audioPlayerStatus = ref(false)
115115
const buttonData = ref(props.data)
116116
const loading = ref(false)
117117
const utterance = ref<SpeechSynthesisUtterance | null>(null)
118+
const audioList = ref<string[]>([])
119+
const currentAudioIndex = ref(0)
118120
119121
function regeneration() {
120122
emit('regeneration')
@@ -170,35 +172,56 @@ const playAnswerText = (text: string) => {
170172
text = markdownToPlainText(text)
171173
// console.log(text)
172174
audioPlayerStatus.value = true
173-
if (props.tts_type === 'BROWSER') {
174-
if (text !== utterance.value?.text) {
175+
// 分割成多份
176+
audioList.value = text.split(/(<audio[^>]*><\/audio>)/)
177+
playAnswerTextPart()
178+
}
179+
180+
const playAnswerTextPart = () => {
181+
// console.log(audioList.value, currentAudioIndex.value)
182+
if (currentAudioIndex.value === audioList.value.length) {
183+
audioPlayerStatus.value = false
184+
currentAudioIndex.value = 0
185+
return
186+
}
187+
if (audioList.value[currentAudioIndex.value].includes('<audio')) {
188+
if (audioPlayer.value) {
189+
audioPlayer.value[currentAudioIndex.value].src = audioList.value[currentAudioIndex.value].match(/src="([^"]*)"/)?.[1] || ''
190+
audioPlayer.value[currentAudioIndex.value].play() // 自动播放音频
191+
audioPlayer.value[currentAudioIndex.value].onended = () => {
192+
currentAudioIndex.value += 1
193+
playAnswerTextPart()
194+
}
195+
}
196+
} else if (props.tts_type === 'BROWSER') {
197+
if (audioList.value[currentAudioIndex.value] !== utterance.value?.text) {
175198
window.speechSynthesis.cancel()
176199
}
177200
if (window.speechSynthesis.paused) {
178201
window.speechSynthesis.resume()
179202
return
180203
}
181204
// 创建一个新的 SpeechSynthesisUtterance 实例
182-
utterance.value = new SpeechSynthesisUtterance(text)
205+
utterance.value = new SpeechSynthesisUtterance(audioList.value[currentAudioIndex.value])
183206
utterance.value.onend = () => {
184-
audioPlayerStatus.value = false
185207
utterance.value = null
208+
currentAudioIndex.value += 1
209+
playAnswerTextPart()
186210
}
187211
utterance.value.onerror = () => {
188212
audioPlayerStatus.value = false
189213
utterance.value = null
190214
}
191215
// 调用浏览器的朗读功能
192216
window.speechSynthesis.speak(utterance.value)
193-
}
194-
if (props.tts_type === 'TTS') {
217+
} else if (props.tts_type === 'TTS') {
195218
// 恢复上次暂停的播放
196-
if (audioPlayer.value?.src) {
197-
audioPlayer.value?.play()
219+
if (audioPlayer.value && audioPlayer.value[currentAudioIndex.value]?.src) {
220+
audioPlayer.value[currentAudioIndex.value].play()
198221
return
199222
}
200223
applicationApi
201-
.postTextToSpeech((props.applicationId as string) || (id as string), { text: text }, loading)
224+
.postTextToSpeech((props.applicationId as string) || (id as string), { text: audioList.value[currentAudioIndex.value] }, loading)
202225
.then(async (res: any) => {
203226
if (res.type === 'application/json') {
204227
const text = await res.text()
@@ -219,11 +242,12 @@ const playAnswerText = (text: string) => {
219242
// link.click()
220243
221244
// 检查 audioPlayer 是否已经引用了 DOM 元素
222-
if (audioPlayer.value instanceof HTMLAudioElement) {
223-
audioPlayer.value.src = url
224-
audioPlayer.value.play() // 自动播放音频
225-
audioPlayer.value.onended = () => {
226-
audioPlayerStatus.value = false
245+
if (audioPlayer.value) {
246+
audioPlayer.value[currentAudioIndex.value].src = url
247+
audioPlayer.value[currentAudioIndex.value].play() // 自动播放音频
248+
audioPlayer.value[currentAudioIndex.value].onended = () => {
249+
currentAudioIndex.value += 1
250+
playAnswerTextPart()
227251
}
228252
} else {
229253
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
@@ -238,7 +262,11 @@ const playAnswerText = (text: string) => {
238262
const pausePlayAnswerText = () => {
239263
audioPlayerStatus.value = false
240264
if (props.tts_type === 'TTS') {
241-
audioPlayer.value?.pause()
265+
if (audioPlayer.value) {
266+
audioPlayer.value?.forEach((item) => {
267+
item.pause()
268+
})
269+
}
242270
}
243271
if (props.tts_type === 'BROWSER') {
244272
window.speechSynthesis.pause()

ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
<EditContentDialog ref="EditContentDialogRef" @refresh="refreshContent" />
5555
<EditMarkDialog ref="EditMarkDialogRef" @refresh="refreshMark" />
5656
<!-- 先渲染,不然不能播放 -->
57-
<audio ref="audioPlayer" controls hidden="hidden"></audio>
57+
<audio ref="audioPlayer" v-for="item in audioList" :key="item" controls hidden="hidden"></audio>
5858
</div>
5959
</div>
6060
</template>
@@ -88,14 +88,16 @@ const props = defineProps({
8888
8989
const emit = defineEmits(['update:data'])
9090
91-
const audioPlayer = ref<HTMLAudioElement | null>(null)
91+
const audioPlayer = ref<HTMLAudioElement[] | null>(null)
9292
9393
const EditContentDialogRef = ref()
9494
const EditMarkDialogRef = ref()
9595
9696
const buttonData = ref(props.data)
9797
const loading = ref(false)
9898
const utterance = ref<SpeechSynthesisUtterance | null>(null)
99+
const audioList = ref<string[]>([])
100+
const currentAudioIndex = ref(0)
99101
100102
function editContent(data: any) {
101103
EditContentDialogRef.value.open(data)
@@ -149,35 +151,56 @@ const playAnswerText = (text: string) => {
149151
text = markdownToPlainText(text)
150152
// console.log(text)
151153
audioPlayerStatus.value = true
152-
if (props.tts_type === 'BROWSER') {
153-
if (text !== utterance.value?.text) {
154+
// 分割成多份
155+
audioList.value = text.split(/(<audio[^>]*><\/audio>)/)
156+
playAnswerTextPart()
157+
}
158+
159+
const playAnswerTextPart = () => {
160+
// console.log(audioList.value, currentAudioIndex.value)
161+
if (currentAudioIndex.value === audioList.value.length) {
162+
audioPlayerStatus.value = false
163+
currentAudioIndex.value = 0
164+
return
165+
}
166+
if (audioList.value[currentAudioIndex.value].includes('<audio')) {
167+
if (audioPlayer.value) {
168+
audioPlayer.value[currentAudioIndex.value].src = audioList.value[currentAudioIndex.value].match(/src="([^"]*)"/)?.[1] || ''
169+
audioPlayer.value[currentAudioIndex.value].play() // 自动播放音频
170+
audioPlayer.value[currentAudioIndex.value].onended = () => {
171+
currentAudioIndex.value += 1
172+
playAnswerTextPart()
173+
}
174+
}
175+
} else if (props.tts_type === 'BROWSER') {
176+
if (audioList.value[currentAudioIndex.value] !== utterance.value?.text) {
154177
window.speechSynthesis.cancel()
155178
}
156179
if (window.speechSynthesis.paused) {
157180
window.speechSynthesis.resume()
158181
return
159182
}
160183
// 创建一个新的 SpeechSynthesisUtterance 实例
161-
utterance.value = new SpeechSynthesisUtterance(text)
184+
utterance.value = new SpeechSynthesisUtterance(audioList.value[currentAudioIndex.value])
162185
utterance.value.onend = () => {
163-
audioPlayerStatus.value = false
164186
utterance.value = null
187+
currentAudioIndex.value += 1
188+
playAnswerTextPart()
165189
}
166190
utterance.value.onerror = () => {
167191
audioPlayerStatus.value = false
168192
utterance.value = null
169193
}
170194
// 调用浏览器的朗读功能
171195
window.speechSynthesis.speak(utterance.value)
172-
}
173-
if (props.tts_type === 'TTS') {
196+
} else if (props.tts_type === 'TTS') {
174197
// 恢复上次暂停的播放
175-
if (audioPlayer.value?.src) {
176-
audioPlayer.value?.play()
198+
if (audioPlayer.value && audioPlayer.value[currentAudioIndex.value]?.src) {
199+
audioPlayer.value[currentAudioIndex.value].play()
177200
return
178201
}
179202
applicationApi
180-
.postTextToSpeech(id || (props.applicationId as string), { text: text }, loading)
203+
.postTextToSpeech((props.applicationId as string) || (id as string), { text: audioList.value[currentAudioIndex.value] }, loading)
181204
.then(async (res: any) => {
182205
if (res.type === 'application/json') {
183206
const text = await res.text()
@@ -198,11 +221,12 @@ const playAnswerText = (text: string) => {
198221
// link.click()
199222
200223
// 检查 audioPlayer 是否已经引用了 DOM 元素
201-
if (audioPlayer.value instanceof HTMLAudioElement) {
202-
audioPlayer.value.src = url
203-
audioPlayer.value.play() // 自动播放音频
204-
audioPlayer.value.onended = () => {
205-
audioPlayerStatus.value = false
224+
if (audioPlayer.value) {
225+
audioPlayer.value[currentAudioIndex.value].src = url
226+
audioPlayer.value[currentAudioIndex.value].play() // 自动播放音频
227+
audioPlayer.value[currentAudioIndex.value].onended = () => {
228+
currentAudioIndex.value += 1
229+
playAnswerTextPart()
206230
}
207231
} else {
208232
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
@@ -217,13 +241,18 @@ const playAnswerText = (text: string) => {
217241
const pausePlayAnswerText = () => {
218242
audioPlayerStatus.value = false
219243
if (props.tts_type === 'TTS') {
220-
audioPlayer.value?.pause()
244+
if (audioPlayer.value) {
245+
audioPlayer.value?.forEach((item) => {
246+
item.pause()
247+
})
248+
}
221249
}
222250
if (props.tts_type === 'BROWSER') {
223251
window.speechSynthesis.pause()
224252
}
225253
}
226254
255+
227256
function refreshMark() {
228257
buttonData.value.improve_paragraph_id_list = []
229258
emit('update:data', buttonData.value)

0 commit comments

Comments
 (0)