git clone --recurse-submodules https://github.com/tonyyunyang/VEED-Hackathon.git && cd VEED-Hackathon
VEED Face Swap is a local full-stack app for:
- uploading a short video
- detecting and clustering faces across frames
- selecting tracked faces to replace
- running a face-swap job
- optionally applying lipsync
- downloading the rendered result
This README is written for other developers who need to deploy the project locally without conda. The supported local workflow documented here is:
npmfor the frontenduvfor the backend environment and thenpm run serverwrapper- no
conda
If you want the shortest path to a working local setup:
-
Clone the repo with submodules:
git clone --recurse-submodules https://github.com/tonyyunyang/VEED-Hackathon.git cd VEED-Hackathon git submodule sync --recursive git submodule update --init --recursive -
Install system prerequisites:
- Node.js
20.19+or22.12+ - npm
10+ - Python
3.11or3.12 uvffmpegffprobecurl
- Node.js
-
Install backend dependencies:
UV_CACHE_DIR=.uv-cache uv sync --directory server --locked --all-groups
-
Install frontend dependencies:
npm install
-
Create your local env file:
cp .env.example .env
-
Add a reference identity source to
.env:FACE_SWAP_REFERENCE_IMAGE=/absolute/path/to/source-face.jpg
Or populate:
server/reference_faces/ -
Start all services:
npm start
This launches the backend, FaceFusion API, and frontend together in one terminal with color-coded logs.
-
Open the app:
- frontend:
http://localhost:5173 - backend:
http://localhost:8000 - backend docs:
http://localhost:8000/docs - FaceFusion API docs:
http://localhost:8001/docs
- frontend:
The app depends on Git submodules. Do not skip them.
Top-level submodules:
face-detect-trackfacefusion-VEED
Nested submodules inside face-detect-track:
BoT-FaceSORT-VEEDinsightface-VEED
Because the root .gitmodules file uses HTTPS URLs, a standard recursive clone is enough:
git clone --recurse-submodules https://github.com/tonyyunyang/VEED-Hackathon.git
cd VEED-HackathonIf you already cloned without submodules:
git submodule sync --recursive
git submodule update --init --recursiveIf you want git pull to recurse into submodules automatically on your machine:
git config --global submodule.recurse true| Path | Purpose |
|---|---|
src/ |
React 19 + TypeScript + Vite frontend |
server/ |
FastAPI backend |
face-detect-track/ |
Repo-local face detection and tracking package |
facefusion-VEED/ |
Optional FaceFusion checkout used by the facefusion backend |
tests/ |
Python API, tracker, swapper, schema, and video-service tests |
Required for a normal local deployment:
face-detect-track/face-detect-track/BoT-FaceSORT-VEED/face-detect-track/insightface-VEED/
Optional:
facefusion-VEED/only matters whenFACE_SWAPPER_BACKEND=facefusion
Use these versions for the least surprising local setup:
- Node.js
20.19+or22.12+ - npm
10+ - Python
3.11or3.12 uvffmpegffprobecurl
Why:
server/pyproject.tomlrequires Python>=3.11,<3.13face-detect-track/pyproject.tomlalso requires Python>=3.11,<3.13- Vite
7.3.1requires Node^20.19.0 || >=22.12.0 - the FaceFusion CLI checks for both
ffmpegandcurl
Version checks:
node --version
npm --version
python3 --version
uv --version
ffmpeg -version
ffprobe -version
curl --versionVerified locally in this repo with:
- Node
20.20.1 - npm
10.8.2 - Python
3.11.7 uv 0.10.12
Example ffmpeg install commands:
# macOS
brew install ffmpeg
# Ubuntu / Debian
sudo apt-get update
sudo apt-get install -y ffmpeg curlFrom the repo root:
UV_CACHE_DIR=.uv-cache uv sync --directory server --locked --all-groupsWhat this does:
- creates or updates
server/.venv - installs the backend dependencies from
server/pyproject.toml - installs
movie-like-shotsfromface-detect-track/as a local editable dependency - uses
server/uv.lock - keeps the
uvcache in.uv-cache/under the repo root
If you prefer the default global uv cache location, omit UV_CACHE_DIR=.uv-cache.
From the repo root:
npm installUse npm install, not npm ci.
Reason:
- the repo contains
package.json - the repo does not contain a committed
package-lock.json - the documented workflow here is
npm installplusnpm run ...
Create your local env file:
cp .env.example .envThe backend loads the repo-root .env automatically.
The checked-in .env.example now defaults to:
FACE_SWAPPER_BACKEND=facefusion
FACEFUSION_DIR=facefusion-VEED
ENABLE_LIPSYNC=falseThat is the easiest first-run local setup in this checkout.
| Variable | Required | Default | Purpose |
|---|---|---|---|
FACE_SWAPPER_BACKEND |
No, but important | facefusion |
Selects the swap backend |
FACEFUSION_DIR |
For facefusion |
facefusion-VEED |
Path to the checked-out FaceFusion repo |
FACEFUSION_PYTHON |
Optional | current Python interpreter | Overrides the Python executable used to launch FaceFusion |
RUNWARE_API_KEY |
Recommended | empty | Runware API key for AI face generation (img2img) |
FACE_SWAP_REFERENCE_IMAGE |
Optional | empty | Uses one fixed source identity image |
FACE_SWAP_REFERENCE_FACES_DIR |
Optional alternative | server/reference_faces |
Uses a local library of source faces |
FACE_SWAP_ALLOW_TARGET_THUMBNAIL_FALLBACK |
Optional | true |
Reuses the tracked face thumbnail if no source image is available |
ENABLE_LIPSYNC |
Optional | false |
Enables fal.ai lipsync |
FAL_KEY |
Only if ENABLE_LIPSYNC=true |
empty | API key for lipsync |
VITE_BACKEND_TARGET |
Optional | http://localhost:8000 |
Changes the frontend proxy target |
STORAGE_DIR |
Optional | server/storage |
Moves uploaded videos and generated outputs elsewhere |
The tracker defaults in .env.example are already aligned with the local movie_like_shots pipeline and usually do not need to change.
The backend resolves a source identity for each detected face using this priority chain. The first match wins:
- User-uploaded reference — uploaded via
POST /api/upload-reference/{video_id}(per-video, highest priority) - Global env image —
FACE_SWAP_REFERENCE_IMAGE=/absolute/path/to/source-face.jpg - Runware AI generation — if
RUNWARE_API_KEYis set, generates a neutral face via img2img from the detected face thumbnail. Accepts an optionalstyle_prompton the swap request (e.g."wearing sunglasses","with face paint") - Reference library — deterministic pick from
server/reference_faces/, matched by gender and age range - Thumbnail fallback — reuses the tracked face's own thumbnail (when
FACE_SWAP_ALLOW_TARGET_THUMBNAIL_FALLBACK=true)
curl -s -F "[email protected]" http://localhost:8000/api/upload-reference/<VIDEO_ID>Accepts .jpg, .jpeg, .png, .webp. Overwrites any previous upload for that video.
Set RUNWARE_API_KEY in .env. The backend sends the detected face thumbnail as a seed image to Runware with a prompt like:
Photorealistic front-facing passport-style portrait of a 25-year-old male person, very neutral usual face, neutral expression, plain white background, even studio lighting
The optional style_prompt field on /api/swap appends user descriptions to the prompt (e.g. "wearing sunglasses and face paint"). Input is sanitized: max 200 characters, non-alphanumeric characters stripped, prompt injection patterns blocked.
Put images under:
server/reference_faces/
male/
female/
If filenames include age ranges like 20-29_name.jpg, the backend picks an age-matched source image.
If nothing else is available, the tracked face's own thumbnail is used when:
FACE_SWAP_ALLOW_TARGET_THUMBNAIL_FALLBACK=trueUseful for smoke tests, not for real identity replacement.
The API surface stays the same either way:
POST /api/uploadPOST /api/upload-reference/{video_id}POST /api/detect-facesPOST /api/swapGET /api/status/{job_id}GET /api/download/{job_id}
The difference is what happens inside /api/swap.
This is the default local path for this repo.
Set:
FACE_SWAPPER_BACKEND=facefusion
FACEFUSION_DIR=facefusion-VEEDNotes:
- the repository already includes the
facefusion-VEEDcheckout - the server calls
facefusion.py headless-rundirectly - the checkout already includes local model assets under
facefusion-VEED/.assets/models/ - FaceFusion uses the same Python interpreter that starts the backend unless you set
FACEFUSION_PYTHON - FaceFusion still needs a usable reference image or reference-face library
You can still use the legacy InsightFace swap path:
FACE_SWAPPER_BACKEND=insightfaceImportant caveat: the repository does not include server/models/inswapper_128.onnx. The InsightFace swap path will fail at swap time until that model file exists at:
server/models/inswapper_128.onnx
So for FACE_SWAPPER_BACKEND=insightface you need both:
- a reference face source
server/models/inswapper_128.onnx
There are two first-run details worth knowing.
movie-like-shots will automatically download the SCRFD detector file into:
face-detect-track/models/scrfd/scrfd_10g_gnkps.onnx
if it is missing on the first detection run.
That means:
- the first face-detection request may take longer
- internet access may be required the first time detection is run on a fresh checkout
This checkout already contains a populated facefusion-VEED/.assets/models/ directory with the models used by the default local FaceFusion configuration in this repo.
If you change FaceFusion model settings later, FaceFusion may still request additional downloads depending on the processors you enable.
npm startThis single command launches all three services with color-coded, prefixed logs:
| Service | Label | URL |
|---|---|---|
| Backend (FastAPI) | [server] |
http://localhost:8000 |
| FaceFusion API | [facefusion] |
http://localhost:8001 |
| Frontend (Vite) | [frontend] |
http://localhost:5173 |
Press Ctrl+C to stop all services at once. If the FaceFusion virtualenv is not found, that service is skipped with a warning.
If you prefer separate terminals:
npm run servercd facefusion-VEED && .venv/bin/python api.pynpm run dev -- --host 127.0.0.1The Vite dev server proxies /api requests to http://localhost:8000 unless you override VITE_BACKEND_TARGET.
If port 8000 is already in use, start the backend directly on another port:
cd server
UV_CACHE_DIR=../.uv-cache uv run uvicorn main:app --host 127.0.0.1 --port 8001 --reloadThen start the frontend with:
VITE_BACKEND_TARGET=http://127.0.0.1:8001 npm run dev -- --host 127.0.0.1You can verify the backend before using the UI.
This should return a 404 JSON payload because the job ID does not exist yet:
curl -i http://127.0.0.1:8000/api/status/nonexistExpected status:
HTTP/1.1 404 Not Found
{"detail":"Job not found"}
The repo already includes tests/fixtures/test_video.mp4:
curl -s \
-F "file=@tests/fixtures/test_video.mp4" \
http://127.0.0.1:8000/api/uploadReplace <VIDEO_ID> with the value from the upload step:
curl -s \
-X POST http://127.0.0.1:8000/api/detect-faces \
-H "Content-Type: application/json" \
-d '{"video_id":"<VIDEO_ID>"}'This only succeeds if you have configured:
- a working swap backend
- a usable reference face source
curl -s \
-X POST http://127.0.0.1:8000/api/swap \
-H "Content-Type: application/json" \
-d '{"video_id":"<VIDEO_ID>","face_ids":["face_0"]}'curl -s http://127.0.0.1:8000/api/status/<JOB_ID>curl -L http://127.0.0.1:8000/api/download/<JOB_ID> --output swapped.mp4By default, the backend writes working files under:
server/storage/
Typical files and folders:
server/storage/<video_id>/
original.mp4
frames/
audio.aac
faces.json
swapped/
output.mp4
If FACE_SWAPPER_BACKEND=facefusion, temporary runtime folders are also created under:
server/storage/<video_id>/.facefusion_runtime/
Those are cleaned up automatically unless:
FACEFUSION_KEEP_INTERMEDIATES=trueThese are the fastest reliable checks for a new developer after setup.
npm install
npm run buildnpm run build was verified successfully in this repo.
curl -I http://127.0.0.1:8000/docsYou should get HTTP/1.1 200 OK once the backend is running.
UV_CACHE_DIR=.uv-cache uv run --directory server pytest ../tests -qUV_CACHE_DIR=.uv-cache uv run --directory server pytest ../tests/test_api.py ../tests/test_face_tracker.py ../tests/test_face_swapper.py ../tests/test_schemas.py ../tests/test_video_service.py -qThe FFmpeg-backed video-service tests are skipped automatically when ffmpeg or ffprobe are missing.
Install uv, then rerun:
UV_CACHE_DIR=.uv-cache uv sync --directory server --all-groupsStart the backend on another port and point Vite at it:
cd server
UV_CACHE_DIR=../.uv-cache uv run uvicorn main:app --host 127.0.0.1 --port 8001 --reloadVITE_BACKEND_TARGET=http://127.0.0.1:8001 npm run dev -- --host 127.0.0.1Check the following first:
FACE_SWAPPER_BACKENDis set to the backend you actually want- you configured
FACE_SWAP_REFERENCE_IMAGEor added files underserver/reference_faces/ - if using
insightface,server/models/inswapper_128.onnxexists - if using
facefusion,FACEFUSION_DIR=facefusion-VEEDis correct
Lipsync is optional. If you enable it, you must set:
ENABLE_LIPSYNC=true
FAL_KEY=...That command is not currently the best deployment smoke test in a workspace that already contains local environments or cache directories. Use npm run build plus the backend startup/tests above as the reliable setup verification path.
| Layer | What is used |
|---|---|
| Frontend | React 19 + TypeScript + Vite + Tailwind CSS 4 |
| Backend API | FastAPI + Uvicorn |
| Face tracking | movie-like-shots from the local face-detect-track submodule |
| Tracker vendor repos | BoT-FaceSORT-VEED and insightface-VEED |
| Swap backends | insightface or the local facefusion-VEED checkout |
| Optional lipsync | fal.ai via FAL_KEY |
| Video I/O | ffmpeg / ffprobe |
Detailed implementation notes live in:
docs/superpowers/specs/2026-03-21-face-swap-design.mddocs/superpowers/plans/2026-03-21-face-swap-implementation.md