caption-generator / caption_generator.js
VinOS Agent
feat: initial PAIPS caption generator — Instagram, LinkedIn, carousel, performance tracker
496ef98
/**
* PAIPS Caption Generator — core logic
* Generates Instagram captions, LinkedIn posts, carousel breakdowns.
* Tracks and analyses post performance.
*/
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const DB_FILE = path.join(__dirname, 'data', 'performance.json');
const MODEL = 'qwen/qwen3.6-plus:free';
function getApiKey() {
return process.env.OPENROUTER_API_KEY;
}
async function llm(prompt, maxTokens = 1024) {
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getApiKey()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: MODEL,
messages: [{ role: 'user', content: prompt }],
max_tokens: maxTokens,
}),
});
if (!res.ok) throw new Error(`OpenRouter ${res.status}: ${await res.text()}`);
const data = await res.json();
return data.choices[0].message.content.trim();
}
async function generateInstagram(tool, creation, beforeAfter, vibe) {
return llm(`Generate an Instagram caption for an AI demo post using the PAIPS framework.
Input:
- AI Tool: ${tool}
- What was created: ${creation}
- Before/After: ${beforeAfter}
- Vibe: ${vibe}
PAIPS Structure (use internally, do NOT print labels):
1. Problem — The challenge or pain point the audience faces
2. Agitate — Why it matters / what's at stake
3. Intrigue — A "what if..." that opens possibility
4. Positive Future — Paint the result/transformation
5. Solution — CTA to learn more
Rules:
- MUST be 150-200 words
- MUST include relevant emojis throughout
- MUST end with a CTA to follow or DM for more
- Vibe must match: ${vibe}
- First person, conversational tone
- No hashtags
- Output ONLY the caption, nothing else`, 512);
}
async function generateLinkedIn(tool, creation, beforeAfter, vibe) {
return llm(`Generate a LinkedIn post for an AI demo using the PAIPS framework reframed for thought leadership.
Input:
- AI Tool: ${tool}
- What was created: ${creation}
- Before/After: ${beforeAfter}
- Vibe: ${vibe}
PAIPS Structure (use internally, do NOT print labels):
1. Problem — A business inefficiency or scaling challenge your audience recognises
2. Agitate — The compounding cost of ignoring this (time, revenue, opportunity)
3. Intrigue — "What if your team/business could..." (scale angle, not cool demo)
4. Positive Future — The concrete business outcome this unlocks
5. Solution — CTA: follow for more, connect, or comment for the breakdown
Rules:
- MUST be 200-300 words
- Professional but authentic — no corporate jargon
- Frame around business value and scale, NOT "cool AI tool"
- 2-3 emojis maximum, used sparingly
- Short punchy first line (LinkedIn hook)
- No hashtags
- Output ONLY the post, nothing else`, 700);
}
async function generateCarousel(tool, creation, beforeAfter, vibe) {
return llm(`Generate a 9-slide Instagram carousel breakdown for an AI demo post.
Input:
- AI Tool: ${tool}
- What was created: ${creation}
- Before/After: ${beforeAfter}
- Vibe: ${vibe}
For each slide provide exactly:
Slide N: [TITLE]
Description: [1-2 sentence copy for the slide]
Visual: [Specific Canva design suggestion — layout, colors, elements]
Slide structure:
1. Hook slide — Bold problem statement or provocative question
2. The old way — What people do without AI (the pain)
3. The turning point — The moment everything changed
4. The tool reveal — What AI tool was used and why
5. Step 1 — First step of the process
6. Step 2 — Second step of the process
7. Step 3 / Result — The output or transformation
8. Business impact — Why this matters for scale/growth
9. CTA slide — What to do next (follow, DM, save)
Rules:
- Each title MUST be under 6 words
- Descriptions are the caption text on the slide
- Visual suggestions must be specific and actionable for Canva
- Match the vibe: ${vibe}
- Output all 9 slides, nothing else`, 900);
}
async function generateAll(tool, creation, beforeAfter, vibe) {
const [instagram, linkedin, carousel] = await Promise.all([
generateInstagram(tool, creation, beforeAfter, vibe),
generateLinkedIn(tool, creation, beforeAfter, vibe),
generateCarousel(tool, creation, beforeAfter, vibe),
]);
return { instagram, linkedin, carousel };
}
// ── Performance Tracker ───────────────────────────────────────────────────────
function loadDb() {
if (!fs.existsSync(DB_FILE)) return [];
return JSON.parse(fs.readFileSync(DB_FILE, 'utf8'));
}
function saveDb(data) {
fs.mkdirSync(path.dirname(DB_FILE), { recursive: true });
fs.writeFileSync(DB_FILE, JSON.stringify(data, null, 2), 'utf8');
}
function engagementScore(ig, li) {
const igScore = ig.likes + ig.comments * 3 + ig.saves * 5 + ig.signups * 10;
const liScore = li.reactions + li.comments * 3 + li.reposts * 5 + li.signups * 10;
return { igScore, liScore, winner: igScore >= liScore ? 'Instagram' : 'LinkedIn' };
}
function trackPost(record) {
const db = loadDb();
const scores = engagementScore(record.instagram, record.linkedin);
const entry = { ...record, date: record.date || new Date().toISOString().slice(0, 10), scores };
db.push(entry);
saveDb(db);
return entry;
}
function getInsights() {
const db = loadDb();
if (!db.length) return { posts: [], toolStats: {}, totals: { ig: 0, li: 0 }, topTool: null, topPlatform: null };
const toolStats = {};
let totalIg = 0, totalLi = 0;
for (const r of db) {
const t = r.tool;
if (!toolStats[t]) toolStats[t] = { posts: 0, igScore: 0, liScore: 0, signups: 0 };
const ig = r.instagram, li = r.linkedin;
const igs = ig.likes + ig.comments * 3 + ig.saves * 5 + ig.signups * 10;
const lis = li.reactions + li.comments * 3 + li.reposts * 5 + li.signups * 10;
toolStats[t].posts++;
toolStats[t].igScore += igs;
toolStats[t].liScore += lis;
toolStats[t].signups += ig.signups + li.signups;
totalIg += igs;
totalLi += lis;
}
const topTool = Object.entries(toolStats).sort((a, b) => b[1].signups - a[1].signups)[0]?.[0];
return {
posts: db,
toolStats,
totals: { ig: totalIg, li: totalLi },
topTool,
topPlatform: totalIg >= totalLi ? 'Instagram' : 'LinkedIn',
};
}
module.exports = { generateAll, trackPost, getInsights };