DEV Community

Olamide Olaniyan
Olamide Olaniyan

Posted on

Build a Cross-Platform Video Repurposer with Node.js

You made a TikTok that went viral.

Now you need to post it on YouTube Shorts, Instagram Reels, Twitter, and LinkedIn.

But each platform has different optimal lengths, caption styles, and hashtag strategies. So you end up manually rewriting everything four times.

That's insane.

In this tutorial, we'll build a Cross-Platform Video Repurposer that:

  1. Extracts the transcript from any viral video
  2. Generates optimized captions for every major platform
  3. Suggests platform-specific hashtags and hooks

One video. Five platforms. Zero manual rewriting.

The Platform Differences That Matter

Each platform has its own rules:

Platform Max Caption Hashtags Tone Hook Style
TikTok 4,000 chars 3-5 viral Casual, trendy Pattern interrupt
YouTube Shorts 100 chars title 3-5 in description Clickbait-y Curiosity gap
Instagram Reels 2,200 chars 10-30 Polished Story hook
Twitter/X 280 chars 1-2 Conversational Hot take
LinkedIn 3,000 chars 3-5 Professional Value prop

Doing this manually is a nightmare. AI does it in seconds.

The Stack

  • Node.js: Runtime
  • SociaVault API: To extract video transcripts
  • OpenAI API: To generate platform-specific content

Step 1: Setup

mkdir video-repurposer
cd video-repurposer
npm init -y
npm install axios openai dotenv
Enter fullscreen mode Exit fullscreen mode

Create .env:

SOCIAVAULT_API_KEY=your_sociavault_key
OPENAI_API_KEY=your_openai_key
Enter fullscreen mode Exit fullscreen mode

Step 2: Extract Video Transcript

First, we need to know what the video is about. Create index.js:

require('dotenv').config();
const axios = require('axios');
const OpenAI = require('openai');

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const SOCIAVAULT_BASE = 'https://api.sociavault.com';

async function getTikTokTranscript(videoUrl) {
  console.log('πŸ“₯ Extracting TikTok transcript...');

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/transcript`, {
      params: { url: videoUrl },
      headers: { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` }
    });

    return response.data.data;
  } catch (error) {
    console.error('Error getting transcript:', error.message);
    return null;
  }
}

async function getYouTubeTranscript(videoUrl) {
  console.log('πŸ“₯ Extracting YouTube transcript...');

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/youtube/video/transcript`, {
      params: { url: videoUrl },
      headers: { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` }
    });

    return response.data.data;
  } catch (error) {
    console.error('Error getting transcript:', error.message);
    return null;
  }
}

async function getTwitterVideoTranscript(tweetUrl) {
  console.log('πŸ“₯ Extracting Twitter video transcript...');

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/twitter/tweet/transcript`, {
      params: { url: tweetUrl },
      headers: { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` }
    });

    return response.data.data;
  } catch (error) {
    console.error('Error getting transcript:', error.message);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Get Video Metadata

Context helps AI write better captions:

async function getTikTokVideoInfo(videoUrl) {
  console.log('πŸ“₯ Fetching video metadata...');

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/video-info`, {
      params: { url: videoUrl, get_transcript: true },
      headers: { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` }
    });

    const data = response.data.data;

    return {
      description: data.desc || data.description,
      likes: data.diggCount || data.stats?.diggCount || 0,
      comments: data.commentCount || data.stats?.commentCount || 0,
      shares: data.shareCount || data.stats?.shareCount || 0,
      views: data.playCount || data.stats?.playCount || 0,
      music: data.music?.title || 'Original Sound',
      author: data.author?.uniqueId || data.authorMeta?.name,
      hashtags: data.challenges?.map(c => c.title) || []
    };
  } catch (error) {
    console.error('Error fetching video info:', error.message);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Platform-Specific Generators

Now the fun part. Let's create specialized generators for each platform:

async function generateTikTokCaption(transcript, videoInfo) {
  const prompt = `
    You are a TikTok content expert. Create an optimized TikTok caption.

    Original video transcript: "${transcript}"
    Original description: "${videoInfo?.description || 'N/A'}"
    Performance: ${videoInfo?.views?.toLocaleString() || 'N/A'} views

    TikTok caption rules:
    - Start with a hook (question, bold statement, or pattern interrupt)
    - Keep it conversational and authentic
    - Use line breaks for readability
    - End with a CTA (comment, follow, etc.)
    - Add 3-5 relevant trending hashtags
    - Can be up to 4,000 characters but shorter usually performs better

    Return JSON:
    {
      "caption": "the full caption with line breaks as actual newlines",
      "hashtags": ["hashtag1", "hashtag2", "hashtag3", "hashtag4", "hashtag5"],
      "cta": "the call to action used",
      "hook": "the hook strategy used"
    }
  `;

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    response_format: { type: 'json_object' }
  });

  return JSON.parse(completion.choices[0].message.content);
}

