Skip to content

feat(ai-restyle): v2 background replacement (stacked on #35)#1

Open
vansteenbergenmatisse wants to merge 21 commits into
feat/ai-restylefrom
feat/ai-restyle-v2-bg-replace
Open

feat(ai-restyle): v2 background replacement (stacked on #35)#1
vansteenbergenmatisse wants to merge 21 commits into
feat/ai-restylefrom
feat/ai-restyle-v2-bg-replace

Conversation

@vansteenbergenmatisse
Copy link
Copy Markdown
Owner

Summary

Pivots AI Restyle from v1 (Nano-Banana relight + fal.ai v2v, mutonby PR mutonby#35) to v2: personalized background replacement with one-time selfie onboarding.

  • Onboarding (one-time): selfie → Gemini generates 5 personalized backgrounds → user picks 1 → profile_id stored in browser localStorage; files in backend/profiles/<id>/.
  • Per-video: detect-bg-clean (warn-only) → fal.ai matting → composite over selected background with gblur sigma=14 (realistic shallow-DOF feel) → mux audio.
  • Caps: 30s duration, 250MB video, 10MB selfie.

Stacking

This PR targets feat/ai-restyle (mutonby PR mutonby#35) on the fork. After mutonby#35 merges to mutonby/main, this branch can be rebased onto main and a fresh PR opened against mutonby:main. Diff shown here is v2-only.

Backend changes

New modules:

  • backend/app/profile/store.py — per-profile JSON+files store with atomic writes
  • backend/app/ml/profile_backgrounds.py — Gemini 5-background generator
  • backend/app/ml/bg_detect.py — OpenCV clean-background heuristic (warn-only)
  • backend/app/ml/video_matte.py — fal.ai matting wrapper with SSRF guard
  • backend/app/video/composite.py — FFmpeg matte-over-blurred-bg composite

Modified:

  • backend/app/restyle/pipeline.py — full rewrite (6-step v2 flow)
  • backend/app/routes/ai_restyle.py — 5 new onboarding routes + POST /api/restyle now takes `profile_id`
  • backend/app/main.py — `OUTPUT_DIR` + `UPLOAD_DIR` env-aware

Deleted (v1 retired): `frame_relight.py`, `video_restyle.py`, `frame_extract.py` + their tests.

Frontend changes

New: `state/profileStore.js`, `Settings/sections/BackgroundProfileSection.jsx`, `AIRestyle/steps/Precheck.jsx`
Modified: `App.jsx` (route swap), `Wizard.jsx` (Precheck step), `Review.jsx` (bg_verdict warning), `ApiKeysSection.jsx` (fal.ai copy)
Deleted: `aiRestylePresets.js`, `AIRestylePresetsSection.jsx`, `Configure.jsx`

Security baseline (per global CLAUDE.md rule)

Final holistic review flagged + fixed two CRITICALs:

  • Profile leak via `/videos` mount — moved `PROFILES_ROOT` out of `OUTPUT_DIR` (new `PROFILES_DIR` env var)
  • `OUTPUT_DIR` inconsistency — unified `main.py` to env-aware form

All LLM-CALL + STATE-MUTATING surfaces require `X-Gemini-Key`. Static profile-file serve uses regex allowlist + UUID format check + realpath traversal guard. 10MB selfie cap + 250MB video cap streamed with mid-stream 413 (no full-body buffer).

Test plan

  • `pytest -m "not e2e"` — 276 passed in docker
  • `npm run build` clean
  • OpenAPI snapshot regenerated for new routes
  • Live onboarding works (set `GEMINI_API_KEY`, POST a selfie, poll status → ready)
  • Live per-video restyle works against 5s test clip with clean black background (needs `FAL_KEY`)
  • Selecting a different background + re-running produces a different composite

Known follow-ups

  • fal.ai matting model is the BiRefNet/v2 fallback — needs Phase 0 spike with `FAL_KEY` to confirm or swap
  • No rate limit / cost cap on `/regenerate` (codebase-wide gap)
  • No file lock on `set_selected` (concurrent /select can race)
  • No end-to-end test chaining all routes

🤖 Generated with Claude Code

vansteenbergenmatisse and others added 21 commits May 20, 2026 21:53
Pivots AI Restyle from relight+v2v to a personalized background
replacement product: one-time selfie onboarding generates 5 Gemini
backgrounds; per-video uses fal.ai matting + camera-blur composite.

Supersedes 2026-05-20-ai-restyle-design.md once PR mutonby#35 (v1) merges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12-task plan derived from the bg-replace design spec. Covers backend
modules (profile store, Gemini bg-gen, clean-bg detect, fal matting,
composite, pipeline rewrite, route changes, v1 deletes), frontend
(profile store, Settings section, Wizard reshape), and the
OpenAPI/ROADMAP/CLAUDE.md regen.

Prerequisite: PR mutonby#35 (v1) merged into main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fie)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex security review flagged two blockers on Task 6 routes:
- POST /select had no X-Gemini-Key check; other mutating routes do.
- POST /profile read the full selfie into RAM before size-checking;
  swap to the streaming chunk pattern that start_restyle already uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…c auth intent

- Move mid-file `import re` and `from fastapi.responses import FileResponse` to
  module-level imports (Fix 1)
- Extract shared `_run_background_generation` module-scope helper replacing the
  near-duplicate `_run_generation` / `_run_regeneration` nested closures; remove
  unused `folder` local in `regenerate_route` (Fix 2)
- Add comment on `select_background_route` explaining why X-Gemini-Key is
  required even though the key is not used downstream (Fix 3)
- Remove unused `from unittest.mock import patch` in test_profile_routes.py (Fix 4)
- Expand `serve_profile_file` docstring to document the unauthenticated-by-design
  rationale; update OpenAPI baseline snapshot accordingly (Fix 5)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop background_prompt + lighting_prompt form fields and MAX_PROMPT_LEN
constant; replace with profile_id. Add profile existence + selection
guard (404/400) before any disk I/O. Wire call site to v2 pipeline
signature (profile_id + fal_key). Update 2 existing tests, add 3 new
contract tests, regenerate OpenAPI snapshot (291 passing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frame_relight, video_restyle, and frame_extract are fully retired.
No remaining importers outside their own test files (verified by grep).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AI Restyle v1 (relight + fal.ai v2v) marked retired 2026-05-21;
v2 (background replacement, fal matting + composite) marked shipped with
full pipeline description. OpenAPI snapshot already current (5/5 contract
tests pass). CLAUDE.md MODULE-MAP regenerated via update_claude_md.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move PROFILES_ROOT out of OUTPUT_DIR so /videos static mount can't
  serve selfies / generated bgs.
- Unify OUTPUT_DIR + UPLOAD_DIR to env-aware in main.py (matches the
  store + pipeline).
- Drop broken original_url from restyle result (file lives in UPLOAD_DIR,
  not OUTPUT_DIR; frontend uses its own blob URL anyway).
- regenerate now wipes stale bg-*.png so generated_count + selected_idx
  never reference dead files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New backend/tests/e2e/test_bg_replace_smoke.py drives the full route chain
in one test (POST /profile → GET /profile poll → POST /select → POST
/api/restyle → GET /api/restyle poll). Mocks Gemini bg generation + fal
matting + the heavy FFmpeg ops; everything else (routes, profile_store,
BackgroundTasks, state machine) runs for real.

Three tests:
- happy: full chain reaches completed status with bg_verdict=clean + four
  canonical milestone logs present.
- rejects-on-no-selection: skipping /select must 400 the restyle POST.
- regenerate-resets-state: regenerate clears stale selected_idx AND
  replenishes to 5 fresh backgrounds.

Catches contract drift that per-module mocks miss (response shapes,
header propagation, profile_id flow, state transitions).

276 unit/api + 3 new e2e + 1 pre-existing e2e all green in docker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Browser-smoke uncovered:

1. BackgroundProfileSection.jsx polling only continued on
   generation_status === 'generating'. Right after upload the status
   is briefly 'pending' (before the bg-gen background task fires
   mark_generation_status). If the first poll lands during that
   window, polling stops AND no UI block matches 'pending' — the
   section renders only its header (blank to the user) until a manual
   reload. Now treats 'pending' the same as 'generating': keeps
   polling, shows the "Generating 5 backgrounds…" spinner.

2. Upload.jsx step-1 helper text still said "We'll relight the
   lighting and replace the background" — a v1 leftover. v2 doesn't
   relight; rewritten to describe matte + composite.

3. AIRestyle/index.jsx top-of-file docstring still mentioned
   "relighting … Nano-Banana + fal.ai v2v". Same pivot leftover;
   rewritten to "background-replace via fal.ai matting + composite".

Also gitignored .tmp-screenshots/ (used by browser-smoke captures).

Verified via chrome-devtools MCP: real Gemini bg-gen → 5 thumbnails
render → pick bg-2 → backend confirms selected_idx=2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant