# Social Media Posting

> Cross-platform social media content creation, posting, and engagement skill. Covers Instagram, LinkedIn, X/Twitter, Threads, YouTube, TikTok, Reddit, dev.to, Facebook, Discord, and Telegram. Handles AI image generation (Nano Banana Pro with 3 options per image at correct aspect ratios), AI video generation (Veo 3.1), carousel creation, platform-specific posting flows, deduplication against recent posts, strategic content planning (hook formulas, psychological angles, content pillars), brand context integration, proof screenshot creation for virality, and daily engagement (replying to 10 threads per platform). Use this skill whenever the user wants to post content to social media, create social media visuals, schedule posts, engage with followers, grow their audience, reply to threads, or manage any social media activity. Also activate when the user mentions any social platform by name, says 'post this', 'share on social', 'engage', 'reply to threads', 'publish a post about', 'social media blast', 'cross-post

- Skill: `anton-abyzov/social-media-posting-2` (Agent Skill, multi-file: 11 files)
- Install (CLI): `npx skillmds@latest add anton-abyzov/social-media-posting-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/anton-abyzov/social-media-posting-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: anton-abyzov (https://skillmd.com/u/anton-abyzov)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/anton-abyzov/social-media-posting-2

---


# Social Media Posting & Engagement

**Primary goal: go viral, grow followers, and promote the user's products and personal brand.**

Every post is an opportunity to attract new followers, build brand authority, and drive awareness of the user's products. Content that delivers value or tells a compelling story while naturally weaving in the brand is the highest-leverage output. Engagement, reach, and follower growth are the metrics that matter — optimize for them, not just for "posting something".

Post content across all major platforms with AI-generated visuals, strategic content planning, mandatory human approval, and daily engagement to grow your audience.

---

## Before Starting

Before writing any content, gather context:

1. **Check for a product/brand context file.** Look for `product-marketing-context.md` in the project's `.claude/` directory first, then the project root. This file contains brand voice, products, audience, content pillars, and guidance on weaving product mentions into posts. If it exists, read it -- every post should be informed by this context. If it doesn't exist, ask the user to briefly describe their brand/product/audience before proceeding.

2. **Check for a config file.** Look for a `config.json` in the skill directory or project. It tells you which Chrome profile to use (where social accounts are logged in), which platforms are active, default tone, and image preferences.

3. **Check published articles / reference material.** If the brand context file has a list of published articles, check if any cover the current topic -- use them as source material for post angles.

4. **Ensure image generation is available.** This skill uses the `nano-banana-pro` skill (Gemini image generation) for AI images. Check that `GEMINI_API_KEY` is set. If not, warn the user and proceed with copy-only output.

5. **Ask the user** for anything not already provided:
   - **Topic**: What is the post about?
   - **Context/angle**: Specific news, data, or event? Reference material?
   - **Tone**: Override default? (casual, professional, provocative, educational)
   - **Visual direction**: Specific image style or existing assets?
   - **Platforms to skip**: Any platforms to exclude?

---

## Core Rules (Non-Negotiable)

### 1. NEVER Post Without Approval

Every piece of content -- posts, comments, replies -- must be shown to you first. The workflow is always:

1. **Draft** the content (text + images)
2. **Present** it for review with a clear summary
3. **Wait** for explicit "go ahead" / "approved" / "post it"
4. **Post** only after approval
5. **Verify** the post is live and report the URL

This exists because social media is public, permanent, and tied to your reputation. A bad post can't be unseen. The cost of waiting 30 seconds for approval is nothing compared to the cost of posting something wrong.

### 2. Always Generate Images

Every post gets AI-generated visuals. Use the `nano-banana-pro` skill (Gemini 3 Pro Image) to create photo-realistic, editorial-quality images. For each image needed, generate **3 options** so the user can pick the best one. See the Image Generation section for exact dimensions per platform.

### 3. Check for Duplicates First

Before drafting any post, read the **last 10 posts** on each target platform. This prevents posting duplicate or near-duplicate content. Look for:
- Same topic covered in the last 48 hours
- Similar headlines or angles
- Overlapping key facts or data points

If you find overlap, flag it: "You posted about [topic] on [platform] [time ago]. Want to skip this platform, change the angle, or post anyway?"

### 4. Track Everything

After every post, collect the exact URL. Log all URLs (posts + engagement comments) to a daily log file. Never say "posted" without providing a clickable link.

### 5. Verify Before Reporting

Navigate to the profile/page after publishing and confirm the post actually appears. Be honest about failures -- never report unverified posts as successful.

---

## Strategic Thinking

Before writing a single word, think strategically:

### North Star

Every post serves one or more of these goals — always be explicit about which ones apply:

1. **Go viral** — high shareability, broad appeal, stops the scroll. Prioritize hooks, proof, and emotional resonance.
2. **Grow followers** — content that makes people think "I want more of this." End with a clear follow CTA or a hook into the next post.
3. **Promote the product** — weave the product in naturally as the tool that enabled the result, solved the problem, or made the story possible. Never feel like an ad.
4. **Build personal brand** — establish the user as a credible, interesting voice in their space. Opinions, behind-the-scenes, and unique perspectives beat generic tips.

Optimizing for all four simultaneously is possible — a viral behind-the-scenes post that mentions the product in passing is the gold standard. The worst outcome is content that does none of them (generic, forgettable, no CTA).

### Hook Formula

The first line determines whether anyone reads the rest. Use one of these patterns:

- **Curiosity gap**: "I was wrong about..." / "Nobody's talking about this..."
- **Contrarian take**: "X is wrong, here's why" / "Unpopular opinion:"
- **Story hook**: "3 years ago I..." / "Last week something happened..."
- **Value hook**: "How to X without Y" / "The 5-minute trick that..."
- **Data hook**: "We analyzed 10,000..." / "The numbers are in:"

### Psychological Angle

What makes this compelling?

- **Social proof**: Numbers, testimonials, adoption stats
- **Curiosity gap**: Incomplete information that demands completion
- **Loss aversion**: What they'll miss if they don't pay attention
- **Authority**: Expertise signal, credentials, experience
- **Reciprocity**: Giving value first, teaching something useful

### Copy Principles

- Clarity over cleverness
- Benefits over features
- Specificity over vagueness ("Cut reporting from 4 hours to 15 minutes" beats "streamline your workflow")
- Customer language over company language
- One idea per post -- don't cram multiple messages

### Content Pillar Check

Does this fit an existing pattern?

- **Educational**: Teaching something useful
- **Behind-the-scenes**: Process, struggles, real numbers
- **Personal story**: Lessons learned, failures, pivots
- **Industry insight**: Trends, analysis, predictions
- **Promotional**: Product launches, features, milestones

Keep promotional under 5% of posts. If a product/brand context file exists:

- ~60% of posts: mention a product as natural context (example, lesson, origin story)
- ~20% of posts: pure value, no product mention (builds trust and reach)
- ~15% of posts: directly about a product (releases, milestones, features)
- ~5% of posts: personal/humanizing, zero product angle

**The test:** Would a reader find the mention helpful, or would they roll their eyes? If helpful, include it. If eye-roll, skip it and just deliver value.

---

## Workflow Overview

```
0. CONTEXT     -> Read product-marketing-context.md + config, check published articles
1. DEDUP       -> Read last 10 posts per platform, flag overlaps
2. STRATEGIZE  -> Hook formula, psychological angle, content pillar selection
3. ANALYZE     -> Check post analytics history, recommend optimal posting time
4. CONTENT     -> Write platform-adapted copy + save per-platform copy files
5. IMAGES      -> Generate 3 options per image with Nano Banana Pro (correct aspect ratio)
6. PROOF       -> For X/Twitter & Threads: create proof/evidence screenshot as first image
7. VIDEO       -> If video content: Veo 3.1 AI video (or Ken Burns fallback)
8. REVIEW      -> Present everything + timing recommendation to user, wait for approval
9. POST        -> Attach image FIRST, wait for preview thumbnail, THEN click Post (X/Twitter + Threads: mandatory verification)
10. VERIFY     -> Navigate to published post URL, screenshot it, check text encoding + image/video present — see "Post-Publication Verification" section
11. ENGAGE     -> Find 10 threads per platform, draft replies, get approval
12. LOG        -> Write daily engagement log with all URLs + save copy files
```

---

## Virality and Proof Screenshots

### Why Proof Beats Generic AI Art

On X/Twitter and Threads, the **first image** in a post determines whether it gets engagement. Generic AI-generated art blends into the feed -- everyone's using it now. Screenshots of real results stop the scroll because they signal **authenticity and evidence**. A terminal showing test passes, a dashboard with real numbers, or a before/after comparison says "this is real" in a way that no AI-rendered abstract gradient ever will.

### What Counts as Proof

- **Terminal output**: Test passes, deployment logs, benchmark results, build output
- **Metrics dashboards**: Download counts, user growth charts, revenue screenshots, analytics
- **Before/after comparisons**: Code diff, performance numbers, UI redesign side-by-side
- **Code snippets with results**: A function and its actual output, a query and its response
- **Error messages that tell a story**: "This error cost us 3 days..." with the actual stack trace
- **Tool output**: npm download stats, GitHub stars graph, Vercel analytics, CI/CD results

### How to Create Proof Screenshots

1. **If the user provides a command or metric**, run it or ask them to screenshot it
2. **Use Puppeteer or Peekaboo** to capture the relevant screen/dashboard
3. **Crop to the key area** -- full-screen screenshots are noisy. Focus on the number, the result, the graph
4. **For terminal output**: Use a clean dark terminal theme, highlight the key numbers or success indicators
5. **For dashboards**: Capture the specific graph or metric that tells the story, not the entire page

### Image Order for X/Twitter and Threads

- **Image 1**: ALWAYS the proof screenshot (this appears in the timeline preview and drives clicks)
- **Image 2+**: AI-generated visuals from Nano Banana Pro (supporting/aesthetic images)

### When Proof Isn't Available

If the topic is abstract (opinion piece, thought leadership, general advice) and no proof screenshot exists:

1. **Ask the user** if they have any relevant screenshot, metric, or terminal output they could share
2. **If they don't**, fall back to **data visualization or infographic-style** AI art -- charts, diagrams, annotated screenshots -- rather than generic abstract imagery
3. **NEVER generate fake/mock terminal screenshots** -- that's dishonest and undermines trust

The workflow should never block over this. If no proof exists and the user confirms, proceed with the best available AI imagery.

---

## Available Tools (Prefer Dedicated Tools)

Multiple tools may be available for posting. Always prefer dedicated CLI/API tools over browser automation -- they're faster, more reliable, and don't break when UIs change.

### Tool Priority Order

| Platform | 1st Choice (Best) | 2nd Choice | 3rd Choice | Last Resort |
|----------|-------------------|------------|------------|-------------|
| X/Twitter | `xurl` CLI (if installed) | Puppeteer/browser | Peekaboo | Chrome-open |
| Discord | `discord` skill actions | Webhook API (`curl`) | Browser automation | Chrome-open |
| Telegram | Telegram Bot API (`curl`) | N/A | N/A | N/A |
| Instagram Carousel | API (`rupload_igphoto` + `configure_sidecar`) | Puppeteer + Chrome profile | Peekaboo | Chrome-open |
| Instagram Reel | API (`rupload_igvideo` + `configure_to_clips`) | Puppeteer + Chrome profile | Peekaboo | Chrome-open |
| LinkedIn | Puppeteer + Chrome profile | Peekaboo | Chrome-open | - |
| Threads | Puppeteer + Chrome profile | Peekaboo | Chrome-open | - |
| YouTube Community | Puppeteer + system clipboard | Peekaboo | Chrome-open | - |
| YouTube Video Upload | AppleScript + browser automation | Peekaboo | Chrome-open | - |
| Reddit | Puppeteer (old.reddit.com) | Peekaboo | Chrome-open | - |
| TikTok | Anti-bot override + AppleScript + cliclick | Peekaboo | Chrome-open | - |
| dev.to | Puppeteer + Chrome profile | Peekaboo | Chrome-open | - |
| Facebook | Puppeteer + Chrome profile | Peekaboo | Chrome-open | - |

### X/Twitter via `xurl` (Preferred)

If the `xurl` CLI is installed (`xurl auth status` to check), use it instead of browser automation:

```bash
# Post with image
xurl media upload image.png           # get MEDIA_ID from response
xurl post "Your tweet text" --media-id MEDIA_ID

