| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- /**
- * 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 { resolveModelFromHeaders } 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,
- stageId,
- agents,
- } = body as {
- outline: SceneOutline;
- allOutlines: SceneOutline[];
- pdfImages?: PdfImage[];
- imageMapping?: ImageMapping;
- stageInfo: {
- name: string;
- description?: string;
- language?: string;
- style?: string;
- };
- stageId: string;
- agents?: AgentInfo[];
- };
- // 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');
- }
- // Ensure outline has language from stageInfo (fallback for older outlines)
- const outline: SceneOutline = {
- ...rawOutline,
- language: rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US') || 'zh-CN',
- };
- // ── Model resolution from request headers ──
- const { model: languageModel, modelInfo, modelString } = await resolveModelFromHeaders(req);
- 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<string> => {
- 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',
- );
- return result.text;
- }
- const result = await callLLM(
- {
- model: languageModel,
- system: systemPrompt,
- prompt: userPrompt,
- maxOutputTokens: modelInfo?.outputWindow,
- },
- 'scene-content',
- );
- 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,
- effectiveOutline.type === 'pbl' ? languageModel : undefined,
- hasVision,
- generatedMediaMapping,
- agents,
- );
- 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));
- }
- }
|