DEV Community

Peiguan
Peiguan

Posted on

How Do You Turn a Tiny Desk AI Clip Into a GIF for Slack or Discord? A Size-Cap Ladder With ffmpeg (Tested)

Checked November 11, 2026 (12:00 CST, UTC+8) against the live pages linked below. I build tinydeskoutstanding.video, an independent studio that turns one to five photos into a 15-second vertical parody of the "Outstanding" Tiny Desk meme. The script below was written for this post and tested on a synthetic 720x1280 test clip, not on real faces.

A question I keep getting after someone makes a clip: "Can I drop just the 'Outstanding!' moment into Slack as a GIF?" You can, but a raw ffmpeg -i clip.mp4 out.gif gives you a 20 MB, banded, flickering file that most chat apps refuse or won't auto-play. This post walks through a small script that does three things properly:

  1. cuts a short window (the payoff, not all 15 seconds),
  2. builds a per-clip colour palette so the GIF doesn't band,
  3. steps down a ladder of width and frame rate until the file fits under whatever cap your chat app enforces.

Why GIFs from video get so big

A GIF stores every frame as indexed colour (max 256 colours) with LZW compression. It has no inter-frame motion compensation the way H.264 does, so file size grows roughly with width × height × frames. That gives you three levers, in the order I'd pull them:

  • Duration: 3–4 seconds is the whole joke. Cut first.
  • Frame rate: 24 fps video looks fine as a 10–15 fps GIF.
  • Width: a 9:16 clip at 480 px wide is already bigger than most chat previews.

Colour count (max_colors) and dithering matter too, but less than the three above.

The two-pass palette trick in one filter graph

ffmpeg's default GIF encoder uses a generic palette. palettegen analyses your actual frames and paletteuse maps them onto it. Doing both inside one -filter_complex with split means you don't need a temp PNG:

fps=15,scale=480:-2:flags=lanczos,split[a][b];
[a]palettegen=max_colors=128:stats_mode=diff[p];
[b][p]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle
Enter fullscreen mode Exit fullscreen mode

stats_mode=diff weights the palette toward pixels that change, which is where the faces and instruments are. diff_mode=rectangle only re-dithers the changed region per frame, which cuts both flicker and size. Bayer dithering compresses better than the default error-diffusion dither.

The script

#!/usr/bin/env python3
"""Turn a short vertical clip into a GIF (or animated WebP) under a size cap.
Tries widths/fps from best to smallest until the file fits."""
import os, subprocess, sys

LADDER = [(480, 15), (400, 12), (360, 12), (320, 10), (270, 10), (240, 8)]

def make_gif(src, out, width, fps, start, dur):
    vf = (f"fps={fps},scale={width}:-2:flags=lanczos,split[a][b];"
          f"[a]palettegen=max_colors=128:stats_mode=diff[p];"
          f"[b][p]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle")
    subprocess.run(["ffmpeg", "-v", "error", "-y", "-ss", str(start), "-t", str(dur),
                    "-i", src, "-filter_complex", vf, "-loop", "0", out], check=True)

def make_webp(src, out, width, fps, start, dur):
    subprocess.run(["ffmpeg", "-v", "error", "-y", "-ss", str(start), "-t", str(dur),
                    "-i", src, "-vf", f"fps={fps},scale={width}:-2:flags=lanczos",
                    "-c:v", "libwebp", "-quality", "70", "-compression_level", "6",
                    "-loop", "0", "-an", out], check=True)

def fit(src, out, cap_mb, start=0.0, dur=4.0):
    maker = make_webp if out.endswith(".webp") else make_gif
    cap = cap_mb * 1024 * 1024
    for width, fps in LADDER:
        maker(src, out, width, fps, start, dur)
        size = os.path.getsize(out)
        print(f"{width}px @ {fps}fps -> {size/1048576:.2f} MB")
        if size <= cap:
            return width, fps, size
    raise SystemExit(f"could not get under {cap_mb} MB; shorten the cut")

if __name__ == "__main__":
    src, out, cap = sys.argv[1], sys.argv[2], float(sys.argv[3])
    start = float(sys.argv[4]) if len(sys.argv) > 4 else 0.0
    dur = float(sys.argv[5]) if len(sys.argv) > 5 else 4.0
    print("fits at", fit(src, out, cap, start, dur))
Enter fullscreen mode Exit fullscreen mode

Usage: python3 clip_to_gif.py clip.mp4 payoff.gif 8 11 4 cuts 4 seconds starting at 11 s and fits it under 8 MB. Give the output a .webp name and it writes an animated WebP instead.

What it did on the test clip

On a 15-second 720x1280 synthetic test pattern (ffmpeg testsrc2, which is busier than a real concert frame), a 4-second window starting at 11 s:

$ python3 clip_to_gif.py test.mp4 out.gif 2 11 4
480px @ 15fps -> 1.72 MB
fits at (480, 15, 1808438)

$ python3 clip_to_gif.py test.mp4 small.gif 0.8 11 4
480px @ 15fps -> 1.72 MB
400px @ 12fps -> 1.01 MB
360px @ 12fps -> 0.82 MB
320px @ 10fps -> 0.62 MB
fits at (320, 10, 646923)

$ python3 clip_to_gif.py test.mp4 out.webp 1 11 4
480px @ 15fps -> 0.70 MB
Enter fullscreen mode Exit fullscreen mode

Two takeaways: the ladder does its job when the cap is tight, and animated WebP at the same width and frame rate came out at well under half the GIF size. If your target app shows WebP inline, use it.

Where to cut

In this meme the joke lands at the end, when the lead's face fills the frame and the band reacts. On a 15-second clip, a window of roughly the last four seconds usually carries it. Scrub it once in any player and pass the start time as the fourth argument rather than guessing.

Size caps change, so measure

I'm deliberately not printing a table of "Slack allows X MB, Discord allows Y MB" here. Those limits differ by plan and change over time. Check your workspace's current limit, pass it as the third argument, and let the ladder find the largest version that fits.

A note on sound

GIFs have no audio. The clips from the studio come with newly generated sound (not the original recording, per the FAQ), so if the reaction depends on the shout, share the MP4 and keep the GIF for reactions and emoji-style replies.

Getting a clip to start from

Each clip is a 15-second 9:16 video with one to five faces. The pricing page lists $9.98 at 480p, $12.98 at 720p, $24.98 at 1080p and $39.98 at 4k as of today, the same price for one face or five. For a GIF, 480p is plenty: the ladder above never needed more than 480 px of width.

Top comments (0)

⌂ 换网址