# Thread: post main tweet, then reply
xurl post "Main tweet"                # get POST_ID
xurl reply POST_ID "Thread continuation"

# Search for engagement threads
xurl search "topic keywords" -n 10

# Reply to a thread
xurl reply POST_ID "Your thoughtful reply"

# Read recent posts (dedup check)
xurl timeline -n 10
```

### Discord via Skill Actions (Preferred)

If the `discord` skill is available, use its native actions:

```json
{
  "action": "sendMessage",
  "to": "channel:<CHANNEL_ID>",
  "content": "Your announcement text",
  "mediaUrl": "file:///path/to/image.png"
}
```

```json
{
  "action": "readMessages",
  "channelId": "<CHANNEL_ID>",
  "limit": 10
}
```

### Peekaboo (macOS UI Fallback)

When browser automation (Puppeteer) isn't available or breaks, use the `peekaboo` skill for macOS UI automation:

```bash
# See what's on screen, identify clickable elements
peekaboo see --app "Google Chrome" --annotate --path /tmp/see.png

# Click an identified element
peekaboo click --on B3 --app "Google Chrome"

# Type text
peekaboo type "Your post content" --app "Google Chrome"

# Paste from clipboard (for editors that reject keyboard input)
echo "Your text" | pbcopy
peekaboo hotkey --keys "cmd,v" --app "Google Chrome"
```

This is particularly useful when:
- Puppeteer can't connect to Chrome
- A platform's DOM changed and selectors broke
- File upload dialogs need native OS interaction
- You need to interact with system dialogs (CAPTCHA, etc.)

### AppleScript File Picker: Keyboard Language Issue

**This is a silent failure.** When AppleScript uses `Cmd+Shift+G` to open the "Go to Folder" dialog in a macOS file picker, any keystrokes go through the currently active system keyboard input source. If the user has a non-Latin layout active (Russian, Japanese, Arabic, etc.), the typed path characters get converted — `/Users/anton/image.png` becomes garbage, the dialog finds nothing, and the file is never attached. No error is thrown.

**Always switch the input source to English before typing any path, then restore it.**

```python
import subprocess, time

