| name | screenshot-carousel |
| description | Turn a landscape talking-head video (event pitch, talk, interview) into a branded Instagram/LinkedIn carousel of subtitled video screenshots, the way podcast/interview clips get reposted as swipeable "reel screenshots". Alternates solo slides (one portrait screenshot) and duo slides (two stacked landscape screenshots), each with burned-in on-brand captions, ending on a CTA slide. Renders offline on CPU with PIL. Brand is set once via brand.json. |
| metadata | {"author":"swanbase"} |
screenshot-carousel
Produce a branded subtitled-screenshot carousel from a landscape speaker video, on your own
brand. Sister skill to vertical-short: it reuses the same source, the same speaker-tracking
data (cmd.txt), and the same brand system, but outputs static carousel slides instead of a
vertical video. Everything runs offline with ffmpeg (frame grabs) + Pillow (composition).
Branding (do this once)
The look (logo, accent, background, url, fonts) lives in brand.json at the repo root. A fresh
clone ships a neutral placeholder. Set your brand once:
python3 ../../brand/setup.py --name "Acme" --url acme.com \
--accent '#FF5A1F' --bg '#0B0F1A' --logo /path/to/your-logo.png
A worked example brand is in examples/brands/swanbase/. For multiple brands in one clone,
point each run at its own file with BRAND_JSON=/path/to/brand.json.
When to use
- You have a landscape (16:9) video of one person talking (pitch, talk, interview).
- You want the "reposted reel as screenshots" format that performs well on Instagram/LinkedIn:
a swipeable carousel of video stills with the spoken line burned in under each shot.
- You want it on-brand: your lockup with a drop shadow, the spoken line with one keyword in your
accent color per shot, a slide counter, and a closing CTA.
Output
<out>/slide-01.jpg ... slide-NN.jpg, 4:5 (1080×1350) JPEGs, the optimal carousel ratio. Post
them in order. Optionally also emit a single multi-page PDF (one slide per page) for sharing or
review: pass --pdf /path/out.pdf or add "pdf": "/abs/path.pdf" to the spec.
Two slide shapes (alternate them)
- solo, one 9:16 portrait screenshot reframed on the speaker, caption at the bottom over a
scrim. Feels like a single reel still.
- duo, two stacked landscape screenshots (different moments), each with its own caption over
its bottom. Reads as "two beats of the same idea".
Alternating solo → duo → solo → duo gives the carousel rhythm and lets a duo pack two spoken
lines into one slide. Close with a cta slide.
Quickstart
cd screenshot-carousel/scripts
python3 carousel.py --spec /path/to/spec.json
The slides are files in <out>/; share/post them however you like. The skill produces the
files, distribution is up to you.
Spec schema
{
"src": "/abs/path/source.mp4",
"track": "/abs/path/cmd.txt",
"lockup":"/abs/path/logo.png",
"out": "/abs/output/dir",
"slides": [
{"type":"solo","t":10.4,"text":"Full sentence here.","violet":["keyword"]},
{"type":"duo","shots":[
{"t":13.5,"text":"First line.","violet":["one"]},
{"t":16.8,"text":"Second line.","violet":["two"]}
]},
{"type":"cta","eyebrow":"YOUR TAGLINE · CITY",
"line":"learn more at ","brand":"yourbrand.com"}
]
}
t is a timestamp (seconds) into the source, pick a frame where the speaker is well-framed
and expressive (mid-sentence, hands up, etc.).
text is the spoken line for that shot. Keep it to one sentence; it wraps to 2-3 lines. Long
lines look crowded on duo slides (smaller font), trim them.
violet is a list of normalized tokens to color in your accent (accents and apostrophes
stripped, lowercased). E.g. to highlight l'IA put "l'ia" and/or "ia". Pick ONE keyword
per shot, the emphasis is the point, not a rainbow. (The key name violet is historical; it
colors with whatever accent your brand.json defines.)
Brand
All colors/fonts/logo come from brand.json via brandlib (run setup.py once). The lockup is
drawn top-left with a soft drop shadow (blurred 55%-alpha black silhouette, offset +4/+6,
Gaussian blur 7) so a white logo stays legible over bright footage (white ceilings, screens).
Without the shadow the logo disappears on light frames, this was an explicit fix.
Color format trap (the #1 mistake): PIL takes an (R,G,B) tuple; ASS/libass take byte-reversed
&HBBGGRR&. brandlib.hex_to_rgb derives the PIL tuple from one canonical hex so the BGR-vs-RGB
mixup (which turns the accent the wrong color) can't happen. See ../../vertical-short/reference/brand.md.
How it works
- Speaker center. If a
track (cmd.txt) is given, each timestamp maps to a crop-x. That
crop-x is on vertical-short's sub-pixel PAN_K grid (track.py emits round(x*PAN_K),
default 4), so it is divided back to source px first (the factor is read from geom.env next to
cmd.txt); the speaker center is then crop_x/PAN_K + 608/2. Solo grabs a 9:16 portrait crop
around that center; duo grabs a wider landscape crop around the same center, sized to the photo
box. No track → center crop. (Skipping the PAN_K divide pins every crop to one frame edge
instead of the speaker; see Gotchas.)
- Composition (PIL). Solo = portrait still windowed to 1080×1350 + bottom scrim + caption.
Duo = two landscape stills stacked with an 8px gap, each with its own bottom scrim + caption.
Both get the shadowed lockup top-left and a
NN / NN counter top-right.
- Captions. Wrapped to the box width, centered, drop-shadowed; tokens whose normalized form
is in
violet are drawn in the accent color.
- CTA. Brand-bg slide, faint radial glow, large centered lockup, eyebrow, and
<line><brand> with the brand fragment in the accent color.
Tuning knobs (in scripts/carousel.py)
SOLO_CROP_W (portrait crop width, 608 = 9:16 on 1080-tall), SOLO_Y_OFF (vertical window into
the portrait still).
DUO_GAP (px between stacked photos).
- Caption font size per shape: spec-level
solo_fsize (default 64) and duo_fsize (default 42).
Smaller duo size fits ~3 lines, so duos can carry fuller, more transcript-like captions. Scrim
darkness/height in caption().
- CTA layout (logo size, y positions, glow) in
cta().
Gotchas
- Accent RGB vs ASS, see Brand above. The #1 trap.
- Pick expressive frames. A still is unforgiving: avoid mid-blink / mouth-mid-word frames.
Nudge
t by ±0.3s and re-render if a face looks off.
- Duo captions are smaller, keep duo
text short or it wraps to 3 lines and crowds the photo.
Prefer solo for the long lines.
- Track is on the PAN_K grid, not source px.
cmd.txt crop-x is round(x*PAN_K) (default
PAN_K=4), so it must be divided by PAN_K (read from the sibling geom.env) before use. The
code does this; if you hand-make a track or drop geom.env, PAN_K falls back to 1. Get it wrong
and every crop clamps to a frame edge (the speaker disappears off-frame).
- Track is per-source. Reusing a
cmd.txt from a different cut/source will reframe on the
wrong x. Regenerate cmd.txt if the source changes.