返回 presentation-ai
route.ts
1 import { createUIMessageStreamResponse } from "ai";
2 import {
3 assertModelIsConfigured,
4 ensureModelIsReady,
5 modelPicker,
6 } from "@/lib/modelPicker";
7 import { createLogger } from "@/lib/observability/logger";
8 import { toUIMessageStream } from "@ai-sdk/langchain";
9 import { auth } from "@/server/auth";
10 import { PromptTemplate } from "@langchain/core/prompts";
11 import { RunnableSequence } from "@langchain/core/runnables";
12 import { NextResponse } from "next/server";
13
14 interface ImageSlidesRequest {
15 title: string;
16 prompt: string;
17 outline: string[];
18 language: string;
19 modelId?: string;
20 modelProvider?: "openai" | "ollama" | "lmstudio";
21 presentationId?: string;
22 }
23
24 const IMAGE_SLIDES_TEMPLATE = `You are an expert visual presentation designer. Create image-based slides where each slide is a full-screen image with ALL text rendered inside the image itself (no separate text overlays).
25
26 # PRESENTATION CONTEXT
27
28 - **Title**: {TITLE}
29 - **Request**: {PROMPT}
30 - **Language**: {LANGUAGE}
31 - **Total Slides**: {TOTAL_SLIDES}
32
33 ## Outline Reference
34 Each outline item below is user-editable markdown. Preserve explicit bullets,
35 code fences, and formatting instructions when turning the item into on-image
36 text.
37
38 BEGIN OUTLINE
39 {OUTLINE_FORMATTED}
40 END OUTLINE
41
42 # OUTPUT FORMAT
43
44 Generate XML with image slides. Each slide should have:
45 1. A highly detailed AI image generation prompt (60-120 words, descriptive, artistic)
46 2. No text elements outside the image (no H1/H2/H3/P etc.)
47
48 \`\`\`xml
49 <PRESENTATION>
50 <SECTION isImageSlide="true">
51 <IMG query="detailed prompt for AI image generation, include style, mood, lighting, composition, AND the exact text that must be rendered in the image" />
52 </SECTION>
53 <!-- More SECTION tags... -->
54 </PRESENTATION>
55 \`\`\`
56
57 # IMAGE PROMPT GUIDELINES
58
59 Create detailed, artistic prompts that:
60 - Describe the visual scene, composition, and mood
61 - Include style references (photorealistic, illustration, cinematic, etc.)
62 - Mention lighting, colors, and atmosphere
63 - Are relevant to the slide topic from the outline
64 - Specify the exact on-image text using quotes
65 - Include typography guidance (font style, size, placement, contrast) to ensure readability
66 - Expand each outline item into the complete, final copy that should appear on the slide (titles, subtitles, bullets, labels, callouts, captions, legends, axes labels, and footnotes as needed)
67 - Do NOT use placeholders, brackets, or vague references; write every word exactly as it must appear in the image
68 - Do NOT leave any information implicit; the image model must not infer missing text
69 - Do NOT mention AI tools, models, or generation technology unless it is explicitly part of the slide content
70
71 # CRITICAL RULES
72
73 1. Generate **EXACTLY {TOTAL_SLIDES} slides** - one for each outline item
74 2. Each slide MUST have isImageSlide="true" attribute
75 3. Each slide MUST have an IMG tag with a detailed query (60-120 words)
76 4. The IMG query MUST include the exact on-image text in quotes
77 5. Do NOT include any other tags (no H1/H2/H3/P/COLUMNS/etc.)
78 6. Make image prompts visually descriptive and creative
79 7. Ensure variety in image styles and compositions across slides
80 8. Every slide must include all text that should appear in the image; do not output topics alone
81 9. If a slide needs multiple text blocks, list each block explicitly with its exact wording and placement
82 10. Do NOT include any example prompts in the output
83
84 Now generate the complete XML presentation with exactly {TOTAL_SLIDES} image slides.
85 `;
86
87 function formatOutlineForPrompt(outline: string[]): string {
88 return outline
89 .map((item, index) => `Slide ${index + 1}:\n${item.trim()}`)
90 .join("\n\n---\n\n");
91 }
92
93 export async function POST(req: Request) {
94 const requestId = crypto.randomUUID();
95 const routeLogger = createLogger("api:presentation-generate-image-slides");
96
97 try {
98 routeLogger.info("Image slide generation request received", { requestId });
99 const session = await auth();
100 if (!session) {
101 routeLogger.warn("Image slide generation request rejected: unauthorized", {
102 requestId,
103 });
104 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
105 }
106 if (!session.user.isAdmin) {
107 routeLogger.warn("Image slide generation request rejected: non-admin user", {
108 requestId,
109 });
110 return NextResponse.json(
111 { error: "This feature is only available for admin users" },
112 { status: 403 },
113 );
114 }
115
116 const {
117 title,
118 prompt: userPrompt,
119 outline,
120 language,
121 modelId,
122 modelProvider = "openai",
123 presentationId,
124 } = (await req.json()) as ImageSlidesRequest;
125
126 if (!title || !outline || !Array.isArray(outline) || !language) {
127 routeLogger.warn(
128 "Image slide generation request rejected: missing required fields",
129 {
130 requestId,
131 hasTitle: Boolean(title),
132 hasOutline: Array.isArray(outline),
133 language,
134 },
135 );
136 return NextResponse.json(
137 { error: "Missing required fields" },
138 { status: 400 },
139 );
140 }
141
142 const totalSlides = outline.length;
143
144 const prompt = PromptTemplate.fromTemplate(IMAGE_SLIDES_TEMPLATE);
145 routeLogger.info("Validated image slide generation request", {
146 requestId,
147 title,
148 totalSlides,
149 language,
150 modelProvider,
151 modelId: modelId || "gpt-4o-mini",
152 presentationId,
153 });
154 try {
155 assertModelIsConfigured(modelProvider, modelId);
156 } catch (error) {
157 routeLogger.error(
158 "Image slide generation request rejected: invalid model configuration",
159 error,
160 {
161 requestId,
162 modelProvider,
163 modelId: modelId || "gpt-4o-mini",
164 },
165 );
166 return NextResponse.json(
167 {
168 error:
169 error instanceof Error
170 ? error.message
171 : "Invalid model configuration",
172 },
173 { status: 400 },
174 );
175 }
176 try {
177 await ensureModelIsReady(modelProvider, modelId);
178 } catch (error) {
179 routeLogger.error(
180 "Image slide generation request rejected: selected model could not be prepared",
181 error,
182 {
183 requestId,
184 modelProvider,
185 modelId: modelId || "gpt-4o-mini",
186 },
187 );
188 return NextResponse.json(
189 {
190 error:
191 error instanceof Error
192 ? error.message
193 : "Failed to prepare selected model",
194 },
195 { status: 503 },
196 );
197 }
198 const model = modelPicker(modelProvider, modelId);
199 const chain = RunnableSequence.from([prompt, model]);
200
201 routeLogger.info("Image slide generation started", {
202 requestId,
203 title,
204 totalSlides,
205 modelProvider,
206 modelId: modelId || "gpt-4o-mini",
207 });
208 const stream = await chain.stream({
209 TITLE: title,
210 PROMPT: userPrompt || "No specific prompt provided",
211 LANGUAGE: language,
212 OUTLINE_FORMATTED: formatOutlineForPrompt(outline),
213 TOTAL_SLIDES: totalSlides,
214 });
215
216 routeLogger.info("Image slide generation stream created", {
217 requestId,
218 title,
219 totalSlides,
220 });
221 return createUIMessageStreamResponse({
222 stream: toUIMessageStream(stream),
223 });
224 } catch (error) {
225 routeLogger.error("Image slide generation failed", error, { requestId });
226 return NextResponse.json(
227 { error: "Failed to generate image slides" },
228 { status: 500 },
229 );
230 }
231 }
232
232 lines TYPESCRIPT