def ensure_english_input_source():
    """Switch to a Latin/English input source before typing paths in macOS dialogs.
    Returns the name of the previous input source so it can be restored."""
    script = '''
tell application "System Events"
    set currentName to name of current input source
    set allSources to input sources
    set englishNames to {"U.S.", "ABC", "U.S. Extended", "U.S. International - PC", "British", "Australian"}
    set alreadyEnglish to false
    repeat with n in englishNames
        if currentName is n then
            set alreadyEnglish to true
            exit repeat
        end if
    end repeat
    if alreadyEnglish then
        return "already_english"
    end if
    repeat with src in allSources
        if (name of src) is in englishNames then
            set current input source to src
            return currentName
        end if
    end repeat
    return "no_english_source_found"
end tell
'''
    result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True)
    return result.stdout.strip()

def restore_input_source(original_name):
    if original_name in ('already_english', 'no_english_source_found', ''):
        return
    script = f'''
tell application "System Events"
    repeat with src in input sources
        if (name of src) is "{original_name}" then
            set current input source to src
            exit repeat
        end if
    end repeat
end tell
'''
    subprocess.run(['osascript', '-e', script])

# Usage pattern:
original_lang = ensure_english_input_source()
time.sleep(0.3)  # give the OS time to switch before typing

# ... Cmd+Shift+G, type the path, press Enter ...