async function generateYouTubeShortsContent(transcript, videoInfo) {
  const prompt = `
    You are a YouTube Shorts expert. Create optimized title and description.

    Video transcript: "${transcript}"
    Original context: "${videoInfo?.description || 'N/A'}"

    YouTube Shorts rules:
    - Title: Max 100 characters, curiosity-driven, slightly clickbait-y
    - Description: Include searchable keywords
    - Hashtags: #Shorts is required, plus 2-4 relevant ones
    - Use numbers and power words in title

    Return JSON:
    {
      "title": "attention-grabbing title under 100 chars",
      "description": "SEO-optimized description with keywords",
      "hashtags": ["Shorts", "other", "relevant", "hashtags"],
      "tags": ["searchable", "video", "tags", "for", "youtube"]
    }
  `;

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    response_format: { type: 'json_object' }
  });

  return JSON.parse(completion.choices[0].message.content);
}

async function generateInstagramReelsCaption(transcript, videoInfo) {
  const prompt = `
    You are an Instagram content strategist. Create an optimized Reels caption.

    Video transcript: "${transcript}"
    Original context: "${videoInfo?.description || 'N/A'}"

    Instagram Reels caption rules:
    - Start with a strong hook (first line is crucial - it's the preview)
    - Tell a mini-story or share a tip
    - Use emojis strategically (not excessively)
    - Break into short paragraphs
    - End with a question or CTA to drive comments
    - Hashtags: 10-20 relevant ones, mix of popular and niche
    - Max 2,200 characters

    Return JSON:
    {
      "caption": "full caption with line breaks",
      "hashtags": ["list", "of", "15", "strategic", "hashtags"],
      "cta": "the engagement hook used",
      "firstLine": "the crucial first line that shows in preview"
    }
  `;

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    response_format: { type: 'json_object' }
  });

  return JSON.parse(completion.choices[0].message.content);
}

async function generateTwitterPost(transcript, videoInfo) {
  const prompt = `
    You are a Twitter/X viral content expert. Create an optimized tweet.

    Video transcript: "${transcript}"
    Original context: "${videoInfo?.description || 'N/A'}"

    Twitter rules:
    - Max 280 characters (including spaces)
    - Hot takes and opinions perform well
    - Conversational, like talking to a friend
    - Can be a thread if needed (indicate with 🧡)
    - 1-2 hashtags maximum (or none)
    - Questions drive replies

    Return JSON:
    {
      "tweet": "the main tweet under 280 chars",
      "threadContinuation": "optional second tweet if needed",
      "hashtags": ["max", "two"],
      "replyBait": "a question or statement to encourage replies"
    }
  `;

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    response_format: { type: 'json_object' }
  });

  return JSON.parse(completion.choices[0].message.content);
}

async function generateLinkedInPost(transcript, videoInfo) {
  const prompt = `
    You are a LinkedIn content strategist. Create a professional video post.

    Video transcript: "${transcript}"
    Original context: "${videoInfo?.description || 'N/A'}"

    LinkedIn rules:
    - Professional but not boring
    - Start with a bold statement or insight
    - Share a lesson, tip, or observation
    - Use line breaks (LinkedIn loves white space)
    - Include a personal angle or story if possible
    - End with a question or "Agree?"
    - 3-5 professional hashtags
    - Max 3,000 characters

    Return JSON:
    {
      "post": "full LinkedIn post with line breaks",
      "hook": "the first line that appears before 'see more'",
      "hashtags": ["professional", "relevant", "hashtags"],
      "engagementQuestion": "the closing question"
    }
  `;

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    response_format: { type: 'json_object' }
  });

  return JSON.parse(completion.choices[0].message.content);
}
Enter fullscreen mode Exit fullscreen mode

Step 5: The Main Repurposer

Tie it all together:

