前端-胡立永 1 month ago
parent
commit
cd86b70a35
6 changed files with 1 additions and 2150 deletions
  1. +0
    -364
      UniApp微信小程序TTS接口调用说明.md
  2. +1
    -1
      jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
  3. +0
    -243
      uniapp-tts-config.js
  4. +0
    -591
      uniapp-tts-example.vue
  5. +0
    -429
      uniapp-tts-service.js
  6. +0
    -522
      uniapp-tts-simple-example.vue

+ 0
- 364
UniApp微信小程序TTS接口调用说明.md View File

@ -1,364 +0,0 @@
# UniApp微信小程序TTS接口调用说明
本文档详细介绍如何在UniApp微信小程序中调用后端的文字转语音(TTS)接口。
## 📁 文件说明
### 1. uniapp-tts-example.vue
完整的TTS功能页面组件,包含:
- 文本输入界面
- 音色选择功能
- 语速和音量调节
- 音频格式选择
- 音频播放功能
- 完整的错误处理
### 2. uniapp-tts-config.js
TTS功能的配置文件,包含:
- API接口配置
- 参数验证工具
- 错误处理配置
- 通用工具函数
## 🚀 快速开始
### 1. 文件集成
将以下文件复制到你的UniApp项目中:
```
your-project/
├── pages/
│ └── tts/
│ └── index.vue # 复制 uniapp-tts-example.vue 内容
├── utils/
│ └── tts-config.js # 复制 uniapp-tts-config.js 内容
└── pages.json # 添加页面路由配置
```
### 2. 页面路由配置
`pages.json` 中添加TTS页面路由:
```json
{
"pages": [
{
"path": "pages/tts/index",
"style": {
"navigationBarTitleText": "文字转语音",
"navigationBarBackgroundColor": "#007aff",
"navigationBarTextStyle": "white"
}
}
]
}
```
### 3. 配置API地址
修改 `uniapp-tts-config.js` 中的API地址:
```javascript
export const API_CONFIG = {
// 开发环境API地址
DEV_BASE_URL: 'http://localhost:8080', // 修改为你的开发环境地址
// 生产环境API地址
PROD_BASE_URL: 'https://your-domain.com', // 修改为你的生产环境地址
};
```
## 🔧 功能特性
### 1. 音色管理
- 自动加载后端音色列表
- 支持音色选择和切换
- 音色信息缓存
### 2. 参数控制
- **语速调节**:支持-2到6的语速范围
- **音量控制**:支持-10到10的音量范围
- **格式选择**:支持WAV、MP3、PCM格式
### 3. 音频处理
- 二进制音频数据处理
- 本地音频文件创建
- 音频播放控制
- 文件大小显示
### 4. 用户体验
- 实时状态反馈
- 转换进度提示
- 错误信息展示
- 响应式界面设计
## 📋 接口说明
### 1. 获取音色列表
**接口地址:** `GET /appletApi/tts/list`
**响应格式:**
```json
{
"success": true,
"result": [
{
"id": 0,
"name": "云小宁",
"description": "甜美女声"
}
]
}
```
### 2. 文字转语音
**接口地址:** `GET /appletApi/tts/textToVoice`
**请求参数:**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| text | String | 是 | 要转换的文本内容 |
| speed | Float | 否 | 语速,范围[-2,6],默认0 |
| voiceType | Integer | 否 | 音色ID,默认0 |
| volume | Float | 否 | 音量,范围[-10,10],默认0 |
| codec | String | 否 | 音频格式,默认wav |
| userId | String | 否 | 用户ID,用于日志记录 |
**响应格式:** 二进制音频数据
## 💡 使用示例
### 1. 基础调用
```javascript
import { API_CONFIG, UTILS } from '@/utils/tts-config.js';
// 调用TTS接口
async function callTTS() {
try {
const response = await uni.request({
url: UTILS.buildApiUrl(API_CONFIG.ENDPOINTS.TEXT_TO_VOICE),
method: 'GET',
data: {
text: '你好,世界!',
speed: 0,
voiceType: 0,
volume: 0,
codec: 'wav',
userId: 'user123'
},
responseType: 'arraybuffer'
});
if (response.statusCode === 200) {
// 处理音频数据
console.log('TTS调用成功');
}
} catch (error) {
console.error('TTS调用失败:', error);
}
}
```
### 2. 参数验证
```javascript
import { UTILS } from '@/utils/tts-config.js';
// 验证文本
const textValidation = UTILS.validateText('要转换的文本');
if (!textValidation.valid) {
console.error(textValidation.message);
return;
}
// 验证语速
const speedValidation = UTILS.validateSpeed(1);
if (!speedValidation.valid) {
console.error(speedValidation.message);
return;
}
```
### 3. 音频播放
```javascript
// 创建音频上下文
const audioContext = wx.createInnerAudioContext();
audioContext.src = audioFilePath;
// 监听播放事件
audioContext.onPlay(() => {
console.log('开始播放');
});
audioContext.onEnded(() => {
console.log('播放结束');
});
audioContext.onError((error) => {
console.error('播放失败:', error);
});
// 开始播放
audioContext.play();
```
## ⚠️ 注意事项
### 1. 权限配置
`manifest.json` 中配置必要的权限:
```json
{
"mp-weixin": {
"permission": {
"scope.writePhotosAlbum": {
"desc": "保存音频文件到相册"
}
}
}
}
```
### 2. 网络配置
在微信小程序后台配置服务器域名,将你的API域名添加到request合法域名中。
### 3. 文件大小限制
- 微信小程序本地文件存储有限制
- 建议音频文件不超过10MB
- 长文本建议分段处理
### 4. 错误处理
```javascript
// 统一错误处理
function handleError(error, context = '') {
console.error(`${context}错误:`, error);
let message = '操作失败,请重试';
if (error.errMsg) {
if (error.errMsg.includes('network')) {
message = '网络连接失败,请检查网络';
} else if (error.errMsg.includes('timeout')) {
message = '请求超时,请重试';
}
}
uni.showToast({
title: message,
icon: 'error'
});
}
```
## 🔍 调试技巧
### 1. 开启调试日志
```javascript
// 在开发环境开启详细日志
if (process.env.NODE_ENV === 'development') {
console.log('TTS请求参数:', params);
console.log('TTS响应数据:', response);
}
```
### 2. 网络请求监控
```javascript
// 监控请求状态
uni.onNetworkStatusChange((res) => {
console.log('网络状态:', res.isConnected ? '已连接' : '已断开');
console.log('网络类型:', res.networkType);
});
```
### 3. 性能监控
```javascript
// 监控转换耗时
const startTime = Date.now();
// ... TTS调用 ...
const endTime = Date.now();
console.log(`TTS转换耗时: ${(endTime - startTime) / 1000}秒`);
```
## 📈 性能优化
### 1. 音色列表缓存
```javascript
// 缓存音色列表,避免重复请求
const VOICE_CACHE_KEY = 'tts_voice_list';
const CACHE_EXPIRE_TIME = 24 * 60 * 60 * 1000; // 24小时
function getCachedVoiceList() {
const cached = uni.getStorageSync(VOICE_CACHE_KEY);
if (cached && (Date.now() - cached.timestamp) < CACHE_EXPIRE_TIME) {
return cached.data;
}
return null;
}
```
### 2. 音频文件管理
```javascript
// 清理过期的音频文件
function cleanupAudioFiles() {
const fileManager = wx.getFileSystemManager();
// 实现文件清理逻辑
}
```
### 3. 请求防抖
```javascript
// 防止重复请求
let isConverting = false;
async function convertToVoice() {
if (isConverting) {
console.log('正在转换中,请稍候...');
return;
}
isConverting = true;
try {
// TTS转换逻辑
} finally {
isConverting = false;
}
}
```
## 🤝 技术支持
如果在使用过程中遇到问题,请检查:
1. **网络连接**:确保设备网络正常
2. **API地址**:确认API地址配置正确
3. **参数格式**:检查请求参数是否符合要求
4. **权限设置**:确认小程序权限配置正确
5. **后端服务**:确认后端TTS服务正常运行
## 📝 更新日志
### v1.0.0 (2025-01-XX)
- 初始版本发布
- 支持基础TTS功能
- 完整的参数控制
- 音频播放功能
- 错误处理机制
---
**注意:** 本示例基于JeecgBoot 3.8.1框架开发,使用腾讯云TTS服务。在实际使用时,请根据你的具体环境进行相应调整。

