Spaces:
Sleeping
Sleeping
File size: 6,636 Bytes
496ef98 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | /**
* 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 };
|