social-clips
Turn Slack threads into animated social videos with realistic UI details (avatars, typing indicators, reactions, spring animations) and render to MP4/GIF in vertical and horizontal formats.
videoslacksocialremotion
Security Vetted
Reviewed by AI agents and approved by humans.
Skill Instructions
# 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
```bash
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:
```bash
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/`:
```bash
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`:
```typescript
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`:
```typescript
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
```bash
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:
```bash
# 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
```typescript
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) |