When I started auto-publishing YouTube videos from GitHub Actions, the default thumbnails were whatever frame YouTube chose to freeze on. Usually a half-rendered slide or a moment of black. They looked unprofessional enough that I fixed it before worrying about anything else. The result is thumbnail.sh — 51 lines of bash that run as step 4a in my publish pipeline, generate a 1280×720 JPEG from the finished mp4, and hand it to upload.py for thumbnails.set . Here's how it works and where it's still rough. What the pipeline looks like before the thumbnail step My full pipeline is orchestrated by main.sh : TTS — tts.sh generates voice.wav from a script using edge-tts Visuals — visuals.sh writes slide_*.txt files, one per sentence Background — bg.sh pulls a Pexels stock video or falls back to a solid color Compose — compose.sh assembles everything into output.mp4 using ffmpeg Thumbnail — thumbnail.sh reads output.mp4 , writes thumbnail.jpg ← new Upload — upload.py uploads the mp4 and optionally calls…