AINode.jsAutomation
AI Social Media Caption Generator
TT
TopicTrick Team
AI Social Media Caption Generator
Generate platform-perfect captions for Instagram, LinkedIn, Twitter/X, and TikTok from a single brief. This tool understands the unique tone, length, and hashtag strategy each platform demands and produces ready-to-post copy in one API call.
This is Tool 28 of the Build 50 AI Automation Tools course.
What You'll Build
POST /generate— topic/brief in, all-platform captions out- Platform-specific tone, length, emoji, and hashtag rules
- Image upload support via GPT-4o Vision
POST /generate/variants— A/B test variant generation
Setup
bash
mkdir ai-captions && cd ai-captions
npm init -y
npm install express multer openai dotenvbash
# .env
OPENAI_API_KEY=sk-your-key-here
PORT=3000Caption Generation Service
js
// src/services/captionService.js
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const PLATFORM_RULES = {
instagram: {
maxChars: 2200,
hashtagCount: '5-10',
emojiLevel: 'moderate',
tone: 'conversational, authentic, and aspirational',
cta: 'Save this post, tag a friend, or drop a comment',
},
linkedin: {
maxChars: 3000,
hashtagCount: '3-5',
emojiLevel: 'minimal (only for bullet points if needed)',
tone: 'professional, insight-driven, thought leadership',
cta: 'Ask a question to encourage professional discussion',
},
twitter: {
maxChars: 280,
hashtagCount: '1-2',
emojiLevel: 'optional',
tone: 'punchy, opinionated, conversational, no fluff',
cta: 'Encourage retweet or reply',
},
tiktok: {
maxChars: 2200,
hashtagCount: '3-5 trending tags',
emojiLevel: 'high — TikTok-native emojis',
tone: 'energetic, trend-aware, informal, Gen-Z friendly hook',
cta: 'Follow for more, comment your answer',
},
};
async function generateCaptions(brief, brand, platforms) {
const platformInstructions = platforms.map(p => {
const rules = PLATFORM_RULES[p];
return `${p.toUpperCase()}:
- Max chars: ${rules.maxChars} (Twitter: MUST be under 280)
- Hashtags: ${rules.hashtagCount}
- Emojis: ${rules.emojiLevel}
- Tone: ${rules.tone}
- CTA: ${rules.cta}`;
}).join('\n\n');
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `You are an expert social media copywriter for brands.
Generate platform-optimised captions for the following platforms.
Platform rules:
${platformInstructions}
Brand context: ${brand || 'Not specified — use a neutral professional brand voice'}
Return ONLY a JSON object with a key for each platform:
{
"instagram": { "caption": "...", "hashtags": ["tag1", "tag2"], "charCount": 150 },
"linkedin": { "caption": "...", "hashtags": ["tag1"], "charCount": 400 },
"twitter": { "caption": "...", "hashtags": ["tag1"], "charCount": 278 },
"tiktok": { "caption": "...", "hashtags": ["tag1", "tag2"], "charCount": 200 }
}
Only include keys for requested platforms.`,
},
{ role: 'user', content: `Brief: ${brief}` },
],
temperature: 0.6,
response_format: { type: 'json_object' },
});
return JSON.parse(response.choices[0].message.content);
}
async function generateFromImage(buffer, mimetype, brief, brand, platforms) {
const dataUrl = `data:${mimetype};base64,${buffer.toString('base64')}`;
// First, describe the image
const descResponse = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Describe this image in detail for social media caption writing. Include the mood, colors, subject, and any text visible.' },
{ type: 'image_url', image_url: { url: dataUrl, detail: 'low' } },
],
},
],
max_tokens: 300,
});
const imageDesc = descResponse.choices[0].message.content;
const enrichedBrief = `${brief ? brief + '\n\n' : ''}Image: ${imageDesc}`;
return generateCaptions(enrichedBrief, brand, platforms);
}
export async function generateAllCaptions({ brief, brand, platforms, buffer, mimetype }) {
const targetPlatforms = platforms?.length > 0
? platforms.filter(p => PLATFORM_RULES[p])
: Object.keys(PLATFORM_RULES);
if (buffer && mimetype) {
return generateFromImage(buffer, mimetype, brief, brand, targetPlatforms);
}
if (!brief?.trim()) throw new Error('Brief or image required');
return generateCaptions(brief, brand, targetPlatforms);
}
export async function generateVariants(brief, platform, count = 3) {
const rules = PLATFORM_RULES[platform];
if (!rules) throw new Error(`Unsupported platform: ${platform}`);
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `Generate ${count} A/B test variants for a ${platform} caption.
Each variant should have a different angle: one emotional, one informational, one CTA-forward.
Rules: ${JSON.stringify(rules)}
Return JSON: { "variants": [{ "angle": "string", "caption": "string", "hashtags": ["..."] }] }`,
},
{ role: 'user', content: brief },
],
temperature: 0.7,
response_format: { type: 'json_object' },
});
return JSON.parse(response.choices[0].message.content);
}Server
js
// src/server.js
import 'dotenv/config';
import express from 'express';
import multer from 'multer';
import { generateAllCaptions, generateVariants } from './services/captionService.js';
const app = express();
app.use(express.json());
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (_req, file, cb) =>
file.mimetype.startsWith('image/') ? cb(null, true) : cb(new Error('Images only')),
});
app.post('/generate', upload.single('image'), async (req, res, next) => {
try {
const { brief, brand, platforms } = req.body;
const parsedPlatforms = typeof platforms === 'string' ? JSON.parse(platforms) : platforms;
if (!brief && !req.file) {
return res.status(400).json({ error: 'brief or image required' });
}
const captions = await generateAllCaptions({
brief,
brand,
platforms: parsedPlatforms,
buffer: req.file?.buffer,
mimetype: req.file?.mimetype,
});
res.json({ success: true, captions });
} catch (err) { next(err); }
});
app.post('/generate/variants', async (req, res, next) => {
try {
const { brief, platform, count } = req.body;
if (!brief || !platform) return res.status(400).json({ error: 'brief and platform required' });
const result = await generateVariants(brief, platform, count);
res.json({ success: true, platform, ...result });
} catch (err) { next(err); }
});
app.use((err, _req, res, _next) => res.status(500).json({ error: err.message }));
app.listen(process.env.PORT ?? 3000, () => console.log('Caption generator running'));Testing
bash
# Text brief — all platforms
curl -X POST http://localhost:3000/generate \
-H "Content-Type: application/json" \
-d '{
"brief": "Launching our new AI-powered project management tool that auto-prioritises tasks",
"brand": "B2B SaaS startup, professional but approachable",
"platforms": ["instagram", "linkedin", "twitter"]
}'
# Image upload
curl -X POST http://localhost:3000/generate \
-F "image=@product-photo.jpg" \
-F "brief=New limited edition sneaker drop" \
-F 'platforms=["instagram","tiktok"]'Sample response (twitter):
json
{
"twitter": {
"caption": "Stop manually sorting your task list.\n\nOur AI does it for you — in real time.\n\nPriorities shift. Your list should too. 🧠\n\n#ProductivityAI #ProjectManagement",
"hashtags": ["ProductivityAI", "ProjectManagement"],
"charCount": 179
}
}Build 50 AI Automation Tools — Tool 28 of 50
Social media caption generator is live. Continue to Tool 29 to build an AI YouTube video summarizer.
Summary
- Platform rules object centralises the tone/length/hashtag constraints — update one object to change all generated content
- Vision-first brief for image uploads means zero manual description effort
- Variant generation enables A/B testing without manually writing multiple versions
- Structured JSON output connects directly to scheduling APIs like Buffer or Hootsuite
- Add a
scheduledAtfield and connect to Buffer's API to build a full social publishing pipeline
Continue to Tool 29: AI YouTube Video Summarizer →
