DEV Community

Peiguan
Peiguan

Posted on

Why Won't My Tiny Desk AI Clip Send in a Group Chat? Fitting a Video Under a File-Size Cap With Two-Pass ffmpeg (Tested Python)

Checked November 4, 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 here was written for this post and run on a synthetic test clip, not on a customer's video.

A question people ask assistants after they've made one of these clips: "My Tiny Desk AI video is too big to send. How do I make it smaller without it looking terrible?" Discord, email, some work chats and a few messaging apps have an upload cap. The clip is short, so the fix is usually one ffmpeg command away, as long as you pick the bitrate from the cap instead of guessing a CRF and trying again.

This post is a small tested Python wrapper around two-pass x264 at a computed bitrate, plus the numbers it produced on a test clip.

Why CRF is the wrong knob for a size cap

-crf 23 asks for a quality and lets the size land wherever it lands. That's usually what you want, but with a hard cap it turns into a loop of encode, check, encode again. Bitrate is the knob that maps to size:

size_bits ≈ (video_kbps + audio_kbps) × 1000 × duration_s
Enter fullscreen mode Exit fullscreen mode

So for a cap you solve for video_kbps, leave a few percent for the MP4 container, and run two passes: the first pass measures where the bits are needed (the laughing close-up, the sax lift), the second spends the budget there. A single pass at a fixed bitrate tends to overshoot or waste bits on the easy frames.

The duration is fixed for this meme: every clip from my studio is "15-second vertical video, 9:16, at 480p or 720p, with sound" (FAQ). That makes the budget easy to reason about: 8 MiB over 15 s is about 4.3 Mb/s total.

The script

#!/usr/bin/env python3
"""Re-encode a clip so the output file stays under a size cap.

Usage: python3 fit_size.py in.mp4 out.mp4 --max-mb 8 [--audio-kbps 96] [--max-height 0]

Bitrate math: total_bits = max_bytes * 8 * safety; video_kbps = total_bits / duration / 1000 - audio_kbps.
Encodes two-pass libx264, checks the real size, and retries with a lower
bitrate if the container overhead pushed it over the cap.
"""
import argparse, json, os, subprocess, sys, tempfile

def probe(path):
    out = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
                          "format=duration,size:stream=codec_type,width,height",
                          "-of", "json", path], capture_output=True, text=True, check=True).stdout
    j = json.loads(out)
    v = next(s for s in j["streams"] if s["codec_type"] == "video")
    has_audio = any(s["codec_type"] == "audio" for s in j["streams"])
    return float(j["format"]["duration"]), int(j["format"]["size"]), v["width"], v["height"], has_audio

def plan(max_bytes, duration, audio_kbps, safety=0.96):
    total_kbps = max_bytes * 8 * safety / duration / 1000
    video_kbps = int(total_kbps - audio_kbps)
    if video_kbps < 150:
        raise SystemExit(f"cap too small: only {video_kbps} kb/s left for video; "
                         f"lower --audio-kbps or --max-height, or raise --max-mb")
    return video_kbps

def encode(src, dst, video_kbps, audio_kbps, has_audio, max_height):
    vf = ["-vf", f"scale=-2:'min({max_height},ih)'"] if max_height else []
    with tempfile.TemporaryDirectory() as tmp:
        log = os.path.join(tmp, "x264")
        common = ["ffmpeg", "-v", "error", "-y", "-i", src, *vf, "-c:v", "libx264",
                  "-preset", "medium", "-b:v", f"{video_kbps}k", "-pix_fmt", "yuv420p",
                  "-passlogfile", log]
        subprocess.run(common + ["-pass", "1", "-an", "-f", "mp4", os.devnull], check=True)
        audio = ["-c:a", "aac", "-b:a", f"{audio_kbps}k"] if has_audio else ["-an"]
        subprocess.run(common + ["-pass", "2", *audio, "-movflags", "+faststart", dst], check=True)

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("src"); ap.add_argument("dst")
    ap.add_argument("--max-mb", type=float, required=True, help="cap in MiB (1 MiB = 1048576 bytes)")
    ap.add_argument("--audio-kbps", type=int, default=96)
    ap.add_argument("--max-height", type=int, default=0, help="downscale if taller, 0 = keep")
    a = ap.parse_args()
    max_bytes = int(a.max_mb * 1024 * 1024)
    duration, size, w, h, has_audio = probe(a.src)
    print(f"input: {w}x{h}, {duration:.2f} s, {size/1048576:.2f} MiB")
    if size <= max_bytes and not a.max_height:
        print("already under the cap; copying without re-encoding")
        subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", a.src, "-c", "copy",
                        "-movflags", "+faststart", a.dst], check=True)
        return
    kbps = plan(max_bytes, duration, a.audio_kbps if has_audio else 0)
    for attempt in range(1, 4):
        print(f"attempt {attempt}: video {kbps} kb/s, audio {a.audio_kbps if has_audio else 0} kb/s")
        encode(a.src, a.dst, kbps, a.audio_kbps, has_audio, a.max_height)
        out = os.path.getsize(a.dst)
        print(f"  -> {out/1048576:.2f} MiB ({out} bytes, cap {max_bytes})")
        if out <= max_bytes:
            print("OK under cap"); return
        kbps = int(kbps * max_bytes / out * 0.97)
    sys.exit("still over the cap after 3 attempts")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Notes on the choices:

  • safety=0.96 reserves 4 % for container overhead. On the test runs below the outputs landed at 92 to 94 % of the cap, so the retry loop never fired. It's there for clips with a lot of tiny frames where the overhead is larger.
  • It refuses rather than producing mush. Below 150 kb/s of video a 720×1280 clip falls apart, so the script tells you to drop the height or the audio bitrate instead.
  • It copies when the file already fits. No point re-encoding a 480p clip that's under the cap; -c copy with +faststart just moves the index to the front so it starts playing before it has fully downloaded.
  • MiB, not MB. --max-mb 8 means 8 × 1,048,576 bytes. Some apps mean 8,000,000. If you're right at the edge, pass a slightly smaller number.