restore_input_source(original_lang)
```

**Checklist when using any AppleScript file picker:**

1. Call `ensure_english_input_source()` BEFORE triggering `Cmd+Shift+G`
2. Add a 0.3s delay after switching — the OS needs a moment to apply the change
3. Type the path
4. Restore the original input source after pressing Enter
5. If `no_english_source_found` is returned, warn the user: "Please temporarily switch your keyboard to English in System Settings → Keyboard → Input Sources, then retry"

**Diagnosing silently failed path input:** If the file picker opened but nothing was selected, run:
```bash
osascript -e 'tell application "System Events" to return name of current input source'
```
If it returns anything other than a Latin layout name, the language mismatch was the cause.

### Chrome-Open (Last Resort)

When all automation fails, open the platform's compose page in Chrome with the user's configured profile:

```bash
open -na "Google Chrome" --args --profile-directory="PROFILE_DIR" "COMPOSE_URL"
```

Then provide a per-platform posting guide for each tab:
- **Copy from**: full absolute path to the `.txt` file
- **Attach image**: full absolute path to the image file
- **First comment**: if applicable, what to paste after publishing

The user pastes, attaches, reviews, and clicks publish.

---

## Image Generation with Nano Banana Pro

Use the `nano-banana-pro` skill for all image generation. The script path is:
```
{nano-banana-pro baseDir}/scripts/generate_image.py
```

**Fallback (direct Gemini API)** -- if the `nano-banana-pro` skill is not installed:

```python
import json, base64, os, urllib.request

api_key = os.environ['GEMINI_API_KEY']
model = 'gemini-3-pro-image-preview'
url = f'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}'

payload = {
    'contents': [{'parts': [{'text': 'PROMPT_HERE'}]}],
    'generationConfig': {'responseModalities': ['TEXT', 'IMAGE']}
}

req = urllib.request.Request(url, data=json.dumps(payload).encode(),
    headers={'Content-Type': 'application/json'}, method='POST')

with urllib.request.urlopen(req, timeout=120) as resp:
    result = json.loads(resp.read())
    for part in result['candidates'][0]['content']['parts']:
        if 'inlineData' in part:
            img_data = base64.b64decode(part['inlineData']['data'])
            with open('output.png', 'wb') as f:
                f.write(img_data)
```

### Platform-Specific Dimensions

Each platform has strict aspect ratio requirements. **Always generate at the correct ratio from the start** — resizing after the fact loses quality. Include the target dimensions in your prompt so the model composes correctly.

| Platform | Best Ratio | Pixels | Allowed Range | Max File Size | JPEG Quality | Resolution Flag |
|----------|-----------|--------|---------------|---------------|--------------|-----------------|
| Instagram Feed (portrait) | **4:5** | 1080x1350 | 0.8–1.91 | 8 MB | 95+ | `--resolution 2K` |
| Instagram Feed (square) | 1:1 | 1080x1080 | 0.8–1.91 | 8 MB | 95+ | `--resolution 2K` |
| Instagram Carousel | 1:1 or 4:5 | 1080x1080 or 1080x1350 | 0.8–1.91 (all slides same ratio) | 8 MB each | 95+ | `--resolution 2K` |
| Instagram Story/Reel | 9:16 | 1080x1920 | 9:16 only | 8 MB | 95+ | `--resolution 2K` |
| TikTok | 9:16 | 1080x1920 | 9:16 strongly preferred | 20 MB | 90+ | `--resolution 2K` |
| X/Twitter | 16:9 | 1200x675 | any, but 16:9 crops best in preview | 5 MB | 85+ | `--resolution 1K` |
| LinkedIn | 1.91:1 | 1200x628 | any | 8 MB | 90+ | `--resolution 1K` |
| Facebook | 1.91:1 | 1200x628 | any | 8 MB | 90+ | `--resolution 1K` |
| YouTube Community | 16:9 | 1280x720 | any | 8 MB | 90+ | `--resolution 1K` |
| YouTube Thumbnail | 16:9 | 1280x720 | 16:9 only | 2 MB | 90+ | `--resolution 1K` |
| Threads | 1:1 | 1080x1080 | 0.8–1.91 | 8 MB | 90+ | `--resolution 1K` |
| Discord | 16:9 | 1280x720 | any | 8 MB | 85+ | `--resolution 1K` |
| Telegram | 16:9 | 1280x720 | any | 10 MB | 85+ | `--resolution 1K` |
| dev.to | 1000x420 | 1000x420 | any | 1 MB | 85+ | `--resolution 1K` |

**Instagram 4:5 portrait is the recommended default** — it takes up the most vertical screen space in the feed, which increases impressions before the user scrolls past.

### Image Aspect Ratio Enforcement

**Always validate and fix aspect ratio BEFORE uploading.** Instagram (and TikTok) will hard-reject images outside their allowed range with an "uploaded image isn't in an allowed aspect ratio" error. This must be caught and fixed, not reported back to the user as a failure.

**Two strategies — choose based on image type:**

| Image Type | Strategy | Why |
|------------|----------|-----|
| AI-generated image | Regenerate with correct ratio in the prompt | Easiest — just add the ratio to the generation prompt |
| User screenshot / proof image | **Pad, never crop** | Cropping cuts content; padding preserves everything |
| Logo or icon | Pad with transparent/dark background | Keeps proportions exact |

**Step 1: Check the aspect ratio before uploading**

```python
from PIL import Image

