Real images. Quiet type. A thumbnail that reads in the feed.
An Agent Skill and local renderer for restrained YouTube thumbnails, for any kind of video,
with Facebook feed and Reels/Shorts versions, plus standalone posts that keep the whole message.
Why Β· Principle Β· Examples Β· Creator Photo Β· Two Variants Β· Official Logo Β· Subject Layer Β· Feed Posts Β· Image Roles Β· Formats Β· Title Look Β· What Ships Β· Quickstart Β· Quality Gates Β· Tradeoffs Β· Installation Β· Verification
Many thumbnails, in gaming, tech, sports and self-help alike, use the same recipe: a cut-out with a glowing edge, a synthetic explosion, oversized distressed lettering, arrows, and one color pushed across the entire frame. Grain on top does not make that scene less manufactured. The lettering and the effects are the problem before any finishing pass begins.
Clean Thumbnail starts from real images: a game capture, a photo, official art, a cut-out of the creator. It gives them space to carry the mood, then typesets the few words a viewer can actually read in a feed. The renderer is deterministic: it does not generate a new world behind anyone or ask an image model to draw letters.
It suits gameplay, trailers, walkthroughs, hardware and settings comparisons, sports, education and vlogs, whenever you have images that represent the video. It cannot make a weak image compelling by itself.
Real images carry the mood; the type stays quiet.
The skill decides what to say from the video title: the big type is whatever makes this video different from every other video about the same thing. The Python script decides where and how to render it. Keeping text out of image generation makes spelling, spacing, and revisions controllable.
Video title ββΊ what makes it different + context ββββββββββββββ
βββΊ A/B pair: master (up to 4K)
Images ββΊ roles: background, panels, subject, inset ββΊ type ββββ + 2 MB phone copy + previews
Each example renders an A/B pair from the same images and text: one variant with the subject big, the other with the hook big. The pair is meant for YouTube's Test & Compare; see Build decisions and limits for what that test does and does not tell you.
For a fictional vlog titled One take, no script: just press record, the thumbnail copy is ONE TAKE / NO SCRIPT. A leads with the recording constraint; B leads with the unscripted approach. A stock model stands in for the creator in this example.
The source photo is by Ron Lach on Pexels, used under the Pexels License. The photo is the background as it is; it needs no separate cut-out. The creator sits in the middle with the camera phone on the right, so --crop 0.2,0,0.85,1 keeps the part from the creator to the phone, the art moves right, and the type takes the dark left side. Both the creator and the phone screen showing him recording stay visible, which is what says "creator video". --feather 0.2 shortens the fade so it doesn't reach his face. With no brand logo to match, both slots use the bundled Outfit family: Bold for the headline and SemiBold for the box, where Outfit stands in for a channel font. The defaults are bundled fonts too, so this renders the same type on every system; naming them just pins the weights.
python skill/clean-thumbnail/scripts/thumb.py demo/creator-photo.jpg --title "ONE TAKE" --label "NO SCRIPT" --style sans --title-font outfit@Bold --label-font outfit@SemiBold --text-side left --crop 0.2,0,0.85,1 --feather 0.2 --aspect 16:9 -o out/creatorWithout the hand-picked crop, marking his face with --subject-box 0.31,0.23,0.42,0.39 lets the script decide: the 16:9 type moves to the right, clear of his face, and the 4:5 and 9:16 crops center on him, with no warnings in any format. The crop above is still the better picture here, because it also keeps the phone in view, which the box doesn't know about.
The included source is resized to 3840Γ2560; the crop keeps 2496Γ2560 of it, and both masters render at 3840Γ2160 without upscaling. The A preview and B preview show the headlines at 168 px and in the squint test. The 1280Γ720 phone copies for A and B are under 2 MB. Source and derivative credits are recorded in the photo license note; the fictional copy does not describe the model's own video or imply an endorsement.
For a video titled Clair Obscur Expedition 33 GOTY - Simon Fight (No hit - Only Parry) | Expert, the subject is SIMON and the hook is NO HIT Β· PARRY ONLY. Which one should be big depends on the audience, so the renderer makes both: A with the subject big, B with the hook big. YouTube's Test & Compare takes up to three thumbnails per video and picks the winner on watch time share, not click-through rate; tests of clearly different thumbnails finish faster, so make the two differ.
The source capture and the preview sheets for A and B are included. Each preview shows the thumbnail at 480, 320, 168, and 120 px in dark and light feed settings with a duration badge, at the 88 px notification size, and as a squint test (grayscale plus blur) that shows what survives a fast scroll. The tool does not predict which variant will perform better.
For a The Blood of Dawnwalker video with the hook OP SKILLS, the game's official logo replaces the title text. A puts the logo first; B sets the hook big with the logo small above it. The key art has the character in the middle of the frame, so --crop keeps 78% of its width around him. Narrower than 16:9, the art moves to the right and fades into the dark side, which leaves the left for the type.
python skill/clean-thumbnail/scripts/thumb.py demo/dawnwalker-art.jpg --crop 0.117,0,0.898,1 --text-side left --logo demo/dawnwalker-logo.png --label "OP Skills" -o out/dawnwalkerThe key art is 2560Γ1440, so both masters render at 2560Γ1440 without upscaling. The previews show the trade the pair is for: in A the logo's thin gold lettering fades at 168 px and in the squint test, while the hook in B holds. The logo is the more recognizable mark; the hook is the more legible one.
For a fictional A home studio in a small room video, the copy is HOME STUDIO / SMALL ROOM. This example uses two photographs: a transparent person cut out from the Ron Lach creator photo, placed over a separate room photo by Curtis Adams. The stock model stands in for a creator; the room is an example setting.
python skill/clean-thumbnail/scripts/thumb.py demo/subject-room.jpg --subject demo/subject-person.png --title "HOME STUDIO" --label "SMALL ROOM" --style sans --title-font outfit@Bold --label-font outfit@SemiBold --text-side right --push 0.35 --aspect 16:9 -o out/subject--subject places the person on the left, away from the type, and --push 0.35 softens the room behind him. The renderer matches the cut-out's color cast, black and white points, sharpness and grain to the background. Both slots use bundled Outfit in white, standing in for a channel font. The transparent PNG is included, so this command needs only NumPy and Pillow; background removal was a one-time preparation step.
The 4000Γ2250 room crop is scaled 0.96Γ and the 1924Γ2328 cut-out about 0.87Γ, so neither is upscaled for the 3840Γ2160 masters. The cut-out's hair edge was cleaned against the original wall and a patch of the original sofa seen through his arm was removed; the preparation note describes how. The A preview and B preview show the headline and person at feed sizes; the A phone copy and B phone copy are 1280Γ720 and under 2 MB. The run reports 14.5:1 and 14.8:1 text contrast with no warnings. Both source photos and the derivatives use the Pexels License; the source and preparation note records the credits.
A thumbnail gets a few words because the video title carries the rest. A standalone post has no title, so its image text is the message. --mode post keeps the user's words: the headline as --title, the message as --body (wrapped, in the label font), and when and where as --details. Nothing is cut, reordered or moved into the caption. The script checks the body and details at the size of a 360 px phone feed and warns when they would be too small. A post renders one 4:5 image by default.
These are fictional demonstrations, including the businesses, events and offers. Each keeps its complete body and details on the image. The photographs are real, licensed sources; the credits note identifies them. Every post below is rendered by the supplied --mode post implementation.
A real creator photo with the person and recording phone kept together. The complete workshop description, date, time, venue and call to action sit on a solid panel below the clean photograph (--backdrop solid), rendered straight from the original photo. The face stays fully visible. The title uses bundled Outfit Bold; the body and details use Outfit Medium, in white.
python skill/clean-thumbnail/scripts/thumb.py demo/creator-photo.jpg --mode post --title 'MAKE YOUR FIRST VIDEO' --body 'Learn to frame a shot, record clear sound and feel natural on camera. Bring your phone; leave with a finished clip.' --details 'Saturday 24 October Β· 10 AMβ12 PM\nStudio 04 Β· Free Β· Reserve your place' --label 'CREATOR WORKSHOP' --style sans --title-font outfit@Bold --label-font outfit@Medium --subject-box 0.31,0.23,0.42,0.39 --backdrop solid --bg '#18252D' -o out/post-creatorMaster Β· 1080 copy Β· Feed preview Β· Photo
A close-up coffee photograph carries the mood, with the type in the calm area above it. The serif headline uses bundled Playfair Display SemiBold; body and details use Outfit Medium. The whole invitation, date, opening hours, venue, drop-in instruction and $5 price remain on the image.
python skill/clean-thumbnail/scripts/thumb.py demo/post-coffee.jpg --mode post --title 'COFFEE & CONVERSATION' --body 'Good coffee. Better conversation.\nJoin our monthly table for local makers.' --details 'Sunday 25 October Β· 9β11 AM\nCedar CafΓ© Β· Drop in Β· $5' --title-font playfair@SemiBold --title-tracking 0.02 --label-font outfit@Medium --crop 0,0,1,0.9 --band top --bg '#1B1712' --shade 0.96 -o out/post-coffeeMaster Β· 1080 copy Β· Feed preview Β· Photo
Stillroom is a fictional studio created for this demo. Its supplied transparent logo keeps the room-plan mark to the left of the name. --logo occupies the title slot, so the complete offer, including the opening sentence, stays in --body. Paragraph spacing separates the promise from the explanation; --details retains the consultation length, price and call to action. The wordmark uses Playfair Display SemiBold, and the body uses Outfit Regular.
python skill/clean-thumbnail/scripts/thumb.py demo/subject-room.jpg --mode post --title 'STILLROOM' --body 'Make room for better work.\n\nA calm workspace, planned around your day. We help you rethink the layout before you buy new furniture.' --details 'Design consultation Β· 30 minutes Β· $45\nBook a studio call' --logo demo/post-stillroom-logo.png --title-font playfair@SemiBold --label-font outfit@Regular --backdrop solid --bg '#18352E' -o out/post-studioMaster Β· 1080 copy Β· Feed preview Β· Photo Β· Logo
The workshop and studio posts are rendered straight from the original wide photographs with --backdrop solid: the renderer keeps the photograph clean in its own rectangle and puts the type on an opaque panel of the --bg color, with a hard edge between them and no input preparation. The cafΓ© uses a portrait photograph with the default fade. The sources are recorded in post-LICENSE.md; all final lettering is rendered locally, with no generated scene or replacement face.
| Post | Master | Body cap at 360 px | Details cap at 360 px | Minimum element contrast |
|---|---|---|---|---|
| Creator workshop | 1440Γ1800 | 10.7 px | 9.0 px | 12.4:1 |
| CafΓ© event | 2160Γ2700 | 10.5 px | 9.1 px | 13.4:1 |
| Studio offer | 1440Γ1800 | 10.5 px | 9.0 px | 10.6:1 |
All three commands finish without warnings. Every body and details word is preserved in order, the creator's face is 100% in frame and in the clean photograph, with no type over it, and the 1080Γ1350 copies are under 2 MB. The two solid-panel posts are 1440Γ1800 because their photographs are 2560 and 2250 px tall; the renderer never upscales. The measured report records the exact text, wrapped lines, sizes and checks. The 360 px previews were also visually reviewed; these checks assess readability and layout, not engagement.
Every thumbnail is a small stack of real images in fixed roles, plus the type. New topics need no new layouts.
| Role | Flag | Use it for |
|---|---|---|
| Background | the capture | One frame, photo, or key art. The type goes on its calmer side over a shade built from its own shadow color. Mark a face or product with --subject-box and the type, its shade and the vertical crops keep clear of it; when the subject sits in the middle, --crop narrows the art so it moves to the far side. |
| Panels | --panel A --panel B |
Comparisons: versions, settings, before and after. A thin divider, a label per panel, the type in a band at the bottom. |
| Subject | --subject cutout.png |
A person, character, or product in front of the background, on the side away from the type. |
| Inset | --inset img --inset-box x0,y0,x1,y1 |
An image inside the background, like a game frame on a monitor. |
Subjects and insets are matched to the photo behind them: color cast, black and white points, sharpness, and grain, so the stack reads as one photograph. There are no white keylines, glows, or halos. Subjects need a transparent background; with rembg installed, the script removes it for you.
The same images and text render for YouTube and for Facebook, Instagram, Reels, Shorts, and Stories.
| Format | Flag | Master | Copy | Where the type goes |
|---|---|---|---|---|
| YouTube 16:9 | default | up to 3840Γ2160 | 1280Γ720, under 2 MB | Beside the image, clear of the duration badge from the 168 px up-next size up |
| Feed 4:5 | --aspect 4:5 |
up to 2160Γ2700 | 1080Γ1350 | Below the image; above it when there is a subject |
| Reels and Shorts 9:16 | --aspect 9:16 |
up to 2160Γ3840 | 1080Γ1920 | Inside the safe zone: clear of the top 14%, bottom 35%, and right 15% |
Vertical formats crop to the busiest part of the image, which is usually the subject, unless you set --focus. Comparisons stack, with the type on the seam so both panels stay visible. --aspect all renders every format. Each gets its own preview: a Facebook-style feed card, or a Reels-style overlay with the profile-grid crop.
Through the skill, you choose formats once. The first time, it asks which platforms you post to and saves the answer as "aspect" in clean-thumbnail.json; after that it renders those formats without asking. Naming a platform in a request ("make a Reels one too") adds that format for that request only. Run directly without a saved setting, the script renders 16:9 and prints a note saying so.
These come from the author's own 4K capture of Death Stranding 2 (the source), with the title in the bundled outfit font at Light weight and wide spacing, like the game's thin, spaced-out logo:
python skill/clean-thumbnail/scripts/thumb.py demo/ds2-capture.jpg --title "DEATH STRANDING 2" --label "4K Β· Max settings" --title-font outfit@Light --title-tracking 0.12 --focus 0.46 --lead title --aspect 4:5,9:16 -o out/ds2The 4:5 preview shows it in a Facebook feed card and the 9:16 preview under a Reels overlay with the profile-grid crop. A wide frame doesn't always suit the vertical formats, so they can come from a second capture and a second run. The safe zone holds 9:16 type in the middle of the frame, so a face belongs in the top third, as Sam's does here. With a centered face, --subject-box moves the type band to the top when that clears it; without a box the band stays at the bottom and shades the face, and --crop can't move the type on this format. --focus 0.46 centers Sam: on its own the crop follows the busiest part of the frame, which in this cabin is the equipment on the wall.
The big type should look like the thing in the video, not like every other thumbnail. The skill picks the look in this order:
- The official logo (
--logo), from the press kit or the official site, as in the Dawnwalker example. The most authentic option, and what publisher thumbnails use. - The closest bundled font (
--title-font cinzel): 16 open-licensed display faces, from Roman capitals and blackletter to stencil and pixel, matched to the logo with a decision table, in weight and letter spacing as well as shape (outfit@Light --title-tracking 0.12for Death Stranding's thin, spaced-out logo). Close in spirit, not the real lettering. - The channel's own font, when nothing bundled fits. A wrong match looks worse than a neutral one. Without a channel font, the defaults are bundled Playfair Display Bold (serif) or Outfit Bold (sans), with Outfit SemiBold in the label box, so the type is the same on every machine.
The label box always keeps the channel font, so every thumbnail looks like its game and the channel still looks like one channel.
Color follows the same restraint: white by default, one color at most. --title-color auto takes the color the image is known for (an eclipse, a sunset, neon, a team kit) and lightens it only as much as it needs to pass 4.5:1 on the dark side. Gray scenes and skin tones don't count, so those stay white. A brand hex code works too. Color the big type or the label box, not both.
White (left) and --title-color auto (right): the art's gold #a7753d is lightened to #ae7f4c so it reads at 5.3:1. Every variant prints the look and colors it used.
| Part | Role |
|---|---|
| Skill instructions | Assigns image roles, writes short thumbnail text or preserves a standalone post message, renders, and reviews the result. |
| Type rules | Turns a video title into a subject and hook, or keeps a post message as headline, body and details. |
| Channel settings example | Style, fonts, title and label colors, lead, size, and formats saved once for every video. |
| Bundled fonts and matching guide | 16 open-licensed display fonts (Roman capitals to pixel) so the big type can look like the game, film, or brand, each with its OFL.txt; catalog.json lists their real family names, weights, spacing and the looks they fit. |
agents/openai.yaml |
Display name, short description and default prompt for Codex's skill list. |
thumb.py |
Composes, matches layers, typesets, checks, and exports. |
| Demo | Five thumbnail examples and three standalone posts: sources, logos, master and upload copies, reproduction commands, credits, and feed-size previews. |
| Tests | Resolution, layout, lead, image roles, layer matching, contrast, formats, cropping, fonts, colors, channel settings, and the CLI. |
The renderer runs locally with NumPy and Pillow. No image-generation service is in the rendering path.
Try the included demo from this repository:
python -m pip install numpy pillow
python skill/clean-thumbnail/scripts/thumb.py demo/simon-capture.jpg --title "SIMON" --label "No hit Β· Parry only" -o out/simonThis writes simon_A.jpg and simon_B.jpg (masters), _phone.jpg copies for phone upload, and a _preview.png for each. The bundled capture is 637Γ358 with pillarbox bars around 358Γ358 art, so after trimming the bars the renderer correctly warns that it upscales about 2Γ; it demonstrates layout rather than production sharpness. The Dawnwalker example starts from a 2560Γ1440 source and renders without upscaling.
For your own video, use a clean, high-resolution image with room for type on one side. The master comes out at the largest standard size your images support without upscaling, up to 3840Γ2160. Official press-kit art can work; publish only images you have the right to use. If the frame is crowded, choose another moment before tuning the layout.
# comparison
python skill/clean-thumbnail/scripts/thumb.py --panel 25h2.png --panel 26h2.png --panel-label 25H2 --panel-label 26H2 --title "GAMING" --label "Which is faster" --style sans
# creator in front of a background
python skill/clean-thumbnail/scripts/thumb.py demo/subject-room.jpg --subject demo/subject-person.png --title "HOME STUDIO" --label "SMALL ROOM" --style sans --title-font outfit@Bold --label-font outfit@SemiBold --text-side right --push 0.35 --aspect 16:9 -o out/subject
# game frame on a monitor
python skill/clean-thumbnail/scripts/thumb.py crt.jpg --inset frame.png --inset-box 0.18,0.14,0.62,0.70 --title "DLSS 5" --label "On a CRT" --style sans| Option | When to use it |
|---|---|
--mode post with --body TEXT and --details TEXT |
A standalone post instead of a thumbnail: keeps the whole message (headline, wrapped body, when and where), checks it reads on a 360 px phone feed, and renders one 4:5 image unless --aspect or --lead is given. |
-o PATH |
Output stem, .jpg name or folder (default <capture>_thumb). Variants and formats add _A, _B, _4x5, _9x16, so a second run with the same -o for other formats keeps the first run's files; an -o that would overwrite an input stops with an error. |
--style serif|sans |
The default title look when no logo or matched font is given: bundled Playfair Display Bold or Outfit Bold. |
--panel A --panel B --panel-label L |
Comparison panels (2 or 3) instead of a capture, each with its label, in the same order. |
--kicker TEXT / --subtitle TEXT |
A small line above or below the big type; off unless given (see the type rules). |
--lead title|label|ab |
Which slot is big. ab (default) renders both. |
--text-side left|right |
Override the automatically chosen calmer side. |
--focus 0..1 |
Move the horizontal crop on a wide source. |
--crop x0,y0,x1,y1 |
Keep part of the capture (fractions of it as given). Narrower than 16:9, the art moves to the far side, which makes room for the type when the subject is in the middle. |
--subject-box x0,y0,x1,y1 |
Mark the face (or a product's label) in the capture, in fractions of it as given: from the brows to the chin, not the hair or the whole body. The 16:9 type takes the side that keeps it clear, the 4:5 and 9:16 crops center on it, the 9:16 type band moves to the top when that shades it less, and a warning fires when it still ends up under type or shade or out of frame, naming one fix: a --crop, --focus or --text-side to try on 16:9, or --band on 4:5 and 9:16. |
--band auto|top|bottom |
Where the type band goes on 4:5 and 9:16. auto (default) keeps the bottom, or takes the top when that keeps --subject-box clearly more in view. |
--feather 0..1 |
Length of the fade where narrow or cropped art meets the dark side, as a share of the art's width (default 0.36). Shorten it when the fade reaches a face. |
--backdrop fade|solid |
What sits behind the type. fade (default): the photo darkens into the shade behind the type. solid: the photo stays clean in its own rectangle and the type sits on a solid --bg panel with a hard edge, in every format and in post mode (channel key backdrop). --shade and --feather have no effect with solid, and --panel comparisons ignore it. |
--shade 0..1 / --bg #hex |
Darkness of the type side (default 0.88) and its color (default: taken from the image's shadows). |
--push 0..1 |
How much to soften the background behind subjects (default 0.5). |
--logo logo.png --logo-white |
Use a transparent logo in place of rendered title text. |
--title-font NAME|FILE |
A bundled font matched to the game's or brand's lettering (cinzel, bebas, orbitron...; --list-fonts), or your own font file. |
--title-tracking 0..0.5 |
Letter spacing of the big type in em. Wide (0.1 to 0.2) for a thin, spaced-out logo; the font's own spacing by default. |
--system-fonts |
Use installed faces (Bodoni MT from Office, Bahnschrift, Didot, Helvetica Neue) for the default title and label instead of the bundled Playfair Display and Outfit. The look then depends on the machine; "system_fonts": true in clean-thumbnail.json makes it the channel default. |
--label-font NAME|FILE |
Your channel's font for the label box: a bundled name (outfit@SemiBold) or a font file. |
--list-fonts |
Print the bundled fonts with their real family names, looks, and the titles they fit. |
--title-color white|auto|#hex |
Big type color. auto takes the image's signature color (a sunset, neon, an eclipse) and lightens it just enough to read; it stays white when the image has none. |
--accent white|auto|#hex |
Label box color. Color the big type or the label box, not both. |
--aspect 16:9,4:5,9:16 |
Formats to render; all for every one. |
--size 720p|1080p|1440p|4k |
Force the 16:9 master size. |
--channel FILE |
Channel settings, including saved formats; ./clean-thumbnail.json loads automatically. Flags win for that run. |
--feed DIR |
Place both variants in a grid with thumbnails from a local folder. |
The type rules keep thumbnail text at eight words or fewer. Posts keep the complete message and check its readability at phone feed size.
| Check | What the renderer does |
|---|---|
| Image preparation | Trims letterbox and pillarbox bars (video black and subtitle bars included, a dark night sky left alone), reads 16-bit and transparent images correctly, applies --crop in the coordinates of the image as given (bars are trimmed either way, including the clean bars of a letterboxed video inside the crop), picks the master size without upscaling, and warns when a small source, cut-out, inset or logo still needs it. |
| Type placement | Picks the calmer side (or the side with room beside an inset, or the side that keeps --subject-box clear), shrinks the type to fit the height and width, keeps cut-outs out of the type, and warns when type overlaps a subject or inset or runs off the frame. |
| Post message | Keeps every body and details word in order; shrinks the headline before the body, warns below 8 px cap height at the phone feed width, and exports one post by default. |
| Subject box | Measures where --subject-box landed in every format (in frame or in the Reels safe zone, visible, under heavy shade, under type), prints it, and warns with the fix when the face is shaded, covered or cut off. Without a box the script finds the subject by edge activity, which can't recognize a face, so it can't warn about one. |
| Layer matching | Matches each subject and inset to the photo behind it in color cast, tone, sharpness, and grain (sharpness against the unblurred photo), and prints what it changed. |
| Contrast | Scores every element separately against WCAG 4.5:1 (title lines, kicker, subtitle, label box, logo, panel labels), darkens the type area once when light type fails because of the background, and names the element and the fix when it can't. |
| Type look | Uses the official logo, else the bundled font closest to the game's or brand's lettering, else the channel font; warns about missing characters (accents, other scripts), all-caps blackletter or script, unknown weights, and type that ends up small. Numbers use lining figures and tracked type keeps its kerning. Colors get the same contrast check; auto colors are lightened until they pass, and two different accent colors get a warning. |
| Safe zones | Keeps 16:9 type out of the duration-badge corner (sized to the badge on the 168 px up-next tile; at 120 px the badge can still touch a right-side label) and 9:16 type and panel labels inside the Reels/Shorts safe zone, and warns when it can't. |
| Inputs | Stops with a clear message on a missing file or font, an opaque logo, a fully transparent cut-out, conflicting roles, a --crop on comparison panels or one that leaves nothing, a --subject-box with panels or a cut-out, bad channel settings, or an -o that would overwrite an input. |
| Export | Writes a master at the largest size the images support, a standard-size copy (1280Γ720 under 2 MB for YouTube, 1080 wide for vertical), and a preview sheet per variant and format. |
These checks catch mechanical failures. They do not measure click-through rate, choose the best image, or guarantee that the subject is legible. Look at the previews and fix open warnings before publishing.
- Real images over generated backdrops: the thumbnail shows what the viewer will actually see, but it depends on useful images. The script cannot invent a better camera angle.
- Quiet compositing over the cut-out template: subjects are matched to the photo instead of outlined and lit up. It reads as a photograph, at the cost of the instant pop the loud style buys.
- Two variants over one guess: whether the subject or the hook should be big depends on the audience, so the renderer makes both and leaves the choice to YouTube's Test & Compare, which picks on watch time share rather than click-through rate. It is a pair, not a promise of a third variant.
- Editable type over model-drawn text: spelling and hierarchy remain controlled, but the result is intentionally quieter than a dramatic logo. Use
--logowhen the official mark carries recognition better. - One color over a palette: white by default, and at most one color taken from the image or the brand.
autopicks the image's most vivid color family, which is not always the one you would choose, and lightens deep colors toward pastel; pass a hex code when you know the color. - Feed-size review over full-size polish: the 168 px and 120 px previews expose unreadable text early. Fine detail that only works at 4K is not a thumbnail advantage.
The tool composes, matches, and grades real images; it does not fabricate scenes, draw insets in perspective, or replace the need to test variants with viewers. Quiet thumbnails are a bigger bet in categories where loud is the norm; the feed grid and Test & Compare (which rewards watch time, not just clicks) will tell you per channel.
Claude Code: copy skill/clean-thumbnail/ to ~/.claude/skills/clean-thumbnail/ (all your projects; %USERPROFILE%\.claude\skills\clean-thumbnail\ on Windows) or to .claude/skills/clean-thumbnail/ in a repository (that project only; commit it and your team gets it too), run python -m pip install numpy pillow, then ask for a thumbnail with a video title and your images, or type /clean-thumbnail.
Codex: copy skill/clean-thumbnail/ to ~/.codex/skills/clean-thumbnail/, install NumPy and Pillow, then invoke $clean-thumbnail with a video title and your images.
Clients accepting Agent Skills ZIPs: download clean-thumbnail.zip from the latest GitHub Release; it is attached to every release, so there is nothing to build. To build it yourself, run python tools/build_zip.py and upload dist/clean-thumbnail.zip.
Direct Python: run thumb.py as in Quickstart. It uses its bundled fonts by default, so the type is the same on every system; --system-fonts uses installed faces instead (Bodoni MT, Bahnschrift, Didot), and any font file can be passed in. For automatic background removal on subjects, also python -m pip install rembg.
python -m unittest discover -s testsThe suite covers master sizing and layout scaling for all three formats, the Reels safe zone, subject-following crops, stacked comparisons, the phone copy limit, calmer-side selection, both lead variants and stacked hooks, panels, subjects, insets, layer matching (cast, grain, sharpness), per-element WCAG contrast, logos, bundled fonts (catalog, weights, lining figures, kerning, missing glyphs), title and label colors (signature color, contrast lift, the one-color rule), cropping (bars trimmed outside the crop and clean video bars inside it, a dark stage inside it kept, any crop narrower than 16:9 kept at full height, inset boxes kept where you drew them and clipped to the art), the subject box (remapping, side choice, centered vertical crops, the 9:16 band flip, each warning and its suggested crop), the Dawnwalker, creator-photo, subject-layer and Death Stranding 2 demo commands, previews, feed grids, channel settings and saved formats, the CLI, and regression tests for every defect found in the outside and multi-agent reviews (bar trimming, crop rounding, panel label placement, inset visibility, input validation, font handling). The colors, crop, tracking, subject-box and default-font (--system-fonts) checks are mutation-tested: python tools/mutate.py puts each fixed bug back into a copy of the renderer and confirms its test fails (a crash in a broken mutant doesn't count). GitHub Actions runs the suite on Windows, macOS and Linux, plus Python 3.8. The demo/ folder is included in the repository for visual inspection; it is not part of the Agent Skills ZIP.
For a short review path, read the type rules, compare the capture with variant A, variant B and a preview, then read the renderer.
MIT; see LICENSE. Built by Rizwan Ahmed.
The bundled fonts are from Google Fonts under the SIL Open Font License; each family's license is next to it in skill/clean-thumbnail/fonts/<name>/OFL.txt: Cinzel, Playfair Display, MedievalSharp, Grenze Gotisch, Bebas Neue, Oswald, Teko, Rajdhani, Orbitron, Outfit, Nunito, Black Ops One, Press Start 2P, Kaushan Script, Zilla Slab, Courier Prime.
The game images in demo/ are third-party game art, included only to show what the renderer does. They are not covered by the MIT license and belong to their owners; The Blood of Dawnwalker key art and logo are by Rebel Wolves, published by Bandai Namco Entertainment; the Death Stranding 2: On the Beach frame is the author's own capture of the game by Kojima Productions and Sony Interactive Entertainment; the Simon painting is official splash art from Clair Obscur: Expedition 33, Β© 2025 Sandfall Interactive, published by Kepler Interactive (the official card credits art director Nicholas Maxson-Francombe), cropped from the copy on the Hero Wiki. Rights holders can ask for any demo image to be removed by opening an issue.
The creator photo, the subject-layer room photo, the coffee photo and their rendered derivatives use the Pexels License, separately from the MIT code license. See the creator photo credit, the subject-layer sources, and the post-demo credits.