+ 1
- 1
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml View File

@ -161,7 +161,7 @@ spring:
#driver-class-name: com.mysql.cj.jdbc.Driver #driver-class-name: com.mysql.cj.jdbc.Driver
#redis 配置 #redis 配置
redis: redis:
database: 1
database: 10
host: 175.178.47.147 host: 175.178.47.147
port: 6379 port: 6379
password: 'Abc.123456' password: 'Abc.123456'


+ 0
- 243
uniapp-tts-config.js View File

@ -1,243 +0,0 @@
/**
* UniApp TTS接口配置文件
* 用于配置文字转语音相关的API接口和参数
*/
// API配置
export const API_CONFIG = {
// 开发环境API地址
DEV_BASE_URL: 'http://localhost:8080',
// 生产环境API地址(请根据实际情况修改)
PROD_BASE_URL: 'https://your-domain.com',
// 接口路径
ENDPOINTS: {
// 获取音色列表
VOICE_LIST: '/appletApi/tts/list',
// 文字转语音
TEXT_TO_VOICE: '/appletApi/tts/textToVoice'
},
// 请求超时时间(毫秒)
TIMEOUT: 30000
};
// TTS参数配置
export const TTS_CONFIG = {
// 语速配置
SPEED: {
MIN: -2,
MAX: 6,
DEFAULT: 0,
OPTIONS: [
{ value: -2, label: '0.6倍速', description: '很慢' },
{ value: -1, label: '0.8倍速', description: '慢' },
{ value: 0, label: '1.0倍速', description: '正常' },
{ value: 1, label: '1.2倍速', description: '快' },
{ value: 2, label: '1.5倍速', description: '很快' },
{ value: 6, label: '2.5倍速', description: '极快' }
]
},
// 音量配置
VOLUME: {
MIN: -10,
MAX: 10,
DEFAULT: 0
},
// 音频格式配置
CODEC: {
OPTIONS: [
{ value: 'wav', label: 'WAV', description: '无损音质,文件较大' },
{ value: 'mp3', label: 'MP3', description: '压缩音质,文件适中' },
{ value: 'pcm', label: 'PCM', description: '原始音频,文件最大' }
],
DEFAULT: 'wav'
},
// 文本限制
TEXT: {
MAX_LENGTH: 500,
MIN_LENGTH: 1
}
};
// 错误码配置
export const ERROR_CODES = {
// 网络错误
NETWORK_ERROR: 'NETWORK_ERROR',
// 参数错误
PARAM_ERROR: 'PARAM_ERROR',
// 服务器错误
SERVER_ERROR: 'SERVER_ERROR',
// 音频播放错误
AUDIO_ERROR: 'AUDIO_ERROR',
// 文件操作错误
FILE_ERROR: 'FILE_ERROR'
};
// 错误消息配置
export const ERROR_MESSAGES = {
[ERROR_CODES.NETWORK_ERROR]: '网络连接失败,请检查网络设置',
[ERROR_CODES.PARAM_ERROR]: '参数错误,请检查输入内容',
[ERROR_CODES.SERVER_ERROR]: '服务器错误,请稍后重试',
[ERROR_CODES.AUDIO_ERROR]: '音频播放失败,请重试',
[ERROR_CODES.FILE_ERROR]: '文件操作失败,请重试'
};
// 工具函数
export const UTILS = {
/**
* 获取当前环境的API基础地址
*/
getBaseUrl() {
// #ifdef MP-WEIXIN
// 微信小程序环境
return process.env.NODE_ENV === 'production' ? API_CONFIG.PROD_BASE_URL : API_CONFIG.DEV_BASE_URL;
// #endif
// #ifdef H5
// H5环境
return process.env.NODE_ENV === 'production' ? API_CONFIG.PROD_BASE_URL : API_CONFIG.DEV_BASE_URL;
// #endif
// #ifdef APP-PLUS
// App环境
return API_CONFIG.PROD_BASE_URL;
// #endif
return API_CONFIG.DEV_BASE_URL;
},
/**
* 构建完整的API地址
* @param {string} endpoint 接口路径
*/
buildApiUrl(endpoint) {
return this.getBaseUrl() + endpoint;
},
/**
* 格式化文件大小
* @param {number} bytes 字节数
*/
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
/**
* 验证文本长度
* @param {string} text 文本内容
*/
validateText(text) {
if (!text || typeof text !== 'string') {
return { valid: false, message: '请输入文本内容' };
}
const trimmedText = text.trim();
if (trimmedText.length < TTS_CONFIG.TEXT.MIN_LENGTH) {
return { valid: false, message: '文本内容不能为空' };
}
if (trimmedText.length > TTS_CONFIG.TEXT.MAX_LENGTH) {
return { valid: false, message: `文本长度不能超过${TTS_CONFIG.TEXT.MAX_LENGTH}个字符` };
}
return { valid: true, text: trimmedText };
},
/**
* 验证语速参数
* @param {number} speed 语速值
*/
validateSpeed(speed) {
if (typeof speed !== 'number') {
return { valid: false, message: '语速参数必须为数字' };
}
if (speed < TTS_CONFIG.SPEED.MIN || speed > TTS_CONFIG.SPEED.MAX) {
return { valid: false, message: `语速范围为${TTS_CONFIG.SPEED.MIN}${TTS_CONFIG.SPEED.MAX}` };
}
return { valid: true, speed };
},
/**
* 验证音量参数
* @param {number} volume 音量值
*/
validateVolume(volume) {
if (typeof volume !== 'number') {
return { valid: false, message: '音量参数必须为数字' };
}
if (volume < TTS_CONFIG.VOLUME.MIN || volume > TTS_CONFIG.VOLUME.MAX) {
return { valid: false, message: `音量范围为${TTS_CONFIG.VOLUME.MIN}${TTS_CONFIG.VOLUME.MAX}` };
}
return { valid: true, volume };
},
/**
* 获取语速描述
* @param {number} speed 语速值
*/
getSpeedDescription(speed) {
const option = TTS_CONFIG.SPEED.OPTIONS.find(item => item.value === speed);
return option ? option.description : '未知';
},
/**
* 获取音频格式描述
* @param {string} codec 音频格式
*/
getCodecDescription(codec) {
const option = TTS_CONFIG.CODEC.OPTIONS.find(item => item.value === codec);
return option ? option.description : '未知格式';
},
/**
* 生成唯一ID
*/
generateId() {
return 'id_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
},
/**
* 获取当前时间戳
*/
getCurrentTimestamp() {
return Date.now();
},
/**
* 格式化时间
* @param {number} timestamp 时间戳
*/
formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
};
// 默认导出配置对象
export default {
API_CONFIG,
TTS_CONFIG,
ERROR_CODES,
ERROR_MESSAGES,
UTILS
};

