返回 presentation-ai
generate-infographic.ts
根目录 / src / app / _actions / apps / image-studio / generate-infographic.ts
1 "use server";
2
3 import { fal } from "@fal-ai/client";
4 import { UTFile } from "uploadthing/server";
5
6 import { utapi } from "@/app/api/uploadthing/lib";
7 import {
8 DEFAULT_IMAGE_MODEL,
9 getFalImageGenerationInput,
10 type ImageModelList,
11 } from "@/constants/image-models";
12 import { env } from "@/env";
13 import { logger } from "@/lib/observability/server/logger";
14 import { auth } from "@/server/auth";
15 import { db } from "@/server/db";
16
17 fal.config({
18 credentials: env.FAL_API_KEY,
19 });
20
21 type GenerateInfographicImageActionInput = {
22 illustrationStyle?: string;
23 layout?: string;
24 model?: ImageModelList;
25 prompt: string;
26 };
27
28 function buildInfographicPrompt({
29 prompt,
30 illustrationStyle,
31 layout,
32 }: Required<
33 Pick<
34 GenerateInfographicImageActionInput,
35 "illustrationStyle" | "layout" | "prompt"
36 >
37 >) {
38 return [
39 "Create a polished, presentation-ready infographic image.",
40 `Topic and source content: ${prompt}`,
41 `Infographic layout: ${layout}.`,
42 `Illustration style: ${illustrationStyle}.`,
43 "Design requirements:",
44 "- Build a clear visual hierarchy with one concise headline, short supporting labels, and meaningful grouped sections.",
45 "- Use item labels of 20 characters or fewer and item descriptions of 60 characters or fewer.",
46 "- For layout-based infographics such as timeline, process, comparison, hierarchy, cycle, roadmap, or matrix layouts, show only the strongest 4 to 5 visible items. Synthesize extra source details into those items instead of adding more sections.",
47 "- Word clouds and chart-style visuals may include more items when useful.",
48 "- Use accurate, readable text only; avoid misspellings, warped letters, fake words, and placeholder gibberish.",
49 "- Convert the topic into a structured infographic with visual flow, icons, labels, connectors, and compact data callouts where useful.",
50 "- Keep the composition uncluttered with generous spacing, strong alignment, and balanced margins for slide embedding.",
51 "- Use a modern editorial presentation aesthetic with crisp vector-like shapes, high contrast, and clean typography.",
52 "- Avoid photorealistic scenes unless the prompt explicitly requires them; prioritize diagrammatic explanation over decoration.",
53 "- Do not include watermarks, UI chrome, browser frames, logos unless requested, QR codes, or stock-photo overlays.",
54 "- The final output must be a single complete infographic image ready to place directly into a presentation.",
55 ].join("\n");
56 }
57
58 export async function generateInfographicImageAction({
59 illustrationStyle = "Bauhaus",
60 layout = "Timeline",
61 model = DEFAULT_IMAGE_MODEL,
62 prompt,
63 }: GenerateInfographicImageActionInput) {
64 const trimmedPrompt = prompt.trim();
65 const actionName = "apps.image-studio.generateInfographicImageAction";
66 const span = logger.startSpan(`notebook.server_action.${actionName}`, {
67 attributes: {
68 "allweone.scope": "notebook",
69 "allweone.action.type": "server_action",
70 "allweone.action.name": actionName,
71 "allweone.server.image_generation.prompt.length": trimmedPrompt.length,
72 "allweone.server.image_generation.requested_model": model,
73 },
74 });
75
76 const session = await auth();
77
78 if (!session?.user?.id) {
79 span.annotate({
80 "allweone.server.image_generation.authorized": false,
81 });
82 span.end();
83 return {
84 success: false,
85 error: "You must be logged in to generate infographics",
86 };
87 }
88
89 if (!trimmedPrompt) {
90 span.end();
91 return {
92 success: false,
93 error: "Prompt is required",
94 };
95 }
96
97 const fullPrompt = buildInfographicPrompt({
98 prompt: trimmedPrompt,
99 illustrationStyle: illustrationStyle.trim() || "Bauhaus",
100 layout: layout.trim() || "Timeline",
101 });
102
103 try {
104 const actualModel = session.user.isAdmin ? model : DEFAULT_IMAGE_MODEL;
105
106 span.annotate({
107 "allweone.server.image_generation.authorized": true,
108 "allweone.server.image_generation.admin": session.user.isAdmin,
109 "allweone.server.image_generation.model": actualModel,
110 "allweone.server.image_generation.user_id": session.user.id,
111 });
112 span.event("allweone.server.image_generation.started", {
113 "allweone.server.image_generation.model": actualModel,
114 });
115
116 const result = await fal.subscribe(actualModel, {
117 input: getFalImageGenerationInput({
118 model: actualModel,
119 prompt: fullPrompt,
120 aspectRatio: "16:9",
121 }),
122 });
123
124 const imageUrl = result.data?.images?.[0]?.url;
125
126 if (!imageUrl) {
127 throw new Error("Failed to generate infographic");
128 }
129
130 span.event("allweone.server.image_generation.image_ready", {
131 "allweone.server.image_generation.source_url_available": true,
132 });
133
134 const imageResponse = await fetch(imageUrl);
135 if (!imageResponse.ok) {
136 throw new Error("Failed to download generated infographic");
137 }
138
139 const imageBlob = await imageResponse.blob();
140 const imageBuffer = await imageBlob.arrayBuffer();
141 const filename = `infographic_${Date.now()}.png`;
142 const utFile = new UTFile([new Uint8Array(imageBuffer)], filename);
143 const uploadResult = await utapi.uploadFiles([utFile]);
144 const permanentUrl = uploadResult[0]?.data?.ufsUrl;
145
146 if (!permanentUrl) {
147 throw new Error("Failed to upload generated infographic");
148 }
149
150 span.event("allweone.server.image_generation.upload_completed", {
151 "allweone.server.image_generation.uploaded": true,
152 });
153
154 const generatedImage = await db.generatedImage.create({
155 data: {
156 url: permanentUrl,
157 prompt: fullPrompt,
158 userId: session.user.id,
159 },
160 select: {
161 id: true,
162 prompt: true,
163 url: true,
164 },
165 });
166
167 span.event("allweone.server.image_generation.completed", {
168 "allweone.server.image_generation.generated_image.id": generatedImage.id,
169 });
170
171 return {
172 success: true,
173 image: generatedImage,
174 };
175 } catch (error) {
176 span.error(error);
177 return {
178 success: false,
179 error:
180 error instanceof Error
181 ? error.message
182 : "Failed to generate infographic",
183 };
184 } finally {
185 span.end();
186 }
187 }
188
188 lines TYPESCRIPT