四零语境后端代码仓库
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.

521 lines
11 KiB

15 hours ago
  1. <template>
  2. <view class="simple-tts-container">
  3. <view class="header">
  4. <text class="title">简单TTS示例</text>
  5. <text class="subtitle">快速集成文字转语音功能</text>
  6. </view>
  7. <!-- 快速转换区域 -->
  8. <view class="quick-section">
  9. <view class="input-group">
  10. <textarea
  11. v-model="quickText"
  12. placeholder="输入要转换的文本..."
  13. class="quick-input"
  14. maxlength="200"
  15. />
  16. <view class="char-count">{{ quickText.length }}/200</view>
  17. </view>
  18. <view class="quick-buttons">
  19. <button
  20. @click="quickConvert"
  21. :disabled="!quickText.trim() || loading"
  22. class="btn-convert"
  23. >
  24. {{ loading ? '转换中...' : '一键转换' }}
  25. </button>
  26. <button
  27. @click="quickPlay"
  28. :disabled="!hasAudio || playing"
  29. class="btn-play"
  30. >
  31. {{ playing ? '播放中...' : '播放' }}
  32. </button>
  33. </view>
  34. </view>
  35. <!-- 高级设置区域 -->
  36. <view class="advanced-section" v-if="showAdvanced">
  37. <view class="section-title" @click="toggleAdvanced">
  38. <text>高级设置</text>
  39. <text class="toggle-icon">{{ showAdvanced ? '▼' : '▶' }}</text>
  40. </view>
  41. <view class="advanced-content">
  42. <!-- 音色选择 -->
  43. <view class="setting-item">
  44. <text class="setting-label">音色</text>
  45. <picker
  46. @change="onVoiceChange"
  47. :value="voiceIndex"
  48. :range="voiceOptions"
  49. range-key="name"
  50. class="setting-picker"
  51. >
  52. <view class="picker-display">
  53. {{ voiceOptions[voiceIndex]?.name || '默认音色' }}
  54. </view>
  55. </picker>
  56. </view>
  57. <!-- 语速设置 -->
  58. <view class="setting-item">
  59. <text class="setting-label">语速{{ speedValue }}</text>
  60. <slider
  61. v-model="speedValue"
  62. :min="-2"
  63. :max="6"
  64. :step="1"
  65. class="setting-slider"
  66. />
  67. </view>
  68. <!-- 音量设置 -->
  69. <view class="setting-item">
  70. <text class="setting-label">音量{{ volumeValue }}</text>
  71. <slider
  72. v-model="volumeValue"
  73. :min="-10"
  74. :max="10"
  75. :step="1"
  76. class="setting-slider"
  77. />
  78. </view>
  79. </view>
  80. </view>
  81. <!-- 状态显示区域 -->
  82. <view class="status-section" v-if="statusInfo">
  83. <view class="status-item">
  84. <text class="status-label">状态</text>
  85. <text class="status-value">{{ statusInfo.status }}</text>
  86. </view>
  87. <view class="status-item" v-if="statusInfo.size">
  88. <text class="status-label">大小</text>
  89. <text class="status-value">{{ statusInfo.size }}</text>
  90. </view>
  91. <view class="status-item" v-if="statusInfo.time">
  92. <text class="status-label">耗时</text>
  93. <text class="status-value">{{ statusInfo.time }}</text>
  94. </view>
  95. </view>
  96. <!-- 预设文本区域 -->
  97. <view class="preset-section">
  98. <view class="section-title">预设文本</view>
  99. <view class="preset-buttons">
  100. <button
  101. v-for="(preset, index) in presetTexts"
  102. :key="index"
  103. @click="usePreset(preset)"
  104. class="preset-btn"
  105. >
  106. {{ preset.name }}
  107. </button>
  108. </view>
  109. </view>
  110. </view>
  111. </template>
  112. <script>
  113. import ttsService from '@/utils/uniapp-tts-service.js';
  114. export default {
  115. data() {
  116. return {
  117. // 基础数据
  118. quickText: '',
  119. loading: false,
  120. playing: false,
  121. hasAudio: false,
  122. // 高级设置
  123. showAdvanced: false,
  124. voiceOptions: [],
  125. voiceIndex: 0,
  126. speedValue: 0,
  127. volumeValue: 0,
  128. // 状态信息
  129. statusInfo: null,
  130. // 预设文本
  131. presetTexts: [
  132. { name: '问候语', text: '你好,欢迎使用文字转语音功能!' },
  133. { name: '感谢语', text: '谢谢您的使用,祝您生活愉快!' },
  134. { name: '提醒语', text: '请注意,您有新的消息需要查看。' },
  135. { name: '测试语', text: '这是一个语音测试,请检查音质是否清晰。' }
  136. ]
  137. }
  138. },
  139. async onLoad() {
  140. await this.initTTS();
  141. },
  142. methods: {
  143. /**
  144. * 初始化TTS服务
  145. */
  146. async initTTS() {
  147. try {
  148. uni.showLoading({ title: '初始化中...' });
  149. const result = await ttsService.init();
  150. if (result.success) {
  151. this.voiceOptions = ttsService.getVoiceList();
  152. console.log('TTS服务初始化成功');
  153. } else {
  154. throw new Error(result.error);
  155. }
  156. } catch (error) {
  157. console.error('TTS初始化失败:', error);
  158. uni.showToast({
  159. title: '初始化失败',
  160. icon: 'error'
  161. });
  162. } finally {
  163. uni.hideLoading();
  164. }
  165. },
  166. /**
  167. * 快速转换
  168. */
  169. async quickConvert() {
  170. if (!this.quickText.trim()) {
  171. uni.showToast({
  172. title: '请输入文本',
  173. icon: 'error'
  174. });
  175. return;
  176. }
  177. this.loading = true;
  178. this.statusInfo = { status: '转换中...' };
  179. try {
  180. const params = {
  181. text: this.quickText.trim(),
  182. speed: this.speedValue,
  183. voiceType: this.voiceOptions[this.voiceIndex]?.id || 0,
  184. volume: this.volumeValue,
  185. codec: 'wav'
  186. };
  187. const result = await ttsService.textToVoice(params);
  188. if (result.success) {
  189. this.hasAudio = true;
  190. this.statusInfo = {
  191. status: '转换成功',
  192. size: result.audioSize,
  193. time: result.convertTime
  194. };
  195. uni.showToast({
  196. title: '转换成功',
  197. icon: 'success'
  198. });
  199. }
  200. } catch (error) {
  201. console.error('转换失败:', error);
  202. this.statusInfo = { status: '转换失败' };
  203. uni.showToast({
  204. title: error.message || '转换失败',
  205. icon: 'error'
  206. });
  207. } finally {
  208. this.loading = false;
  209. }
  210. },
  211. /**
  212. * 快速播放
  213. */
  214. async quickPlay() {
  215. if (!this.hasAudio) {
  216. uni.showToast({
  217. title: '请先转换文本',
  218. icon: 'error'
  219. });
  220. return;
  221. }
  222. this.playing = true;
  223. try {
  224. await ttsService.playAudio();
  225. // 监听播放结束
  226. setTimeout(() => {
  227. this.playing = false;
  228. }, 100);
  229. } catch (error) {
  230. console.error('播放失败:', error);
  231. this.playing = false;
  232. uni.showToast({
  233. title: '播放失败',
  234. icon: 'error'
  235. });
  236. }
  237. },
  238. /**
  239. * 切换高级设置显示
  240. */
  241. toggleAdvanced() {
  242. this.showAdvanced = !this.showAdvanced;
  243. },
  244. /**
  245. * 音色选择变化
  246. */
  247. onVoiceChange(e) {
  248. this.voiceIndex = e.detail.value;
  249. },
  250. /**
  251. * 使用预设文本
  252. */
  253. usePreset(preset) {
  254. this.quickText = preset.text;
  255. uni.showToast({
  256. title: `已选择:${preset.name}`,
  257. icon: 'success'
  258. });
  259. },
  260. /**
  261. * 清空文本
  262. */
  263. clearText() {
  264. this.quickText = '';
  265. this.hasAudio = false;
  266. this.statusInfo = null;
  267. },
  268. /**
  269. * 停止播放
  270. */
  271. stopPlay() {
  272. ttsService.stopAudio();
  273. this.playing = false;
  274. }
  275. },
  276. onUnload() {
  277. // 页面卸载时清理资源
  278. ttsService.destroy();
  279. }
  280. }
  281. </script>
  282. <style scoped>
  283. .simple-tts-container {
  284. padding: 30rpx;
  285. background-color: #f8f9fa;
  286. min-height: 100vh;
  287. }
  288. .header {
  289. text-align: center;
  290. margin-bottom: 40rpx;
  291. }
  292. .title {
  293. display: block;
  294. font-size: 40rpx;
  295. font-weight: bold;
  296. color: #333;
  297. margin-bottom: 10rpx;
  298. }
  299. .subtitle {
  300. font-size: 28rpx;
  301. color: #666;
  302. }
  303. .quick-section {
  304. background-color: #fff;
  305. border-radius: 20rpx;
  306. padding: 30rpx;
  307. margin-bottom: 30rpx;
  308. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
  309. }
  310. .input-group {
  311. position: relative;
  312. margin-bottom: 30rpx;
  313. }
  314. .quick-input {
  315. width: 100%;
  316. min-height: 160rpx;
  317. padding: 20rpx;
  318. border: 2rpx solid #e9ecef;
  319. border-radius: 12rpx;
  320. font-size: 30rpx;
  321. background-color: #fafbfc;
  322. resize: none;
  323. }
  324. .char-count {
  325. position: absolute;
  326. bottom: 10rpx;
  327. right: 15rpx;
  328. font-size: 24rpx;
  329. color: #999;
  330. }
  331. .quick-buttons {
  332. display: flex;
  333. gap: 20rpx;
  334. }
  335. .btn-convert, .btn-play {
  336. flex: 1;
  337. padding: 24rpx;
  338. border-radius: 12rpx;
  339. font-size: 32rpx;
  340. font-weight: 500;
  341. border: none;
  342. }
  343. .btn-convert {
  344. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  345. color: #fff;
  346. }
  347. .btn-convert:disabled {
  348. background: #ccc;
  349. }
  350. .btn-play {
  351. background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  352. color: #fff;
  353. }
  354. .btn-play:disabled {
  355. background: #ccc;
  356. }
  357. .advanced-section {
  358. background-color: #fff;
  359. border-radius: 20rpx;
  360. margin-bottom: 30rpx;
  361. overflow: hidden;
  362. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
  363. }
  364. .section-title {
  365. display: flex;
  366. justify-content: space-between;
  367. align-items: center;
  368. padding: 30rpx;
  369. font-size: 32rpx;
  370. font-weight: 500;
  371. color: #333;
  372. border-bottom: 1rpx solid #f0f0f0;
  373. cursor: pointer;
  374. }
  375. .toggle-icon {
  376. font-size: 24rpx;
  377. color: #999;
  378. }
  379. .advanced-content {
  380. padding: 30rpx;
  381. }
  382. .setting-item {
  383. display: flex;
  384. align-items: center;
  385. margin-bottom: 30rpx;
  386. }
  387. .setting-item:last-child {
  388. margin-bottom: 0;
  389. }
  390. .setting-label {
  391. width: 120rpx;
  392. font-size: 28rpx;
  393. color: #333;
  394. }
  395. .setting-picker {
  396. flex: 1;
  397. }
  398. .picker-display {
  399. padding: 20rpx;
  400. border: 2rpx solid #e9ecef;
  401. border-radius: 8rpx;
  402. background-color: #fafbfc;
  403. font-size: 28rpx;
  404. }
  405. .setting-slider {
  406. flex: 1;
  407. margin-left: 20rpx;
  408. }
  409. .status-section {
  410. background-color: #fff;
  411. border-radius: 20rpx;
  412. padding: 30rpx;
  413. margin-bottom: 30rpx;
  414. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
  415. }
  416. .status-item {
  417. display: flex;
  418. justify-content: space-between;
  419. margin-bottom: 15rpx;
  420. }
  421. .status-item:last-child {
  422. margin-bottom: 0;
  423. }
  424. .status-label {
  425. font-size: 28rpx;
  426. color: #666;
  427. }
  428. .status-value {
  429. font-size: 28rpx;
  430. color: #333;
  431. font-weight: 500;
  432. }
  433. .preset-section {
  434. background-color: #fff;
  435. border-radius: 20rpx;
  436. padding: 30rpx;
  437. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
  438. }
  439. .preset-buttons {
  440. display: flex;
  441. flex-wrap: wrap;
  442. gap: 15rpx;
  443. }
  444. .preset-btn {
  445. padding: 15rpx 25rpx;
  446. border-radius: 25rpx;
  447. font-size: 26rpx;
  448. background-color: #f8f9fa;
  449. color: #495057;
  450. border: 2rpx solid #e9ecef;
  451. }
  452. .preset-btn:active {
  453. background-color: #e9ecef;
  454. }
  455. </style>