async function repurposeVideo(videoUrl, platform = 'tiktok') {
  console.log('\nπŸ”„ CROSS-PLATFORM VIDEO REPURPOSER\n');
  console.log('═══════════════════════════════════════════\n');

  // 1. Get transcript and video info
  let transcript, videoInfo;

  if (platform === 'tiktok') {
    transcript = await getTikTokTranscript(videoUrl);
    videoInfo = await getTikTokVideoInfo(videoUrl);
  } else if (platform === 'youtube') {
    transcript = await getYouTubeTranscript(videoUrl);
    videoInfo = { description: 'YouTube video' };
  } else if (platform === 'twitter') {
    transcript = await getTwitterVideoTranscript(videoUrl);
    videoInfo = { description: 'Twitter video' };
  }

  if (!transcript) {
    console.log('❌ Could not extract transcript. The video may not have speech.');
    return;
  }

  console.log('πŸ“ Transcript extracted');
  console.log(`   Length: ${transcript.length} characters\n`);

  if (videoInfo) {
    console.log('πŸ“Š Original Performance:');
    console.log(`   ${videoInfo.views?.toLocaleString() || 'N/A'} views`);
    console.log(`   ${videoInfo.likes?.toLocaleString() || 'N/A'} likes\n`);
  }

  // 2. Generate all platform versions
  console.log('πŸ€– Generating platform-specific versions...\n');

  const [tiktok, youtube, instagram, twitter, linkedin] = await Promise.all([
    generateTikTokCaption(transcript, videoInfo),
    generateYouTubeShortsContent(transcript, videoInfo),
    generateInstagramReelsCaption(transcript, videoInfo),
    generateTwitterPost(transcript, videoInfo),
    generateLinkedInPost(transcript, videoInfo)
  ]);

  // 3. Output results
  console.log('═══════════════════════════════════════════');
  console.log('πŸ“± TIKTOK');
  console.log('═══════════════════════════════════════════\n');
  console.log(tiktok.caption);
  console.log('\nHashtags:', tiktok.hashtags.map(h => `#${h}`).join(' '));
  console.log('Hook strategy:', tiktok.hook);

  console.log('\n═══════════════════════════════════════════');
  console.log('🎬 YOUTUBE SHORTS');
  console.log('═══════════════════════════════════════════\n');
  console.log('Title:', youtube.title);
  console.log('\nDescription:', youtube.description);
  console.log('\nHashtags:', youtube.hashtags.map(h => `#${h}`).join(' '));
  console.log('Tags:', youtube.tags.join(', '));

  console.log('\n═══════════════════════════════════════════');
  console.log('πŸ“Έ INSTAGRAM REELS');
  console.log('═══════════════════════════════════════════\n');
  console.log(instagram.caption);
  console.log('\nHashtags:');
  console.log(instagram.hashtags.map(h => `#${h}`).join(' '));

  console.log('\n═══════════════════════════════════════════');
  console.log('🐦 TWITTER/X');
  console.log('═══════════════════════════════════════════\n');
  console.log(twitter.tweet);
  if (twitter.threadContinuation) {
    console.log('\n🧡 Thread:', twitter.threadContinuation);
  }
  console.log('\nReply bait:', twitter.replyBait);

  console.log('\n═══════════════════════════════════════════');
  console.log('πŸ’Ό LINKEDIN');
  console.log('═══════════════════════════════════════════\n');
  console.log(linkedin.post);
  console.log('\nHashtags:', linkedin.hashtags.map(h => `#${h}`).join(' '));

  return { tiktok, youtube, instagram, twitter, linkedin };
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Run It

async function main() {
  const videoUrl = process.argv[2] || 'https://www.tiktok.com/@garyvee/video/7299831234567890';
  await repurposeVideo(videoUrl, 'tiktok');
}

main();
Enter fullscreen mode Exit fullscreen mode

Run with:

node index.js https://www.tiktok.com/@creator/video/1234567890
Enter fullscreen mode Exit fullscreen mode

Sample Output

πŸ”„ CROSS-PLATFORM VIDEO REPURPOSER
═══════════════════════════════════════════

πŸ“ Transcript extracted
   Length: 847 characters

πŸ“Š Original Performance:
   2,450,000 views
   312,000 likes

πŸ€– Generating platform-specific versions...

═══════════════════════════════════════════
πŸ“± TIKTOK
═══════════════════════════════════════════

Stop making this mistake with your morning routine 🚫

I used to wake up and immediately check my phone.

Emails. Notifications. Other people's problems.

By 8am my brain was fried.

Now I do this instead:
β€’ 10 min walk (no phone)
β€’ Cold water on face
β€’ Write 3 priorities for the day

My productivity went up 3x.

Try it tomorrow and comment what happened πŸ‘‡

Hashtags: #morningroutine #productivity #lifehacks #growthmindset #habits
Hook strategy: Pattern interrupt with "Stop making this mistake"

═══════════════════════════════════════════
🎬 YOUTUBE SHORTS
═══════════════════════════════════════════

Title: I Tried This Morning Routine for 30 Days (Life-Changing Results)

Description: The simple morning routine that 10x'd my productivity. No phone for the first hour, cold water therapy, and priority setting. Here's exactly what changed in my life after doing this for 30 days...

Hashtags: #Shorts #MorningRoutine #Productivity #LifeHacks
Tags: morning routine, productivity tips, habits, self improvement, morning habits

═══════════════════════════════════════════
πŸ“Έ INSTAGRAM REELS
═══════════════════════════════════════════

I was sabotaging my entire day before 8am β˜€οΈ

Here's what I mean...

Every morning I'd wake up and immediately grab my phone. Emails, notifications, DMsβ€”everyone else's priorities flooding my brain before I even got out of bed.

I felt productive. I wasn't.

The switch? πŸ‘‡

β€’ First hour: Phone stays in another room
β€’ 10 minute walk outside (rain or shine)
β€’ Cold water on my face
β€’ Write down 3 things that would make today a win

Small changes. Massive results.

The best part? My anxiety dropped and my focus went through the roof.

What's your morning routine? Drop it below πŸ‘‡

Hashtags:
#morningroutine #productivitytips #healthyhabits #selfimprovement #mindsetshift #dailyhabits #morningmotivation #successhabits #growthmindset #wellnessjourney #mindfulness #personalgrowth #lifestylechange #motivation #healthylifestyle

═══════════════════════════════════════════
🐦 TWITTER/X
═══════════════════════════════════════════

Hot take: Checking your phone first thing in the morning is why you're unproductive.

Your brain starts the day solving OTHER people's problems instead of your own.

Try: No phone for 60 min after waking. Just did this for a month. Changed everything.

Reply bait: What's the first thing you do when you wake up? Be honest.

═══════════════════════════════════════════
πŸ’Ό LINKEDIN
═══════════════════════════════════════════

I made one change to my morning routine.

It transformed my productivity.

For years, I started every day the same way:
β†’ Wake up
β†’ Grab phone
β†’ Check emails
β†’ Answer Slack messages

By 8am, I was mentally exhausted.

All my creative energy? Gone. Spent on everyone else's priorities.

So I tried something different:

No phone for the first 60 minutes of my day.

Instead:
β€’ Short walk outside
β€’ Write down 3 priorities
β€’ Actually eat breakfast

The results after 30 days:

βœ… 3x more deep work hours
βœ… Less anxiety
βœ… Better decisions
βœ… More energy at 5pm than 9am

The lesson?

How you start your day determines how you finish it.

What's your morning routine look like?

Hashtags: #productivity #leadership #morningroutine #worklifebalance #personaldevelopment
Enter fullscreen mode Exit fullscreen mode

Adding Batch Processing

For content teams repurposing multiple videos:

async function batchRepurpose(videoUrls) {
  const results = [];

  for (const url of videoUrls) {
    console.log(`\n${'─'.repeat(60)}\n`);
    const result = await repurposeVideo(url, 'tiktok');
    results.push({ url, ...result });

    // Rate limiting
    await new Promise(r => setTimeout(r, 2000));
  }

  // Export to JSON for your content calendar
  const fs = require('fs');
  fs.writeFileSync('repurposed-content.json', JSON.stringify(results, null, 2));

  console.log('\nβœ… All content exported to repurposed-content.json');

  return results;
}

// Usage
batchRepurpose([
  'https://www.tiktok.com/@creator/video/111',
  'https://www.tiktok.com/@creator/video/222',
  'https://www.tiktok.com/@creator/video/333'
]);
Enter fullscreen mode Exit fullscreen mode

Adding a Simple API

Turn it into a service your team can use:

const express = require('express');
const app = express();

app.use(express.json());

app.post('/repurpose', async (req, res) => {
  const { url, platform = 'tiktok' } = req.body;

  if (!url) {
    return res.status(400).json({ error: 'Video URL required' });
  }

  try {
    const results = await repurposeVideo(url, platform);
    res.json({ success: true, content: results });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => {
  console.log('Repurposer API running on http://localhost:3000');
});
Enter fullscreen mode Exit fullscreen mode

Cost Comparison

Manual repurposing:

  • Time: 30-60 minutes per video
  • If you value your time at $50/hour: $25-50 per video

This tool:

  • SociaVault transcript: $0.01
  • OpenAI (5 generations): $0.02
  • Total: $0.03 per video

That's a 1000x cost reduction.

What You Just Built

Content teams pay for tools like:

  • Repurpose.io: $25/month
  • Opus Clip: $19/month
  • Descript: $24/month

Your version does the caption/copy part for essentially free. Add video editing and you've got a full repurposing suite.

Get Started

  1. Get your SociaVault API Key
  2. Add your OpenAI key
  3. Paste any video URL
  4. Get all platforms instantly

One viral video deserves to live everywhere.


Time spent manually rewriting captions: 0. Content output: 5x.

Top comments (0)