feat: 功能
This commit is contained in:
@@ -7,72 +7,97 @@ import { message } from 'ant-design-vue'
|
||||
import { VoiceService } from '@/api/voice'
|
||||
import { normalizeProviderType, VOICE_PROVIDER_TYPES } from '@/config/voiceConfig'
|
||||
|
||||
// 兼容旧代码的导出
|
||||
// ========== 常量 ==========
|
||||
|
||||
/** 兼容旧代码的导出 */
|
||||
const TTS_PROVIDERS = VOICE_PROVIDER_TYPES
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
apiEndpoint: '/api/tik/voice/tts',
|
||||
audioFormat: 'mp3',
|
||||
supportedFormats: ['mp3', 'wav']
|
||||
/** 默认配置 */
|
||||
const DEFAULT_CONFIG = {
|
||||
apiEndpoint: '/api/tik/voice/tts',
|
||||
audioFormat: 'mp3',
|
||||
supportedFormats: ['mp3', 'wav']
|
||||
}
|
||||
|
||||
/** 最大预览缓存数量 */
|
||||
const MAX_PREVIEW_CACHE_SIZE = 50
|
||||
|
||||
// ========== 类型定义 ==========
|
||||
|
||||
/**
|
||||
* @typedef {Object} AudioData
|
||||
* @property {Blob} blob - 音频 Blob
|
||||
* @property {string} objectUrl - 对象 URL
|
||||
* @property {string} format - 音频格式
|
||||
*/
|
||||
|
||||
export function useTTS(options = {}) {
|
||||
const {
|
||||
provider = VOICE_PROVIDER_TYPES.SILICONFLOW,
|
||||
customConfig = {}
|
||||
} = options
|
||||
|
||||
// 状态管理
|
||||
// 状态管理(移到函数内部,避免模块级状态污染)
|
||||
const previewAudioCache = new Map()
|
||||
const MAX_PREVIEW_CACHE_SIZE = 50
|
||||
const previewLoadingVoiceId = ref(null)
|
||||
const playingPreviewVoiceId = ref(null)
|
||||
const ttsText = ref('')
|
||||
const speechRate = ref(1.0)
|
||||
|
||||
// 音频实例
|
||||
// 音频实例(移到函数内部)
|
||||
let previewAudio = null
|
||||
let previewObjectUrl = ''
|
||||
|
||||
// 获取当前供应商配置
|
||||
const getProviderConfig = () => {
|
||||
// ========== 辅助函数 ==========
|
||||
|
||||
function getProviderConfig() {
|
||||
return DEFAULT_CONFIG
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放音频预览
|
||||
* @param {string} url 音频URL
|
||||
* @param {Object} options 播放选项
|
||||
* @param {string} url - 音频 URL
|
||||
* @param {Object} playOptions - 播放选项
|
||||
* @param {boolean} [playOptions.revokeOnEnd=false] - 播放结束后是否释放 URL
|
||||
* @param {Function} [playOptions.onEnded] - 播放结束回调
|
||||
*/
|
||||
const playAudioPreview = (url, options = {}) => {
|
||||
if (!url) return message.warning('暂无可试听的音频')
|
||||
function playAudioPreview(url, playOptions = {}) {
|
||||
if (!url) {
|
||||
message.warning('暂无可试听的音频')
|
||||
return
|
||||
}
|
||||
|
||||
// 停止当前播放
|
||||
try {
|
||||
previewAudio?.pause?.()
|
||||
previewAudio = null
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// 忽略停止播放的错误
|
||||
}
|
||||
|
||||
const audio = new Audio(url)
|
||||
const cleanup = () => {
|
||||
if (options.revokeOnEnd && url.startsWith('blob:')) {
|
||||
|
||||
function cleanup() {
|
||||
if (playOptions.revokeOnEnd && url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(url)
|
||||
previewObjectUrl === url && (previewObjectUrl = '')
|
||||
if (previewObjectUrl === url) {
|
||||
previewObjectUrl = ''
|
||||
}
|
||||
}
|
||||
previewAudio = null
|
||||
options.onEnded && options.onEnded()
|
||||
playOptions.onEnded?.()
|
||||
}
|
||||
|
||||
audio.play()
|
||||
.then(() => {
|
||||
.then(function() {
|
||||
previewAudio = audio
|
||||
audio.onended = cleanup
|
||||
audio.onerror = () => {
|
||||
audio.onerror = function() {
|
||||
cleanup()
|
||||
message.error('播放失败')
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(function() {
|
||||
cleanup()
|
||||
message.error('播放失败')
|
||||
})
|
||||
@@ -80,10 +105,10 @@ export function useTTS(options = {}) {
|
||||
|
||||
/**
|
||||
* 生成预览缓存键
|
||||
* @param {Object} voice 音色对象
|
||||
* @param {Object} voice - 音色对象
|
||||
* @returns {string} 缓存键
|
||||
*/
|
||||
const generatePreviewCacheKey = (voice) => {
|
||||
function generatePreviewCacheKey(voice) {
|
||||
const voiceId = voice.voiceId || voice.rawId || voice.id
|
||||
const text = ttsText.value.trim()
|
||||
const rate = speechRate.value
|
||||
@@ -92,12 +117,12 @@ export function useTTS(options = {}) {
|
||||
|
||||
/**
|
||||
* 解码并缓存Base64音频
|
||||
* @param {string} audioBase64 Base64编码的音频数据
|
||||
* @param {string} format 音频格式
|
||||
* @param {string} cacheKey 缓存键
|
||||
* @param {string} audioBase64 - Base64 编码的音频数据
|
||||
* @param {string} [format='mp3'] - 音频格式
|
||||
* @param {string} cacheKey - 缓存键
|
||||
* @returns {Promise<Object>} 音频数据
|
||||
*/
|
||||
const decodeAndCacheBase64 = async (audioBase64, format = 'mp3', cacheKey) => {
|
||||
async function decodeAndCacheBase64(audioBase64, format = 'mp3', cacheKey) {
|
||||
const byteCharacters = window.atob(audioBase64)
|
||||
const byteNumbers = new Uint8Array(byteCharacters.length)
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
@@ -123,10 +148,10 @@ export function useTTS(options = {}) {
|
||||
|
||||
/**
|
||||
* 播放缓存的音频
|
||||
* @param {Object} audioData 音频数据
|
||||
* @param {Function} onEnded 播放结束回调
|
||||
* @param {Object} audioData - 音频数据
|
||||
* @param {Function} onEnded - 播放结束回调
|
||||
*/
|
||||
const playCachedAudio = (audioData, onEnded) => {
|
||||
function playCachedAudio(audioData, onEnded) {
|
||||
if (previewObjectUrl && previewObjectUrl !== audioData.objectUrl) {
|
||||
URL.revokeObjectURL(previewObjectUrl)
|
||||
}
|
||||
@@ -134,7 +159,10 @@ export function useTTS(options = {}) {
|
||||
|
||||
playAudioPreview(previewObjectUrl, {
|
||||
revokeOnEnd: false,
|
||||
onEnded: () => {
|
||||
onEnded: function() {
|
||||
if (audioData.objectUrl?.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(audioData.objectUrl)
|
||||
}
|
||||
onEnded && onEnded()
|
||||
}
|
||||
})
|
||||
@@ -143,17 +171,17 @@ export function useTTS(options = {}) {
|
||||
/**
|
||||
* 重置预览状态
|
||||
*/
|
||||
const resetPreviewState = () => {
|
||||
function resetPreviewState() {
|
||||
previewLoadingVoiceId.value = null
|
||||
playingPreviewVoiceId.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取ID从字符串
|
||||
* @param {string} idStr 包含前缀的ID字符串
|
||||
* @param {string} idStr - 包含前缀的ID字符串
|
||||
* @returns {number|null} 提取的ID
|
||||
*/
|
||||
const extractIdFromString = (idStr) => {
|
||||
function extractIdFromString(idStr) {
|
||||
if (typeof idStr !== 'string' || !idStr.startsWith('user-')) return null
|
||||
const extractedId = parseInt(idStr.replace('user-', ''))
|
||||
return Number.isNaN(extractedId) ? null : extractedId
|
||||
@@ -161,10 +189,10 @@ export function useTTS(options = {}) {
|
||||
|
||||
/**
|
||||
* 构建预览参数
|
||||
* @param {Object} voice 音色对象
|
||||
* @param {Object} voice - 音色对象
|
||||
* @returns {Object|null} 预览参数
|
||||
*/
|
||||
const buildPreviewParams = (voice) => {
|
||||
function buildPreviewParams(voice) {
|
||||
const configId = voice.rawId || extractIdFromString(voice.id)
|
||||
if (!configId) {
|
||||
message.error('配音配置无效')
|
||||
@@ -184,11 +212,11 @@ export function useTTS(options = {}) {
|
||||
|
||||
/**
|
||||
* 播放音色试听
|
||||
* @param {Object} voice 音色对象
|
||||
* @param {Function} onSuccess 成功回调
|
||||
* @param {Function} onError 错误回调
|
||||
* @param {Object} voice - 音色对象
|
||||
* @param {Function} onSuccess - 成功回调
|
||||
* @param {Function} onError - 错误回调
|
||||
*/
|
||||
const playVoiceSample = async (voice, onSuccess, onError) => {
|
||||
async function playVoiceSample(voice, onSuccess, onError) {
|
||||
if (!voice) return
|
||||
if (previewLoadingVoiceId.value === voice.id || playingPreviewVoiceId.value === voice.id) {
|
||||
return
|
||||
@@ -197,7 +225,8 @@ export function useTTS(options = {}) {
|
||||
try {
|
||||
previewAudio?.pause?.()
|
||||
previewAudio = null
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,21 +254,30 @@ export function useTTS(options = {}) {
|
||||
if (res.code !== 0) {
|
||||
message.error(res.msg || '试听失败')
|
||||
resetPreviewState()
|
||||
onError && onError(new Error(res.msg || '试听失败'))
|
||||
onError?.(new Error(res.msg || '试听失败'))
|
||||
return
|
||||
}
|
||||
|
||||
if (res.data?.audioUrl) {
|
||||
playAudioPreview(res.data.audioUrl, { onEnded: resetPreviewState })
|
||||
onSuccess && onSuccess(res.data)
|
||||
resetPreviewState()
|
||||
playAudioPreview(res.data.audioUrl, {
|
||||
revokeOnEnd: true,
|
||||
onEnded: function() {
|
||||
URL.revokeObjectURL(res.data.audioUrl)
|
||||
}
|
||||
})
|
||||
onSuccess?.(res.data)
|
||||
} else if (res.data?.audioBase64) {
|
||||
const audioData = await decodeAndCacheBase64(res.data.audioBase64, res.data.format, cacheKey)
|
||||
playCachedAudio(audioData, resetPreviewState)
|
||||
onSuccess && onSuccess(audioData)
|
||||
resetPreviewState()
|
||||
playCachedAudio(audioData, function() {
|
||||
URL.revokeObjectURL(audioData.objectUrl)
|
||||
})
|
||||
onSuccess?.(audioData)
|
||||
} else {
|
||||
message.error('试听失败')
|
||||
resetPreviewState()
|
||||
onError && onError(new Error('未收到音频数据'))
|
||||
onError?.(new Error('未收到音频数据'))
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('试听失败')
|
||||
@@ -250,10 +288,10 @@ export function useTTS(options = {}) {
|
||||
|
||||
/**
|
||||
* TTS文本转语音
|
||||
* @param {Object} params TTS参数
|
||||
* @returns {Promise<Object>} TTS结果
|
||||
* @param {Object} params - TTS 参数
|
||||
* @returns {Promise<Object>} TTS 结果
|
||||
*/
|
||||
const synthesize = async (params) => {
|
||||
async function synthesize(params) {
|
||||
const providerConfig = getProviderConfig()
|
||||
|
||||
const ttsParams = {
|
||||
@@ -269,25 +307,25 @@ export function useTTS(options = {}) {
|
||||
|
||||
/**
|
||||
* 设置文本
|
||||
* @param {string} text 要设置的文本
|
||||
* @param {string} text - 要设置的文本
|
||||
*/
|
||||
const setText = (text) => {
|
||||
function setText(text) {
|
||||
ttsText.value = text
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置语速
|
||||
* @param {number} rate 语速倍率
|
||||
* @param {number} rate - 语速倍率
|
||||
*/
|
||||
const setSpeechRate = (rate) => {
|
||||
function setSpeechRate(rate) {
|
||||
speechRate.value = rate
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除音频缓存
|
||||
*/
|
||||
const clearAudioCache = () => {
|
||||
previewAudioCache.forEach((audioData) => {
|
||||
function clearAudioCache() {
|
||||
previewAudioCache.forEach(function(audioData) {
|
||||
URL.revokeObjectURL(audioData.objectUrl)
|
||||
})
|
||||
previewAudioCache.clear()
|
||||
@@ -296,14 +334,17 @@ export function useTTS(options = {}) {
|
||||
/**
|
||||
* 停止当前播放
|
||||
*/
|
||||
const stopCurrentPlayback = () => {
|
||||
function stopCurrentPlayback() {
|
||||
try {
|
||||
previewAudio?.pause?.()
|
||||
previewAudio = null
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 返回接口 ==========
|
||||
|
||||
return {
|
||||
// 状态
|
||||
previewLoadingVoiceId,
|
||||
|
||||
Reference in New Issue
Block a user