DEV Community

Peiguan
Peiguan

Posted on

Can You Get a Bama Bound AI Video in 1080p? What Upscaling a 480p or 720p Clip Actually Does, Measured With ffmpeg SSIM and PSNR

November 3, 2026. I build bamabound.app, a small studio that turns one photo into the eight-second "I'M BAMA BOUND" field-banner clip at 480p or 720p. People keep asking whether they can get it in 1080p. This post is my honest answer, with a test you can rerun.

Short answer: not from my studio. The FAQ says it plainly: "There is no prompt, no duration control and no 1080p option" (FAQ). You can upscale the file to 1080x1920 with one ffmpeg command, and some editing apps will do it anyway. But upscaling makes the frame bigger. It doesn't add detail. In my test, going from 480p to 720p at generation time did more for the banner letters than any upscaler did afterwards. If the letters matter to you, pick 720p ($9.99 instead of $7.99, pricing). Don't count on 1080p rescuing a 480p file.

Why the resolution is decided at generation time

An image-to-video model renders the clip at a fixed pixel grid. For this meme the hard part of that grid is the banner: three words in white on red, on fabric that moves while the person jumps. Each letter gets some number of pixels at render time, and that's all the detail it will ever have. A later scaler can only interpolate between those pixels. It can't learn what the letter edge "should" have been.

So the 480p/720p choice really asks how many pixels the model gets to draw the person and the banner. That's also why the 480p vs 720p guide suggests 720p "if you expect to crop it later". A crop throws pixels away, and you can't get them back.

I wanted numbers rather than adjectives, so I measured it.

The test

You can't measure an upscale against a real 1080p "truth" you don't have. So I made one:

  1. A synthetic 1080x1920, 8-second, 30 fps clip. It has a green field with per-frame noise for texture, a red strip, and white I'M BAMA BOUND letters that bob 40 px up and down inside it. No person, and no generator output.
  2. "Delivered" copies at 480x854 and 720x1280 (lanczos downscale, x264 CRF 20), standing in for what a generator hands you.
  3. Each copy upscaled back to 1080x1920 with three ffmpeg scalers: neighbor, bicubic and lanczos.
  4. Every upscale scored against the truth with ffmpeg's ssim and psnr filters, once for the whole frame and once for a 900x260 strip around the banner.

Here's the whole script. It needs ffmpeg with libx264 and the DejaVu Sans Bold font.

#!/usr/bin/env python3
"""upscale_test.py: does upscaling a 480p/720p vertical clip to 1080x1920 bring back detail?

Builds a synthetic 1080x1920 "truth" clip (green field texture + a red banner with
white I'M BAMA BOUND letters that bobs up and down), makes 480p and 720p copies the
way a generator would deliver them, upscales each back to 1080x1920 with several
ffmpeg scalers, and scores every result against the truth with SSIM and PSNR,
for the whole frame and for the banner strip only.

Usage: python3 upscale_test.py [workdir]
Needs ffmpeg with libx264 and the DejaVu Sans Bold font.
"""
import os
import re
import subprocess
import sys

FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
W, H, SECS, FPS = 1080, 1920, 8, 30
BANNER = (90, 260, 900, 260)          # x, y, w, h of the strip we score (banner area)
SIZES = {"480p": (480, 854), "720p": (720, 1280)}
SCALERS = ["neighbor", "bicubic", "lanczos"]


def run(cmd):
    p = subprocess.run(cmd, capture_output=True, text=True)
    if p.returncode:
        sys.exit(f"command failed: {' '.join(cmd)}\n{p.stderr[-800:]}")
    return p.stderr


def make_truth(path):
    graph = (
        f"color=c=0x3f8f3a:s={W}x{H}:r={FPS}:d={SECS},format=yuv420p,"
        "noise=alls=28:allf=t,"                                   # grass-like texture
        f"drawbox=x=100:y='300+40*sin(2*PI*t)':w=880:h=200:c=0xb5122e:t=fill,"
        f"drawtext=fontfile={FONT}:text='I\\'M BAMA BOUND':fontsize=104:fontcolor=white:"
        "x=(w-text_w)/2:y='300+40*sin(2*PI*t)+(200-text_h)/2'"
    )
    run(["ffmpeg", "-y", "-v", "error", "-f", "lavfi", "-i", graph,
         "-c:v", "libx264", "-crf", "8", "-preset", "medium", "-pix_fmt", "yuv420p", path])


def deliver(truth, path, w, h):
    run(["ffmpeg", "-y", "-v", "error", "-i", truth, "-vf", f"scale={w}:{h}:flags=lanczos",
         "-c:v", "libx264", "-crf", "20", "-preset", "medium", "-pix_fmt", "yuv420p", path])


def upscale(src, path, flags):
    run(["ffmpeg", "-y", "-v", "error", "-i", src, "-vf", f"scale={W}:{H}:flags={flags}",
         "-c:v", "libx264", "-crf", "12", "-preset", "medium", "-pix_fmt", "yuv420p", path])


