Turn a pile of photos and clips into a finished 9:16 Instagram Reel β the right length, in your own voice, with a title, captions, and an optional countdown timer β by having a short conversation with Claude. No editing software, no timeline.
Prerequisites (three things):
- Claude in VS Code, with this repo open. The scripts are already in this folder.
- ffmpeg + ffprobe in
tools/. These aren't shipped in the repo (too large). Putffmpeg.exeandffprobe.exein thetools/folder before building β download from https://www.gyan.dev/ffmpeg/builds/ (grab the "essentials" build, unzip, copy the two.exes from itsbin/intotools/), or install withwinget install Gyan.FFmpeg. Claude checks for these and will walk you through it if they're missing. - An ElevenLabs voice clone already set up, with your
ELEVEN_API_KEYandELEVEN_VOICE_IDfilled into the real.envfile (see "One-time setup" below). This is what lets quickVid narrate in your own voice. (Only skippable if you never want a cloned-voice voiceover β i.e. you'll go silent or supply your own audio file.)
Drag this file (ReadME.MD) into the Claude console and type:
let's get started
The file you drag in selects the workflow: ReadME.MD (this file) for the general
photos-and-clips builder, or potterySkill.MD for the one-video "process reel"
format with flashing points. Both start with the same phrase β "let's get started" β
so the dragged-in file is what tells Claude which recipe to follow.
Claude reads this file and walks you through it β asking for your media, the reel
length, a title, whether you want a voiceover, captions, and a timer β then builds
the reel and saves it to final/ with a date-and-time stamp.
That's the intended path. You can also run the steps by hand (see the bottom).
- A 9:16 vertical video (1080Γ1920), the format Instagram Reels expects.
- Your photos and clips stitched in the order you choose.
- A length you pick from four bands (max-reach 7-15s, sweet-spot 15-30s, deep 30-60s, or long-form up to 20 min); flash speed and clip trimming are tuned to hit it and look good.
- An optional title overlaid on the reel.
- Voiceover in YOUR own voice (an ElevenLabs voice clone, set up once), or an audio file you already have, or no voiceover.
- Optional burned-in captions (auto from your script, or your own
.srt) β ~85% of reels are watched muted. - An optional countdown timer (1/100 s) β tell it how long the reel took to make; it starts there and ticks to 00:00.00 across the reel.
- Music is left to Instagram β add it in one tap on the upload screen (faster, and you get the licensed IG music library).
- Output to
final/reel_<date>_<time>.mp4. Nothing is ever overwritten.
quickVid/
ββ voice_sample/ β ONE-TIME: drop a sample of your voice here to clone it
ββ media/ β your photos & videos (Claude puts them here for you)
ββ aroll/ β talking-head base video: phone selfie or HeyGen avatar clip
ββ broll/ β b-roll clips used as timed overlay panels (optional)
ββ chips/ β rendered hook-chip PNGs (make_chip.py output)
ββ voiceover/ β your own voiceover audio (only if not using the clone)
ββ prompts/ β script.txt, timeline .json, caption .srt files
ββ final/ β finished reels appear here, date-time stamped
ββ tools/ β PUT ffmpeg.exe + ffprobe.exe HERE (not shipped β see Prerequisites)
ββ make_reel.py β slideshow builder (length, title, captions, timer)
ββ compose_reel.py β talking-head compositor (overlays over an A-roll)
ββ make_chip.py β hook-text chips ("π¨ β¦") as transparent PNGs
ββ make_cards.py β 9:16 infographic cards (toolkit + per-video deck)
ββ script_fit.py β how many script words fit a given reel length
ββ add_timer.py β overlays a countdown timer after the reel is built
ββ elevenlabs_voice.py β voice-clone setup + turns a script into your voiceover
ββ requirements.txt
ββ .env.example β copy to .env; put your ELEVEN_API_KEY + voice id here
quickVid narrates in your own cloned voice. Set this up once:
- Drop a voice sample in
voice_sample/β 1-5 min of you talking, clean audio (see the note in that folder). - Create the clone at https://elevenlabs.io/app/voice-lab β Add Voice β Instant Voice Clone β upload that sample β save.
- Add your API key β copy
.env.exampleto.envand setELEVEN_API_KEY=...(from https://elevenlabs.io/app/settings/api-keys)..envis the one place your key goes. - Verify + get your voice id:
Confirms the key, shows remaining characters, lists your voices. Copy your clone's
python elevenlabs_voice.py --checkvoice_id. - Save it in
.envasELEVEN_VOICE_ID=..., then re-run--checkuntil it says SETUP COMPLETE.
(Prefer no voiceover, or already have an audio file? You can skip this.)
Have a short conversation, one topic at a time. Ask before generating; don't assume. Use sensible defaults and keep it moving.
Which builder? This flow (steps 0-9) is the slideshow path: photos and
clips flashed in sequence by make_reel.py. If the user wants the
talking-head / creator format β a person (or their HeyGen avatar) on camera
the whole time, with cards, b-roll and a hook chip layered over them β skip to
"FOR CLAUDE β talking-head recipe" further down instead.
Before anything else, confirm tools/ffmpeg.exe and tools/ffprobe.exe exist
(they're not shipped in the repo). If either is missing, stop and ask the user
to install them β the build can't run without them. Point them to the easiest path
and offer to help:
- Recommended:
winget install Gyan.FFmpeg, then copyffmpeg.exe+ffprobe.exeintotools/; or - Download the "essentials" build from https://www.gyan.dev/ffmpeg/builds/, unzip,
and copy the two
.exes from itsbin/intotools/.
Re-check that both files are present before continuing. Don't start the media step until ffmpeg is in place.
Always ask which file(s) to use at the start of every session β even if media/
already has files in it. Leftover media from a previous reel may still be sitting
in media/; never silently reuse whatever happens to be there. First list anything
already in media/ and ask the user whether to use it, add to it, or clear it β
then remove whatever they don't want so only the chosen files remain before you build.
Ask the user for the paths or links to the photos/videos they want (a folder, a list of files, or URLs). Then:
- Copy/move them into
media/, renaming with numeric prefixes (01_,02_, β¦) to set play order. For local paths usecp; for URLs download them. For a shared Google Drive folder,gdown --folder <url>pulls every file at once (needsbeautifulsoup4). - Play order is whatever the user wants β it's just the numeric prefix, so you
can honor any request: "put the sunset first," "these three at the end," a fully
custom sequence, etc. The user can also reuse the same image in more than one
slot β e.g. "make IMG_3111 the first and last frame" β copy it to both
01_and the final prefix so it bookends the reel. Duplicating a file under two prefixes is fine; each prefix is an independent slot. - List what's now in
media/back to the user in play order and confirm the order is right (offer to reorder, duplicate, or drop any by renaming). - Supported β photos:
.jpg .jpeg .png .webp; videos:.mp4 .mov .m4v .webm. - Screen recordings / landscape content: if the user's media is a screen capture
or any landscape (16:9 or wider) source, add
--fitto the build command. This fits the content inside the 9:16 frame with black bars instead of cropping into it.
Ask, offering four options:
How long should the reel be?
- Max reach (7-15s) β fastest cuts; discovery / hook-and-go.
- Sweet spot (15-30s, recommended) β the highest-performing Reel length.
- Deep engagement (30-60s) β room for a fuller story.
- Long form (60s-20min) β the user names the exact length.
You decide the pacing to hit the length and look good (per-case judgement):
pick a concrete target (midpoint of the band, or the user's number) to pass as
--target-seconds <N>, and decide video truncation with --max-clip-seconds
(short bands + long clips β trim to ~2-4s each; longer bands β keep clips fuller).
Sanity-check with python script_fit.py --target <N> β it recommends the flash
speed and warns about too much / too little media.
Ask: "Want a title overlaid on the reel? If so, what should it say?" If yes,
pass --title "<text>". Default shows for the first 3s at the top; offer
--title-seconds 0 (whole reel) or --title-position middle if they want.
Ask: "Do you want a voiceover?"
- Yes, my cloned voice (default):
- Run
python elevenlabs_voice.py --check. If it doesn't say SETUP COMPLETE, walk them through the one-time setup (voice sample β clone at https://elevenlabs.io/app/voice-lab βELEVEN_API_KEY+ELEVEN_VOICE_IDin.env). Don't generate until--checkpasses. - Tell the user the word budget before asking for the script. From the
chosen length, quote the target at ~150 wpm so they write to the right size β
e.g. "A 12s reel fits about 30 words (~26-33); send me your script and I'll
check it." Rough guide: 7-15s β ~18-37 words, 15-30s β ~37-75, 30-60s β
~75-150, then ~150 words per added minute. Run
script_fit.py --target <N>for the exact budget for the picked length. - Get the script into
prompts/script.txt(ask for the words, or write what they dictate). - Check it fits (step 6 below), then generate:
python elevenlabs_voice.py --script prompts/script.txtβ writesvoiceover/vo_eleven.mp3. It prints the characters used / cost β relay that. Add--style energetic|authoritativeif they want a different read. - Build with
--sound voiceover.
- Run
- Yes, a file I have: confirm an audio file is in
voiceover/(or ask its path); build with--sound voiceover. - No: build with
--sound none; remind them music is added on Instagram.
Ask: "Add burned-in captions? (recommended β most reels are watched muted.)"
- From the script β
--subtitles auto(usesprompts/script.txt). - From my
.srtβ--subtitles file --sub-file "<path>". - No β skip.
Before generating a voiceover or auto-captions, run
python script_fit.py --target <N>. It reports the reel length, the word
budget (~150 wpm), and whether prompts/script.txt fits. If it's too long, tell
the user the budget and offer to summarize the script down to fit (keep the
hook and CTA), rewrite prompts/script.txt, and re-check.
Ask this before the build, so the timer is baked into the same render (one
file, no second pass): "How long did it take you to create this? I can overlay a
countdown that starts there and ticks to 00:00.00 across the reel β or skip it."
- If they give a time β add
--timer --timer-seconds <e.g. 23:00 or 1380>to the build flags in step 8. (Counts down by default;--timer-direction upcounts up. Top-center by default so it clears the title/captions.) - If they skip β no timer flags.
- Only fall back to
add_timer.py(below) if the reel was already built and they decide they want a timer after the fact.
Assemble the flags from the answers (including any timer flags from step 7) and run
make_reel.py with C:\Users\DKYLE\.conda\envs\api\python.exe (falls back to
python). Example:
python make_reel.py --sound voiceover --target-seconds 22 --max-clip-seconds 3 \
--title "MY EPIC TRIP" --subtitles auto --timer --timer-seconds 23:00
Give the output path (final/reel_<stamp>.mp4), its length, and the Built in:
time; if silent, remind them to add music on Instagram. If they included a timer,
confirm it's counting from their time.
After-the-fact timer (only if the reel was already built without one):
python add_timer.py --seconds <e.g. 23:00 or 1380>
It reads the newest reel and writes a new reel_<stamp>_timer.mp4 (original
untouched). Report the new path.
Every step writes a new timestamped file β nothing is overwritten.
Pick a band; the tool tunes photo-flash speed (and clip trimming if you ask) to hit it. Longer target = slower, lingering cuts; shorter = faster flashes.
| Band | Length | Feel |
|---|---|---|
| Max reach | 7-15s | Fast, punchy; built for the scroll |
| Sweet spot (recommended) | 15-30s | Highest-performing Reel length |
| Deep engagement | 30-60s | Room for a fuller story |
| Long form | 60s-20min | You name the exact length |
- Build to a target:
make_reel.py --target-seconds 22(add--max-clip-seconds 3to trim clips). Photos flash no faster than 0.4s and hold no longer than 8s; within that the target is matched exactly. - Plan first (no render):
script_fit.py --band sweet-spot(or--target 22).
Tell it how long the reel took to make; it starts the clock there and runs it to zero across the whole reel, precise to the hundredth of a second.
- "This reel took 23 minutes" on a 45s reel β shows
23:00.00on the first frame,11:30.00at the halfway point,00:00.00at the end. The 23-minute span is compressed into the reel's 45 seconds. - If you don't give a time, it counts down from the reel's own length.
- Add it after building (
add_timer.py) or in the same build (make_reel.py --timer). Top-center by default so it never collides with the title or captions.--direction up(or--timer-direction up) counts up instead. - Whenever the timer is shown, the label "Estimated Time to Create" is drawn directly above it, so viewers know what the running clock represents.
make_reel.py concatenates: one clip at a time, source audio dropped. The
reels that consistently outperform are built the other way around β they
composite: a person talks to camera full-screen for the whole reel (the
A-roll), and the evidence slides in as timed panels over them. Notes
from comparing a top-performing talking-head wildfire explainer against a
cards-only slideshow of the same story:
- A face + continuous voice is the retention engine. Faces stop the scroll; speech sets the pace so the viewer never decides to keep watching. A silent slideshow asks the viewer to read β that's work, and they swipe.
- Something must move every few seconds. A static card held 13s is an eternity; the same card as a 6s panel over a talking head is a highlight.
- The hook is decided in the first 2 seconds: dramatic first frame plus a text chip like "π¨ Nobody is Talking About This".
- Receipts beat polish: real footage, live maps and raw charts read as credible. Keep the polished cards β but layer them over a human voice.
- Record yourself: phone camera, 9:16 portrait, good light, mic close, talking the script straight into the lens.
- HeyGen avatar clone: paste the script into HeyGen (your avatar + your cloned voice), portrait/9:16 output, download the .mp4. If Claude has the HeyGen connector, it can drive this from the script directly.
Drop the file in aroll/. Its speech is kept and loudness-normalized;
its length is the reel's length (~150 words of script per minute).
python make_chip.py --text "π¨ Nobody is Talking About This"
Renders a transparent PNG chip β rounded dark lozenge, Segoe UI Black, real
color emoji β into chips/. --style light inverts it; --fontsize /
--max-width tune size and wrapping. Timelines can also render chips inline
("type": "chip") so you rarely run this by hand.
A JSON file (see prompts/timeline_example.json):
{
"aroll": "aroll/take1.mp4",
"overlays": [
{ "type": "chip", "text": "π¨ Nobody is Talking About This",
"start": 0.4, "end": 4.5, "pos": "upper" },
{ "type": "video", "file": "broll/smoke.mp4", "from": 3,
"start": 4.5, "end": 10, "pos": "top", "width": 1.0, "height": 0.38,
"slide": "top", "fade": 0.3 },
{ "type": "image", "file": "cards/card1.png",
"start": 11, "end": 18, "pos": "center", "height": 0.55 }
]
}Overlay fields: start/end (seconds on screen) Β· pos = top / upper / center / lower / bottom (or x/y fractions 0-1) Β· width/height as
fractions of the frame β give both to cover-crop into that box (the
full-width band over a talking head), give one to keep the media's own
aspect (a whole card, scaled) Β· fade seconds (default 0.25) Β· slide =
top/bottom/left/right ease-in Β· from = source offset for video clips.
Top-level extras: "voice" (audio file that replaces the a-roll's track),
"music" ({"file", "gain_db", "duck"} β a bed auto-ducked under speech),
"loudnorm" (default true), "fit" (letterbox the a-roll instead of crop).
python compose_reel.py prompts/timeline.json
python compose_reel.py prompts/timeline.json --srt prompts/captions.srt
Output: final/composed_<datetime>.mp4 (1080x1920, 30 fps, audio kept).
--srt burns captions in the same style as make_reel.py.
Drop any .mp4/.mov into broll/ and reference it from the timeline. Two
things make sourcing easy: the cover-crop band means landscape footage works
fine (a 16:9 clip cropped into a top band loses little), and the "from"
field pulls the best 5 seconds out of any long clip. Where to find footage,
in order of preference:
- Screen-record public dashboards and live sites β the most credible asset there is, and free. Government data portals, live tracker maps (wildfires, flights, earthquakes, weather), official rankings and price boards: open the page in a browser and screen-record it (Windows: Win+Alt+R Game Bar; or record your phone scrolling the site). Leave the site's own logo, timestamp and legend in frame β the raw UI chrome is what makes it read as a receipt, not a graphic. Claude can automate these captures with a browser.
- Free licensed stock β Pexels (pexels.com), Pixabay (pixabay.com), Mixkit (mixkit.co): free for commercial use, no attribution required, searchable by keyword ("wildfire aerial", "city timelapse"...). Filter vertical if you can, but landscape is fine (see above). Both Pexels and Pixabay have free APIs if you want fetching scripted.
- Public-domain government footage β U.S. federal agency footage is generally public domain: NASA (images.nasa.gov), USFS/BLM/National Interagency Fire Center, NOAA, the National Archives. Slower to search than stock, but often the most dramatic real-event footage you can use with zero rights risk.
- Viral / eyewitness clips β caution. Commentary creators repost these constantly and it clearly performs, but it's someone else's copyrighted footage: a strike risk on an account you're growing, especially monetized. Prefer a stock or public-domain equivalent of the same shot; if a specific clip is irreplaceable, credit the source on screen and keep it short inside your own commentary β and know it can still be struck.
- Charts: plot the data yourself instead of screenshotting someone's
graphic. Public datasets (government statistics portals, agency
databases) + matplotlib or the
make_cards.pytoolkit give you a clean chart you own β cite the dataset on the card face, like the source footers the cards already use.
- Script first. Ask for the topic and hook. Word budget = target length
Γ ~150 wpm (a 60s reel β 150 words). Write/collect the script into
prompts/script.txt. The first sentence must be the hook. - Get the A-roll. Ask: "Record yourself reading it, or generate it
with your HeyGen clone?" For HeyGen: give the user the final script to
paste (or drive the HeyGen connector if available) β portrait 9:16 β
and have the .mp4 land in
aroll/. Don't proceed without the A-roll; everything is timed against it. - Build the assets while/after the A-roll is made: a card deck
(
make_cards.pyβ swap the DECK half), b-roll clips intobroll/(see "Sourcing B-roll" above β prefer dashboard screen-captures, free stock, and public-domain agency footage; warn before using viral clips), chip text(s) agreed with the user. - Write the timeline to
prompts/timeline_<topic>.json, timed against the script at ~150 wpm (or probe the A-roll for its real length). Rules of thumb: hook chip at ~0.4-4.5s; first panel on screen by ~5s; something new every 4-7s; no overlay longer than ~8s; panels intop/upper/centerso they clear the caption band; face visible at least a third of the time. - Compose with
compose_reel.py. Offer burned-in captions: hand-write a.srtcued to the script (most viewers are muted) and pass--srt. - Report the output path and length; remind that music can be added on Instagram if no bed was mixed in.
There's a third source format besides "photos/clips you shot" and "a fresh
A-roll you recorded": a long existing recording (a webinar, a demo call,
a screen capture) that needs to be chopped into several short, cropped clips
β reusing the speaker's real voice, but showing only specific regions of the
screen (e.g. a product demo pane) and skipping other parts entirely (slides,
the presenter's face, off-topic sections). There's no single script for this
yet β it's hand-built per project with tools/ffmpeg.exe directly β but the
workflow and gotchas below make the next one much faster.
- Map the recording first. Extract the audio and transcribe it
(
whisper,smallmodel is enough) to get timestamped text for every section β this is how you find quotable lines and locate topic boundaries.whisper's internal audio loader shells out toffmpegon PATH β it does NOT know abouttools/ffmpeg.exe. Prepend the tools dir for that one call:PATH="$PATH:/path/to/quickVid/tools" python transcribe_script.py(bash) β otherwise it fails with aWinError 2FileNotFoundErrorthat has nothing to do with the transcription itself. - Build a coarse contact sheet (one frame every ~30s, tiled with timestamp labels via PIL) to map the recording's sections β where the slides are, where a live demo starts, where a governance/legal section begins, etc. This is enough resolution to plan which sections are usable at all.
- Before trusting any specific clip window inside a chosen section, re-sample that section at 0.5-1s resolution. A coarse 30s-grid sheet makes content look far steadier than it is β a produced sizzle/intro video can cut to a brand-new scene every 0.5-1s. Picking a 3-4s window off the coarse sheet risks spanning three unrelated scenes. Confirm stability at fine resolution first, then pick the window.
- Calibrate each crop rectangle by iterating on the actual crop, not by eyeballing a scaled reference image. Drawing a coordinate grid over a downscaled screenshot and reading pixel values off it is unreliable β labels on a scaled-down annotated image are very easy to misread by a large factor (a 2x+ error is easy to make and easy to miss). Instead: crop a generous guess region straight out of the source with ffmpeg, view the actual result, then tighten in 1-2 more passes. It's slower per crop but it's the only version that's actually correct.
- Extract each clip with the crop baked in, using the blur-fill filter below to go from landscape screen content to 9:16 with no black bars and no lost content.
- Verify every extracted clip β and the final muxed output β by decoding
it fully and sequentially, not by spot-checking with
-ss. See the-sswarning below; this step caught real problems (a clip whose window ran long enough to drift into an unrelated scene) that spot-checks with-sscompletely missed, because the spot-check frames themselves were sometimes wrong. - Concat the verified clips, generate/attach audio (either the
speaker's real voice trimmed to clean sentence boundaries, or a fresh
script through
elevenlabs_voice.pyfor a montage assembled from unrelated source moments), then pad the video to the audio's length (tpad=stop_mode=clone:stop_duration=<N>, holding the last frame) rather than stretching or trimming the audio.
ffmpeg -ss <t> -i file -frames:v 1 out.png (seeking, whether placed before
or after -i) can silently return a frame from a different moment than
requested on some encodes/filter combinations β not a rounding-error few
frames off, but a completely different scene. This showed up repeatedly:
a "confirmed" timestamp would show one thing via a plain -ss grab, then
show something else entirely once a crop filter was added, then show yet
another thing on a fresh re-grab of the exact same command. It affects
verification just as much as extraction β don't trust a -ss
screenshot of a finished render either.
The fix: decode the clip (or the region of interest) sequentially from the
start, no seeking at all, and sample with the fps filter:
ffmpeg -i clip.mp4 -vf "fps=2,scale=160:-1" frames/f_%03d.png
Tile the results into a contact sheet and actually look through the whole thing before trusting a clip's content. It's more frames than a couple of spot checks, but it's the only version of "verified" that's actually true. This is slower, so reserve it for: (a) picking clip windows in a fast-cutting section, (b) checking every extracted clip before concat, and (c) checking the final render before calling it done β not for quick casual looks where being roughly right is fine.
A screen recording is landscape (and the content pane inside it is usually
also landscape, e.g. a 16:9 embedded video within an even wider screen
capture) β going to 9:16 either crops away real content or, with
make_reel.py --fit, leaves large plain black bars (at these aspect ratios,
often 60%+ of the frame). Better: crop tight to the region of interest, then
fill the leftover vertical space with a blurred, zoomed copy of the same
footage instead of black β the standard "make landscape read as native
vertical" technique. Not exposed as a flag anywhere yet; built per clip as a
filter_complex:
ffmpeg -ss <start> -t <dur> -i source.mp4 -filter_complex \
"[0:v]crop=<w>:<h>:<x>:<y>,setsar=1,split=2[bg][fg]; \
[bg]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=35,eq=brightness=-0.05[bgblur]; \
[fg]scale=1080:-2:force_original_aspect_ratio=decrease[fgs]; \
[bgblur][fgs]overlay=(W-w)/2:(H-h)/2:format=auto,format=yuv420p[v]" \
-map "[v]" -an -r 30 -c:v libx264 -crf 18 -pix_fmt yuv420p out.mp4
crop=<w>:<h>:<x>:<y> is the calibrated region of interest (step 4 above).
Every distinct screen layout in the source (a different app pane, a
different slide template) needs its own calibrated crop rect β a rect
measured against one layout will silently crop the wrong thing if reused
against a different one, even a visually similar one.
- A recording's UI layout is not constant. The same "branded card" section can switch between a big full-bleed video pane and a small YouTube-embed-with-chrome (title bar, progress bar, captions, a "Watch on YouTube" button) within the same minute. Recalibrate the crop rect per layout, not just per section.
- Watch for a scene change disguised as a glitch. Content that looks like an error (e.g. a clip drifting into an unrelated scene) is sometimes real footage β the source ad/video genuinely cuts there. Don't assume "weird visual" always means "bug in my pipeline"; check the source at that exact timestamp before concluding either way.
- A ~50min 4K screen recording transcribes in a few minutes on CPU with
whisper'ssmallmodel β no need for a GPU or a smaller model for a one-off job like this.
The specifics above (ffmpeg, whisper, this webinar) will date; these won't. Worth re-reading before any task that involves extracting, cropping, or verifying pieces of a larger source β video or otherwise:
- Never verify with the same mechanism that produced the artifact.
Picking clip timestamps with
-ssand then "verifying" them with more-ssmade a systematic seeking error invisible, because the check shared the broken assumption with the extraction. The bug only surfaced once verification used a different method (full sequential decode). If a tool can lie to you, checking your work with the same tool doesn't help. - Coarse sampling creates false confidence, not just imprecision. A 30-second-interval overview made content look stable for many seconds; reality changed every 0.5s in places. The risk isn't that coarse data is less accurate β it's that it looks trustworthy enough to stop checking. Sample rate has to match the actual rate of change, which is often unknown until you check at finer resolution.
- Prefer empirical iteration over reading measurements off a representation. Eyeballing pixel coordinates from a scaled, annotated reference image caused a 2x+ error, made easily and missed easily. "Crop a guess, look at the real result, adjust" was slower per attempt but correct every time β a proxy for the truth is worse than a slower loop against the truth itself.
- An anomaly is data until proven to be a bug. Unexpected content (a scene that looked like corruption, a clip that drifted somewhere unrelated) turned out to be real source footage both times it came up. Assuming "weird output = my pipeline is broken" and patching it would have produced a confident, silent, wrong fix; checking the source first avoided that.
- The dangerous failures are silent, not loud. The seeking bug never threw an error β it returned a plausible-looking wrong frame. A verification step only earns its keep if it would actually catch a wrong-but-plausible answer, not just a crash.
- Match verification cost to the stakes at that checkpoint. Full sequential-decode verification is slow, so it isn't for every casual look β spend it where being wrong would propagate: after extraction, before concat, before calling a render final. Cheap checks elsewhere, expensive checks at commit points.
- Pay down calibration cost once, then trust the reusable primitive. The second reel built this way went faster than the first not because the footage was easier, but because the crop-calibration method and the blur-fill filter chain were already trustworthy β the remaining work was applying a known-good method, not re-deriving it.
9:16 cards (1080x1920) drawn with PIL at 2x and downsampled. The file is two
halves: a reusable TOOLKIT (background, header/footer chrome, panels,
stat rows, spline/tapered-limb primitives, a small map projection, icons) and
a per-video DECK (the card_*() functions). To make cards for a new
reel, replace only the DECK half and re-run python make_cards.py [out_dir].
Keep text minimal (the toolkit's header + stat_row + one para is a full
card); put years/dates only in source footers so card faces don't date. Use
the cards as slideshow frames (make_reel.py) or as timed panels over a
talking head (compose_reel.py).
# one-time: install the two small deps
C:\Users\DKYLE\.conda\envs\api\python.exe -m pip install -r requirements.txt
# one-time: verify your ElevenLabs voice clone (key, clone, voice id)
C:\Users\DKYLE\.conda\envs\api\python.exe elevenlabs_voice.py --check
# plan a length: recommend flash speed for a sweet-spot (15-30s) reel
C:\Users\DKYLE\.conda\envs\api\python.exe script_fit.py --band sweet-spot
# default flow: narrate prompts/script.txt in your cloned voice, then build 22s
C:\Users\DKYLE\.conda\envs\api\python.exe script_fit.py --target 22 # does the script fit?
C:\Users\DKYLE\.conda\envs\api\python.exe elevenlabs_voice.py --script prompts/script.txt
C:\Users\DKYLE\.conda\envs\api\python.exe make_reel.py --sound voiceover --target-seconds 22 --title "MY TITLE"
# silent 12s max-reach reel: trim long clips to 3s, add auto captions
C:\Users\DKYLE\.conda\envs\api\python.exe make_reel.py --sound none --target-seconds 12 --max-clip-seconds 3 --subtitles auto
# screen recording / landscape content: fit inside the frame with black bars
C:\Users\DKYLE\.conda\envs\api\python.exe make_reel.py --sound none --target-seconds 60 --max-clip-seconds 60 --speed-to-fit --fit --title "My Title"
# AFTER building: overlay a countdown of how long it took you to make it
C:\Users\DKYLE\.conda\envs\api\python.exe add_timer.py --seconds 23:00Useful make_reel.py flags: --target-seconds 22 (hit a length) or
--image-seconds 4 (fixed per-photo time); --max-clip-seconds 6 (trim clips);
--title "..." with --title-seconds / --title-position; --subtitles auto|file;
--timer with --timer-seconds; --no-ken-burns; --fit-audio;
--fit (screen recordings / landscape content β letterbox instead of crop);
--speed-to-fit (with --max-clip-seconds: speed up to fit duration rather than cut).
- Source-clip audio is dropped so the reel has one clean soundtrack (silence or your voiceover). Want spoken audio? Use a voiceover. Want music? Use Instagram.
- Keep reels under ~90s for the Instagram feed (long-form supports up to 20 min).
- The bundled
tools/ffmpeg.exe/ffprobe.execover the video side; the voiceover needsrequests+python-dotenv(inrequirements.txt). - Voiceover cost is per character (ElevenLabs); each generation prints characters
used and your remaining quota. Set
ELEVEN_USD_PER_1K_CHARSin.envfor a$estimate.