aditya-rAj19 commited on
Commit
f9ff1a1
·
1 Parent(s): e7a5a81

feat: CI/CD pipeline + Docker + HuggingFace Spaces deployment

Browse files

- Add Dockerfile (multi-stage: Node build + Python runtime)
- Add startup.sh (auto-downloads inswapper model on cold start)
- Add requirements-deploy.txt (CPU-only for cloud)
- Add GitHub Actions CI workflow (backend tests + React build)
- Add GitHub Actions deploy workflow (push to HF Spaces with Firebase config injection)
- Add README.md with HuggingFace Spaces metadata
- Flask PORT now reads from environment variable (7860 for HF Spaces)
- Replace emoji feature icons with SVG icons

.github/workflows/ci.yml ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ # ── Backend Tests ─────────────────────────────────────────────────────────
11
+ backend:
12
+ name: Backend Tests
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Set up Python 3.10
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.10"
23
+ cache: pip
24
+
25
+ - name: Install CI dependencies (lightweight subset)
26
+ run: |
27
+ pip install --upgrade pip
28
+ pip install \
29
+ opencv-python-headless==4.9.0.80 \
30
+ numpy==1.26.4 \
31
+ Pillow==10.3.0 \
32
+ scipy==1.13.0 \
33
+ scikit-image==0.23.2 \
34
+ flask==3.0.3 \
35
+ flask-cors==4.0.1 \
36
+ python-dotenv==1.0.1 \
37
+ pytest==8.2.0
38
+
39
+ - name: Run tests
40
+ run: python -m pytest tests/ -v --tb=short
41
+
42
+ # ── Frontend Build ────────────────────────────────────────────────────────
43
+ frontend:
44
+ name: Frontend Build
45
+ runs-on: ubuntu-latest
46
+
47
+ steps:
48
+ - name: Checkout
49
+ uses: actions/checkout@v4
50
+
51
+ - name: Set up Node 20
52
+ uses: actions/setup-node@v4
53
+ with:
54
+ node-version: "20"
55
+ cache: npm
56
+ cache-dependency-path: frontend/package-lock.json
57
+
58
+ - name: Install dependencies
59
+ working-directory: frontend
60
+ run: npm ci --silent
61
+
62
+ - name: Build React app
63
+ working-directory: frontend
64
+ run: npm run build
65
+
66
+ - name: Upload React build artifact
67
+ uses: actions/upload-artifact@v4
68
+ with:
69
+ name: react-build
70
+ path: static/react/
71
+ retention-days: 7
.github/workflows/deploy.yml ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face Spaces
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ # ── Wait for CI to pass first ─────────────────────────────────────────────
9
+ deploy:
10
+ name: Push to Hugging Face Spaces
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout (full history)
15
+ uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+
19
+ # ── Inject Firebase config from GitHub Secrets ──────────────────────
20
+ - name: Inject Firebase config into firebase.js
21
+ run: |
22
+ cat > frontend/src/firebase.js << 'EOF'
23
+ import { initializeApp } from 'firebase/app';
24
+ import { getAuth, GoogleAuthProvider, signInWithPopup, signOut, onAuthStateChanged } from 'firebase/auth';
25
+
26
+ const firebaseConfig = {
27
+ apiKey: "${{ secrets.FIREBASE_API_KEY }}",
28
+ authDomain: "${{ secrets.FIREBASE_AUTH_DOMAIN }}",
29
+ projectId: "${{ secrets.FIREBASE_PROJECT_ID }}",
30
+ storageBucket: "${{ secrets.FIREBASE_STORAGE_BUCKET }}",
31
+ messagingSenderId: "${{ secrets.FIREBASE_MESSAGING_SENDER_ID }}",
32
+ appId: "${{ secrets.FIREBASE_APP_ID }}",
33
+ };
34
+
35
+ const fbApp = initializeApp(firebaseConfig);
36
+ const auth = getAuth(fbApp);
37
+ const googleProvider = new GoogleAuthProvider();
38
+
39
+ export { auth, googleProvider, signInWithPopup, signOut, onAuthStateChanged };
40
+ EOF
41
+
42
+ # ── Build React with real Firebase config ────────────────────────────
43
+ - name: Set up Node 20
44
+ uses: actions/setup-node@v4
45
+ with:
46
+ node-version: "20"
47
+ cache: npm
48
+ cache-dependency-path: frontend/package-lock.json
49
+
50
+ - name: Install frontend dependencies
51
+ working-directory: frontend
52
+ run: npm ci --silent
53
+
54
+ - name: Build React app (with real Firebase config)
55
+ working-directory: frontend
56
+ run: npm run build
57
+
58
+ # ── Push to Hugging Face Spaces ──────────────────────────────────────
59
+ - name: Configure git for HF push
60
+ run: |
61
+ git config user.email "adivid198986@gmail.com"
62
+ git config user.name "ADiTyaRaj8969"
63
+
64
+ - name: Add built React to commit
65
+ run: |
66
+ git add static/react/ -f
67
+ git add frontend/src/firebase.js -f
68
+ git diff --cached --quiet || git commit -m "ci: build React with Firebase config [skip ci]"
69
+
70
+ - name: Push to Hugging Face Spaces
71
+ env:
72
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
73
+ run: |
74
+ git remote add hf https://ADiTyaRaj8969:${HF_TOKEN}@huggingface.co/spaces/ADiTyaRaj8969/FaceSwap || true
75
+ git push hf main --force
Dockerfile ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Stage 1: Build React frontend ────────────────────────────────────────────
2
+ FROM node:20-slim AS frontend-builder
3
+
4
+ WORKDIR /app
5
+ COPY frontend/package*.json frontend/
6
+ RUN cd frontend && npm ci --silent
7
+ COPY frontend/ frontend/
8
+ RUN mkdir -p static/react
9
+ RUN cd frontend && npm run build
10
+ # Output is at /app/static/react (vite outDir: ../static/react)
11
+
12
+
13
+ # ── Stage 2: Python runtime ───────────────────────────────────────────────────
14
+ FROM python:3.10-slim
15
+
16
+ # System libs required by OpenCV and MediaPipe
17
+ RUN apt-get update && apt-get install -y --no-install-recommends \
18
+ libgl1-mesa-glx \
19
+ libglib2.0-0 \
20
+ libsm6 \
21
+ libxrender1 \
22
+ libxext6 \
23
+ wget \
24
+ git \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ WORKDIR /app
28
+
29
+ # Install Python dependencies (CPU-only for cloud deployment)
30
+ COPY requirements-deploy.txt .
31
+ RUN pip install --no-cache-dir -r requirements-deploy.txt
32
+
33
+ # Copy React build from stage 1
34
+ COPY --from=frontend-builder /app/static/react ./static/react
35
+
36
+ # Copy application code
37
+ COPY . .
38
+
39
+ # Create runtime directories
40
+ RUN mkdir -p models uploads/temp outputs/results
41
+
42
+ # HuggingFace Spaces listens on 7860
43
+ ENV PORT=7860
44
+ EXPOSE 7860
45
+
46
+ # Make startup script executable
47
+ RUN chmod +x startup.sh
48
+
49
+ CMD ["./startup.sh"]
README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: DeepFace Studio
3
+ colorFrom: green
4
+ colorTo: yellow
5
+ sdk: docker
6
+ app_port: 7860
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # DeepFace Studio
12
+
13
+ AI-powered face swap web application with seamless hair-to-neck blending, skin tone matching, and Firebase authentication.
14
+
15
+ ## Features
16
+ - Live camera capture or image upload for source face
17
+ - Upload target face image
18
+ - Deep face swap using InsightFace inswapper model
19
+ - Hair-to-neck seamless blending (Laplacian + Poisson)
20
+ - Skin tone matching (CIE LAB colour transfer)
21
+ - Quality metrics after every swap
22
+ - Firebase Google Sign-in with 10-day session
23
+ - GPU + CPU support (auto-detected)
24
+
25
+ ## Local Setup
26
+
27
+ ### Requirements
28
+ - Python 3.10+
29
+ - Node.js 20+
30
+
31
+ ### Installation
32
+ ```bash
33
+ # Install Python dependencies
34
+ pip install -r requirements.txt
35
+
36
+ # Download the face swap model (~528 MB)
37
+ python scripts/download_models.py
38
+
39
+ # Build the React frontend
40
+ cd frontend
41
+ npm install
42
+ npm run build
43
+ cd ..
44
+
45
+ # Start the app
46
+ python web_app.py
47
+ ```
48
+
49
+ Open http://localhost:5000
50
+
51
+ ### Firebase Setup
52
+ 1. Go to https://console.firebase.google.com
53
+ 2. Create a new project
54
+ 3. Add a Web App
55
+ 4. Enable Google Sign-In under Authentication
56
+ 5. Copy the config into `frontend/src/firebase.js`
57
+ 6. Rebuild the frontend: `cd frontend && npm run build`
58
+
59
+ ## Deployment (HuggingFace Spaces)
60
+ The app deploys automatically via GitHub Actions on every push to `main`.
61
+ The inswapper model is downloaded automatically at container startup.
62
+
63
+ ## Tech Stack
64
+ - **Frontend**: React 18 + Vite + TailwindCSS + Framer Motion
65
+ - **Backend**: Python + Flask
66
+ - **AI Models**: InsightFace (inswapper_128), MediaPipe, OpenCV
67
+ - **Auth**: Firebase Google Sign-In
68
+
69
+ ## Note
70
+ This tool is for educational purposes only. Use responsibly and only with images you own or have explicit consent to use.
71
+
72
+ ---
73
+ &copy; Aditya Raj 2026
frontend/src/pages/LandingPage.jsx CHANGED
@@ -11,13 +11,22 @@ import AnimatedContent from '../components/ui/AnimatedContent';
11
  import Magnet from '../components/ui/Magnet';