+ 0
- 591
uniapp-tts-example.vue View File

@ -1,591 +0,0 @@
<template>
<view class="container">
<view class="header">
<text class="title">文字转语音示例</text>
</view>
<view class="form-section">
<!-- 文本输入 -->
<view class="form-item">
<text class="label">输入文本</text>
<textarea
v-model="formData.text"
placeholder="请输入要转换的文本内容"
class="textarea"
maxlength="500"
/>
</view>
<!-- 音色选择 -->
<view class="form-item">
<text class="label">音色</text>
<picker
@change="onVoiceTypeChange"
:value="voiceTypeIndex"
:range="voiceTypeList"
range-key="name"
>
<view class="picker">
{{ voiceTypeList[voiceTypeIndex]?.name || '请选择音色' }}
</view>
</picker>
</view>
<!-- 语速调节 -->
<view class="form-item">
<text class="label">语速{{ formData.speed }}</text>
<slider
v-model="formData.speed"
:min="-2"
:max="6"
:step="1"
show-value
class="slider"
/>
</view>
<!-- 音量调节 -->
<view class="form-item">
<text class="label">音量{{ formData.volume }}</text>
<slider
v-model="formData.volume"
:min="-10"
:max="10"
:step="1"
show-value
class="slider"
/>
</view>
<!-- 音频格式选择 -->
<view class="form-item">
<text class="label">音频格式</text>
<radio-group @change="onCodecChange" class="radio-group">
<label class="radio-item" v-for="codec in codecList" :key="codec.value">
<radio :value="codec.value" :checked="formData.codec === codec.value" />
<text>{{ codec.name }}</text>
</label>
</radio-group>
</view>
</view>
<!-- 操作按钮 -->
<view class="button-section">
<button
@click="loadVoiceTypes"
:disabled="loading"
class="btn btn-secondary"
>
{{ loading ? '加载中...' : '加载音色列表' }}
</button>
<button
@click="convertToVoice"
:disabled="!formData.text || converting"
class="btn btn-primary"
>
{{ converting ? '转换中...' : '开始转换' }}
</button>
<button
@click="playAudio"
:disabled="!audioUrl || playing"
class="btn btn-success"
>
{{ playing ? '播放中...' : '播放音频' }}
</button>
</view>
<!-- 结果显示 -->
<view class="result-section" v-if="audioUrl">
<text class="result-title">转换结果</text>
<view class="audio-info">
<text>音频大小{{ audioSize }}</text>
<text>转换耗时{{ convertTime }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
//
formData: {
text: '你好,这是一个文字转语音的测试。',
speed: 0,
volume: 0,
codec: 'wav'
},
//
voiceTypeList: [],
voiceTypeIndex: 0,
//
codecList: [
{ name: 'WAV', value: 'wav' },
{ name: 'MP3', value: 'mp3' },
{ name: 'PCM', value: 'pcm' }
],
//
loading: false,
converting: false,
playing: false,
//
audioUrl: '',
audioSize: '',
convertTime: 0,
//
audioContext: null
}
},
onLoad() {
//
this.loadVoiceTypes();
//
this.getUserInfo();
},
methods: {
/**
* 获取用户信息
*/
getUserInfo() {
// ID
//
const userInfo = uni.getStorageSync('userInfo');
if (userInfo && userInfo.id) {
this.userId = userInfo.id;
} else {
// ID
this.userId = 'temp_' + Date.now();
}
},
/**
* 加载音色列表
*/
async loadVoiceTypes() {
this.loading = true;
try {
const response = await this.request({
url: '/appletApi/tts/list',
method: 'GET'
});
if (response.success && response.result) {
this.voiceTypeList = response.result;
if (this.voiceTypeList.length > 0) {
this.voiceTypeIndex = 0;
}
uni.showToast({
title: '音色列表加载成功',
icon: 'success'
});
} else {
throw new Error(response.message || '加载音色列表失败');
}
} catch (error) {
console.error('加载音色列表失败:', error);
uni.showToast({
title: '加载音色列表失败',
icon: 'error'
});
} finally {
this.loading = false;
}
},
/**
* 音色选择变化
*/
onVoiceTypeChange(e) {
this.voiceTypeIndex = e.detail.value;
},
/**
* 音频格式选择变化
*/
onCodecChange(e) {
this.formData.codec = e.detail.value;
},
/**
* 文字转语音
*/
async convertToVoice() {
if (!this.formData.text.trim()) {
uni.showToast({
title: '请输入要转换的文本',
icon: 'error'
});
return;
}
this.converting = true;
const startTime = Date.now();
try {
//
const params = {
text: this.formData.text,
speed: this.formData.speed,
voiceType: this.voiceTypeList[this.voiceTypeIndex]?.id || 0,
volume: this.formData.volume,
codec: this.formData.codec,
};
//
const response = await this.requestBinary({
url: '/appletApi/tts/textToVoice',
method: 'GET',
data: params,
responseType: 'arraybuffer'
});
if (response) {
//
this.convertTime = ((Date.now() - startTime) / 1000).toFixed(2);
//
await this.createAudioFile(response, this.formData.codec);
//
this.audioSize = this.formatFileSize(response.byteLength);
uni.showToast({
title: '转换成功',
icon: 'success'
});
} else {
throw new Error('转换失败,未返回音频数据');
}
} catch (error) {
console.error('文字转语音失败:', error);
uni.showToast({
title: '转换失败: ' + error.message,
icon: 'error'
});
} finally {
this.converting = false;
}
},
/**
* 创建音频文件
*/
async createAudioFile(arrayBuffer, codec) {
return new Promise((resolve, reject) => {
// ArrayBufferBase64
const uint8Array = new Uint8Array(arrayBuffer);
let binary = '';
for (let i = 0; i < uint8Array.length; i++) {
binary += String.fromCharCode(uint8Array[i]);
}
const base64 = btoa(binary);
//
const fileName = `tts_${Date.now()}.${codec}`;
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
//
wx.getFileSystemManager().writeFile({
filePath: filePath,
data: arrayBuffer,
success: () => {
this.audioUrl = filePath;
resolve(filePath);
},
fail: (error) => {
console.error('创建音频文件失败:', error);
reject(error);
}
});
});
},
/**
* 播放音频
*/
playAudio() {
if (!this.audioUrl) {
uni.showToast({
title: '没有可播放的音频',
icon: 'error'
});
return;
}
this.playing = true;
//
if (this.audioContext) {
this.audioContext.destroy();
}
this.audioContext = wx.createInnerAudioContext();
this.audioContext.src = this.audioUrl;
//
this.audioContext.onPlay(() => {
console.log('开始播放');
});
this.audioContext.onEnded(() => {
console.log('播放结束');
this.playing = false;
});
this.audioContext.onError((error) => {
console.error('播放失败:', error);
this.playing = false;
uni.showToast({
title: '播放失败',
icon: 'error'
});
});
//
this.audioContext.play();
},
/**
* 格式化文件大小
*/
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
/**
* 通用请求方法
*/
request(options) {
return new Promise((resolve, reject) => {
uni.request({
url: this.getApiUrl(options.url),
method: options.method || 'GET',
data: options.data || {},
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(error);
}
});
});
},
/**
* 二进制数据请求方法
*/
requestBinary(options) {
return new Promise((resolve, reject) => {
uni.request({
url: this.getApiUrl(options.url),
method: options.method || 'GET',
data: options.data || {},
responseType: 'arraybuffer',
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(error);
}
});
});
},
/**
* 获取完整的API地址
*/
getApiUrl(path) {
// API
const baseUrl = 'http://localhost:8080'; //
// const baseUrl = 'https://your-domain.com'; //
return baseUrl + path;
}
},
onUnload() {
//
if (this.audioContext) {
this.audioContext.destroy();
}
}
}
</script>
<style scoped>
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 40rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.form-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.form-item {
margin-bottom: 30rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
font-weight: 500;
}
.textarea {
width: 100%;
min-height: 120rpx;
padding: 20rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
font-size: 28rpx;
background-color: #fafafa;
}
.picker {
padding: 20rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
background-color: #fafafa;
font-size: 28rpx;
}
.slider {
margin-top: 20rpx;
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.radio-item {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 28rpx;
}
.button-section {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 30rpx;
}
.btn {
padding: 24rpx;
border-radius: 12rpx;
font-size: 30rpx;
font-weight: 500;
border: none;
}
.btn-primary {
background-color: #007aff;
color: #fff;
}
.btn-primary:disabled {
background-color: #ccc;
}
.btn-secondary {
background-color: #6c757d;
color: #fff;
}
.btn-secondary:disabled {
background-color: #ccc;
}
.btn-success {
background-color: #28a745;
color: #fff;
}
.btn-success:disabled {
background-color: #ccc;
}
.result-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.result-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.audio-info {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.audio-info text {
font-size: 28rpx;
color: #666;
}
</style>

+ 0
- 429
uniapp-tts-service.js View File

@ -1,429 +0,0 @@
/**
* 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;

+ 0
- 522
uniapp-tts-simple-example.vue View File

@ -1,522 +0,0 @@
<template>
<view class="simple-tts-container">
<view class="header">
<text class="title">简单TTS示例</text>
<text class="subtitle">快速集成文字转语音功能</text>
</view>
<!-- 快速转换区域 -->
<view class="quick-section">
<view class="input-group">
<textarea
v-model="quickText"
placeholder="输入要转换的文本..."
class="quick-input"
maxlength="200"
/>
<view class="char-count">{{ quickText.length }}/200</view>
</view>
<view class="quick-buttons">
<button
@click="quickConvert"
:disabled="!quickText.trim() || loading"
class="btn-convert"
>
{{ loading ? '转换中...' : '一键转换' }}
</button>
<button
@click="quickPlay"
:disabled="!hasAudio || playing"
class="btn-play"
>
{{ playing ? '播放中...' : '播放' }}
</button>
</view>
</view>
<!-- 高级设置区域 -->
<view class="advanced-section" v-if="showAdvanced">
<view class="section-title" @click="toggleAdvanced">
<text>高级设置</text>
<text class="toggle-icon">{{ showAdvanced ? '▼' : '▶' }}</text>
</view>
<view class="advanced-content">
<!-- 音色选择 -->
<view class="setting-item">
<text class="setting-label">音色</text>
<picker
@change="onVoiceChange"
:value="voiceIndex"
:range="voiceOptions"
range-key="name"
class="setting-picker"
>
<view class="picker-display">
{{ voiceOptions[voiceIndex]?.name || '默认音色' }}
</view>
</picker>
</view>
<!-- 语速设置 -->
<view class="setting-item">
<text class="setting-label">语速{{ speedValue }}</text>
<slider
v-model="speedValue"
:min="-2"
:max="6"
:step="1"
class="setting-slider"
/>
</view>
<!-- 音量设置 -->
<view class="setting-item">
<text class="setting-label">音量{{ volumeValue }}</text>
<slider
v-model="volumeValue"
:min="-10"
:max="10"
:step="1"
class="setting-slider"
/>
</view>
</view>
</view>
<!-- 状态显示区域 -->
<view class="status-section" v-if="statusInfo">
<view class="status-item">
<text class="status-label">状态</text>
<text class="status-value">{{ statusInfo.status }}</text>
</view>
<view class="status-item" v-if="statusInfo.size">
<text class="status-label">大小</text>
<text class="status-value">{{ statusInfo.size }}</text>
</view>
<view class="status-item" v-if="statusInfo.time">
<text class="status-label">耗时</text>
<text class="status-value">{{ statusInfo.time }}</text>
</view>
</view>
<!-- 预设文本区域 -->
<view class="preset-section">
<view class="section-title">预设文本</view>
<view class="preset-buttons">
<button
v-for="(preset, index) in presetTexts"
:key="index"
@click="usePreset(preset)"
class="preset-btn"
>
{{ preset.name }}
</button>
</view>
</view>
</view>
</template>
<script>
import ttsService from '@/utils/uniapp-tts-service.js';
export default {
data() {
return {
//
quickText: '',
loading: false,
playing: false,
hasAudio: false,
//
showAdvanced: false,
voiceOptions: [],
voiceIndex: 0,
speedValue: 0,
volumeValue: 0,
//
statusInfo: null,
//
presetTexts: [
{ name: '问候语', text: '你好,欢迎使用文字转语音功能!' },
{ name: '感谢语', text: '谢谢您的使用,祝您生活愉快!' },
{ name: '提醒语', text: '请注意,您有新的消息需要查看。' },
{ name: '测试语', text: '这是一个语音测试,请检查音质是否清晰。' }
]
}
},
async onLoad() {
await this.initTTS();
},
methods: {
/**
* 初始化TTS服务
*/
async initTTS() {
try {
uni.showLoading({ title: '初始化中...' });
const result = await ttsService.init();
if (result.success) {
this.voiceOptions = ttsService.getVoiceList();
console.log('TTS服务初始化成功');
} else {
throw new Error(result.error);
}
} catch (error) {
console.error('TTS初始化失败:', error);
uni.showToast({
title: '初始化失败',
icon: 'error'
});
} finally {
uni.hideLoading();
}
},
/**
* 快速转换
*/
async quickConvert() {
if (!this.quickText.trim()) {
uni.showToast({
title: '请输入文本',
icon: 'error'
});
return;
}
this.loading = true;
this.statusInfo = { status: '转换中...' };
try {
const params = {
text: this.quickText.trim(),
speed: this.speedValue,
voiceType: this.voiceOptions[this.voiceIndex]?.id || 0,
volume: this.volumeValue,
codec: 'wav'
};
const result = await ttsService.textToVoice(params);
if (result.success) {
this.hasAudio = true;
this.statusInfo = {
status: '转换成功',
size: result.audioSize,
time: result.convertTime
};
uni.showToast({
title: '转换成功',
icon: 'success'
});
}
} catch (error) {
console.error('转换失败:', error);
this.statusInfo = { status: '转换失败' };
uni.showToast({
title: error.message || '转换失败',
icon: 'error'
});
} finally {
this.loading = false;
}
},
/**
* 快速播放
*/
async quickPlay() {
if (!this.hasAudio) {
uni.showToast({
title: '请先转换文本',
icon: 'error'
});
return;
}
this.playing = true;
try {
await ttsService.playAudio();
//
setTimeout(() => {
this.playing = false;
}, 100);
} catch (error) {
console.error('播放失败:', error);
this.playing = false;
uni.showToast({
title: '播放失败',
icon: 'error'
});
}
},
/**
* 切换高级设置显示
*/
toggleAdvanced() {
this.showAdvanced = !this.showAdvanced;
},
/**
* 音色选择变化
*/
onVoiceChange(e) {
this.voiceIndex = e.detail.value;
},
/**
* 使用预设文本
*/
usePreset(preset) {
this.quickText = preset.text;
uni.showToast({
title: `已选择:${preset.name}`,
icon: 'success'
});
},
/**
* 清空文本
*/
clearText() {
this.quickText = '';
this.hasAudio = false;
this.statusInfo = null;
},
/**
* 停止播放
*/
stopPlay() {
ttsService.stopAudio();
this.playing = false;
}
},
onUnload() {
//
ttsService.destroy();
}
}
</script>
<style scoped>
.simple-tts-container {
padding: 30rpx;
background-color: #f8f9fa;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 40rpx;
}
.title {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.subtitle {
font-size: 28rpx;
color: #666;
}
.quick-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
.input-group {
position: relative;
margin-bottom: 30rpx;
}
.quick-input {
width: 100%;
min-height: 160rpx;
padding: 20rpx;
border: 2rpx solid #e9ecef;
border-radius: 12rpx;
font-size: 30rpx;
background-color: #fafbfc;
resize: none;
}
.char-count {
position: absolute;
bottom: 10rpx;
right: 15rpx;
font-size: 24rpx;
color: #999;
}
.quick-buttons {
display: flex;
gap: 20rpx;
}
.btn-convert, .btn-play {
flex: 1;
padding: 24rpx;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
}
.btn-convert {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
.btn-convert:disabled {
background: #ccc;
}
.btn-play {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: #fff;
}
.btn-play:disabled {
background: #ccc;
}
.advanced-section {
background-color: #fff;
border-radius: 20rpx;
margin-bottom: 30rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
font-size: 32rpx;
font-weight: 500;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
cursor: pointer;
}
.toggle-icon {
font-size: 24rpx;
color: #999;
}
.advanced-content {
padding: 30rpx;
}
.setting-item {
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.setting-item:last-child {
margin-bottom: 0;
}
.setting-label {
width: 120rpx;
font-size: 28rpx;
color: #333;
}
.setting-picker {
flex: 1;
}
.picker-display {
padding: 20rpx;
border: 2rpx solid #e9ecef;
border-radius: 8rpx;
background-color: #fafbfc;
font-size: 28rpx;
}
.setting-slider {
flex: 1;
margin-left: 20rpx;
}
.status-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
.status-item {
display: flex;
justify-content: space-between;
margin-bottom: 15rpx;
}
.status-item:last-child {
margin-bottom: 0;
}
.status-label {
font-size: 28rpx;
color: #666;
}
.status-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.preset-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
.preset-buttons {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
}
.preset-btn {
padding: 15rpx 25rpx;
border-radius: 25rpx;
font-size: 26rpx;
background-color: #f8f9fa;
color: #495057;
border: 2rpx solid #e9ecef;
}
.preset-btn:active {
background-color: #e9ecef;
}
</style>

Loading…
Cancel
Save