This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
title-card-stills-finder — Setup & User Guide
This is the hands-on manual: how to install it, every flag, the two scoring modes, and the full end-to-end TitleCardMaker (TCM) workflow. For what it is and why, see the README.
1. What you need
- Your show's video library laid out Plex-style, with
SxxEyyin each filename (e.g.... - S04E09 - Kir'Shara ...mkv). Season-subfolders are fine. ffmpeg+ffprobeon PATH, and Python 3 with Pillow and numpy.- Optionally TitleCardMaker, if you want the stills to actually become cards.
You can install those directly, or skip all of it with the bundled Docker image (below).
2. Install
Option A — Docker (recommended)
The repo ships a Dockerfile that bundles ffmpeg + Pillow + numpy. Build it once on a
machine that can build images:
git clone http://YOUR-GIT-HOST/xenarathon/title-card-stills-finder.git
docker build -t titlecard-stills:latest title-card-stills-finder
Then run it with your library and output folder mounted in (see §4). The container's entrypoint is the script, so everything after the image name are the script's flags.
Option B — run the script directly
pip install pillow numpy # ffmpeg must already be installed
python3 titlecard_stills.py --help
3. The two scoring modes
Every sampled frame is scored for sharpness (motion blur loses) and exposure (near-black / blown-out / blank frames are dropped). On top of that you pick one composition rule:
| Mode | Flag | Use when |
|---|---|---|
| Right-2/3 (default) | --right-bias 0..1 |
Your card's title is flush-left. Rewards detail in the middle+right with a clear left third. |
| Text-zone | --text-zone x0,y0,x1,y1 |
Your title sits anywhere else (centered, center-left, lower-third). Rewards frames whose detail stays out of the rectangle you give. |
Finding your --text-zone rectangle
The rectangle is x0,y0,x1,y1 as fractions of width/height (0,0 = top-left,
1,1 = bottom-right). The easy way to find it:
- Open one card TCM already built for this show.
- Draw a box over the area the title text occupies.
- Convert the box corners to fractions of the image size.
A center-left title (like a Horizon-style card) is roughly 0.05,0.40,0.58,0.66.
Frames where that band is busy (a face there) score low; frames with the subject pushed
to the side score high. Tune how hard it pushes with --text-zone-weight (default 0.55).
Every contact sheet drawn in text-zone mode has the box outlined on each thumbnail, so a text-over-face collision is obvious at a glance.
4. Running it
The shape of a run (Docker form — the script form is the same flags without the
docker run/mounts):
docker run --rm \
-v "/path/to/Show":/library:ro \
-v "/path/to/tcm/config/source/Show (Year)":/src \
-v "/path/to/out":/out \
titlecard-stills:latest \
--library /library --source /src --out /out \
--candidates 4 --right-bias 0.45
--sourceis only used to detect which episodes already have asXeY.jpg(so they can be skipped). It's repeatable. Mount it read-only unless you use--auto-place.- Output candidates land in
/out/sXeY/, each with a_contactsheet.jpg.
Always dry-run first to confirm the episode list: add --dry-run.
All flags
| Flag | Default | What it does |
|---|---|---|
--library |
(required) | Plex-style show folder (Season XX subdirs OK) |
--source |
— | TCM source dir(s) to check for existing sXeY.jpg (repeatable) |
--out |
(required) | Output dir for candidate stills |
--candidates |
4 | Top frames to keep per episode |
--interval |
12 | Seconds between sampled frames (raise to go faster, fewer candidates) |
--skip-head / --skip-tail |
0.06 / 0.08 | Fraction of runtime to skip at start/end (intros/credits) |
--right-bias |
0.45 | 0..1 weight of right-2/3 composition vs raw sharpness |
--text-zone |
— | x0,y0,x1,y1 fractions: score for a CLEAR title band instead (see §3) |
--text-zone-weight |
0.55 | 0..1 weight of zone-clear vs sharpness when --text-zone is set |
--dedup-dist |
12 | Max perceptual-hash distance treated as a duplicate |
--only |
— | Season filter, e.g. S35,S36 or 35,36 |
--episodes |
— | Explicit list, e.g. s1e5,s4e9 |
--limit |
0 | Process at most N episodes (handy for a tuning run) |
--all |
off | Process every episode, not just those missing a source |
--auto-place |
off | Also copy each #1 pick into the source dir as sXeY.jpg |
--place-dir |
— | Where --auto-place writes (defaults to first --source) |
--overwrite |
off | Reprocess episodes that already have an out/ dir |
--io-workers / --ep-workers |
6 / 2 | Parallel frame grabs / episodes in flight |
--dry-run |
off | List the episodes that would be processed, then exit |
5. End-to-end TitleCardMaker workflow
A. Fill gaps (episodes with no TMDb still)
--dry-runto confirm the missing list.- Run without
--auto-place; review the contact sheets inout/. - Copy your pick into the TCM source dir as
sXeY.jpg(TCM uses unpadded names, e.g.s4e9.jpg), or re-run with--auto-placeto drop each #1 pick in for you. - Rebuild cards in TCM (its UI, or
POST /api/v2/series/series/<id>/process).
B. Replace stills that collide with the title
- Find the offenders: build all your current cards, then eyeball them (overlay your title rectangle so collisions are obvious).
- Re-mine just those with
--all --episodes s1e5,s4e9,... --text-zone <your box>. - Back up the originals before overwriting (e.g.
cp -ninto a_backup_DATE/subfolder — and leave non-episode assets likebadge.png/logo.pngalone). - Place your picks, then force a rebuild. TCM caches cards, so the reliable way to make
it regenerate is delete the cards, then process:
DELETE /api/v2/cards/series/<id>→POST /api/v2/series/series/<id>/process.
6. Tips & gotchas
- 4K is slow. HEVC 2160p decodes on CPU at roughly 3–5 min/episode. For a big run,
launch the container detached (
docker run -d) and poll the logs; it survives your SSH session dropping. Raise--intervalto trade candidates for speed. - Let Docker make the out dir. A
-vmount auto-creates the host path as root. If youmkdirit yourself as a non-root user inside a root-owned tree, you'll hit permission errors — just let the mount create it. - Watch for burned-in credit text. Opening-credits frames (writer/producer/actor
names, ~2–9 min in) often score well but have text baked in.
--skip-headwon't fully clear them (credits run into act one); eyeball the contact sheet and pick a deeper candidate if the #1 landed on a credit. - Multi-episode files. A combined
S01E01-E02file is parsed ass1e1only;s1e2won't get its own still from it. - Relative scoring caveat. On mostly-dark space/night frames, any detail in the text band can read as "busy" relative to the black average. Trust your eyes on the contact sheet over the raw number for those.
This guide is updated on each release. Built with help from Claude (Anthropic Opus 4.8).