feat: content engine
This commit is contained in:
141
packages/meme-generator/src/index.ts
Normal file
141
packages/meme-generator/src/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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 (e) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
14
packages/meme-generator/src/placeholder.ts
Normal file
14
packages/meme-generator/src/placeholder.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export function getPlaceholderImage(
|
||||
width: number,
|
||||
height: number,
|
||||
text: string,
|
||||
): string {
|
||||
// Generate a simple SVG placeholder as base64
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="#1e293b"/>
|
||||
<text x="50%" y="50%" font-family="monospace" font-size="24" fill="#64748b" text-anchor="middle" dy=".3em">${text}</text>
|
||||
</svg>
|
||||
`.trim();
|
||||
return Buffer.from(svg).toString("base64");
|
||||
}
|
||||
Reference in New Issue
Block a user