283 lines
7.4 KiB
JavaScript
283 lines
7.4 KiB
JavaScript
import { computed, onUnmounted, ref } from 'vue'
|
|
import { createTtsTask, getTtsTask, getTtsTaskBySource } from '../services/tts.js'
|
|
import analytics from '../services/analytics.js'
|
|
|
|
const readResponseData = (response) => {
|
|
if (!response) return null
|
|
if (Object.prototype.hasOwnProperty.call(response, 'data')) {
|
|
return response.data ?? null
|
|
}
|
|
return response
|
|
}
|
|
|
|
export const useTtsPlayer = ({ pagePath = '', sourceType = 'epic_script' } = {}) => {
|
|
const task = ref(null)
|
|
const loading = ref(false)
|
|
const playing = ref(false)
|
|
const statusText = ref('')
|
|
let audio = null
|
|
let timer = null
|
|
let prewarmPromise = null
|
|
let prewarmSourceId = ''
|
|
|
|
const buttonText = computed(() => {
|
|
if (loading.value) return '正在生成朗读'
|
|
if (task.value?.status === 'success') return playing.value ? '暂停朗读' : '播放朗读'
|
|
if (task.value?.status === 'failed') return '重试朗读'
|
|
return '语音播放'
|
|
})
|
|
|
|
const clearTimer = () => {
|
|
if (timer) clearInterval(timer)
|
|
timer = null
|
|
}
|
|
|
|
const stopAudio = () => {
|
|
if (!audio) return
|
|
audio.stop()
|
|
audio.destroy()
|
|
audio = null
|
|
playing.value = false
|
|
}
|
|
|
|
const setTask = (nextTask) => {
|
|
task.value = nextTask
|
|
if (audio && nextTask?.audioUrl && audio.src !== nextTask.audioUrl) {
|
|
stopAudio()
|
|
}
|
|
}
|
|
|
|
const track = (eventName, sourceId, payload = {}) => {
|
|
analytics.track(eventName, {
|
|
source_id: sourceId || '',
|
|
task_id: task.value?.id || '',
|
|
...payload
|
|
}, { eventType: 'tts', pagePath })
|
|
}
|
|
|
|
const markFailed = (message) => {
|
|
loading.value = false
|
|
statusText.value = message || '朗读暂时不可用'
|
|
uni.showToast({ title: statusText.value, icon: 'none' })
|
|
}
|
|
|
|
const pollTask = (taskId, sourceId) => {
|
|
clearTimer()
|
|
timer = setInterval(async () => {
|
|
try {
|
|
const response = await getTtsTask(taskId)
|
|
const nextTask = readResponseData(response)
|
|
setTask(nextTask)
|
|
|
|
if (nextTask?.status === 'success') {
|
|
loading.value = false
|
|
statusText.value = ''
|
|
clearTimer()
|
|
track('script_tts_success', sourceId, { task_id: nextTask.id })
|
|
play(sourceId)
|
|
return
|
|
}
|
|
|
|
if (nextTask?.status === 'failed') {
|
|
clearTimer()
|
|
track('script_tts_error', sourceId, { error: nextTask.errorMessage || '' })
|
|
markFailed(nextTask.errorMessage || '朗读生成失败')
|
|
}
|
|
} catch (error) {
|
|
clearTimer()
|
|
track('script_tts_error', sourceId, { error: error?.message || error?.errMsg || 'poll failed' })
|
|
markFailed('朗读状态获取失败')
|
|
}
|
|
}, 2500)
|
|
}
|
|
|
|
const play = (sourceId) => {
|
|
if (!task.value?.audioUrl) return
|
|
|
|
if (!audio) {
|
|
audio = uni.createInnerAudioContext()
|
|
audio.src = task.value.audioUrl
|
|
audio.autoplay = false
|
|
|
|
audio.onPlay(() => {
|
|
playing.value = true
|
|
track('script_tts_play', sourceId)
|
|
})
|
|
|
|
audio.onPause(() => {
|
|
playing.value = false
|
|
track('script_tts_pause', sourceId)
|
|
})
|
|
|
|
audio.onEnded(() => {
|
|
playing.value = false
|
|
track('script_tts_complete', sourceId)
|
|
})
|
|
|
|
audio.onError((error) => {
|
|
playing.value = false
|
|
track('script_tts_error', sourceId, { error: error?.errMsg || 'play failed' })
|
|
markFailed('音频播放失败')
|
|
})
|
|
}
|
|
|
|
if (playing.value) {
|
|
audio.pause()
|
|
} else {
|
|
audio.play()
|
|
}
|
|
}
|
|
|
|
const createAndPoll = async (sourceId) => {
|
|
loading.value = true
|
|
statusText.value = ''
|
|
track('script_tts_request', sourceId)
|
|
|
|
try {
|
|
const response = await createTtsTask({ sourceType, sourceId })
|
|
const nextTask = readResponseData(response)
|
|
setTask(nextTask)
|
|
|
|
if (nextTask?.status === 'success') {
|
|
loading.value = false
|
|
track('script_tts_success', sourceId, { task_id: nextTask.id })
|
|
play(sourceId)
|
|
return
|
|
}
|
|
|
|
if (nextTask?.status === 'failed') {
|
|
track('script_tts_error', sourceId, { error: nextTask.errorMessage || '' })
|
|
markFailed(nextTask.errorMessage || '朗读生成失败')
|
|
return
|
|
}
|
|
|
|
if (nextTask?.id) {
|
|
pollTask(nextTask.id, sourceId)
|
|
return
|
|
}
|
|
|
|
markFailed('朗读任务创建失败')
|
|
} catch (error) {
|
|
track('script_tts_error', sourceId, { error: error?.message || error?.errMsg || 'create failed' })
|
|
markFailed(error?.message || '朗读任务创建失败')
|
|
}
|
|
}
|
|
|
|
const prewarmSource = async (sourceId) => {
|
|
if (!sourceId) return null
|
|
if (task.value?.status === 'success' && task.value?.sourceId === sourceId) {
|
|
return task.value
|
|
}
|
|
if (prewarmPromise && prewarmSourceId === sourceId) {
|
|
return prewarmPromise
|
|
}
|
|
|
|
prewarmSourceId = sourceId
|
|
prewarmPromise = (async () => {
|
|
try {
|
|
const existingResponse = await getTtsTaskBySource({ sourceType, sourceId })
|
|
const existingTask = readResponseData(existingResponse)
|
|
if (existingTask?.id) {
|
|
setTask(existingTask)
|
|
}
|
|
if (existingTask?.status === 'success' || existingTask?.status === 'pending' || existingTask?.status === 'processing') {
|
|
return existingTask
|
|
}
|
|
} catch (error) {
|
|
// Old scripts may not have a task yet; create one silently below.
|
|
}
|
|
|
|
try {
|
|
const createResponse = await createTtsTask({ sourceType, sourceId })
|
|
const nextTask = readResponseData(createResponse)
|
|
if (nextTask?.id) {
|
|
setTask(nextTask)
|
|
}
|
|
return nextTask
|
|
} catch (error) {
|
|
track('script_tts_error', sourceId, { error: error?.message || error?.errMsg || 'prewarm failed' })
|
|
return null
|
|
} finally {
|
|
prewarmPromise = null
|
|
prewarmSourceId = ''
|
|
}
|
|
})()
|
|
|
|
return prewarmPromise
|
|
}
|
|
|
|
const playSource = async (sourceId) => {
|
|
if (!sourceId) {
|
|
uni.showToast({ title: '生成保存后可播放', icon: 'none' })
|
|
return
|
|
}
|
|
if (loading.value) return
|
|
|
|
if (prewarmPromise && prewarmSourceId === sourceId) {
|
|
const warmedTask = await prewarmPromise
|
|
if (warmedTask?.status === 'success') {
|
|
setTask(warmedTask)
|
|
play(sourceId)
|
|
return
|
|
}
|
|
if ((warmedTask?.status === 'pending' || warmedTask?.status === 'processing') && warmedTask?.id) {
|
|
setTask(warmedTask)
|
|
loading.value = true
|
|
pollTask(warmedTask.id, sourceId)
|
|
return
|
|
}
|
|
}
|
|
|
|
if (task.value?.status === 'success') {
|
|
play(sourceId)
|
|
return
|
|
}
|
|
|
|
try {
|
|
const response = await getTtsTaskBySource({ sourceType, sourceId })
|
|
const existingTask = readResponseData(response)
|
|
setTask(existingTask)
|
|
|
|
if (existingTask?.status === 'success') {
|
|
play(sourceId)
|
|
return
|
|
}
|
|
|
|
if (existingTask?.status === 'pending' || existingTask?.status === 'processing') {
|
|
loading.value = true
|
|
pollTask(existingTask.id, sourceId)
|
|
return
|
|
}
|
|
} catch (error) {
|
|
// Missing existing task should fall through and create a new one.
|
|
}
|
|
|
|
await createAndPoll(sourceId)
|
|
}
|
|
|
|
const reset = () => {
|
|
clearTimer()
|
|
stopAudio()
|
|
setTask(null)
|
|
statusText.value = ''
|
|
loading.value = false
|
|
prewarmPromise = null
|
|
prewarmSourceId = ''
|
|
}
|
|
|
|
onUnmounted(reset)
|
|
|
|
return {
|
|
task,
|
|
loading,
|
|
playing,
|
|
statusText,
|
|
buttonText,
|
|
prewarmSource,
|
|
playSource,
|
|
reset
|
|
}
|
|
}
|
|
|
|
export default useTtsPlayer
|