| |
| |
| |
| |
|
|
| import { nanoid } from 'nanoid'; |
| import type { |
| SceneOutline, |
| GeneratedSlideContent, |
| GeneratedQuizContent, |
| GeneratedInteractiveContent, |
| GeneratedPBLContent, |
| PdfImage, |
| ImageMapping, |
| } from '@/lib/types/generation'; |
| import type { LanguageModel } from 'ai'; |
| import type { Slide, SlideTheme } from '@/lib/types/slides'; |
| import type { Scene } from '@/lib/types/stage'; |
| import type { Action } from '@/lib/types/action'; |
| import { applyOutlineFallbacks } from './outline-generator'; |
| import { generateSceneContent, generateSceneActions } from './scene-generator'; |
| import type { AgentInfo, SceneGenerationContext, AICallFn } from './pipeline-types'; |
| import { buildLanguageText } from './prompt-formatters'; |
| import { createLogger } from '@/lib/logger'; |
| const log = createLogger('Generation'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function uniquifyMediaElementIds(outlines: SceneOutline[]): SceneOutline[] { |
| const idMap = new Map<string, string>(); |
|
|
| |
| for (const outline of outlines) { |
| if (!outline.mediaGenerations) continue; |
| for (const mg of outline.mediaGenerations) { |
| if (!idMap.has(mg.elementId)) { |
| const prefix = mg.type === 'video' ? 'gen_vid_' : 'gen_img_'; |
| idMap.set(mg.elementId, `${prefix}${nanoid(8)}`); |
| } |
| } |
| } |
|
|
| if (idMap.size === 0) return outlines; |
|
|
| |
| return outlines.map((outline) => { |
| if (!outline.mediaGenerations) return outline; |
| return { |
| ...outline, |
| mediaGenerations: outline.mediaGenerations.map((mg) => ({ |
| ...mg, |
| elementId: idMap.get(mg.elementId) || mg.elementId, |
| })), |
| }; |
| }); |
| } |
|
|
| |
| |
| |
| |
| export async function buildSceneFromOutline( |
| outline: SceneOutline, |
| aiCall: AICallFn, |
| stageId: string, |
| assignedImages?: PdfImage[], |
| imageMapping?: ImageMapping, |
| languageModel?: LanguageModel, |
| visionEnabled?: boolean, |
| ctx?: SceneGenerationContext, |
| agents?: AgentInfo[], |
| onPhaseChange?: (phase: 'content' | 'actions') => void, |
| userProfile?: string, |
| languageDirective?: string, |
| ): Promise<Scene | null> { |
| |
| outline = applyOutlineFallbacks(outline, !!languageModel); |
|
|
| const langText = buildLanguageText(languageDirective, outline.languageNote); |
|
|
| |
| onPhaseChange?.('content'); |
| log.debug(`Step 1: Generating content for: ${outline.title}`); |
| if (assignedImages && assignedImages.length > 0) { |
| log.debug( |
| `Using ${assignedImages.length} assigned images: ${assignedImages.map((img) => img.id).join(', ')}`, |
| ); |
| } |
| log.debug( |
| `imageMapping available: ${imageMapping ? Object.keys(imageMapping).length + ' keys' : 'undefined'}`, |
| ); |
| const content = await generateSceneContent(outline, aiCall, { |
| assignedImages, |
| imageMapping, |
| languageModel, |
| visionEnabled, |
| agents, |
| languageDirective: langText, |
| }); |
| if (!content) { |
| log.error(`Failed to generate content for: ${outline.title}`); |
| return null; |
| } |
|
|
| |
| onPhaseChange?.('actions'); |
| log.debug(`Step 2: Generating actions for: ${outline.title}`); |
| const actions = await generateSceneActions(outline, content, aiCall, { |
| ctx, |
| agents, |
| userProfile, |
| languageDirective: langText, |
| }); |
| log.debug(`Generated ${actions.length} actions for: ${outline.title}`); |
|
|
| |
| return buildCompleteScene(outline, content, actions, stageId); |
| } |
|
|
| |
| |
| |
| export function buildCompleteScene( |
| outline: SceneOutline, |
| content: |
| | GeneratedSlideContent |
| | GeneratedQuizContent |
| | GeneratedInteractiveContent |
| | GeneratedPBLContent, |
| actions: Action[], |
| stageId: string, |
| ): Scene | null { |
| const sceneId = nanoid(); |
|
|
| if (outline.type === 'slide' && 'elements' in content) { |
| |
| const defaultTheme: SlideTheme = { |
| backgroundColor: '#ffffff', |
| themeColors: ['#5b9bd5', '#ed7d31', '#a5a5a5', '#ffc000', '#4472c4'], |
| fontColor: '#333333', |
| fontName: 'Microsoft YaHei', |
| outline: { color: '#d14424', width: 2, style: 'solid' }, |
| shadow: { h: 0, v: 0, blur: 10, color: '#000000' }, |
| }; |
|
|
| const slide: Slide = { |
| id: nanoid(), |
| viewportSize: 1000, |
| viewportRatio: 0.5625, |
| theme: defaultTheme, |
| elements: content.elements, |
| background: content.background, |
| }; |
|
|
| return { |
| id: sceneId, |
| stageId, |
| type: 'slide', |
| title: outline.title, |
| order: outline.order, |
| content: { |
| type: 'slide', |
| canvas: slide, |
| }, |
| actions, |
| createdAt: Date.now(), |
| updatedAt: Date.now(), |
| }; |
| } |
|
|
| if (outline.type === 'quiz' && 'questions' in content) { |
| return { |
| id: sceneId, |
| stageId, |
| type: 'quiz', |
| title: outline.title, |
| order: outline.order, |
| content: { |
| type: 'quiz', |
| questions: content.questions, |
| }, |
| actions, |
| createdAt: Date.now(), |
| updatedAt: Date.now(), |
| }; |
| } |
|
|
| if (outline.type === 'interactive' && 'html' in content) { |
| return { |
| id: sceneId, |
| stageId, |
| type: 'interactive', |
| title: outline.title, |
| order: outline.order, |
| content: { |
| type: 'interactive', |
| url: '', |
| html: content.html, |
| |
| widgetType: content.widgetType, |
| widgetConfig: content.widgetConfig, |
| teacherActions: content.teacherActions, |
| }, |
| actions, |
| createdAt: Date.now(), |
| updatedAt: Date.now(), |
| }; |
| } |
|
|
| if (outline.type === 'pbl' && 'projectConfig' in content) { |
| return { |
| id: sceneId, |
| stageId, |
| type: 'pbl', |
| title: outline.title, |
| order: outline.order, |
| content: { |
| type: 'pbl', |
| projectConfig: content.projectConfig, |
| }, |
| actions, |
| createdAt: Date.now(), |
| updatedAt: Date.now(), |
| }; |
| } |
|
|
| return null; |
| } |
|
|