def check_aspect_ratio(img_path, platform='instagram'):
    """Check if image ratio is within platform's allowed range."""
    img = Image.open(img_path)
    w, h = img.size
    ratio = w / h

    limits = {
        'instagram': (0.8, 1.91),   # 4:5 portrait to 1.91:1 landscape
        'threads':   (0.8, 1.91),
        'tiktok':    (0.5, 1.0),    # strongly prefer 9:16 = 0.5625
        'twitter':   (0.33, 3.0),   # very permissive
    }
    lo, hi = limits.get(platform, (0.5, 2.0))

    if ratio < lo:
        return False, ratio, 'too_tall'
    elif ratio > hi:
        return False, ratio, 'too_wide'
    return True, ratio, 'ok'
```

**Step 2: Fix it — pad, never crop (especially for screenshots)**

```python
from PIL import Image, ImageFilter

def fit_to_ratio(input_path, output_path, target_w, target_h, background='blur'):
    """
    Fit image into target dimensions without cropping any content.

    background='blur'  → blurred + darkened version of the image fills the padding (looks natural, no black bars)
    background='dark'  → solid dark (#0f0f0f) padding (cleaner for screenshots with dark backgrounds)
    background='white' → solid white padding (cleaner for screenshots with white backgrounds)
    """
    img = Image.open(input_path).convert('RGB')
    src_w, src_h = img.size
    src_ratio = src_w / src_h
    tgt_ratio = target_w / target_h

    # Build background canvas
    if background == 'blur':
        bg = img.resize((target_w, target_h), Image.LANCZOS)
        bg = bg.filter(ImageFilter.GaussianBlur(radius=40))
        # Darken so the content stands out
        dark = Image.new('RGB', (target_w, target_h), (0, 0, 0))
        bg = Image.blend(bg, dark, 0.45)
    elif background == 'white':
        bg = Image.new('RGB', (target_w, target_h), (255, 255, 255))
    else:
        bg = Image.new('RGB', (target_w, target_h), (15, 15, 15))

    # Scale image to fit, preserving ALL content (letterbox/pillarbox)
    if src_ratio > tgt_ratio:
        new_w = target_w
        new_h = int(target_w / src_ratio)
    else:
        new_h = target_h
        new_w = int(target_h * src_ratio)

    img_scaled = img.resize((new_w, new_h), Image.LANCZOS)

    # Center on background
    x = (target_w - new_w) // 2
    y = (target_h - new_h) // 2
    bg.paste(img_scaled, (x, y))

    bg.save(output_path, 'JPEG', quality=95, optimize=True, progressive=True)
    print(f"Saved {output_path} ({target_w}x{target_h}, original: {src_w}x{src_h})")
    return output_path
```

**Step 3: Choose the right target for Instagram**

```python
def get_instagram_target(img_path):
    """Return the best Instagram canvas size for this image."""
    img = Image.open(img_path)
    w, h = img.size
    ratio = w / h

    if ratio > 1.91:
        # Way too wide (e.g., ultrawide screenshot) → fit into 4:5 canvas
        return (1080, 1350)  # 4:5 portrait — max vertical real estate
    elif ratio > 1.0:
        # Landscape screenshot → fit into square (keeps it neutral)
        return (1080, 1080)
    elif ratio >= 0.8:
        # Already in range → just resize to 1080 width, keep ratio
        new_h = int(1080 / ratio)
        return (1080, new_h)
    else:
        # Too tall → fit into 4:5
        return (1080, 1350)
```

**Full workflow for any image before Instagram upload:**

```python
valid, ratio, status = check_aspect_ratio(img_path, platform='instagram')
if not valid:
    target_w, target_h = get_instagram_target(img_path)
    # For screenshots: use 'blur' background (matches dark terminal themes)
    # For logos/icons: use 'dark' background
    # For light-background screenshots: use 'white' background
    background = 'blur'  # default; adjust based on image content
    img_path = fit_to_ratio(img_path, img_path.replace('.png', '-ig.jpg'), target_w, target_h, background)
    print(f"Fixed aspect ratio: {ratio:.2f} → {target_w/target_h:.2f} ({target_w}x{target_h})")
