/**
|
|
* 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;
|