feat: upgrade thread image prompts to memetic infographic style

Replace generic "dark themed, minimal" prompts with detailed
infographic-style prompts that generate bold, shareable visuals
with typography overlays, iconic symbols, and data-viz elements.

Add try/catch error handling around fal.ai calls to prevent
unhandled network errors from crashing the request handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-05 20:44:08 -08:00
parent ed837d8e4f
commit f8557b1940
2 changed files with 53 additions and 22 deletions

View File

@ -21,22 +21,29 @@ export async function generateImageFromPrompt(prompt: string): Promise<string |
const FAL_KEY = process.env.FAL_KEY || "";
if (!FAL_KEY) return null;
const falRes = await fetch("https://fal.run/fal-ai/flux-pro/v1.1", {
method: "POST",
headers: {
Authorization: `Key ${FAL_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt,
image_size: "landscape_4_3",
num_images: 1,
safety_tolerance: "2",
}),
});
let falRes: Response;
try {
falRes = await fetch("https://fal.run/fal-ai/flux-pro/v1.1", {
method: "POST",
headers: {
Authorization: `Key ${FAL_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt,
image_size: "landscape_4_3",
num_images: 1,
safety_tolerance: "2",
}),
});
} catch (e: any) {
console.error("[image-gen] fal.ai network error:", e.message);
return null;
}
if (!falRes.ok) {
console.error("[image-gen] fal.ai error:", await falRes.text());
const errText = await falRes.text().catch(() => "unknown");
console.error(`[image-gen] fal.ai error (${falRes.status}):`, errText);
return null;
}

View File

@ -187,14 +187,26 @@ routes.post("/api/threads/:id/image", async (c) => {
if (!process.env.FAL_KEY) return c.json({ error: "FAL_KEY not configured" }, 503);
const summary = thread.tweets.slice(0, 3).join(" ").substring(0, 200);
const prompt = `Social media thread preview card about: ${summary}. Dark themed, modern, minimal style with abstract shapes.`;
const summary = thread.tweets.slice(0, 3).join(" ").substring(0, 300);
const prompt = `Create a bold, visually striking infographic-style image that captures the core message of this social media thread: "${summary}". Style: dark background (#0f172a to #1e293b gradient), bold typography with key phrases as large text overlays, iconic symbols and geometric shapes that represent the concepts, data visualization elements where relevant, modern flat illustration style with vibrant accent colors (electric blue, purple, teal). The image should work as a standalone visual summary — memetic, shareable, and instantly communicating the thread's main insight. No lorem ipsum or placeholder text.`;
const cdnUrl = await generateImageFromPrompt(prompt);
let cdnUrl: string | null;
try {
cdnUrl = await generateImageFromPrompt(prompt);
} catch (e: any) {
console.error("[rSocials] Thread image generation error:", e.message);
return c.json({ error: "Image generation failed: " + (e.message || "unknown error") }, 502);
}
if (!cdnUrl) return c.json({ error: "Image generation failed" }, 502);
const filename = `thread-${id}.png`;
const imageUrl = await downloadAndSaveImage(cdnUrl, filename);
let imageUrl: string | null;
try {
imageUrl = await downloadAndSaveImage(cdnUrl, filename);
} catch (e: any) {
console.error("[rSocials] Image download error:", e.message);
return c.json({ error: "Failed to download image: " + (e.message || "unknown error") }, 502);
}
if (!imageUrl) return c.json({ error: "Failed to download image" }, 502);
// Update Automerge doc with image URL
@ -302,10 +314,16 @@ routes.post("/api/threads/:id/tweet/:index/image", async (c) => {
if (!process.env.FAL_KEY) return c.json({ error: "FAL_KEY not configured" }, 503);
const tweetText = thread.tweets[tweetIndex].substring(0, 200);
const prompt = `Social media post image about: ${tweetText}. Dark themed, modern, minimal style with abstract shapes.`;
const tweetText = thread.tweets[tweetIndex].substring(0, 300);
const prompt = `Create a high-impact memetic infographic for this social media post: "${tweetText}". Style: dark background (#0f172a), bold statement typography with the key insight as large readable text, iconic flat-design symbols representing the core concept, subtle data-viz or diagram elements if the content references numbers/processes/systems, vibrant accent colors (electric blue #38bdf8, violet #a78bfa, teal #2dd4bf) on dark. The image should be instantly understandable, shareable, and visually distill the tweet's message into a single powerful graphic. No lorem ipsum or placeholder text. Landscape 4:3 ratio.`;
const cdnUrl = await generateImageFromPrompt(prompt);
let cdnUrl: string | null;
try {
cdnUrl = await generateImageFromPrompt(prompt);
} catch (e: any) {
console.error("[rSocials] Tweet image generation error:", e.message);
return c.json({ error: "Image generation failed: " + (e.message || "unknown error") }, 502);
}
if (!cdnUrl) return c.json({ error: "Image generation failed" }, 502);
const filename = `thread-${id}-tweet-${index}.png`;
@ -313,7 +331,13 @@ routes.post("/api/threads/:id/tweet/:index/image", async (c) => {
const oldUrl = thread.tweetImages?.[index];
if (oldUrl) await deleteOldImage(oldUrl, filename);
const imageUrl = await downloadAndSaveImage(cdnUrl, filename);
let imageUrl: string | null;
try {
imageUrl = await downloadAndSaveImage(cdnUrl, filename);
} catch (e: any) {
console.error("[rSocials] Image download error:", e.message);
return c.json({ error: "Failed to download image: " + (e.message || "unknown error") }, 502);
}
if (!imageUrl) return c.json({ error: "Failed to download image" }, 502);
const docId = socialsDocId(space);