Pull the best frame from a video clip and lift it to a post-ready, high-resolution still.
Built for the exact job: you shot a clip on your phone, and you want one crisp, well-composed, correctly-coloured image — sized for a LinkedIn post — without hand-scrubbing the timeline or fighting washed-out HDR colours.
video ──► probe ──► score every frame ──► pick best, diverse candidates
│
contact sheet ◄───┤
▼
chosen frame ──► AI upscale ──► reframe + sharpen ──► LinkedIn JPEG
- Probe the clip and detect HDR (iPhone clips are usually HLG / BT.2020).
- Tone-map HDR → SDR correctly so the still isn't grey/washed-out (libplacebo BT.2390 for the final frame; fast zscale+Hable for the scoring sweep).
- Score every frame for sharpness (Tenengrad + FFT + Laplacian), exposure, contrast and faces, then pick the top diverse candidates (spread out in time).
- Contact sheet so you can eyeball the finalists and choose.
- AI upscale the chosen frame ~4× (GPU via PyTorch+spandrel, or CPU via ONNX, or Lanczos as a no-model fallback).
- Reframe to a LinkedIn aspect ratio (face-aware crop), sharpen gently, and export an sRGB JPEG.
- Tone-mapping: HLG footage must be tone-mapped to BT.709/sRGB or it looks
desaturated on LinkedIn.
libplacebo=tonemapping=bt.2390(GPU/Vulkan) gives the most natural colour; the CPUzscale + tonemap=hablechain is the robust fallback. - Upscaler model: for compressed phone footage, a model trained on JPEG/WEBP
degradation beats a generic one. Default is 4xRealWebPhoto_v4_dat2 (DAT-2).
realesrgan-x4plusis the fast, faithful alternative. - Runtime: the modern, maintained path is PyTorch (CUDA) + spandrel — it
works on Python 3.13 / NumPy 2.x, unlike the abandoned
basicsr/realesrganpackages (which import atorchvisionmodule that no longer exists). - Faces: face restoration is opt-in / selective, not default — tiny crowd faces (20–60 px) can be hallucinated into a different person. Use it only on 1–3 foreground subjects, conservatively (CodeFormer w≈0.7).
- LinkedIn: 1080×1350 (4:5 portrait) wins the most mobile feed space; export sRGB JPEG q≈88, under 3 MB. A 9:16 phone frame is reframed to 4:5, not letterboxed.
- Caveat: no upscaler reconstructs unreadable on-screen slide text at 480p — it invents plausible-but-wrong glyphs. Crop/retype if legibility matters.
Core (frame selection, tone-mapping, Lanczos/ONNX upscale) needs only:
pip install -r requirements.txt
# and FFmpeg on PATH (https://www.gyan.dev/ffmpeg/builds/)GPU AI upscaling (recommended on an NVIDIA card) — use an isolated venv:
python -m venv .venv
.\.venv\Scripts\python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cu128
.\.venv\Scripts\python -m pip install -r requirements.txt spandrel "PySide6-Essentials==6.8.1.1"PySide6 6.8.1.1 is pinned because newer builds are binary-incompatible with the Anaconda CPython used here ("procedure not found" on import). On a standard python.org install, the latest PySide6 is fine.
# Just inspect
python -m framelift probe "clip.mov"
# Score + build a contact sheet of the best candidates
python -m framelift analyze "clip.mov" --out output
# One-shot: pick a frame, upscale, export a LinkedIn-ready still
python -m framelift run "clip.mov" --pick 1 --preset portrait
python -m framelift run "clip.mov" --pick 7 --backend spandrel --model realweb-dat2
python -m framelift run "clip.mov" --no-upscale # frame only
python -m framelift run "clip.mov" --backend lanczos # no model downloadOutputs land in output/:
<stem>_contacts.png, <stem>_frame.png, <stem>_upscaled.png, <stem>_linkedin_<preset>.jpg.
| preset | size | use |
|---|---|---|
portrait |
1080×1350 | 4:5 — max mobile feed real estate (default) |
square |
1080×1080 | 1:1 — safe all-rounder |
landscape |
1200×627 | 1.91:1 — link/landscape card |
banner |
1584×396 | profile background banner |
original |
as-is | keep source aspect ratio |
A modern dark PySide6 (Qt) app — scrub to any exact frame, see the LinkedIn crop
live, and export. (Install once: pip install "PySide6-Essentials==6.8.1.1".)
.\.venv\Scripts\python -m framelift gui "clip.mov" # or just: python -m framelift gui- Slider + filmstrip scrub every frame instantly (frames are pre-decoded once, then swapped on the fly — snappy); ◀ ▶ step one frame at a time.
- The dimmed preview shows the exact crop rectangle for the chosen format.
- Custom toggle for AI upscale; the engine badge shows what's in use (auto: GPU spandrel here, CPU/Lanczos elsewhere).
- Model selector: Detailed (DAT-2, crisp) vs Smooth (Real-ESRGAN, fewer artifacts) — switch if a frame looks over-processed.
- Format pills include Orig — exports the exact frame at native resolution (no crop, no enlargement) as a lossless PNG. With AI upscale off the button reads Export frame (native); the LinkedIn presets export an sRGB JPEG.
- Export opens a Save As dialog (name + folder, pre-filled to
output/), runs on a worker thread (UI never blocks), then reveals the file.
AI upscalers amplify whatever they're fed, so:
- Protect faces (on by default). Far-away / small faces are the worst case — the model invents texture and they come out smeared or blended into the background. With protection on, faces are detected (YuNet) and rebuilt from a clean, lightly sharpened resize, then feathered over the AI result — the scene still gets crisp, faces come out a little better and natural instead of hallucinated. Turn it off to let the model fully resynthesize faces (only worth it for large, sharp close-ups).
- Pick a sharp frame. Motion-blurred frames upscale into directional streaks — use the slider/★ and watch the quality readout.
- Post-sharpening is kept very low on AI output (the model is already sharp); a strong UnsharpMask on top causes halo/colour-fringe streaks.
- For the cleanest faces/edges try the Smooth model; Detailed maximises crispness.
- No upscaler can make unreadable on-screen slide text legible at 480p.
Falls back to a basic Tkinter UI automatically if PySide6 isn't available — which is what the portable build below uses.
On your machine (GPU, one-click):
powershell -ExecutionPolicy Bypass -File .\make_shortcut.ps1Creates a Desktop FrameLift shortcut that launches the GUI with the GPU venv
(no console window). Or pin FrameLift.pyw.
Portable build to hand to others (no Python, no GPU):
.\.venv\Scripts\python -m pip install pyinstaller onnxruntime
.\.venv\Scripts\python -m PyInstaller packaging\framelift.spec --noconfirm
# -> dist\FrameLift\FrameLift.exe (zip the folder and share)The portable build excludes PyTorch to stay ~150 MB and upscales via ONNX/Lanczos. Bundling CUDA PyTorch is multi-GB and fragile — for full GPU quality, run from the venv.
framelift/
probe.py ffprobe wrapper + HDR detection
extract.py HDR→SDR tone-mapping + frame streaming/re-extraction
score.py sharpness / exposure / contrast / face scoring
select.py full analysis + diverse top-N selection
contact.py labelled contact sheet
upscale.py pluggable backends: spandrel(GPU) / onnx / lanczos
models.py model registry + lazy download
linkedin.py reframe / sharpen / export to LinkedIn specs
gui_qt.py modern PySide6 desktop app (filmstrip + scrubber) ← default
gui.py Tkinter fallback app (used by the portable build)
cli.py command-line interface
FrameLift.pyw double-clickable GUI launcher
make_shortcut.ps1 creates a Desktop shortcut (GPU venv)
packaging/ PyInstaller spec for a portable build
