/** * Scene Content Generation API * * Generates scene content (slides/quiz/interactive/pbl) from an outline. * This is the first half of the two-step scene generation pipeline. * Does NOT generate actions — use /api/generate/scene-actions for that. */ import { NextRequest } from 'next/server'; import { callLLM } from '@/lib/ai/llm'; import { applyOutlineFallbacks, generateSceneContent, buildVisionUserContent, } from '@/lib/generation/generation-pipeline'; import type { AgentInfo } from '@/lib/generation/generation-pipeline'; import type { SceneOutline, PdfImage, ImageMapping } from '@/lib/types/generation'; import { createLogger } from '@/lib/logger'; import { apiError, apiSuccess } from '@/lib/server/api-response'; import { resolveModelFromRequest } from '@/lib/server/resolve-model'; const log = createLogger('Scene Content API'); export const maxDuration = 300; export async function POST(req: NextRequest) { let outlineTitle: string | undefined; let resolvedModelString: string | undefined; try { const body = await req.json(); const { outline: rawOutline, allOutlines, pdfImages, imageMapping, stageInfo: _stageInfo, stageId, agents, languageDirective, } = body as { outline: SceneOutline; allOutlines: SceneOutline[]; pdfImages?: PdfImage[]; imageMapping?: ImageMapping; stageInfo: { name: string; description?: string; style?: string; }; stageId: string; agents?: AgentInfo[]; languageDirective?: string; }; // Validate required fields if (!rawOutline) { return apiError('MISSING_REQUIRED_FIELD', 400, 'outline is required'); } if (!allOutlines || allOutlines.length === 0) { return apiError( 'MISSING_REQUIRED_FIELD', 400, 'allOutlines is required and must not be empty', ); } if (!stageId) { return apiError('MISSING_REQUIRED_FIELD', 400, 'stageId is required'); } const outline: SceneOutline = { ...rawOutline }; // ── Model resolution from request headers/body ── const { model: languageModel, modelInfo, modelString, thinkingConfig, } = await resolveModelFromRequest(req, body); outlineTitle = rawOutline?.title; resolvedModelString = modelString; // Detect vision capability const hasVision = !!modelInfo?.capabilities?.vision; // Vision-aware AI call function const aiCall = async ( systemPrompt: string, userPrompt: string, images?: Array<{ id: string; src: string }>, ): Promise => { if (images?.length && hasVision) { const result = await callLLM( { model: languageModel, system: systemPrompt, messages: [ { role: 'user' as const, content: buildVisionUserContent(userPrompt, images), }, ], maxOutputTokens: modelInfo?.outputWindow, }, 'scene-content', undefined, thinkingConfig, ); return result.text; } const result = await callLLM( { model: languageModel, system: systemPrompt, prompt: userPrompt, maxOutputTokens: modelInfo?.outputWindow, }, 'scene-content', undefined, thinkingConfig, ); return result.text; }; // ── Apply fallbacks ── const effectiveOutline = applyOutlineFallbacks(outline, !!languageModel); // ── Filter images assigned to this outline ── let assignedImages: PdfImage[] | undefined; if ( pdfImages && pdfImages.length > 0 && effectiveOutline.suggestedImageIds && effectiveOutline.suggestedImageIds.length > 0 ) { const suggestedIds = new Set(effectiveOutline.suggestedImageIds); assignedImages = pdfImages.filter((img) => suggestedIds.has(img.id)); } // ── Media generation is handled client-side in parallel (media-orchestrator.ts) ── // The content generator receives placeholder IDs (gen_img_1, gen_vid_1) as-is. // resolveImageIds() in generation-pipeline.ts will keep these placeholders in elements. const generatedMediaMapping: ImageMapping = {}; // ── Generate content ── log.info( `Generating content: "${effectiveOutline.title}" (${effectiveOutline.type}) [model=${modelString}]`, ); const content = await generateSceneContent(effectiveOutline, aiCall, { assignedImages, imageMapping, languageModel: effectiveOutline.type === 'pbl' ? languageModel : undefined, visionEnabled: hasVision, generatedMediaMapping, agents, languageDirective, thinkingConfig, }); if (!content) { log.error(`Failed to generate content for: "${effectiveOutline.title}"`); return apiError( 'GENERATION_FAILED', 500, `Failed to generate content: ${effectiveOutline.title}`, ); } log.info(`Content generated successfully: "${effectiveOutline.title}"`); return apiSuccess({ content, effectiveOutline }); } catch (error) { log.error( `Scene content generation failed [scene="${outlineTitle ?? 'unknown'}", model=${resolvedModelString ?? 'unknown'}]:`, error, ); return apiError('INTERNAL_ERROR', 500, error instanceof Error ? error.message : String(error)); } }