12
  import ShinyText from '../components/ui/ShinyText';
13
 
 
 
 
 
 
 
 
 
 
14
  const FEATURES = [
15
- { icon: '📸', title: 'Live Camera + Upload', desc: 'Capture from webcam or upload any image. Instant face detection with bounding box feedback.' },
16
- { icon: '🌿', title: 'Hair to Neck Blending', desc: 'Seamless multi-layer mask blending from hairline through face to neck using Poisson cloning.' },
17
- { icon: '✨', title: 'Skin Tone Matching', desc: 'CIE LAB colour transfer adapts the swapped face to match target skin across all skin types.' },
18
- { icon: '📊', title: 'Quality Metrics', desc: 'Alignment score, blend quality, colour Delta E, and naturalness score after every swap.' },
19
- { icon: '⚡', title: 'GPU Accelerated', desc: 'CUDA optimised InsightFace with ONNX Runtime for sub 2s inference on supported hardware.' },
20
- { icon: '🔒', title: 'Fully Private', desc: 'All processing runs on your own machine. No images are uploaded to any external server.' },
21
  ];
22
 
23
  const PIPELINE = ['Detect', 'Align', 'Swap', 'Tone Match', 'Blend', 'Enhance', 'Output'];
@@ -193,7 +202,7 @@ export default function LandingPage() {
193
  {FEATURES.map((f, i) => (
194
  <AnimatedContent key={f.title} delay={i * 0.07} direction="up">
195
  <SpotlightCard className="bg-bg3 border border-forest rounded-2xl p-5 sm:p-7 h-full hover:border-olive hover:-translate-y-1 transition-all duration-200">
196
- <div className="w-10 h-10 sm:w-11 sm:h-11 bg-olive/20 border border-olive/40 rounded-xl flex items-center justify-center text-lg sm:text-xl mb-3 sm:mb-4">
197
  {f.icon}
198
  </div>
199
  <h3 className="text-lime font-bold mb-1.5 text-sm sm:text-base">{f.title}</h3>
 
11
  import Magnet from '../components/ui/Magnet';
12
  import ShinyText from '../components/ui/ShinyText';
13
 
14
+ const FEATURE_ICONS = {
15
+ camera: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5"><path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>,
16
+ layers: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>,
17
+ palette: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M12 13v2"/><circle cx="9" cy="17" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="18" r="1" fill="currentColor" stroke="none"/><circle cx="15" cy="17" r="1" fill="currentColor" stroke="none"/></svg>,
18
+ chart: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>,
19
+ zap: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>,
20
+ lock: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} className="w-5 h-5"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>,
21
+ };
22
+
23
  const FEATURES = [
24
+ { icon: FEATURE_ICONS.camera, title: 'Live Camera + Upload', desc: 'Capture from webcam or upload any image. Instant face detection with bounding box feedback.' },
25
+ { icon: FEATURE_ICONS.layers, title: 'Hair to Neck Blending', desc: 'Seamless multi-layer mask blending from hairline through face to neck using Poisson cloning.' },
26
+ { icon: FEATURE_ICONS.palette, title: 'Skin Tone Matching', desc: 'CIE LAB colour transfer adapts the swapped face to match target skin across all skin types.' },
27
+ { icon: FEATURE_ICONS.chart, title: 'Quality Metrics', desc: 'Alignment score, blend quality, colour Delta E, and naturalness score after every swap.' },
28
+ { icon: FEATURE_ICONS.zap, title: 'GPU + CPU Support', desc: 'CUDA optimised InsightFace on GPU, automatic CPU fallback. Same output quality on both.' },
29
+ { icon: FEATURE_ICONS.lock, title: 'Fully Private', desc: 'All processing runs on your own machine. No images are uploaded to any external server.' },
30
  ];
31
 
32
  const PIPELINE = ['Detect', 'Align', 'Swap', 'Tone Match', 'Blend', 'Enhance', 'Output'];
 
202
  {FEATURES.map((f, i) => (
203
  <AnimatedContent key={f.title} delay={i * 0.07} direction="up">
204
  <SpotlightCard className="bg-bg3 border border-forest rounded-2xl p-5 sm:p-7 h-full hover:border-olive hover:-translate-y-1 transition-all duration-200">
205
+ <div className="w-10 h-10 sm:w-11 sm:h-11 bg-olive/20 border border-olive/40 rounded-xl flex items-center justify-center text-lime mb-3 sm:mb-4">
206
  {f.icon}
207
  </div>
208
  <h3 className="text-lime font-bold mb-1.5 text-sm sm:text-base">{f.title}</h3>
requirements-deploy.txt ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # requirements-deploy.txt
2
+ # CPU-only deployment (HuggingFace Spaces / Render / Railway)
3
+ # GPU version: swap onnxruntime -> onnxruntime-gpu and torch CPU -> CUDA build
4
+
5
+ # Core web
6
+ flask==3.0.3
7
+ flask-cors==4.0.1
8
+
9
+ # Image processing
10
+ opencv-python-headless==4.9.0.80
11
+ numpy==1.26.4
12
+ Pillow==10.3.0
13
+ scipy==1.13.0
14
+ scikit-image==0.23.2
15
+ imageio==2.34.1
16
+
17
+ # Face detection and landmarks
18
+ insightface==0.7.3
19
+ onnxruntime==1.18.0
20
+ mediapipe==0.10.14
21
+
22
+ # PyTorch CPU-only (much smaller than CUDA build)
23
+ torch==2.3.0+cpu
24
+ torchvision==0.18.0+cpu
25
+ --extra-index-url https://download.pytorch.org/whl/cpu
26
+
27
+ # Color and utils
28
+ colormath==3.0.0
29
+ python-dotenv==1.0.1
30
+ requests==2.32.2
31
+ tqdm==4.66.4
32
+ pyyaml==6.0.1
startup.sh ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo ""
5
+ echo "======================================"
6
+ echo " DeepFace Studio — Starting Up"
7
+ echo "======================================"
8
+ echo ""
9
+
10
+ # Download inswapper_128.onnx if not already present
11
+ MODEL_PATH="models/inswapper_128.onnx"
12
+ MODEL_URL="https://huggingface.co/ezioruan/inswapper_128.onnx/resolve/main/inswapper_128.onnx"
13
+ MODEL_SIZE_MB=528
14
+
15
+ if [ ! -f "$MODEL_PATH" ]; then
16
+ echo "Downloading face swap model (~${MODEL_SIZE_MB} MB)..."
17
+ mkdir -p models
18
+ python3 - <<'PYEOF'
19
+ import os, requests, sys
20
+
21
+ url = "https://huggingface.co/ezioruan/inswapper_128.onnx/resolve/main/inswapper_128.onnx"
22
+ dest = "models/inswapper_128.onnx"
23
+
24
+ resp = requests.get(url, stream=True, timeout=300)
25
+ total = int(resp.headers.get("content-length", 0))
26
+ done = 0
27
+
28
+ with open(dest, "wb") as f:
29
+ for chunk in resp.iter_content(chunk_size=4 * 1024 * 1024):
30
+ f.write(chunk)
31
+ done += len(chunk)
32
+ if total:
33
+ pct = done * 100 // total
34
+ mb = done // 1024 // 1024
35
+ print(f"\r {mb} / {total // 1024 // 1024} MB ({pct}%)", end="", flush=True)
36
+
37
+ print("\n Model downloaded successfully.")
38
+ PYEOF
39
+ else
40
+ echo "Model already present — skipping download."
41
+ fi
42
+
43
+ echo ""
44
+ echo "Starting Flask server on port ${PORT:-7860}..."
45
+ echo ""
46
+
47
+ exec python web_app.py
web_app.py CHANGED
@@ -230,6 +230,7 @@ def api_download(filename):
230
 
231
 
232
  if __name__ == "__main__":
233
- print("Starting Face Swap Web App...")
234
- print("Open http://localhost:5000 in your browser")
235
- app.run(debug=True, host="0.0.0.0", port=5000)
 
 
230
 
231
 
232
  if __name__ == "__main__":
233
+ port = int(os.environ.get("PORT", 5000))
234
+ debug = os.environ.get("FLASK_DEBUG", "true").lower() == "true"
235
+ print(f"Starting Face Swap Web App on port {port}...")
236
+ app.run(debug=debug, host="0.0.0.0", port=port)