def score(a, truth, crop=None):
    pre = f"crop={crop[2]}:{crop[3]}:{crop[0]}:{crop[1]}," if crop else ""
    graph = (f"[0:v]{pre}split[a1][a2];[1:v]{pre}split[b1][b2];"
             "[a1][b1]ssim[s];[a2][b2]psnr[p]")
    err = run(["ffmpeg", "-v", "info", "-i", a, "-i", truth, "-filter_complex", graph,
               "-map", "[s]", "-f", "null", "-", "-map", "[p]", "-f", "null", "-"])
    ssim = float(re.search(r"SSIM .*?All:([0-9.]+)", err).group(1))
    psnr = float(re.search(r"PSNR .*?average:([0-9.inf]+)", err).group(1))
    return ssim, psnr


def main():
    work = sys.argv[1] if len(sys.argv) > 1 else "upscale-work"
    os.makedirs(work, exist_ok=True)
    truth = os.path.join(work, "truth_1080x1920.mp4")
    make_truth(truth)
    print(f"{'source':6} {'scaler':9} {'SSIM all':>8} {'PSNR all':>8} {'SSIM banner':>11} {'PSNR banner':>11}")
    for name, (w, h) in SIZES.items():
        small = os.path.join(work, f"delivered_{name}.mp4")
        deliver(truth, small, w, h)
        for flags in SCALERS:
            up = os.path.join(work, f"{name}_up_{flags}.mp4")
            upscale(small, up, flags)
            s, p = score(up, truth)
            sb, pb = score(up, truth, BANNER)
            print(f"{name:6} {flags:9} {s:8.4f} {p:8.2f} {sb:11.4f} {pb:11.2f}")


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

A note on drawbox: its y expression didn't move the box per frame in my build (ffmpeg 7.1.5), so the red strip stays put and only the letters bob. That's fine for this test. I'm scoring the strip, and the letters stay inside it.

The results

This is the real output from my box (ffmpeg 7.1.5, Python 3.13, 8 cores, about 3 minutes):

source scaler    SSIM all PSNR all SSIM banner PSNR banner
480p   neighbor    0.3817    24.68      0.8066       26.28
480p   bicubic     0.3891    24.95      0.8148       28.01
480p   lanczos     0.3978    24.97      0.8145       28.11
720p   neighbor    0.5321    25.37      0.8565       27.77
720p   bicubic     0.5735    26.11      0.8774       29.97
720p   lanczos     0.5897    26.18      0.8800       30.10
Enter fullscreen mode Exit fullscreen mode

(SSIM: 1.0 means identical to the truth. PSNR is in dB, and higher is closer.)

How I read it:

  • The whole-frame numbers are low on purpose. The grass texture is fresh random noise every frame. No scaler can recover random detail that was averaged away, and that's exactly the point. Fine texture like grass, hair and fabric weave is what a low-resolution file loses for good.
  • The scaler barely matters. On the banner strip, bicubic and lanczos are within 0.0003 SSIM and 0.1 dB of each other at 480p. neighbor is clearly worse (blocky letter edges), so don't pick it. Past that, the scaler is a rounding error.
  • The source resolution matters most. Starting from 720p instead of 480p lifts the banner strip from 0.815 to 0.880 SSIM and adds about 2 dB PSNR, whichever smooth scaler you use. Any upscaler, given a 480p input, stays at 480p detail.

There's one caveat I'd put in bold if I could. This is a synthetic clip with crisp computer-drawn letters, not a generated one. A real clip's letters are drawn by the model and can be wrong at any resolution. That's a separate check: pause and read every letter before you post (banner words guide). Resolution decides how sharp the letters are. It doesn't decide whether they're spelled right.

If you need a 1080x1920 file anyway

Some editors and upload flows want a 1080x1920 project. Upscale once, with a smooth scaler, and copy the audio:

ffmpeg -i clip.mp4 -vf "scale=1080:1920:flags=lanczos" \
  -c:v libx264 -crf 18 -preset medium -c:a copy -movflags +faststart clip_1080x1920.mp4
Enter fullscreen mode Exit fullscreen mode

I ran this on a synthetic 720x1280 test clip with audio. ffprobe reports video,1080,1920, an audio stream and a duration of 8.000000. Do this once, at the end. Every extra encode, like generator, then editor, then app, costs a little more quality, and upscaling early only means the later steps push more pixels around.

What not to expect: an "AI upscaler" (a learned super-resolution model) will invent plausible texture, and on a banner it can invent letter shapes too. That's fine for grass. On the one part of this meme that has to be spelled right, I'd avoid it.

So which resolution should you buy?

Here are the facts from the pricing page: one clip is $7.99 at 480p (6 credits) or $9.99 at 720p (13 credits). Both are the same eight-second, 9:16 clip with new cheering sound. 720p is the sharper file, and there's no 1080p option.

My rule of thumb, which follows from the test above:

  • Posting straight to a phone feed, no crop: 480p, which is the default in the studio.
  • Cropping to 4:5 or square, zooming in, or showing it on a big screen: 720p, because cropping and zooming spend pixels you only get at generation time.
  • Banner looks soft or the letters look off: check the photo first. The clear photo guide and the help page both point out that raised hands, busy shirt text or a second person can bleed into the banner, and a better photo helps more than a higher resolution.

If you want to rerun the numbers with your own font, banner size or noise level, change the constants at the top of upscale_test.py. The script writes everything to upscale-work/, so you can scrub through the files yourself.

Questions about what the studio does and doesn't make are answered on the FAQ. The clip is a staged joke, not a University of Alabama acceptance.

Top comments (0)