feat : tts正常播放暂存
This commit is contained in:
@@ -8,13 +8,14 @@ import useUserStore from '@/stores/useUserStore';
|
|||||||
* @param {Object} config - TTS配置
|
* @param {Object} config - TTS配置
|
||||||
* @param {string} config.apiUrl - 语音合成API地址
|
* @param {string} config.apiUrl - 语音合成API地址
|
||||||
* @param {number} config.maxSegmentLength - 最大分段长度
|
* @param {number} config.maxSegmentLength - 最大分段长度
|
||||||
|
* @param {number} config.minQueueSize - 最小队列缓存数量
|
||||||
* @returns {Object} TTS相关方法和状态
|
* @returns {Object} TTS相关方法和状态
|
||||||
*/
|
*/
|
||||||
export const useAudioSpeak = (config = {}) => {
|
export const useAudioSpeak = (config = {}) => {
|
||||||
const {
|
const {
|
||||||
// apiUrl = 'http://39.98.44.136:19527/synthesize',
|
apiUrl = `${globalConfig.baseUrl}/app/speech/tts`,
|
||||||
apiUrl = `${globalConfig.baseUrl}/app/synthesize`,
|
maxSegmentLength = 30,
|
||||||
maxSegmentLength = 30
|
minQueueSize = 3 // 最小队列缓存数量
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
@@ -31,16 +32,14 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
// 音频相关
|
// 音频相关
|
||||||
let audioContext = null
|
let audioContext = null
|
||||||
let audioSource = null
|
let audioSource = null
|
||||||
let audioQueue = [] // 播放队列 [{blob, segmentIndex, text, isMerged?}, ...]
|
let audioQueue = [] // 播放队列 [{blob, segmentIndex, text, isProcessing?}, ...]
|
||||||
let isPlayingQueue = false
|
let isPlayingQueue = false
|
||||||
let isCancelled = false
|
let isCancelled = false
|
||||||
let segments = []
|
let segments = []
|
||||||
let allRequestsCompleted = false
|
|
||||||
let pendingMergeSegments = [] // 存储所有片段的原始数据 [{arrayBuffer, segmentIndex}]
|
|
||||||
let firstSegmentHeader = null
|
|
||||||
let lastPlayedIndex = -1
|
let lastPlayedIndex = -1
|
||||||
let currentPlayingIndex = -1 // 当前正在播放的片段索引
|
let currentPlayingIndex = -1 // 当前正在播放的片段索引
|
||||||
let isInterrupted = false // 是否被中断
|
let isInterrupted = false // 是否被中断
|
||||||
|
let segmentRequests = new Map() // 存储正在进行中的请求 {index: promise}
|
||||||
|
|
||||||
// 按标点分割文本
|
// 按标点分割文本
|
||||||
const splitTextByPunctuation = (text) => {
|
const splitTextByPunctuation = (text) => {
|
||||||
@@ -91,26 +90,6 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
return finalSegments.filter(seg => seg && seg.trim())
|
return finalSegments.filter(seg => seg && seg.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测WAV头部大小
|
|
||||||
const detectWavHeaderSize = (arrayBuffer) => {
|
|
||||||
try {
|
|
||||||
const header = new Uint8Array(arrayBuffer.slice(0, 100))
|
|
||||||
|
|
||||||
if (header[0] === 0x52 && header[1] === 0x49 && header[2] === 0x46 && header[3] === 0x46) {
|
|
||||||
for (let i = 36; i < 60; i++) {
|
|
||||||
if (header[i] === 0x64 && header[i+1] === 0x61 && header[i+2] === 0x74 && header[i+3] === 0x61) {
|
|
||||||
return i + 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 44
|
|
||||||
} catch (error) {
|
|
||||||
console.error('检测WAV头部大小失败:', error)
|
|
||||||
return 44
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请求音频片段
|
// 请求音频片段
|
||||||
const fetchAudioSegment = async (text, index) => {
|
const fetchAudioSegment = async (text, index) => {
|
||||||
try {
|
try {
|
||||||
@@ -120,19 +99,14 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
if (useUserStore().token) {
|
if (useUserStore().token) {
|
||||||
Authorization = `${useUserStore().token}`
|
Authorization = `${useUserStore().token}`
|
||||||
}
|
}
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: 'POST',
|
const response = await fetch(`${apiUrl}?text=${text}`, {
|
||||||
|
method: 'get',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization':encodeURIComponent(Authorization)
|
'Authorization': encodeURIComponent(Authorization)
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
text: text,
|
|
||||||
speed: 1.0,
|
|
||||||
volume: 1.0,
|
|
||||||
pitch: 1.0,
|
|
||||||
voice_type: 1
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -147,33 +121,75 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
|
|
||||||
console.log(`第${index + 1}段音频获取成功,大小: ${audioBlob.size} 字节`)
|
console.log(`第${index + 1}段音频获取成功,大小: ${audioBlob.size} 字节`)
|
||||||
|
|
||||||
const arrayBuffer = await audioBlob.arrayBuffer()
|
|
||||||
|
|
||||||
// 保存原始数据用于可能的合并
|
|
||||||
pendingMergeSegments.push({
|
|
||||||
arrayBuffer: arrayBuffer,
|
|
||||||
segmentIndex: index
|
|
||||||
})
|
|
||||||
|
|
||||||
// 如果是第一个片段,保存其WAV头部用于合并
|
|
||||||
if (index === 0) {
|
|
||||||
const headerSize = detectWavHeaderSize(arrayBuffer)
|
|
||||||
firstSegmentHeader = new Uint8Array(arrayBuffer.slice(0, headerSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
blob: audioBlob,
|
blob: audioBlob,
|
||||||
segmentIndex: index,
|
segmentIndex: index,
|
||||||
text: text,
|
text: text,
|
||||||
arrayBuffer: arrayBuffer
|
isProcessing: false
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`获取第${index + 1}段音频失败:`, error)
|
console.error(`获取第${index + 1}段音频失败:`, error)
|
||||||
throw error
|
throw error
|
||||||
|
} finally {
|
||||||
|
// 请求完成,从请求映射中移除
|
||||||
|
segmentRequests.delete(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预加载音频片段(填充队列)
|
||||||
|
const preloadAudioSegments = async (startIndex) => {
|
||||||
|
if (isCancelled || isInterrupted) {
|
||||||
|
console.log('播放已中断,停止预加载')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算需要预加载的片段
|
||||||
|
const neededSegments = []
|
||||||
|
for (let i = startIndex; i < Math.min(startIndex + minQueueSize, segments.length); i++) {
|
||||||
|
// 检查是否已经在队列中或正在请求中
|
||||||
|
const isInQueue = audioQueue.some(item => item.segmentIndex === i)
|
||||||
|
const isRequesting = segmentRequests.has(i)
|
||||||
|
|
||||||
|
if (!isInQueue && !isRequesting && i > lastPlayedIndex) {
|
||||||
|
neededSegments.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (neededSegments.length === 0) {
|
||||||
|
console.log('无需预加载,所有需要的片段已在队列或请求中')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`预加载 ${neededSegments.length} 个片段: ${neededSegments.map(i => i + 1).join(', ')}`)
|
||||||
|
|
||||||
|
// 并发请求需要的片段
|
||||||
|
const requests = neededSegments.map(index => {
|
||||||
|
console.log(`开始预加载第${index + 1}个片段`)
|
||||||
|
segmentRequests.set(index, true) // 标记为正在请求
|
||||||
|
|
||||||
|
return fetchAudioSegment(segments[index], index)
|
||||||
|
.then(audioItem => {
|
||||||
|
if (isCancelled || isInterrupted) {
|
||||||
|
console.log('播放已中断,丢弃预加载的片段')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到队列
|
||||||
|
addToQueue(audioItem)
|
||||||
|
console.log(`预加载片段${index + 1}完成,队列长度: ${audioQueue.length}`)
|
||||||
|
|
||||||
|
return audioItem
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`预加载片段${index + 1}失败:`, error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(requests)
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化音频上下文
|
// 初始化音频上下文
|
||||||
const initAudioContext = () => {
|
const initAudioContext = () => {
|
||||||
if (!audioContext || audioContext.state === 'closed') {
|
if (!audioContext || audioContext.state === 'closed') {
|
||||||
@@ -219,6 +235,10 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
audioSource = null
|
audioSource = null
|
||||||
lastPlayedIndex = segmentIndex
|
lastPlayedIndex = segmentIndex
|
||||||
currentPlayingIndex = -1
|
currentPlayingIndex = -1
|
||||||
|
|
||||||
|
// 片段播放完成后,检查是否需要预加载更多
|
||||||
|
checkAndPreloadNextSegments()
|
||||||
|
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,58 +274,22 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并剩余音频片段
|
// 检查并预加载下一个片段
|
||||||
const mergeRemainingSegments = (segmentsToMerge) => {
|
const checkAndPreloadNextSegments = () => {
|
||||||
if (segmentsToMerge.length === 0 || !firstSegmentHeader) {
|
if (isCancelled || isInterrupted) {
|
||||||
console.log('没有待合并的片段或缺少头部信息')
|
return
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 计算当前队列中未播放的片段数量
|
||||||
// 按segmentIndex排序
|
const unplayedInQueue = audioQueue.filter(item => item.segmentIndex > lastPlayedIndex).length
|
||||||
segmentsToMerge.sort((a, b) => a.segmentIndex - b.segmentIndex)
|
|
||||||
|
console.log(`队列检查: 已播放=${lastPlayedIndex + 1}, 队列中未播放=${unplayedInQueue}, 最小队列要求=${minQueueSize}`)
|
||||||
console.log(`开始合并${segmentsToMerge.length}个剩余片段`)
|
|
||||||
|
// 如果队列中的未播放片段少于最小要求,预加载更多
|
||||||
// 计算总数据大小
|
if (unplayedInQueue < minQueueSize && lastPlayedIndex < segments.length - 1) {
|
||||||
let totalAudioDataSize = 0
|
const nextIndex = Math.max(lastPlayedIndex + 1, 0)
|
||||||
for (const segment of segmentsToMerge) {
|
console.log(`队列不足,开始预加载从第${nextIndex + 1}个片段开始`)
|
||||||
const headerSize = detectWavHeaderSize(segment.arrayBuffer)
|
preloadAudioSegments(nextIndex)
|
||||||
totalAudioDataSize += segment.arrayBuffer.byteLength - headerSize
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`剩余音频数据总大小: ${totalAudioDataSize}字节`)
|
|
||||||
|
|
||||||
// 创建合并后的数组
|
|
||||||
const headerSize = firstSegmentHeader.length
|
|
||||||
const totalSize = headerSize + totalAudioDataSize
|
|
||||||
const mergedArray = new Uint8Array(totalSize)
|
|
||||||
|
|
||||||
// 设置头部
|
|
||||||
mergedArray.set(firstSegmentHeader, 0)
|
|
||||||
|
|
||||||
// 更新头部中的data大小
|
|
||||||
const view = new DataView(mergedArray.buffer)
|
|
||||||
view.setUint32(40, totalAudioDataSize, true)
|
|
||||||
view.setUint32(4, 36 + totalAudioDataSize, true)
|
|
||||||
|
|
||||||
// 合并所有音频数据
|
|
||||||
let offset = headerSize
|
|
||||||
for (const segment of segmentsToMerge) {
|
|
||||||
const segmentHeaderSize = detectWavHeaderSize(segment.arrayBuffer)
|
|
||||||
const segmentData = new Uint8Array(segment.arrayBuffer.slice(segmentHeaderSize))
|
|
||||||
mergedArray.set(segmentData, offset)
|
|
||||||
offset += segmentData.length
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`音频合并完成,总大小: ${mergedArray.length}字节`)
|
|
||||||
|
|
||||||
// 创建Blob
|
|
||||||
return new Blob([mergedArray], { type: 'audio/wav' })
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('合并音频片段失败:', error)
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,10 +303,21 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
while (audioQueue.length > 0 && !isCancelled && !isInterrupted) {
|
while (audioQueue.length > 0 && !isCancelled && !isInterrupted) {
|
||||||
const audioItem = audioQueue[0]
|
// 找到下一个应该播放的片段(按segmentIndex顺序)
|
||||||
|
const nextItemIndex = audioQueue.findIndex(item => item.segmentIndex === lastPlayedIndex + 1)
|
||||||
|
|
||||||
|
if (nextItemIndex === -1) {
|
||||||
|
// 没有找到下一个片段,等待一下再检查
|
||||||
|
console.log(`等待下一个片段(需要${lastPlayedIndex + 2}),当前队列:`,
|
||||||
|
audioQueue.map(item => item.segmentIndex + 1))
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioItem = audioQueue[nextItemIndex]
|
||||||
currentSegmentIndex.value = audioItem.segmentIndex
|
currentSegmentIndex.value = audioItem.segmentIndex
|
||||||
|
|
||||||
// 播放第一个音频
|
// 播放音频
|
||||||
console.log(`准备播放第${audioItem.segmentIndex + 1}个片段: "${audioItem.text}"`)
|
console.log(`准备播放第${audioItem.segmentIndex + 1}个片段: "${audioItem.text}"`)
|
||||||
await decodeAndPlayBlob(audioItem.blob, audioItem.segmentIndex)
|
await decodeAndPlayBlob(audioItem.blob, audioItem.segmentIndex)
|
||||||
|
|
||||||
@@ -331,8 +326,8 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放完成后移除
|
// 播放完成后从队列中移除
|
||||||
audioQueue.shift()
|
audioQueue.splice(nextItemIndex, 1)
|
||||||
console.log(`片段${audioItem.segmentIndex + 1}播放完成,队列剩余: ${audioQueue.length}`)
|
console.log(`片段${audioItem.segmentIndex + 1}播放完成,队列剩余: ${audioQueue.length}`)
|
||||||
|
|
||||||
// 更新进度
|
// 更新进度
|
||||||
@@ -343,9 +338,11 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否所有片段都播放完成
|
// 检查是否所有片段都播放完成
|
||||||
if (audioQueue.length === 0 && lastPlayedIndex === totalSegments.value - 1) {
|
if (lastPlayedIndex === totalSegments.value - 1) {
|
||||||
console.log('所有音频片段播放完成')
|
console.log('所有音频片段播放完成')
|
||||||
progress.value = 100
|
progress.value = 100
|
||||||
|
isSpeaking.value = false
|
||||||
|
isPaused.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -363,6 +360,19 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果已经播放过或正在播放,不添加
|
||||||
|
if (audioItem.segmentIndex <= lastPlayedIndex) {
|
||||||
|
console.log(`片段${audioItem.segmentIndex + 1}已经播放过,跳过添加`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经在队列中
|
||||||
|
const exists = audioQueue.some(item => item.segmentIndex === audioItem.segmentIndex)
|
||||||
|
if (exists) {
|
||||||
|
console.log(`片段${audioItem.segmentIndex + 1}已经在队列中,跳过添加`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 按segmentIndex插入到正确位置
|
// 按segmentIndex插入到正确位置
|
||||||
let insertIndex = 0
|
let insertIndex = 0
|
||||||
for (let i = audioQueue.length - 1; i >= 0; i--) {
|
for (let i = audioQueue.length - 1; i >= 0; i--) {
|
||||||
@@ -373,99 +383,14 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
audioQueue.splice(insertIndex, 0, audioItem)
|
audioQueue.splice(insertIndex, 0, audioItem)
|
||||||
console.log(`音频片段${audioItem.segmentIndex + 1}已添加到队列,队列长度: ${audioQueue.length}`)
|
console.log(`音频片段${audioItem.segmentIndex + 1}已添加到队列位置${insertIndex},队列长度: ${audioQueue.length}`)
|
||||||
|
|
||||||
// 如果队列中有音频且当前没有在播放,开始播放
|
// 如果队列中有音频且当前没有在播放,开始播放
|
||||||
if (!isPlayingQueue && audioQueue.length === 1) {
|
if (!isPlayingQueue && audioQueue.some(item => item.segmentIndex === 0)) {
|
||||||
playFromQueue()
|
playFromQueue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试合并剩余片段
|
|
||||||
const tryMergeRemainingSegments = () => {
|
|
||||||
if (!allRequestsCompleted) {
|
|
||||||
console.log('合并检查: 请求未完成,跳过')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取真正未播放的片段(不包括已经播放的和正在播放的)
|
|
||||||
const trulyUnplayedSegments = audioQueue.filter(item => {
|
|
||||||
// 排除已经播放完成的
|
|
||||||
if (item.segmentIndex <= lastPlayedIndex) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 排除当前正在播放的
|
|
||||||
if (currentPlayingIndex !== -1 && item.segmentIndex === currentPlayingIndex) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
const shouldMerge = trulyUnplayedSegments.length > 1
|
|
||||||
|
|
||||||
console.log(`🔀合并检查: 已播放到=${lastPlayedIndex}, 正在播放=${currentPlayingIndex}, 真正未播放片段=${trulyUnplayedSegments.length}, 应该合并=${shouldMerge}`)
|
|
||||||
|
|
||||||
if (!shouldMerge) {
|
|
||||||
console.log('不符合合并条件(真正未播放片段数量 <= 1)')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✔️符合合并条件,开始合并剩余片段')
|
|
||||||
|
|
||||||
// 获取这些片段的原始数据
|
|
||||||
const segmentsToMergeData = []
|
|
||||||
for (const item of trulyUnplayedSegments) {
|
|
||||||
const segmentData = pendingMergeSegments.find(s => s.segmentIndex === item.segmentIndex)
|
|
||||||
if (segmentData) {
|
|
||||||
segmentsToMergeData.push(segmentData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segmentsToMergeData.length === 0) {
|
|
||||||
console.log('没有找到待合并的原始数据')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并这些片段
|
|
||||||
const mergedBlob = mergeRemainingSegments(segmentsToMergeData)
|
|
||||||
|
|
||||||
if (mergedBlob) {
|
|
||||||
// 从audioQueue中移除这些将被合并的片段
|
|
||||||
const segmentIndicesToRemove = trulyUnplayedSegments.map(s => s.segmentIndex)
|
|
||||||
|
|
||||||
for (let i = audioQueue.length - 1; i >= 0; i--) {
|
|
||||||
if (segmentIndicesToRemove.includes(audioQueue[i].segmentIndex)) {
|
|
||||||
audioQueue.splice(i, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将合并后的音频添加到队列的合适位置
|
|
||||||
const firstSegmentIndex = Math.min(...segmentIndicesToRemove)
|
|
||||||
const mergedText = trulyUnplayedSegments.map(s => s.text).join(' ')
|
|
||||||
|
|
||||||
const mergedAudioItem = {
|
|
||||||
blob: mergedBlob,
|
|
||||||
segmentIndex: firstSegmentIndex,
|
|
||||||
text: mergedText,
|
|
||||||
isMerged: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 插入到正确位置(按segmentIndex)
|
|
||||||
let insertIndex = 0
|
|
||||||
for (let i = audioQueue.length - 1; i >= 0; i--) {
|
|
||||||
if (audioQueue[i].segmentIndex < firstSegmentIndex) {
|
|
||||||
insertIndex = i + 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audioQueue.splice(insertIndex, 0, mergedAudioItem)
|
|
||||||
console.log(`合并后的音频已添加到队列位置${insertIndex},包含${trulyUnplayedSegments.length}个原始片段,队列长度: ${audioQueue.length}`)
|
|
||||||
} else {
|
|
||||||
console.log('合并失败,保持原始片段')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文本提取工具函数
|
// 文本提取工具函数
|
||||||
function extractSpeechText(markdown) {
|
function extractSpeechText(markdown) {
|
||||||
if (!markdown || markdown.indexOf('job-json') === -1) {
|
if (!markdown || markdown.indexOf('job-json') === -1) {
|
||||||
@@ -499,16 +424,16 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
const jobTexts = jobs.map((job, index) => {
|
const jobTexts = jobs.map((job, index) => {
|
||||||
// 处理薪资格式
|
// 处理薪资格式
|
||||||
let salaryText = job.salary;
|
let salaryText = job.salary;
|
||||||
if (salaryText) {
|
if (salaryText) {
|
||||||
// 匹配 "XXXXX-XXXXX元/月" 格式
|
// 匹配 "XXXXX-XXXXX元/月" 格式
|
||||||
const rangeMatch = salaryText.match(/(\d+)-(\d+)元\/月/);
|
const rangeMatch = salaryText.match(/(\d+)-(\d+)元\/月/);
|
||||||
if (rangeMatch) {
|
if (rangeMatch) {
|
||||||
const minSalary = parseInt(rangeMatch[1], 10);
|
const minSalary = parseInt(rangeMatch[1], 10);
|
||||||
const maxSalary = parseInt(rangeMatch[2], 10);
|
const maxSalary = parseInt(rangeMatch[2], 10);
|
||||||
|
|
||||||
// 转换为千位单位
|
// 转换为千位单位
|
||||||
const minK = Math.round(minSalary / 1000);
|
const minK = Math.round(minSalary / 1000);
|
||||||
const maxK = Math.round(maxSalary / 1000);
|
const maxK = Math.round(maxSalary / 1000);
|
||||||
|
|
||||||
salaryText = `${minK}千到${maxK}千每月`;
|
salaryText = `${minK}千到${maxK}千每月`;
|
||||||
}
|
}
|
||||||
@@ -547,13 +472,13 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
console.log('音频上下文已关闭')
|
console.log('音频上下文已关闭')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取消所有进行中的请求
|
||||||
|
segmentRequests.clear()
|
||||||
|
|
||||||
audioContext = null
|
audioContext = null
|
||||||
audioQueue = []
|
audioQueue = []
|
||||||
segments = []
|
segments = []
|
||||||
isPlayingQueue = false
|
isPlayingQueue = false
|
||||||
allRequestsCompleted = false
|
|
||||||
pendingMergeSegments = []
|
|
||||||
firstSegmentHeader = null
|
|
||||||
lastPlayedIndex = -1
|
lastPlayedIndex = -1
|
||||||
currentPlayingIndex = -1
|
currentPlayingIndex = -1
|
||||||
|
|
||||||
@@ -580,6 +505,9 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
audioSource = null
|
audioSource = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取消所有进行中的请求
|
||||||
|
segmentRequests.clear()
|
||||||
|
|
||||||
audioQueue = []
|
audioQueue = []
|
||||||
isPlayingQueue = false
|
isPlayingQueue = false
|
||||||
currentPlayingIndex = -1
|
currentPlayingIndex = -1
|
||||||
@@ -617,9 +545,7 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
isSpeaking.value = true
|
isSpeaking.value = true
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
audioQueue = []
|
audioQueue = []
|
||||||
allRequestsCompleted = false
|
segmentRequests.clear()
|
||||||
pendingMergeSegments = []
|
|
||||||
firstSegmentHeader = null
|
|
||||||
lastPlayedIndex = -1
|
lastPlayedIndex = -1
|
||||||
currentPlayingIndex = -1
|
currentPlayingIndex = -1
|
||||||
|
|
||||||
@@ -640,59 +566,39 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
// 初始化音频上下文
|
// 初始化音频上下文
|
||||||
initAudioContext()
|
initAudioContext()
|
||||||
|
|
||||||
// 1. 串行请求所有音频片段
|
// 1. 初始预加载前N个片段(N = minQueueSize)
|
||||||
for (let i = 0; i < segments.length; i++) {
|
console.log(`初始预加载前${Math.min(minQueueSize, segments.length)}个片段`)
|
||||||
if (isCancelled || isInterrupted) {
|
await preloadAudioSegments(0)
|
||||||
console.log('播放被取消或中断,停止请求')
|
|
||||||
break
|
if (isCancelled || isInterrupted) {
|
||||||
}
|
console.log('播放被取消或中断')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 开始播放
|
||||||
|
console.log('开始播放音频队列')
|
||||||
|
playFromQueue()
|
||||||
|
|
||||||
|
// 3. 等待播放完成或中断
|
||||||
|
while (lastPlayedIndex < segments.length - 1 && !isCancelled && !isInterrupted) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
console.log(`串行请求第${i + 1}/${segments.length}个片段`)
|
// 更新加载状态
|
||||||
|
if (isLoading.value && audioQueue.length > 0) {
|
||||||
// 更新进度(请求进度)
|
|
||||||
progress.value = Math.floor(((i + 1) / segments.length) * 50)
|
|
||||||
|
|
||||||
// 请求音频片段
|
|
||||||
const audioItem = await fetchAudioSegment(segments[i], i)
|
|
||||||
|
|
||||||
if (isCancelled || isInterrupted) {
|
|
||||||
console.log('播放被取消或中断,停止添加队列')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加到播放队列
|
|
||||||
addToQueue({
|
|
||||||
blob: audioItem.blob,
|
|
||||||
segmentIndex: i,
|
|
||||||
text: segments[i]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 如果是第一个片段,取消loading状态
|
|
||||||
if (i === 0 && isLoading.value) {
|
|
||||||
console.log('第一个音频片段已就绪,开始播放')
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCancelled || isInterrupted) {
|
if (isCancelled || isInterrupted) {
|
||||||
console.log('播放被取消或中断,退出播放')
|
console.log('播放被取消或中断')
|
||||||
return
|
} else {
|
||||||
|
console.log('音频播放完成')
|
||||||
|
isSpeaking.value = false
|
||||||
|
isPaused.value = false
|
||||||
|
isLoading.value = false
|
||||||
|
progress.value = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 所有请求完成
|
|
||||||
console.log('所有音频片段请求完成')
|
|
||||||
allRequestsCompleted = true
|
|
||||||
|
|
||||||
// 3. 立即检查是否可以合并剩余片段
|
|
||||||
tryMergeRemainingSegments()
|
|
||||||
|
|
||||||
// 4. 等待所有音频播放完成
|
|
||||||
while (audioQueue.length > 0 && !isCancelled && !isInterrupted) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('音频播放完成')
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('语音播报失败:', error)
|
console.error('语音播报失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -700,12 +606,6 @@ export const useAudioSpeak = (config = {}) => {
|
|||||||
if (isCancelled || isInterrupted) {
|
if (isCancelled || isInterrupted) {
|
||||||
console.log('播放被取消或中断,进行清理')
|
console.log('播放被取消或中断,进行清理')
|
||||||
cleanup()
|
cleanup()
|
||||||
} else {
|
|
||||||
console.log('播放正常完成')
|
|
||||||
isSpeaking.value = false
|
|
||||||
isPaused.value = false
|
|
||||||
isLoading.value = false
|
|
||||||
progress.value = 100
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user