route.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. /**
  2. * Single TTS Generation API
  3. *
  4. * Generates TTS audio for a single text string and returns base64-encoded audio.
  5. * Called by the client in parallel for each speech action after a scene is generated.
  6. *
  7. * POST /api/generate/tts
  8. */
  9. import { NextRequest } from 'next/server';
  10. import { generateTTS } from '@/lib/audio/tts-providers';
  11. import { resolveTTSApiKey, resolveTTSBaseUrl } from '@/lib/server/provider-config';
  12. import type { TTSProviderId } from '@/lib/audio/types';
  13. import { createLogger } from '@/lib/logger';
  14. import { apiError, apiSuccess } from '@/lib/server/api-response';
  15. import { validateUrlForSSRF } from '@/lib/server/ssrf-guard';
  16. const log = createLogger('TTS API');
  17. export const maxDuration = 30;
  18. export async function POST(req: NextRequest) {
  19. let ttsProviderId: string | undefined;
  20. let ttsVoice: string | undefined;
  21. let audioId: string | undefined;
  22. try {
  23. const body = await req.json();
  24. const { text, ttsModelId, ttsSpeed, ttsApiKey, ttsBaseUrl } = body as {
  25. text: string;
  26. audioId: string;
  27. ttsProviderId: TTSProviderId;
  28. ttsModelId?: string;
  29. ttsVoice: string;
  30. ttsSpeed?: number;
  31. ttsApiKey?: string;
  32. ttsBaseUrl?: string;
  33. };
  34. ttsProviderId = body.ttsProviderId;
  35. ttsVoice = body.ttsVoice;
  36. audioId = body.audioId;
  37. // Validate required fields
  38. if (!text || !audioId || !ttsProviderId || !ttsVoice) {
  39. return apiError(
  40. 'MISSING_REQUIRED_FIELD',
  41. 400,
  42. 'Missing required fields: text, audioId, ttsProviderId, ttsVoice',
  43. );
  44. }
  45. // Reject browser-native TTS — must be handled client-side
  46. if (ttsProviderId === 'browser-native-tts') {
  47. return apiError('INVALID_REQUEST', 400, 'browser-native-tts must be handled client-side');
  48. }
  49. const clientBaseUrl = ttsBaseUrl || undefined;
  50. if (clientBaseUrl && process.env.NODE_ENV === 'production') {
  51. const ssrfError = await validateUrlForSSRF(clientBaseUrl);
  52. if (ssrfError) {
  53. return apiError('INVALID_URL', 403, ssrfError);
  54. }
  55. }
  56. const apiKey = clientBaseUrl
  57. ? ttsApiKey || ''
  58. : resolveTTSApiKey(ttsProviderId, ttsApiKey || undefined);
  59. const baseUrl = clientBaseUrl
  60. ? clientBaseUrl
  61. : resolveTTSBaseUrl(ttsProviderId, ttsBaseUrl || undefined);
  62. // Build TTS config
  63. const config = {
  64. providerId: ttsProviderId as TTSProviderId,
  65. modelId: ttsModelId,
  66. voice: ttsVoice,
  67. speed: ttsSpeed ?? 1.0,
  68. apiKey,
  69. baseUrl,
  70. };
  71. log.info(
  72. `Generating TTS: provider=${ttsProviderId}, model=${ttsModelId || 'default'}, voice=${ttsVoice}, audioId=${audioId}, textLen=${text.length}`,
  73. );
  74. // Generate audio
  75. const { audio, format } = await generateTTS(config, text);
  76. // Convert to base64
  77. const base64 = Buffer.from(audio).toString('base64');
  78. return apiSuccess({ audioId, base64, format });
  79. } catch (error) {
  80. log.error(
  81. `TTS generation failed [provider=${ttsProviderId ?? 'unknown'}, voice=${ttsVoice ?? 'unknown'}, audioId=${audioId ?? 'unknown'}]:`,
  82. error,
  83. );
  84. return apiError(
  85. 'GENERATION_FAILED',
  86. 500,
  87. error instanceof Error ? error.message : String(error),
  88. );
  89. }
  90. }