muthuk1's picture
Convert OpenMAIC from Next.js to React (Vite)
f56a29b verified
/**
* 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<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',
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));
}
}