Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions skills/social-clips/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
out/
dist/
.DS_Store
245 changes: 245 additions & 0 deletions skills/social-clips/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
---
name: social-clips
description: >
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.
tags: [video, slack, social, remotion]
version: 1.0.0
---

# 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) |
Loading