route.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. /**
  2. * Scene Actions Generation API
  3. *
  4. * Generates actions for a scene given its outline and content,
  5. * then assembles the complete Scene object.
  6. * This is the second half of the two-step scene generation pipeline.
  7. */
  8. import { NextRequest } from 'next/server';
  9. import { callLLM } from '@/lib/ai/llm';
  10. import {
  11. generateSceneActions,
  12. buildCompleteScene,
  13. buildVisionUserContent,
  14. type SceneGenerationContext,
  15. type AgentInfo,
  16. } from '@/lib/generation/generation-pipeline';
  17. import type { SceneOutline } from '@/lib/types/generation';
  18. import type {
  19. GeneratedSlideContent,
  20. GeneratedQuizContent,
  21. GeneratedInteractiveContent,
  22. GeneratedPBLContent,
  23. } from '@/lib/types/generation';
  24. import type { SpeechAction } from '@/lib/types/action';
  25. import { createLogger } from '@/lib/logger';
  26. import { apiError, apiSuccess } from '@/lib/server/api-response';
  27. import { resolveModelFromHeaders } from '@/lib/server/resolve-model';
  28. const log = createLogger('Scene Actions API');
  29. export const maxDuration = 60;
  30. export async function POST(req: NextRequest) {
  31. let outlineTitle: string | undefined;
  32. let resolvedModelString: string | undefined;
  33. try {
  34. const body = await req.json();
  35. const {
  36. outline,
  37. allOutlines,
  38. content,
  39. stageId,
  40. agents,
  41. previousSpeeches: incomingPreviousSpeeches,
  42. userProfile,
  43. } = body as {
  44. outline: SceneOutline;
  45. allOutlines: SceneOutline[];
  46. content:
  47. | GeneratedSlideContent
  48. | GeneratedQuizContent
  49. | GeneratedInteractiveContent
  50. | GeneratedPBLContent;
  51. stageId: string;
  52. agents?: AgentInfo[];
  53. previousSpeeches?: string[];
  54. userProfile?: string;
  55. };
  56. // Validate required fields
  57. if (!outline) {
  58. return apiError('MISSING_REQUIRED_FIELD', 400, 'outline is required');
  59. }
  60. if (!allOutlines || allOutlines.length === 0) {
  61. return apiError(
  62. 'MISSING_REQUIRED_FIELD',
  63. 400,
  64. 'allOutlines is required and must not be empty',
  65. );
  66. }
  67. if (!content) {
  68. return apiError('MISSING_REQUIRED_FIELD', 400, 'content is required');
  69. }
  70. if (!stageId) {
  71. return apiError('MISSING_REQUIRED_FIELD', 400, 'stageId is required');
  72. }
  73. // ── Model resolution from request headers ──
  74. const { model: languageModel, modelInfo, modelString } = await resolveModelFromHeaders(req);
  75. outlineTitle = outline?.title;
  76. resolvedModelString = modelString;
  77. // Detect vision capability
  78. const hasVision = !!modelInfo?.capabilities?.vision;
  79. // AI call function (actions typically don't use vision, but kept for consistency)
  80. const aiCall = async (
  81. systemPrompt: string,
  82. userPrompt: string,
  83. images?: Array<{ id: string; src: string }>,
  84. ): Promise<string> => {
  85. if (images?.length && hasVision) {
  86. const result = await callLLM(
  87. {
  88. model: languageModel,
  89. system: systemPrompt,
  90. messages: [
  91. {
  92. role: 'user' as const,
  93. content: buildVisionUserContent(userPrompt, images),
  94. },
  95. ],
  96. maxOutputTokens: modelInfo?.outputWindow,
  97. },
  98. 'scene-actions',
  99. );
  100. return result.text;
  101. }
  102. const result = await callLLM(
  103. {
  104. model: languageModel,
  105. system: systemPrompt,
  106. prompt: userPrompt,
  107. maxOutputTokens: modelInfo?.outputWindow,
  108. },
  109. 'scene-actions',
  110. );
  111. return result.text;
  112. };
  113. // ── Build cross-scene context ──
  114. const allTitles = allOutlines.map((o) => o.title);
  115. const pageIndex = allOutlines.findIndex((o) => o.id === outline.id);
  116. const ctx: SceneGenerationContext = {
  117. pageIndex: (pageIndex >= 0 ? pageIndex : 0) + 1,
  118. totalPages: allOutlines.length,
  119. allTitles,
  120. previousSpeeches: incomingPreviousSpeeches ?? [],
  121. };
  122. // ── Generate actions ──
  123. log.info(`Generating actions: "${outline.title}" (${outline.type}) [model=${modelString}]`);
  124. const actions = await generateSceneActions(outline, content, aiCall, ctx, agents, userProfile);
  125. log.info(`Generated ${actions.length} actions for: "${outline.title}"`);
  126. // ── Build complete scene ──
  127. const scene = buildCompleteScene(outline, content, actions, stageId);
  128. if (!scene) {
  129. log.error(`Failed to build scene: "${outline.title}"`);
  130. return apiError('GENERATION_FAILED', 500, `Failed to build scene: ${outline.title}`);
  131. }
  132. // ── Extract speeches for cross-scene coherence ──
  133. const outputPreviousSpeeches = (scene.actions || [])
  134. .filter((a): a is SpeechAction => a.type === 'speech')
  135. .map((a) => a.text);
  136. log.info(
  137. `Scene assembled successfully: "${outline.title}" — ${scene.actions?.length ?? 0} actions`,
  138. );
  139. return apiSuccess({ scene, previousSpeeches: outputPreviousSpeeches });
  140. } catch (error) {
  141. log.error(
  142. `Scene actions generation failed [scene="${outlineTitle ?? 'unknown'}", model=${resolvedModelString ?? 'unknown'}]:`,
  143. error,
  144. );
  145. return apiError('INTERNAL_ERROR', 500, error instanceof Error ? error.message : String(error));
  146. }
  147. }