四零语境后端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

522 lines
11 KiB

<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>