| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 |
- /**
- * Video Generation API
- *
- * Generates a video from a text prompt using the specified provider.
- * Uses async task pattern (submit → poll) so maxDuration is set to 5 minutes.
- *
- * POST /api/generate/video
- *
- * Headers:
- * x-video-provider: VideoProviderId (default: 'seedance')
- * x-video-model: string (optional model override)
- * x-api-key: string (optional, server fallback)
- * x-base-url: string (optional, server fallback)
- *
- * Body: { prompt, duration?, aspectRatio?, resolution? }
- * Response: { success: boolean, result?: VideoGenerationResult, error?: string }
- */
- import { NextRequest } from 'next/server';
- import { generateVideo, normalizeVideoOptions } from '@/lib/media/video-providers';
- import { resolveVideoApiKey, resolveVideoBaseUrl } from '@/lib/server/provider-config';
- import type { VideoProviderId, VideoGenerationOptions } from '@/lib/media/types';
- import { createLogger } from '@/lib/logger';
- import { apiError, apiSuccess } from '@/lib/server/api-response';
- import { validateUrlForSSRF } from '@/lib/server/ssrf-guard';
- const log = createLogger('VideoGeneration API');
- export const maxDuration = 300;
- export async function POST(request: NextRequest) {
- try {
- const body = (await request.json()) as VideoGenerationOptions;
- if (!body.prompt) {
- return apiError('MISSING_REQUIRED_FIELD', 400, 'Missing prompt');
- }
- const providerId = (request.headers.get('x-video-provider') || 'seedance') as VideoProviderId;
- const clientApiKey = request.headers.get('x-api-key') || undefined;
- const clientBaseUrl = request.headers.get('x-base-url') || undefined;
- const clientModel = request.headers.get('x-video-model') || undefined;
- if (clientBaseUrl && process.env.NODE_ENV === 'production') {
- const ssrfError = await validateUrlForSSRF(clientBaseUrl);
- if (ssrfError) {
- return apiError('INVALID_URL', 403, ssrfError);
- }
- }
- const apiKey = clientBaseUrl
- ? clientApiKey || ''
- : resolveVideoApiKey(providerId, clientApiKey);
- if (!apiKey) {
- return apiError(
- 'MISSING_API_KEY',
- 401,
- `No API key configured for video provider: ${providerId}`,
- );
- }
- const baseUrl = clientBaseUrl ? clientBaseUrl : resolveVideoBaseUrl(providerId, clientBaseUrl);
- // Normalize options against provider capabilities
- const options = normalizeVideoOptions(providerId, body);
- log.info(
- `Generating video: provider=${providerId}, model=${clientModel || 'default'}, ` +
- `prompt="${body.prompt.slice(0, 80)}...", duration=${options.duration ?? 'auto'}, ` +
- `aspect=${options.aspectRatio ?? 'auto'}, resolution=${options.resolution ?? 'auto'}`,
- );
- const result = await generateVideo(
- { providerId, apiKey, baseUrl, model: clientModel },
- options,
- );
- log.info(
- `Video generated: url=${result.url ? 'yes' : 'no'}, ${result.width}x${result.height}, ${result.duration}s`,
- );
- return apiSuccess({ result });
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- // Detect content safety filter rejections (e.g. Seedance SensitiveContent errors)
- if (message.includes('SensitiveContent') || message.includes('sensitive information')) {
- log.warn(`Video blocked by content safety filter: ${message}`);
- return apiError('CONTENT_SENSITIVE', 400, message);
- }
- log.error(
- `Video generation failed [provider=${request.headers.get('x-video-provider') ?? 'kling'}, model=${request.headers.get('x-video-model') ?? 'default'}]:`,
- error,
- );
- return apiError('INTERNAL_ERROR', 500, message);
- }
- }
|