-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathoutline-generator.ts
More file actions
184 lines (167 loc) · 6.84 KB
/
outline-generator.ts
File metadata and controls
184 lines (167 loc) · 6.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/**
* Stage 1: Generate scene outlines from user requirements.
* Also contains outline fallback logic.
*/
import { nanoid } from 'nanoid';
import { MAX_PDF_CONTENT_CHARS, MAX_VISION_IMAGES } from '@/lib/constants/generation';
import type {
UserRequirements,
SceneOutline,
PdfImage,
ImageMapping,
} from '@/lib/types/generation';
import { buildPrompt, PROMPT_IDS } from './prompts';
import { getGenerationLanguageSpec } from './language';
import { formatImageDescription, formatImagePlaceholder } from './prompt-formatters';
import { parseJsonResponse } from './json-repair';
import { uniquifyMediaElementIds } from './scene-builder';
import type { AICallFn, GenerationResult, GenerationCallbacks } from './pipeline-types';
import { createLogger } from '@/lib/logger';
const log = createLogger('Generation');
/**
* Generate scene outlines from user requirements
* Now uses simplified UserRequirements with just requirement text and language
*/
export async function generateSceneOutlinesFromRequirements(
requirements: UserRequirements,
pdfText: string | undefined,
pdfImages: PdfImage[] | undefined,
aiCall: AICallFn,
callbacks?: GenerationCallbacks,
options?: {
visionEnabled?: boolean;
imageMapping?: ImageMapping;
imageGenerationEnabled?: boolean;
videoGenerationEnabled?: boolean;
researchContext?: string;
teacherContext?: string;
},
): Promise<GenerationResult<SceneOutline[]>> {
const languageSpec = getGenerationLanguageSpec(requirements.language);
// Build available images description for the prompt
let availableImagesText = languageSpec.noImagesAvailableText;
let visionImages: Array<{ id: string; src: string }> | undefined;
if (pdfImages && pdfImages.length > 0) {
if (options?.visionEnabled && options?.imageMapping) {
// Vision mode: split into vision images (first N) and text-only (rest)
const allWithSrc = pdfImages.filter((img) => options.imageMapping![img.id]);
const visionSlice = allWithSrc.slice(0, MAX_VISION_IMAGES);
const textOnlySlice = allWithSrc.slice(MAX_VISION_IMAGES);
const noSrcImages = pdfImages.filter((img) => !options.imageMapping![img.id]);
const visionDescriptions = visionSlice.map((img) =>
formatImagePlaceholder(img, requirements.language),
);
const textDescriptions = [...textOnlySlice, ...noSrcImages].map((img) =>
formatImageDescription(img, requirements.language),
);
availableImagesText = [...visionDescriptions, ...textDescriptions].join('\n');
visionImages = visionSlice.map((img) => ({
id: img.id,
src: options.imageMapping![img.id],
width: img.width,
height: img.height,
}));
} else {
// Text-only mode: full descriptions
availableImagesText = pdfImages
.map((img) => formatImageDescription(img, requirements.language))
.join('\n');
}
}
// Build user profile string for prompt injection
const userProfileText =
requirements.userNickname || requirements.userBio
? `## Student Profile\n\nStudent: ${requirements.userNickname || 'Unknown'}${requirements.userBio ? ` — ${requirements.userBio}` : ''}\n\nConsider this student's background when designing the course. Adapt difficulty, examples, and teaching approach accordingly.\n\n---`
: '';
// Build media generation policy based on enabled flags
const imageEnabled = options?.imageGenerationEnabled ?? false;
const videoEnabled = options?.videoGenerationEnabled ?? false;
let mediaGenerationPolicy = '';
if (!imageEnabled && !videoEnabled) {
mediaGenerationPolicy =
'**IMPORTANT: Do NOT include any mediaGenerations in the outlines. Both image and video generation are disabled.**';
} else if (!imageEnabled) {
mediaGenerationPolicy =
'**IMPORTANT: Do NOT include any image mediaGenerations (type: "image") in the outlines. Image generation is disabled. Video generation is allowed.**';
} else if (!videoEnabled) {
mediaGenerationPolicy =
'**IMPORTANT: Do NOT include any video mediaGenerations (type: "video") in the outlines. Video generation is disabled. Image generation is allowed.**';
}
// Use simplified prompt variables
const prompts = buildPrompt(PROMPT_IDS.REQUIREMENTS_TO_OUTLINES, {
// New simplified variables
requirement: requirements.requirement,
language: requirements.language,
pdfContent: pdfText ? pdfText.substring(0, MAX_PDF_CONTENT_CHARS) : languageSpec.noneText,
availableImages: availableImagesText,
userProfile: userProfileText,
mediaGenerationPolicy,
researchContext: options?.researchContext || languageSpec.noneText,
// Server-side generation populates this via options; client-side populates via formatTeacherPersonaForPrompt
teacherContext: options?.teacherContext || '',
});
if (!prompts) {
return { success: false, error: 'Prompt template not found' };
}
try {
callbacks?.onProgress?.({
currentStage: 1,
overallProgress: 20,
stageProgress: 50,
statusMessage: '正在分析需求,生成场景大纲...',
scenesGenerated: 0,
totalScenes: 0,
});
const response = await aiCall(prompts.system, prompts.user, visionImages);
const outlines = parseJsonResponse<SceneOutline[]>(response);
if (!outlines || !Array.isArray(outlines)) {
return {
success: false,
error: 'Failed to parse scene outlines response',
};
}
// Ensure IDs, order, and language
const enriched = outlines.map((outline, index) => ({
...outline,
id: outline.id || nanoid(),
order: index + 1,
language: requirements.language,
}));
// Replace sequential gen_img_N/gen_vid_N with globally unique IDs
const result = uniquifyMediaElementIds(enriched);
callbacks?.onProgress?.({
currentStage: 1,
overallProgress: 50,
stageProgress: 100,
statusMessage: `已生成 ${result.length} 个场景大纲`,
scenesGenerated: 0,
totalScenes: result.length,
});
return { success: true, data: result };
} catch (error) {
return { success: false, error: String(error) };
}
}
/**
* Apply type fallbacks for outlines that can't be generated as their declared type.
* - interactive without interactiveConfig → slide
* - pbl without pblConfig or languageModel → slide
*/
export function applyOutlineFallbacks(
outline: SceneOutline,
hasLanguageModel: boolean,
): SceneOutline {
if (outline.type === 'interactive' && !outline.interactiveConfig) {
log.warn(
`Interactive outline "${outline.title}" missing interactiveConfig, falling back to slide`,
);
return { ...outline, type: 'slide' };
}
if (outline.type === 'pbl' && (!outline.pblConfig || !hasLanguageModel)) {
log.warn(
`PBL outline "${outline.title}" missing pblConfig or languageModel, falling back to slide`,
);
return { ...outline, type: 'slide' };
}
return outline;
}