/** * 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 };