v1.0.0
Builds an eye-aligned face timelapse from the photos of one person in a private Immich library. It picks the frames where the subject is looking at the camera and warps every one of them onto the same eye positions, so the years pass without the face jittering around the frame.
What's in it
The pipeline — eight stages, each independently runnable and resumable from a SQLite manifest: index, faces, fetch, analyze, select, align, review, encode. Interrupting any of them costs only the item in flight.
No face recognition. Immich already labels which detected face belongs to whom, so even group photos yield the right bounding box. The pipeline reads that association rather than reproducing it.
Filtering on five signals from one MediaPipe FaceLandmarker pass: head pose from the transformation matrix, gaze measured geometrically from where each iris sits between its own eye corners, blink blendshapes, landmarks falling outside the frame, plus sharpness and exposure. Gaze is separate from pose deliberately — a face can point straight at the lens while the eyes look elsewhere.
Exact alignment, no smoothing. Each frame is solved independently for a similarity transform that lands both eyes on the canonical positions. Scale is normalised by interocular distance, so growing up reads as changing facial proportions rather than a head that inflates.
An interactive threshold tuner. out/rejects.html carries a slider per filter threshold and shows exactly which photos a change would add or drop. The page and the Python filter interpret the same serialised rule table, and the test suite runs the page's own filter under node to prove they agree.
Trial runs. grow-up trial -n 100 is a partial real run — it goes through to a video, banks everything it downloads, and projects the full set from measured per-stage timings.
A time/accuracy dial. analyze.effort trades speed for detection recall and landmark precision; grow-up trial --compare measures all three levels over the same sample so the choice is a number rather than a guess.
Incremental by default. A bare grow-up run picks up where the last successful one finished, using updatedAfter rather than takenAfter so back-dated imports are not lost, plus asset-count drift detection for the case updatedAfter cannot see — someone tagged in an old photo.
Setup
pip install -e ".[dev]"
cp config.example.toml config.toml # then edit person_name
grow-up fetch-model
export IMMICH_URL=https://immich.example.com
export IMMICH_API_KEY=… # needs person.read, asset.read, face.read, asset.download
grow-up index && grow-up trial -n 50Credentials come from the environment only, never from config.toml. Every setting in config.example.toml carries a comment explaining what turning it does.
Notes
This project was written entirely by Claude (Anthropic's AI assistant, running as Claude Code) and supervised by the repository owner. The model never had access to the Immich instance or its API key, and all verification against real photographs was done by the owner. See the authorship notice in the README, and CLAUDE.md for the invariants and the mistakes already made.
473 tests, running on Python 3.11, 3.12 and 3.13 — no network, no Immich instance, no model download and no ffmpeg required.