| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- /**
- * Scene Actions Generation API
- *
- * Generates actions for a scene given its outline and content,
- * then assembles the complete Scene object.
- * This is the second half of the two-step scene generation pipeline.
- */
- import { NextRequest } from 'next/server';
- import { callLLM } from '@/lib/ai/llm';
- import {
- generateSceneActions,
- buildCompleteScene,
- buildVisionUserContent,
- type SceneGenerationContext,
- type AgentInfo,
- } from '@/lib/generation/generation-pipeline';
- import type { SceneOutline } from '@/lib/types/generation';
- import type {
- GeneratedSlideContent,
- GeneratedQuizContent,
- GeneratedInteractiveContent,
- GeneratedPBLContent,
- } from '@/lib/types/generation';
- import type { SpeechAction } from '@/lib/types/action';
- import { createLogger } from '@/lib/logger';
- import { apiError, apiSuccess } from '@/lib/server/api-response';
- import { resolveModelFromHeaders } from '@/lib/server/resolve-model';
- const log = createLogger('Scene Actions API');
- export const maxDuration = 60;
- export async function POST(req: NextRequest) {
- let outlineTitle: string | undefined;
- let resolvedModelString: string | undefined;
- try {
- const body = await req.json();
- const {
- outline,
- allOutlines,
- content,
- stageId,
- agents,
- previousSpeeches: incomingPreviousSpeeches,
- userProfile,
- } = body as {
- outline: SceneOutline;
- allOutlines: SceneOutline[];
- content:
- | GeneratedSlideContent
- | GeneratedQuizContent
- | GeneratedInteractiveContent
- | GeneratedPBLContent;
- stageId: string;
- agents?: AgentInfo[];
- previousSpeeches?: string[];
- userProfile?: string;
- };
- // Validate required fields
- if (!outline) {
- 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 (!content) {
- return apiError('MISSING_REQUIRED_FIELD', 400, 'content is required');
- }
- if (!stageId) {
- return apiError('MISSING_REQUIRED_FIELD', 400, 'stageId is required');
- }
- // ── Model resolution from request headers ──
- const { model: languageModel, modelInfo, modelString } = await resolveModelFromHeaders(req);
- outlineTitle = outline?.title;
- resolvedModelString = modelString;
- // Detect vision capability
- const hasVision = !!modelInfo?.capabilities?.vision;
- // AI call function (actions typically don't use vision, but kept for consistency)
- 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-actions',
- );
- return result.text;
- }
- const result = await callLLM(
- {
- model: languageModel,
- system: systemPrompt,
- prompt: userPrompt,
- maxOutputTokens: modelInfo?.outputWindow,
- },
- 'scene-actions',
- );
- return result.text;
- };
- // ── Build cross-scene context ──
- const allTitles = allOutlines.map((o) => o.title);
- const pageIndex = allOutlines.findIndex((o) => o.id === outline.id);
- const ctx: SceneGenerationContext = {
- pageIndex: (pageIndex >= 0 ? pageIndex : 0) + 1,
- totalPages: allOutlines.length,
- allTitles,
- previousSpeeches: incomingPreviousSpeeches ?? [],
- };
- // ── Generate actions ──
- log.info(`Generating actions: "${outline.title}" (${outline.type}) [model=${modelString}]`);
- const actions = await generateSceneActions(outline, content, aiCall, ctx, agents, userProfile);
- log.info(`Generated ${actions.length} actions for: "${outline.title}"`);
- // ── Build complete scene ──
- const scene = buildCompleteScene(outline, content, actions, stageId);
- if (!scene) {
- log.error(`Failed to build scene: "${outline.title}"`);
- return apiError('GENERATION_FAILED', 500, `Failed to build scene: ${outline.title}`);
- }
- // ── Extract speeches for cross-scene coherence ──
- const outputPreviousSpeeches = (scene.actions || [])
- .filter((a): a is SpeechAction => a.type === 'speech')
- .map((a) => a.text);
- log.info(
- `Scene assembled successfully: "${outline.title}" — ${scene.actions?.length ?? 0} actions`,
- );
- return apiSuccess({ scene, previousSpeeches: outputPreviousSpeeches });
- } catch (error) {
- log.error(
- `Scene actions generation failed [scene="${outlineTitle ?? 'unknown'}", model=${resolvedModelString ?? 'unknown'}]:`,
- error,
- );
- return apiError('INTERNAL_ERROR', 500, error instanceof Error ? error.message : String(error));
- }
- }
|