/** * UniApp TTS服务类 * 封装文字转语音功能,提供简单易用的API */ import { API_CONFIG, TTS_CONFIG, UTILS, ERROR_CODES, ERROR_MESSAGES } from './uniapp-tts-config.js'; class TTSService { constructor() { this.audioContext = null; this.isPlaying = false; this.isConverting = false; this.voiceList = []; this.currentAudioUrl = ''; } /** * 初始化TTS服务 * @param {Object} options 配置选项 */ async init(options = {}) { try { // 加载音色列表 await this.loadVoiceList(); // 初始化音频上下文 this.initAudioContext(); console.log('TTS服务初始化成功'); return { success: true }; } catch (error) { console.error('TTS服务初始化失败:', error); return { success: false, error: error.message }; } } /** * 加载音色列表 */ async loadVoiceList() { try { // 先尝试从缓存获取 const cached = this.getCachedVoiceList(); if (cached) { this.voiceList = cached; return cached; } // 从服务器获取 const response = await this.request({ url: API_CONFIG.ENDPOINTS.VOICE_LIST, method: 'GET' }); if (response.success && response.result) { this.voiceList = response.result; this.cacheVoiceList(this.voiceList); return this.voiceList; } else { throw new Error(response.message || '获取音色列表失败'); } } catch (error) { console.error('加载音色列表失败:', error); throw error; } } /** * 文字转语音 * @param {Object} params 转换参数 * @param {string} params.text 文本内容 * @param {number} params.speed 语速 * @param {number} params.voiceType 音色ID * @param {number} params.volume 音量 * @param {string} params.codec 音频格式 * @param {string} params.userId 用户ID */ async textToVoice(params) { if (this.isConverting) { throw new Error('正在转换中,请稍候...'); } // 参数验证 const validation = this.validateParams(params); if (!validation.valid) { throw new Error(validation.message); } this.isConverting = true; const startTime = Date.now(); try { // 构建请求参数 const requestParams = { text: params.text, speed: params.speed || TTS_CONFIG.SPEED.DEFAULT, voiceType: params.voiceType || 0, volume: params.volume || TTS_CONFIG.VOLUME.DEFAULT, codec: params.codec || TTS_CONFIG.CODEC.DEFAULT, userId: params.userId || this.generateUserId() }; // 发起请求 const audioData = await this.requestBinary({ url: API_CONFIG.ENDPOINTS.TEXT_TO_VOICE, method: 'GET', data: requestParams }); if (!audioData || audioData.byteLength === 0) { throw new Error('转换失败,未返回音频数据'); } // 创建音频文件 const audioUrl = await this.createAudioFile(audioData, requestParams.codec); // 计算转换耗时 const convertTime = ((Date.now() - startTime) / 1000).toFixed(2); // 更新当前音频URL this.currentAudioUrl = audioUrl; return { success: true, audioUrl: audioUrl, audioSize: UTILS.formatFileSize(audioData.byteLength), convertTime: convertTime, params: requestParams }; } catch (error) { console.error('文字转语音失败:', error); throw error; } finally { this.isConverting = false; } } /** * 播放音频 * @param {string} audioUrl 音频文件路径(可选,默认使用最后转换的音频) */ async playAudio(audioUrl) { const targetUrl = audioUrl || this.currentAudioUrl; if (!targetUrl) { throw new Error('没有可播放的音频文件'); } if (this.isPlaying) { this.stopAudio(); } return new Promise((resolve, reject) => { try { this.initAudioContext(); this.audioContext.src = targetUrl; this.isPlaying = true; this.audioContext.onPlay(() => { console.log('音频开始播放'); resolve({ success: true, action: 'play_started' }); }); this.audioContext.onEnded(() => { console.log('音频播放结束'); this.isPlaying = false; }); this.audioContext.onError((error) => { console.error('音频播放失败:', error); this.isPlaying = false; reject(new Error('音频播放失败')); }); this.audioContext.play(); } catch (error) { this.isPlaying = false; reject(error); } }); } /** * 停止音频播放 */ stopAudio() { if (this.audioContext && this.isPlaying) { this.audioContext.stop(); this.isPlaying = false; console.log('音频播放已停止'); } } /** * 暂停音频播放 */ pauseAudio() { if (this.audioContext && this.isPlaying) { this.audioContext.pause(); console.log('音频播放已暂停'); } } /** * 获取音色列表 */ getVoiceList() { return this.voiceList; } /** * 根据ID获取音色信息 * @param {number} voiceId 音色ID */ getVoiceById(voiceId) { return this.voiceList.find(voice => voice.id === voiceId); } /** * 获取当前播放状态 */ getPlayStatus() { return { isPlaying: this.isPlaying, isConverting: this.isConverting, currentAudioUrl: this.currentAudioUrl }; } /** * 清理资源 */ destroy() { if (this.audioContext) { this.audioContext.destroy(); this.audioContext = null; } this.isPlaying = false; this.isConverting = false; this.currentAudioUrl = ''; console.log('TTS服务已销毁'); } // ==================== 私有方法 ==================== /** * 初始化音频上下文 */ initAudioContext() { if (this.audioContext) { this.audioContext.destroy(); } // #ifdef MP-WEIXIN this.audioContext = wx.createInnerAudioContext(); // #endif // #ifdef H5 this.audioContext = uni.createInnerAudioContext(); // #endif } /** * 参数验证 */ validateParams(params) { if (!params || typeof params !== 'object') { return { valid: false, message: '参数格式错误' }; } // 验证文本 const textValidation = UTILS.validateText(params.text); if (!textValidation.valid) { return textValidation; } // 验证语速 if (params.speed !== undefined) { const speedValidation = UTILS.validateSpeed(params.speed); if (!speedValidation.valid) { return speedValidation; } } // 验证音量 if (params.volume !== undefined) { const volumeValidation = UTILS.validateVolume(params.volume); if (!volumeValidation.valid) { return volumeValidation; } } return { valid: true }; } /** * 创建音频文件 */ async createAudioFile(arrayBuffer, codec) { return new Promise((resolve, reject) => { const fileName = `tts_${Date.now()}.${codec}`; // #ifdef MP-WEIXIN const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`; wx.getFileSystemManager().writeFile({ filePath: filePath, data: arrayBuffer, success: () => resolve(filePath), fail: (error) => reject(new Error('创建音频文件失败: ' + error.errMsg)) }); // #endif // #ifdef H5 // H5环境下创建Blob URL const blob = new Blob([arrayBuffer], { type: `audio/${codec}` }); const url = URL.createObjectURL(blob); resolve(url); // #endif }); } /** * 生成用户ID */ generateUserId() { // 尝试从存储获取用户ID let userId = uni.getStorageSync('tts_user_id'); if (!userId) { userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); uni.setStorageSync('tts_user_id', userId); } return userId; } /** * 缓存音色列表 */ cacheVoiceList(voiceList) { const cacheData = { data: voiceList, timestamp: Date.now() }; uni.setStorageSync('tts_voice_cache', cacheData); } /** * 获取缓存的音色列表 */ getCachedVoiceList() { try { const cached = uni.getStorageSync('tts_voice_cache'); if (cached && cached.data) { // 检查缓存是否过期(24小时) const expireTime = 24 * 60 * 60 * 1000; if (Date.now() - cached.timestamp < expireTime) { return cached.data; } } } catch (error) { console.error('获取缓存失败:', error); } return null; } /** * 通用请求方法 */ request(options) { return new Promise((resolve, reject) => { uni.request({ url: UTILS.buildApiUrl(options.url), method: options.method || 'GET', data: options.data || {}, timeout: API_CONFIG.TIMEOUT, header: { 'Content-Type': 'application/json', // 如果需要token认证,在这里添加 // 'Authorization': 'Bearer ' + uni.getStorageSync('token') }, success: (res) => { if (res.statusCode === 200) { resolve(res.data); } else { reject(new Error(`HTTP ${res.statusCode}: ${res.data?.message || '请求失败'}`)); } }, fail: (error) => { reject(new Error(ERROR_MESSAGES[ERROR_CODES.NETWORK_ERROR] || error.errMsg)); } }); }); } /** * 二进制数据请求方法 */ requestBinary(options) { return new Promise((resolve, reject) => { uni.request({ url: UTILS.buildApiUrl(options.url), method: options.method || 'GET', data: options.data || {}, responseType: 'arraybuffer', timeout: API_CONFIG.TIMEOUT, header: { // 如果需要token认证,在这里添加 // 'Authorization': 'Bearer ' + uni.getStorageSync('token') }, success: (res) => { if (res.statusCode === 200) { resolve(res.data); } else { reject(new Error(`HTTP ${res.statusCode}: 请求失败`)); } }, fail: (error) => { reject(new Error(ERROR_MESSAGES[ERROR_CODES.NETWORK_ERROR] || error.errMsg)); } }); }); } } // 创建单例实例 const ttsService = new TTSService(); // 导出服务实例和类 export { TTSService, ttsService }; export default ttsService;