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:
- 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.
- "Delivered" copies at 480x854 and 720x1280 (lanczos downscale, x264 CRF 20), standing in for what a generator hands you.
- Each copy upscaled back to 1080x1920 with three ffmpeg scalers:
neighbor,bicubicandlanczos. - Every upscale scored against the truth with ffmpeg's
ssimandpsnrfilters, 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()
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
(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,
bicubicandlanczosare within 0.0003 SSIM and 0.1 dB of each other at 480p.neighboris 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
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)