# Now safe to upload
```

**For AI-generated images:** don't resize — instead re-run generation with the correct ratio in the prompt. Resizing an AI image introduces quality loss. Only use `fit_to_ratio` for user-provided screenshots and assets.

**Quality checklist before uploading any image:**
- [ ] Aspect ratio is within platform's allowed range (checked programmatically)
- [ ] Long edge is at least 1080px (Instagram rejects undersized images)
- [ ] File size is under the platform limit (use `os.path.getsize()`)
- [ ] JPEG quality is 95+ for Instagram, 90+ for others
- [ ] For screenshots: padding strategy chosen to match the screenshot's background tone

### Generating 3 Options Per Image

For every image, generate 3 distinct options with varied composition. Include the aspect ratio in the prompt:

```bash
# Option A -- dramatic angle
uv run {baseDir}/scripts/generate_image.py \
  --prompt "square 1:1 composition, photorealistic tech editorial: [topic], cinematic lighting, dark gradient, clean typography overlay" \
  --filename "2026-03-04-topic-option-a.png" --resolution 2K

# Option B -- data/infographic style
uv run {baseDir}/scripts/generate_image.py \
  --prompt "square 1:1 composition, [alternative visual: data visualization, charts, key stats]" \
  --filename "2026-03-04-topic-option-b.png" --resolution 2K

# Option C -- human/product focus
uv run {baseDir}/scripts/generate_image.py \
  --prompt "square 1:1 composition, [third approach: device mockup, person using tech, product shot]" \
  --filename "2026-03-04-topic-option-c.png" --resolution 2K
```

Present all 3 to the user: "Here are 3 image options. Which do you prefer? (A/B/C, or I can regenerate)"

### Carousels (Instagram/TikTok)

Generate **5 slides**, each with **3 options** (15 images total). Present as a grid so the user can mix and match:

```
Slide 1 (Hook):    [A1] [B1] [C1]
Slide 2 (Point 1): [A2] [B2] [C2]
Slide 3 (Point 2): [A3] [B3] [C3]
Slide 4 (Point 3): [A4] [B4] [C4]
Slide 5 (CTA):     [A5] [B5] [C5]
```

**Logo handling:** When mentioning companies, scrape and download their official logos first. Pass logos as input images (`-i logo.png`) to Nano Banana Pro so they render correctly.

**Carousel compositing with Pillow** (for text overlays on backgrounds):

```python
from PIL import Image, ImageDraw, ImageFont, ImageFilter

