| 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 |