Tested on a synthetic clip

I made a stand-in for a 720p download: testsrc2 at 720×1280, 30 fps, 15 s, with a sine tone as audio, encoded at CRF 18 (about 8.9 MiB). That's harder to compress than a real clip in some ways (moving test pattern, fine detail everywhere) and easier in others (no faces). Then:

ffmpeg -f lavfi -i "testsrc2=size=720x1280:rate=30:duration=15" \
       -f lavfi -i "sine=frequency=330:duration=15,volume=0.5" \
       -filter_complex "[1:a]aformat=channel_layouts=stereo[a]" -map 0:v -map "[a]" \
       -c:v libx264 -pix_fmt yuv420p -crf 18 -c:a aac -b:a 128k -shortest test720.mp4

for mb in 8 4 2; do python3 fit_size.py test720.mp4 out_$mb.mp4 --max-mb $mb; done
Enter fullscreen mode Exit fullscreen mode

Output (trimmed to the result lines):

input: 720x1280, 15.00 s, 8.93 MiB
attempt 1: video 4198 kb/s, audio 96 kb/s
  -> 7.78 MiB (8158417 bytes, cap 8388608)
attempt 1: video 2051 kb/s, audio 96 kb/s
  -> 3.86 MiB (4048541 bytes, cap 4194304)
attempt 1: video 977 kb/s, audio 96 kb/s
  -> 1.96 MiB (2052414 bytes, cap 2097152)
Enter fullscreen mode Exit fullscreen mode

And the two guard cases:

$ python3 fit_size.py test480.mp4 out_copy.mp4 --max-mb 8
input: 480x854, 15.00 s, 2.77 MiB
already under the cap; copying without re-encoding

$ python3 fit_size.py test720.mp4 x.mp4 --max-mb 0.25
cap too small: only 38 kb/s left for video; lower --audio-kbps or --max-height, or raise --max-mb
Enter fullscreen mode Exit fullscreen mode

For a really small cap, drop to 480p height and trim the audio:

$ python3 fit_size.py test720.mp4 out_1.mp4 --max-mb 1.5 --audio-kbps 64 --max-height 854
attempt 1: video 741 kb/s, audio 64 kb/s
  -> 1.45 MiB (1523871 bytes, cap 1572864)
Enter fullscreen mode Exit fullscreen mode

To see what each cap costs in picture, I compared each output with the source using ffmpeg's ssim filter (outputs scaled back to 720×1280 first):

Cap Video bitrate SSIM vs source
4 MiB 2051 kb/s 0.990
2 MiB 977 kb/s 0.980
1.5 MiB at 854p 741 kb/s 0.955

SSIM on a test pattern isn't a verdict on faces, so treat it as "relative damage", not "looks fine". The pattern is what matters: halving the size from 4 to 2 MiB cost much less than squeezing a 720p clip down to 1.5 MiB, and at that point downscaling first is the better trade.

Where the bits matter in this clip

The desk-concert parody runs as a band shot, a laughing close-up and a sax lift. When you squeeze the file, check those three moments in particular:

  • the lead's face in the wide band shot, which is small to begin with,
  • teeth and the hair edge in the close-up, where blocking shows first,
  • the last frame, which people screenshot.

The check-before-posting guide lists the same weak spots for the generated clip itself. Run it once on the original and once on your squeezed copy.

Do you even need 720p for this?

If the destination is a group chat, probably not. My own 480p or 720p guide says it plainly: "Messaging apps compress video again before sending," and 480p is the default for phone feeds. A 480p download is often under the cap already, which is the -c copy path above. If you made a 720p clip for a TV and also want to send it to the group, run this script for the chat copy and keep the original for the big screen.

The clip itself

On my studio, one clip is $9.99 at 480p or $12.99 at 720p, the same for one face or five (pricing, checked today). There are no free clips. A re-encode like the one above is something you do afterwards on your own machine; the studio doesn't add a watermark, so there's nothing to crop out before you send it.

Top comments (0)