tts.controller.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import Router from '@koa/router';
  2. import { Context } from 'koa';
  3. import * as TtsService from './tts.service';
  4. import { BadRequestError, NotFoundError } from '../../middleware/errorHandler';
  5. import { optionalAuth } from '../../middleware/auth';
  6. import { usageLimitMiddleware, checkWordLimit } from '../../middleware/usageLimit';
  7. import { prisma } from '../../models';
  8. const router = new Router();
  9. // 获取可用音色列表
  10. router.get('/voices', async (ctx: Context) => {
  11. const voices = TtsService.getVoices();
  12. ctx.body = {
  13. code: 0,
  14. message: 'success',
  15. data: { voices },
  16. };
  17. });
  18. // 测试 MySQL 连接
  19. router.get('/test-db', async (ctx: Context) => {
  20. try {
  21. const count = await prisma.book.count();
  22. ctx.body = {
  23. code: 0,
  24. message: 'success',
  25. data: { count, connectionState: 'connected' },
  26. };
  27. } catch (error: any) {
  28. ctx.body = {
  29. code: 500,
  30. message: 'error',
  31. data: { error: error.message },
  32. };
  33. }
  34. });
  35. // 生成音频(异步模式)
  36. router.post(
  37. '/generate',
  38. optionalAuth,
  39. async (ctx: Context) => {
  40. const userId = ctx.state.user?.userId;
  41. const { text, voiceId, voiceParams, bookId, chapterTitle } = ctx.request.body as {
  42. text: string;
  43. voiceId: string;
  44. voiceParams?: { speed?: number; pitch?: number; volume?: number };
  45. bookId?: string;
  46. chapterTitle?: string;
  47. };
  48. // 调试日志
  49. console.log('📥 收到 TTS 请求:', { textLength: text?.length, voiceId, voiceParams, bookId, chapterTitle });
  50. // 参数验证
  51. if (!text || text.trim().length === 0) {
  52. throw new BadRequestError('请输入要转换的文本');
  53. }
  54. if (!voiceId) {
  55. throw new BadRequestError('请选择音色');
  56. }
  57. // 如果指定了 bookId,验证书籍是否存在
  58. if (bookId) {
  59. const book = await prisma.book.findUnique({ where: { id: parseInt(bookId) } });
  60. if (!book) {
  61. throw new NotFoundError('书籍不存在');
  62. }
  63. }
  64. // 检查字数限制(测试模式:无限制)
  65. const wordCount = text.length;
  66. const quota = ctx.state.userQuota || { dailyLimit: -1, wordLimit: -1 };
  67. if (quota.wordLimit !== -1 && wordCount > quota.wordLimit) {
  68. throw new BadRequestError(`文本字数超出限制(${quota.wordLimit}字)`);
  69. }
  70. // 默认参数
  71. const params = {
  72. speed: voiceParams?.speed || 1.0,
  73. pitch: voiceParams?.pitch || 0,
  74. volume: voiceParams?.volume || 50,
  75. };
  76. // 异步生成音频(立即返回)
  77. const result = await TtsService.generateAudio(userId, text, voiceId, params, undefined, {
  78. bookId,
  79. chapterTitle,
  80. });
  81. // 如果用户已登录,更新使用次数
  82. if (userId) {
  83. const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
  84. if (user) {
  85. await prisma.user.update({
  86. where: { id: parseInt(userId) },
  87. data: { dailyUsage: user.dailyUsage + 1 },
  88. });
  89. }
  90. }
  91. ctx.body = {
  92. code: 0,
  93. message: '音频生成任务已创建',
  94. data: result, // { audioId, audioUrl: '' }
  95. };
  96. }
  97. );
  98. // 获取音频生成状态
  99. router.get('/status/:audioId', async (ctx: Context) => {
  100. const { audioId } = ctx.params;
  101. const result = await TtsService.getAudioStatus(audioId);
  102. if (result.status === 'not_found') {
  103. throw new NotFoundError('音频不存在');
  104. }
  105. ctx.body = {
  106. code: 0,
  107. message: 'success',
  108. data: result,
  109. };
  110. });
  111. // 预览音色 - 生成短音频
  112. router.post('/preview', async (ctx: Context) => {
  113. const { voiceId, voiceParams } = ctx.request.body as {
  114. voiceId: string;
  115. voiceParams?: { speed?: number; pitch?: number; volume?: number };
  116. };
  117. if (!voiceId) {
  118. throw new BadRequestError('请选择音色');
  119. }
  120. // 预览文本(固定短文本)
  121. const previewText = '你好,欢迎使用AI有声书';
  122. try {
  123. // 生成预览音频
  124. const result = await TtsService.generatePreview(voiceId, voiceParams);
  125. ctx.body = {
  126. code: 0,
  127. message: 'success',
  128. data: {
  129. previewText,
  130. voiceId,
  131. audioUrl: result.audioUrl,
  132. },
  133. };
  134. } catch (error: any) {
  135. ctx.body = {
  136. code: 500,
  137. message: '预览生成失败:' + error.message,
  138. data: null,
  139. };
  140. }
  141. });
  142. export default router;