Files
at-mintel/packages/meme-generator/src/index.ts

142 lines
4.3 KiB
TypeScript

import OpenAI from "openai";
export interface MemeSuggestion {
template: string;
captions: string[];
explanation: string;
}
/**
* Mapping of common meme names to memegen.link template IDs.
* See https://api.memegen.link/templates for the full list.
*/
export const MEMEGEN_TEMPLATES: Record<string, string> = {
drake: "drake",
"drake hotline bling": "drake",
"distracted boyfriend": "db",
distracted: "db",
"expanding brain": "brain",
expanding: "brain",
"this is fine": "fine",
fine: "fine",
clown: "clown-applying-makeup",
"clown applying makeup": "clown-applying-makeup",
"two buttons": "daily-struggle",
"daily struggle": "daily-struggle",
ds: "daily-struggle",
gru: "gru",
"change my mind": "cmm",
"always has been": "ahb",
"uno reverse": "uno",
"disaster girl": "disastergirl",
"is this a pigeon": "pigeon",
"roll safe": "rollsafe",
rollsafe: "rollsafe",
"surprised pikachu": "pikachu",
"batman slapping robin": "slap",
"left exit 12": "exit",
"one does not simply": "mordor",
"panik kalm panik": "panik",
};
/**
* Resolve a human-readable meme name to a memegen.link template ID.
* Falls back to slugified version of the name.
*/
export function resolveTemplateId(name: string): string {
if (!name) return "drake";
const normalized = name.toLowerCase().trim();
// Check if it's already a valid memegen ID
const validIds = new Set(Object.values(MEMEGEN_TEMPLATES));
if (validIds.has(normalized)) return normalized;
// Check mapping
if (MEMEGEN_TEMPLATES[normalized]) return MEMEGEN_TEMPLATES[normalized];
// STRICT FALLBACK: Prevent 404 image errors on the frontend
return "drake";
}
export class MemeGenerator {
private openai: OpenAI;
constructor(
apiKey: string,
baseUrl: string = "https://openrouter.ai/api/v1",
) {
this.openai = new OpenAI({
apiKey,
baseURL: baseUrl,
defaultHeaders: {
"HTTP-Referer": "https://mintel.me",
"X-Title": "Mintel AI Meme Generator",
},
});
}
async generateMemeIdeas(content: string): Promise<MemeSuggestion[]> {
const templateList = Object.keys(MEMEGEN_TEMPLATES)
.filter((k, i, arr) => arr.indexOf(k) === i)
.slice(0, 20)
.join(", ");
const response = await this.openai.chat.completions.create({
model: "google/gemini-2.5-flash",
messages: [
{
role: "system",
content: `You are a high-end Meme Architect for "Mintel.me", a boutique digital architecture studio.
Your persona is Marc Mintel: a technical expert, performance-obsessed, and "no-BS" digital architect.
Your Goal: Analyze the blog post content and suggest 3 high-fidelity, highly sarcastic, and provocative technical memes that would appeal to (and trigger) CEOs, CTOs, and high-level marketing engineers.
Meme Guidelines:
1. Tone: Extremely sarcastic, provocative, and "triggering". It must mock typical B2B SaaS/Agency mediocrity. Pure sarcasm that forces people to share it because it hurts (e.g. throwing 20k ads at an 8-second loading page, blaming weather for bounce rates).
2. Language: Use German for the captions. Use biting technical/business terms (e.g., "ROI-Killer", "Tracking-Müll", "WordPress-Hölle", "Marketing-Budget verbrennen").
3. Quality: Must be ruthless. Avoid generic "Low Effort" memes. The humor should stem from the painful reality of bad tech decisions.
IMPORTANT: Use ONLY template IDs from this list for the "template" field:
${templateList}
Return ONLY a JSON object:
{
"memes": [
{
"template": "memegen_template_id",
"captions": ["Top caption", "Bottom caption"],
"explanation": "Brief context on why this fits the strategy"
}
]
}
IMPORTANT: Return ONLY the JSON object. No markdown wrappers.`,
},
{
role: "user",
content,
},
],
response_format: { type: "json_object" },
});
const body = response.choices[0].message.content || '{"memes": []}';
let result;
try {
result = JSON.parse(body);
} catch {
console.error("Failed to parse AI response", body);
return [];
}
// Normalize template IDs
const memes: MemeSuggestion[] = (result.memes || []).map(
(m: MemeSuggestion) => ({
...m,
template: resolveTemplateId(m.template),
}),
);
return memes;
}
}