social-clips

Installation
SKILL.md

Social Clips

Turn Slack threads into animated social videos. Slack dark mode with real profile photos, typing indicators, reactions, and spring animations.

Outputs: MP4 (vertical + horizontal) and GIF.

Quick Start

npm install
npm run studio           # preview in browser
npm run render:stories   # 1080x1920 MP4
npm run render:landscape # 1920x1080 MP4
npm run gif:stories      # 1080x1920 GIF
npm run gif:landscape    # 1920x1080 GIF

Or render any composition directly:

npx remotion render <composition-id> out/<name>.mp4 --codec=h264 --crf=18

Making a New Clip

1. Pull the Slack thread

mcp__slack__slack_get_thread_replies(channel_id, thread_ts)

Extract thread_ts from the URL: p1234567890123456 → 1234567890.123456

2. Get avatar photos

mcp__slack__slack_get_users(limit: 200)

Download image_512 URLs into src/assets/avatars/:

curl -sL -o src/assets/avatars/name.jpg "https://avatars.slack-edge.com/..."

3. Add senders

In src/slack-types.ts:

  1. Add to the SlackSender union type
  2. Import the avatar image
  3. Add a SenderConfig entry with avatarPhoto

The avatar component renders the photo when available, falls back to colored initials.

4. Write the data file

Create src/data/<clip-name>.ts:

import type { SlackMessage, SlackTimedEvent } from '../slack-types';

export const MESSAGES: SlackMessage[] = [
  { id: 0, sender: 'dan', text: 'Opening message' },
  { id: 1, sender: 'r2c2', text: 'Reply with *bold* and @mentions' },
  { id: 2, sender: 'austin', text: 'Another message', reactions: [{ emoji: '🔥', count: 3 }] },
];

export const TIMELINE: SlackTimedEvent[] = [
  // Messages
  { type: 'message', messageIndex: 0, startFrame: 30, durationFrames: 40 },

  // Typing indicator before a reply
  { type: 'typing', typingSender: 'r2c2', startFrame: 75, durationFrames: 40 },
  { type: 'message', messageIndex: 1, startFrame: 115, durationFrames: 40 },

  // Human messages just appear (no typing indicator)
  { type: 'message', messageIndex: 2, startFrame: 165, durationFrames: 40 },

  // Reaction pops in after a message
  { type: 'reaction', messageIndex: 2, reactionIndex: 0, startFrame: 215, durationFrames: 20 },

  // Pause for tension
  { type: 'pause', typingSender: 'dan', startFrame: 240, durationFrames: 60 },
];

export const TOTAL_FRAMES = 1800; // 60s at 30fps
export const FPS = 30;

Text supports: @mentions, *bold*, \n newlines, • bullets

Consecutive messages from the same sender collapse the avatar + name automatically.

5. Register the composition

In src/Root.tsx:

import { MESSAGES, TIMELINE, TOTAL_FRAMES, FPS } from './data/my-clip';

// Vertical
<Composition
  id="my-clip-stories"
  component={SlackScreen}
  durationInFrames={TOTAL_FRAMES}
  fps={FPS}
  width={1080}
  height={1920}
  defaultProps={{ messages: MESSAGES, timeline: TIMELINE, layout: 'portrait' }}
/>

// Horizontal
<Composition
  id="my-clip-landscape"
  component={SlackScreen}
  durationInFrames={TOTAL_FRAMES}
  fps={FPS}
  width={1920}
  height={1080}
  defaultProps={{ messages: MESSAGES, timeline: TIMELINE, layout: 'landscape' }}
/>

6. Render

npx remotion render my-clip-stories out/my-clip-stories.mp4 --codec=h264 --crf=18
npx remotion render my-clip-landscape out/my-clip-landscape.mp4 --codec=h264 --crf=18

GIF conversion:

# Vertical
ffmpeg -y -i out/my-clip-stories.mp4 \
  -vf "fps=15,scale=540:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3" \
  out/my-clip-stories.gif

# Horizontal
ffmpeg -y -i out/my-clip-landscape.mp4 \
  -vf "fps=15,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3" \
  out/my-clip-landscape.gif

Narrative Arc

Find the spine of any thread:

Beat Msgs Look for
Hook 1-2 The inciting question
Brainstorm 3-6 Ideas flying, agents riffing
Conflict 2-3 Challenge, wrong turn, pushback
Breakthrough 1-2 The idea that lights everyone up
Eruption 3-5 Pile-on, excitement, reactions
Close 1-2 The line that crystallizes it

Rules:

  • ~80 words max per message
  • 15-21 messages for 60-75s
  • Agents get typing indicators, humans don't
  • Put the longest pause before the breakthrough
  • Eruption = fast pile-up (20-30 frame gaps)
  • Final hold: 7+ seconds

Timeline Reference

30fps. 30 frames = 1 second.

Duration Frames Messages
60s 1800 15-17
75s 2250 18-21
90s 2700 22-25
Event Frames Notes
Short message 25-35 ~1s read
Long message 45-60 ~2s read
Typing (fast) 25-35 Agent is quick
Typing (thinking) 45-55 Agent is deliberating
Brief pause 20-40 Beat
Big pause 80-120 Before breakthrough
Reaction 20 Quick pop
Final hold 200-360 Let it breathe

Components

File What
SlackScreen Main composition — header, messages, typing, input bar
SlackMessageRow Avatar, name, APP badge, text, reactions
SlackAvatar Photo with colored-initial fallback
SlackHeader "Thread" header with channel name (configurable)
SlackTypingIndicator Animated dots with sender name
SlackReactionPill Emoji + count pill
SlackInputBar Input field chrome

Types

type SlackSender = string;  // extend union in slack-types.ts

interface SlackMessage {
  id: number;
  text: string;
  sender: SlackSender;
  reactions?: Array<{ emoji: string; count: number }>;
}

interface SlackTimedEvent {
  type: 'message' | 'typing' | 'reaction' | 'pause';
  messageIndex?: number;
  reactionIndex?: number;
  typingSender?: SlackSender;
  startFrame: number;
  durationFrames: number;
}

interface SenderConfig {
  name: string;
  initials: string;
  avatarColor: string;
  isApp: boolean;
  avatarPhoto?: string;  // imported image path
}

Existing Clips

ID Size Content
plus-one-slack-stories 1080x1920 Plus One naming (75s)
plus-one-slack-landscape 1920x1080 Plus One naming (75s)
Weekly Installs
1
GitHub Stars
26
First Seen
Apr 4, 2026