Spaces:
Sleeping
Sleeping
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 }; | |