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