img = Image.open('background.png').convert('RGBA').resize((1080, 1080), Image.LANCZOS)
overlay = Image.new('RGBA', img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
# Dark gradient at bottom 45%
for y in range(594, 1080):
    alpha = int(40 + 200 * ((y - 594) / 486))
    draw.rectangle([(0, y), (1080, y+1)], fill=(0, 0, 0, min(alpha, 230)))
img = Image.alpha_composite(img, overlay)
# Add text with ImageFont
```

### Image Style Guidelines

- Photo-realistic, cinematic lighting, editorial quality
- Dark gradient backgrounds with clean typography overlays
- Professional tech-forward aesthetic
- Use timestamps in filenames: `yyyy-mm-dd-topic-name-option-X.png`

**IMPORTANT -- Always use full absolute paths and specify per-platform.** After generation, present a clear table with **full absolute paths** and which platform each image is for. Show images inline using the Read tool so the user can review without opening Finder.

---

## Video Generation

For content that benefits from video (especially TikTok and Instagram Reels), use **Veo 3.1** via the Gemini API for AI-generated cinematic video clips.

### First Frame / Cover: The Most Important Asset

**The first frame of a video (or first slide of a carousel) is the only thing that determines whether anyone watches the rest.** Algorithms surface this as the thumbnail in the feed — it must stop the scroll.

**Rules for the first frame/slide:**

1. **If the user provides a screenshot, proof image, or specific asset — it goes first.** Use it as Slide 1 or extract/composite it into the video's opening 1-2 seconds. This is the same principle as proof screenshots on X/Twitter: real evidence beats generated art every time.
2. **If no user-provided image exists**, generate a dedicated cover image/frame — do NOT reuse a generic slide. The cover needs:
   - A bold, high-contrast visual (proof screenshot, dramatic number, product shot, or striking scene)
   - Readable text at a glance: the hook or the single biggest claim from the post
   - For video: a scene that conveys motion/energy in a still frame (fire, speed, contrast)
3. **Carousels (Instagram, TikTok photo mode)**: Slide 1 = hook image with the boldest headline. Slides 2-N can carry the detailed content. Never bury the most compelling visual in the middle.
4. **Video (TikTok, Reels, Shorts)**: The first 1-2 seconds must not be a slow fade-in or black screen. Start on the strongest visual frame. If using ffmpeg, trim the opening to start on action.
5. **Custom thumbnail for YouTube and TikTok**: Always generate a separate 1280x720 (YouTube) or 1080x1920 (TikTok) thumbnail image — do not rely on an auto-selected frame. The thumbnail is what drives clicks from search and suggested.

**Cover image checklist before attaching:**
- [ ] Is this the strongest, most attention-grabbing visual in the set?
- [ ] Does it convey the hook or main claim in under 2 seconds?
- [ ] If user provided a screenshot/image, is it this one?
- [ ] For video: does it show action from the first second (not a fade or black)?

### Option A: AI Video with Veo 3.1 (Recommended)

**Model**: `veo-3.1-generate-preview`
**Endpoint**: `POST https://generativelanguage.googleapis.com/v1beta/models/veo-3.1-generate-preview:predictLongRunning`
**Auth**: Same `GEMINI_API_KEY` used for image generation

**Step 1: Generate video clips (one per slide)**

```python
import json, os, urllib.request

api_key = os.environ['GEMINI_API_KEY']
endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/veo-3.1-generate-preview:predictLongRunning'

payload = {
    "instances": [{"prompt": "Cinematic scene description here"}],
    "parameters": {
        "aspectRatio": "9:16",    # 9:16 for TikTok/Reels, 16:9 for landscape
        "resolution": "720p",      # 720p or 1080p
        "durationSeconds": 6       # 4, 6, or 8 seconds (MUST be number, not string)
    }
}

req = urllib.request.Request(
    f'{endpoint}?key={api_key}',
    data=json.dumps(payload).encode(),
    headers={'Content-Type': 'application/json'}, method='POST')

with urllib.request.urlopen(req, timeout=60) as resp:
    result = json.loads(resp.read())
    operation_name = result['name']  # e.g. "models/veo-3.1-generate-preview/operations/xxxxx"
```

**Step 2: Poll for completion (2-5 minutes)**

```python
poll_url = f'https://generativelanguage.googleapis.com/v1beta/{operation_name}?key={api_key}'
# GET request, check result['done'] == True
# Video URI at: result['response']['generateVideoResponse']['generatedSamples'][0]['video']['uri']
```

**Step 3: Download video**

```python
download_url = f"{video_uri}&key={api_key}"  # Append API key to download URL
```

**Step 4: Scale up + text overlays with ffmpeg**

Veo outputs 720x1280. Upscale to 1080x1920 and add text using `drawtext` filter:

```bash
ffmpeg -y -i veo-clip.mp4 -vf "
  scale=1080:1920:flags=lanczos,
  drawbox=x=0:y=ih*0.55:w=iw:h=ih*0.45:color=black@0.55:t=fill,
  drawbox=x=60:y=ih*0.60:w=4:h=80:color=0xD4A04A@0.9:t=fill,
  drawtext=fontfile='/System/Library/Fonts/Avenir Next.ttc':text='01':fontcolor=white@0.9:fontsize=52:x=80:y=h*0.60+10,
  drawtext=fontfile='/System/Library/Fonts/Avenir Next.ttc':text='Title Here':fontcolor=white:fontsize=46:x=60:y=h*0.72,
  drawtext=fontfile='/System/Library/Fonts/Avenir Next.ttc':text='Body text here':fontcolor=white@0.8:fontsize=28:x=60:y=h*0.84
" -c:v libx264 -preset slow -crf 20 -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart output.mp4
```

**Step 5: Concatenate with crossfade transitions**

```bash
ffmpeg -y -i slide1.mp4 -i slide2.mp4 -i slide3.mp4 -i slide4.mp4 -i slide5.mp4 \
  -filter_complex "
    [0:v][1:v]xfade=transition=fade:duration=0.8:offset=5.2[v01];
    [v01][2:v]xfade=transition=fade:duration=0.8:offset=10.4[v02];
    [v02][3:v]xfade=transition=fade:duration=0.8:offset=15.6[v03];
    [v03][4:v]xfade=transition=fade:duration=0.8:offset=20.8[vout];
    [0:a][1:a]acrossfade=d=0.8[a01];
    [a01][2:a]acrossfade=d=0.8[a02];
    [a02][3:a]acrossfade=d=0.8[a03];
    [a03][4:a]acrossfade=d=0.8[aout]
  " -map "[vout]" -map "[aout]" \
  -c:v libx264 -preset slow -crf 20 -pix_fmt yuv420p -c:a aac -b:a 128k \
  -movflags +faststart final-video.mp4
```

**Key Veo 3.1 details:**
- Generates native audio (ambient soundscape) automatically
- Output: h264 + AAC, 24fps
- 720x1280 (9:16) or 1280x720 (16:9) native resolution
- Fire all 5 clip generations in parallel (async API), poll all at once
- Typical generation time: 2-5 minutes per clip
- `durationSeconds` MUST be a number (not string) or you get 400 error
- Download URLs require `&key=API_KEY` appended

**Prompt tips for cinematic results:**
- Describe camera movement: "slow aerial drift", "dolly forward", "push-in", "camera drift backward"
- Include atmosphere: "moody storm clouds", "warm Edison lights", "volumetric lighting"
- Specify color grading: "teal and amber", "cold desaturated", "golden hour"
- Keep prompts specific and visual -- Veo excels at photorealistic scenes

### Option B: Ken Burns Slideshow (Fallback)

If Veo is unavailable, create a Ken Burns zoom slideshow from still images using ffmpeg:

```bash
ffmpeg -loop 1 -t 5 -i slide.jpg -vf "zoompan=z='1+0.0006*on':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=150:s=1080x1920:fps=30" output.mp4
```

For multi-slide videos, concatenate with crossfade transitions (same ffmpeg pattern as Step 5 above).

### Option C: UGC Filming Script

If the user prefers to film themselves, provide a structured script:

1. **Hook** (0-2s): Pattern interrupt or bold claim
2. **Setup** (2-5s): Context and stakes
3. **Value** (5-25s): The actual content, tips, or demonstration
4. **CTA** (25-30s): Follow, engage, share, or link in bio

---

## Platform Posting

Each platform has quirks. Read `references/platform-posting.md` for the detailed technical guide (selectors, upload flows, encoding requirements, workarounds).

### ⚠️ IMAGE ATTACHMENT: THE #1 FAILURE POINT

**Image attachment fails silently more often than any other step.** This is especially true on X/Twitter and Threads. The post goes live but without the image — and the user has to redo it manually. Do not let this happen.

**Rules before clicking Post on any platform:**

1. **If the user's prompt includes a screenshot, image, or file path — that image is REQUIRED.** Capture it at the start, confirm the path exists, and treat attachment failure as a blocking error (not a warning).
2. **After attaching any image, wait for the image preview to render** in the compose UI before clicking Post. If no preview appears within 10 seconds, the attachment failed — retry before proceeding.
3. **Never post without verifying the image preview is visible.** "I attached the file" is not enough — confirm the UI shows the thumbnail.
4. **If attachment repeatedly fails**, stop and report it instead of posting without the image. A text-only post is worse than no post when the user explicitly asked for an image.

**Platform-specific image attachment flows are documented below for X/Twitter and Threads** — read them before posting to either platform.

### Supported Platforms

| Platform | Content Type | Char Limit | Hashtags | Image Support |
|----------|-------------|------------|----------|---------------|
| X/Twitter | Text + image | 280 (free) / 25K (premium) | 1-3 | Yes |
| LinkedIn | Text + image | 3,000 | 3-5 | Yes |
| Instagram Carousel | Carousel (API) | 2,200 caption | Up to 30 | Required |
| Instagram Reel | Reel (API) | 2,200 caption | Up to 30 | Video |
| Threads | Text + image | 500 | 3-5 | Yes |
| TikTok | Video (from carousel) | 2,200 caption | 3-5 | Video only |
| YouTube Community | Community post | ~5,000 | N/A | Yes |
| YouTube Video | Video upload (Studio) | 5,000 title+desc | Up to 15 tags | Video |
| Reddit | Text post | Unlimited | N/A | Optional |
| dev.to | Article (markdown) | Unlimited | Max 4 tags | Optional |
| Facebook | Text + image | 63,206 | 1-3 | Yes |
| Discord | Message | 2,000 | N/A | Yes (embed) |
| Telegram | Message | 4,096 | N/A | Yes |

### Content Adaptation

Don't copy-paste the same text everywhere. Adapt for each platform:

- **X/Twitter**: Punchy, concise, hook in first line. Thread for longer content.
- **LinkedIn**: Professional tone, insight-driven, mention implications for the industry. Links in first comment, not post body.
- **Instagram**: Visual-first. Decide the post type explicitly before writing anything — see "Instagram: Image-Only vs Text+Image" section below.
- **Threads**: Casual, conversational, like talking to a friend.
- **TikTok**: Upload via TikTok Studio with anti-bot override + AppleScript file picker. Convert photo carousels to video with ffmpeg first (`-preset slow -crf 18 -movflags +faststart`, 1080x1920, 16s minimum). Caption via Playwright `type` only — clipboard paste is silently ignored by DraftJS. The publish API (`webmssdk`) blocks CDP-attached browsers with `{code: 7, Permission Denied}` — scroll Post button into view, cliclick it, then **verify in a separate tab** (`tiktok.com/@username`). If blocked, inform the user to click Post manually (video is already uploaded). Never re-upload without checking the profile first. See `references/platform-posting.md` TikTok section for the full workflow.
- **Reddit**: Match the subreddit's tone. Informative, no self-promo smell. Check flair requirements.
- **dev.to**: Full article format with markdown. Technical depth.
- **Facebook**: Conversational, longer-form OK. Links in post body (not penalized like LinkedIn). Use images.
- **Discord**: Short, punchy, chat-style. No markdown tables (Discord renders them ugly). Use **bold** for emphasis, not headers.
- **Telegram**: Clean, formatted message. Use markdown formatting.
- **YouTube Community**: Community post style -- short, conversational, pose a question to drive comments. Link to full videos or articles in text.
- **YouTube Video**: Upload via Studio with hybrid AppleScript + browser automation. See `references/youtube-studio-upload.md` for the complete workflow. Set video language to English for auto-generated captions and auto-translate support.

### Instagram: Image-Only vs Text+Image

**This is the most common Instagram ambiguity.** When the user asks for "a post with both an image and text", they could mean several different things. Decide the post type explicitly and confirm it before posting.

#### The Three Instagram Post Modes

| Mode | When to use | Caption | Image |
|------|----

…(truncated)
