142 lines
4.3 KiB
TypeScript
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;
|
|
}
|
|
}
|