Skip to content

Repository files navigation

Lifelapse

Upload 10–20 real photos of one person across their life, order them youngest → oldest, and get back one continuous "growing up" morph video. Each segment is anchored on two real photos (photo N as the start frame, photo N+1 as the end frame) generated through Kling O1 first-frame-last-frame on fal.ai — which is what keeps identity consistent across the whole film.

Single-user and self-hosted: there's no login — whoever runs it owns everything. Warm, photographic UI. The signature moment is the timeline filling in, segment by segment, as generation completes.

Demo

Watch the Lifelapse demo

▶️ Watch the demo on YouTube

Stack

  • Next.js 16 (App Router, TypeScript) + Tailwind v4
  • PostgreSQL via Prisma 6
  • pg-boss job queue (Postgres-backed — no Redis) + a separate worker process
  • fal.ai Kling O1 via the queue + webhook API, behind a VideoEngine interface
  • S3-compatible object storage (AWS S3 / Cloudflare R2 / MinIO), or local disk in dev
  • ffmpeg for stitching; sharp for photo normalization

Requirements

  • Node 22 (.nvmrcnvm use)
  • PostgreSQL 14+ (local or via docker compose up)
  • ffmpeg on PATH

Quick start (local, no cost, no cloud)

Runs the entire flow with a mock engine (fabricates real crossfade clips locally) and local-disk storage — no fal.ai key or S3 needed, and no cost.

nvm use                      # Node 22
npm install
cp .env.example .env.local   # then set FAL_MODE=mock and STORAGE_MODE=local

# Postgres: either `docker compose up -d postgres`, or a local server + this DB:
#   createdb lifelapse && psql -c "CREATE ROLE lifelapse LOGIN PASSWORD 'lifelapse' CREATEDB"
npm run db:migrate

# two processes:
npm run dev                  # web  → http://localhost:3000
npm run worker               # background worker (generation + stitch)

With FAL_MODE=mock and STORAGE_MODE=local, open the app, create a project, drop in some photos, order them, hit Generate, and watch the timeline fill in — entirely offline and free.

Cost & safety

⚠️ Live mode makes paid API calls. Every segment is a fal.ai Kling O1 video generation — real motion, not the mock crossfade — at roughly $0.112/s (≈ $0.56 per 5s segment). VISION_MODE=live additionally calls hosted vision models per photo. The mock/local defaults are completely free and offline. Review fal.ai pricing before enabling live mode. Your keys live in .env.local, which is gitignored — keep it that way.

Going live (real services)

Set these in .env.local (see .env.example for the full list):

  • FAL_MODE=live, FAL_KEY, FAL_WEBHOOK_SECRET
  • VISION_MODE=live (real photo analysis + motion prompts) — optional; mock still works live
  • STORAGE_MODE=s3 + S3_* (bucket, keys, region). fal fetches the photos over the public internet, so the store must be reachable from fal's servers.
  • APP_URL = the public base URL fal can reach for webhooks (optional locally — see below)

Notes when switching to S3:

  • The store must be internet-reachable. url() mints presigned GET URLs, so a private bucket is fine — no public-read needed. Local disk / localhost MinIO will not work (fal can't reach localhost); the worker fails such segments with a clear message.
  • AWS S3: leave S3_ENDPOINT blank (don't leave the MinIO localhost:9000 value). Cloudflare R2: set S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com and S3_FORCE_PATH_STYLE=1.
  • Webhooks: if APP_URL is localhost, fal can't POST the webhook — that's OK, the poll-stuck poller retrieves results (~1 min/segment). For instant results, expose APP_URL publicly.
  • Restart both processes (npm run dev and npm run worker) after editing .env.local — env is read once at startup. The worker prints its effective FAL_MODE/STORAGE_MODE and warns on misconfig.
  • Start a fresh project after switching to S3. Photos uploaded in local mode live on disk, not in the bucket, so re-generating an old project would fail to fetch them.

docker compose up -d also starts MinIO (S3-compatible) with a lifelapse bucket — but MinIO on localhost is only reachable by your machine, not fal, so it's for local S3-mode plumbing tests, not live generation.

Scripts

command what
npm run dev Next dev server
npm run worker job worker (loads .env.local)
npm run db:migrate Prisma migrate (dev)
npm run db:studio Prisma Studio
npm test unit tests (vitest)
npm run build production build

How generation works

  1. Generate validates 2–20 photos and creates one Segment per consecutive pair, then enqueues a generate-segment job each.
  2. generate-segment (worker): live → submits to fal with a webhook URL; mock → fabricates a crossfade clip locally and completes immediately.
  3. /api/fal/webhook (live): secret-verified, downloads the output to storage, marks the segment complete. A fallback poller recovers missed webhooks.
  4. When all segments complete, stitch-project concatenates them with ffmpeg (-c copy, falling back to a uniform re-encode) into the final MP4.
  5. Failed segments retry individually without redoing the chain.

Photos are normalized (center-crop or letterbox) to identical dimensions per the project aspect ratio, so segment outputs share dimensions and concat cleanly.

Design

The UI is a faithful implementation of the design prototype in design/Lifelapse.dc.html (Spectral + IBM Plex Mono, warm neutral palette, the filling-timeline interaction). Design tokens live in src/app/globals.css.

Credits

  • Video morphs by fal.ai running Kling O1 (first-frame-last-frame).
  • Photo analysis + motion prompts by hosted vision models via fal.ai.

License

MIT © 2026 Ady Levy.

About

Turn photos of one person across their life into a single continuous AI age-progression morph video. Self-hosted Next.js app powered by fal.ai Kling O1, with a live timeline that fills in segment by segment.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages