Deepfake Authenticator commited on
Commit ·
0424077
1
Parent(s): 3df7b7b
chore: remove unused files — frontend React, stitch designs, stripe scripts, setup scripts
Browse files- .gitignore +32 -15
- DEPLOY_HF.md +0 -101
- STRIPE_SETUP.md +0 -315
- backend/api_keys.json +0 -15
- backend/create_owner_key.py +0 -27
- backend/stripe_integration.py +0 -264
- frontend/.gitignore +0 -24
- frontend/README.md +0 -16
- frontend/eslint.config.js +0 -21
- frontend/index.html +0 -14
- frontend/package-lock.json +0 -0
- frontend/package.json +0 -37
- frontend/public/favicon.svg +0 -1
- frontend/public/icons.svg +0 -24
- frontend/script.js +0 -438
- frontend/src/App.css +0 -184
- frontend/src/App.jsx +0 -225
- frontend/src/App.tsx +0 -183
- frontend/src/api.ts +0 -20
- frontend/src/assets/hero.png +0 -0
- frontend/src/assets/react.svg +0 -1
- frontend/src/assets/vite.svg +0 -1
- frontend/src/components/CyberCard.jsx +0 -364
- frontend/src/components/DeepfakeOrb.tsx +0 -103
- frontend/src/components/Loader.jsx +0 -97
- frontend/src/components/ResultPanel.tsx +0 -346
- frontend/src/components/UploadZone.tsx +0 -190
- frontend/src/index.css +0 -43
- frontend/src/main.jsx +0 -10
- frontend/src/main.tsx +0 -10
- frontend/src/store.ts +0 -59
- frontend/vite.config.js +0 -18
- render.yaml +0 -12
- setup.bat +0 -44
- setup.sh +0 -55
- start.bat +0 -45
- start.sh +0 -34
.gitignore
CHANGED
|
@@ -10,18 +10,31 @@ dist/
|
|
| 10 |
build/
|
| 11 |
.eggs/
|
| 12 |
|
| 13 |
-
# Uploads
|
| 14 |
backend/uploads/
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
# Environment
|
| 21 |
.env
|
| 22 |
.env.local
|
| 23 |
*.env
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# IDE
|
| 26 |
.vscode/settings.json
|
| 27 |
.idea/
|
|
@@ -33,19 +46,23 @@ backend/uploads/
|
|
| 33 |
Thumbs.db
|
| 34 |
desktop.ini
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
#
|
| 40 |
*.png
|
| 41 |
*.jpg
|
| 42 |
*.jpeg
|
| 43 |
*.gif
|
| 44 |
*.ico
|
| 45 |
|
| 46 |
-
#
|
| 47 |
-
|
| 48 |
-
*.avi
|
| 49 |
-
*.mov
|
| 50 |
-
*.mkv
|
| 51 |
-
*.webm
|
|
|
|
| 10 |
build/
|
| 11 |
.eggs/
|
| 12 |
|
| 13 |
+
# Uploads
|
| 14 |
backend/uploads/
|
| 15 |
|
| 16 |
+
# Sensitive files
|
| 17 |
+
backend/api_keys.json
|
| 18 |
+
backend/usage.json
|
|
|
|
|
|
|
| 19 |
.env
|
| 20 |
.env.local
|
| 21 |
*.env
|
| 22 |
|
| 23 |
+
# Unused folders
|
| 24 |
+
frontend/
|
| 25 |
+
stitch_authrix_deepfake_detection_engine/
|
| 26 |
+
|
| 27 |
+
# One-time / internal scripts
|
| 28 |
+
backend/create_owner_key.py
|
| 29 |
+
backend/stripe_integration.py
|
| 30 |
+
STRIPE_SETUP.md
|
| 31 |
+
DEPLOY_HF.md
|
| 32 |
+
render.yaml
|
| 33 |
+
setup.bat
|
| 34 |
+
setup.sh
|
| 35 |
+
start.bat
|
| 36 |
+
start.sh
|
| 37 |
+
|
| 38 |
# IDE
|
| 39 |
.vscode/settings.json
|
| 40 |
.idea/
|
|
|
|
| 46 |
Thumbs.db
|
| 47 |
desktop.ini
|
| 48 |
|
| 49 |
+
# Logs
|
| 50 |
+
*.log
|
| 51 |
+
logs/
|
| 52 |
+
|
| 53 |
+
# Video test artifacts
|
| 54 |
+
*.mp4
|
| 55 |
+
*.avi
|
| 56 |
+
*.mov
|
| 57 |
+
*.mkv
|
| 58 |
+
*.webm
|
| 59 |
|
| 60 |
+
# Binary / image files (force-add icons explicitly with git add -f)
|
| 61 |
*.png
|
| 62 |
*.jpg
|
| 63 |
*.jpeg
|
| 64 |
*.gif
|
| 65 |
*.ico
|
| 66 |
|
| 67 |
+
# Node
|
| 68 |
+
node_modules/
|
|
|
|
|
|
|
|
|
|
|
|
DEPLOY_HF.md
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
# Deploy Authrix to Hugging Face Spaces (Free)
|
| 2 |
-
|
| 3 |
-
## Why HF Spaces?
|
| 4 |
-
- FREE 16GB RAM
|
| 5 |
-
- FREE CPU (and optional GPU)
|
| 6 |
-
- Always-on (no sleep on free tier for public spaces)
|
| 7 |
-
- Built for AI apps exactly like this
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## Step-by-Step Deployment
|
| 12 |
-
|
| 13 |
-
### 1. Create HuggingFace Account
|
| 14 |
-
Go to https://huggingface.co/join and sign up (free)
|
| 15 |
-
|
| 16 |
-
### 2. Create a New Space
|
| 17 |
-
1. Go to https://huggingface.co/new-space
|
| 18 |
-
2. Fill in:
|
| 19 |
-
- **Space name:** authrix (or authrix-deepfake-detector)
|
| 20 |
-
- **License:** MIT
|
| 21 |
-
- **SDK:** Docker
|
| 22 |
-
- **Visibility:** Public (required for free tier)
|
| 23 |
-
3. Click **Create Space**
|
| 24 |
-
|
| 25 |
-
### 3. Push Your Code
|
| 26 |
-
|
| 27 |
-
Install Git LFS first:
|
| 28 |
-
```bash
|
| 29 |
-
# Windows
|
| 30 |
-
winget install Git.Git
|
| 31 |
-
git lfs install
|
| 32 |
-
```
|
| 33 |
-
|
| 34 |
-
Then push:
|
| 35 |
-
```bash
|
| 36 |
-
# In your project root (E:\DeepFake Detect)
|
| 37 |
-
git init
|
| 38 |
-
git add .
|
| 39 |
-
git commit -m "Initial deployment"
|
| 40 |
-
|
| 41 |
-
# Add HuggingFace remote (replace YOUR_USERNAME)
|
| 42 |
-
git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/authrix
|
| 43 |
-
|
| 44 |
-
# Push (will ask for HF username + token)
|
| 45 |
-
git push hf main
|
| 46 |
-
```
|
| 47 |
-
|
| 48 |
-
Get your HF token at: https://huggingface.co/settings/tokens
|
| 49 |
-
(Create a token with "write" permission)
|
| 50 |
-
|
| 51 |
-
### 4. Wait for Build
|
| 52 |
-
- Build takes 10-15 minutes (downloading models)
|
| 53 |
-
- Watch progress at: https://huggingface.co/spaces/YOUR_USERNAME/authrix
|
| 54 |
-
|
| 55 |
-
### 5. Update Extension API URL
|
| 56 |
-
Once deployed, your app will be at:
|
| 57 |
-
`https://YOUR_USERNAME-authrix.hf.space`
|
| 58 |
-
|
| 59 |
-
Update `extension/background.js`:
|
| 60 |
-
```javascript
|
| 61 |
-
const API_BASE = 'https://YOUR_USERNAME-authrix.hf.space';
|
| 62 |
-
```
|
| 63 |
-
|
| 64 |
-
Update `extension/popup.js`:
|
| 65 |
-
```javascript
|
| 66 |
-
const API_BASE = 'https://YOUR_USERNAME-authrix.hf.space';
|
| 67 |
-
```
|
| 68 |
-
|
| 69 |
-
Update `extension/content.js` (Open Authrix App button):
|
| 70 |
-
```javascript
|
| 71 |
-
window.open('https://YOUR_USERNAME-authrix.hf.space', '_blank');
|
| 72 |
-
```
|
| 73 |
-
|
| 74 |
-
---
|
| 75 |
-
|
| 76 |
-
## Limitations of Free HF Spaces
|
| 77 |
-
|
| 78 |
-
| Feature | Free Tier |
|
| 79 |
-
|---------|-----------|
|
| 80 |
-
| RAM | 16GB ✅ |
|
| 81 |
-
| CPU | 2 vCPUs ✅ |
|
| 82 |
-
| GPU | ❌ (CPU only) |
|
| 83 |
-
| Storage | 50GB ✅ |
|
| 84 |
-
| Sleep | Never (public spaces) ✅ |
|
| 85 |
-
| Custom domain | ❌ (need Pro $9/mo) |
|
| 86 |
-
| Private space | ❌ (need Pro) |
|
| 87 |
-
|
| 88 |
-
## Upgrade Options
|
| 89 |
-
- **HF Pro ($9/mo):** Custom domain, private spaces, more storage
|
| 90 |
-
- **HF GPU Space ($0.60/hr):** 10x faster inference with T4 GPU
|
| 91 |
-
|
| 92 |
-
---
|
| 93 |
-
|
| 94 |
-
## After Deployment
|
| 95 |
-
|
| 96 |
-
Your app will be live at:
|
| 97 |
-
`https://YOUR_USERNAME-authrix.hf.space`
|
| 98 |
-
|
| 99 |
-
Share this URL with anyone — no installation needed!
|
| 100 |
-
|
| 101 |
-
The browser extension will also work with this URL once you update `API_BASE`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
STRIPE_SETUP.md
DELETED
|
@@ -1,315 +0,0 @@
|
|
| 1 |
-
# 💳 Stripe Subscription Setup Guide
|
| 2 |
-
|
| 3 |
-
## Your Owner API Key
|
| 4 |
-
|
| 5 |
-
```
|
| 6 |
-
🔑 API Key: authrix_vx5b5HqXIEtAuhUJw92p-aU7Ucz34RtWHzpBCzbKqKE
|
| 7 |
-
⭐ Tier: Owner (Unlimited Access)
|
| 8 |
-
```
|
| 9 |
-
|
| 10 |
-
**Save this key securely!** You have unlimited analyses.
|
| 11 |
-
|
| 12 |
-
---
|
| 13 |
-
|
| 14 |
-
## Quick Start: Accept Payments in 30 Minutes
|
| 15 |
-
|
| 16 |
-
### Step 1: Create Stripe Account (5 min)
|
| 17 |
-
|
| 18 |
-
1. Go to https://dashboard.stripe.com/register
|
| 19 |
-
2. Sign up with your email
|
| 20 |
-
3. Complete business verification
|
| 21 |
-
|
| 22 |
-
### Step 2: Get API Keys (2 min)
|
| 23 |
-
|
| 24 |
-
1. Go to https://dashboard.stripe.com/apikeys
|
| 25 |
-
2. Copy your **Secret Key** (starts with `sk_test_` or `sk_live_`)
|
| 26 |
-
3. Add to your environment:
|
| 27 |
-
|
| 28 |
-
```bash
|
| 29 |
-
# Windows
|
| 30 |
-
set STRIPE_SECRET_KEY=sk_test_YOUR_KEY_HERE
|
| 31 |
-
|
| 32 |
-
# Linux/Mac
|
| 33 |
-
export STRIPE_SECRET_KEY=sk_test_YOUR_KEY_HERE
|
| 34 |
-
```
|
| 35 |
-
|
| 36 |
-
### Step 3: Create Products & Prices (10 min)
|
| 37 |
-
|
| 38 |
-
1. Go to https://dashboard.stripe.com/products
|
| 39 |
-
2. Click **+ Add product**
|
| 40 |
-
|
| 41 |
-
**Create these products:**
|
| 42 |
-
|
| 43 |
-
#### Product 1: Authrix Pro
|
| 44 |
-
- **Name:** Authrix Pro
|
| 45 |
-
- **Description:** 100 video analyses per month
|
| 46 |
-
- **Pricing:**
|
| 47 |
-
- Monthly: $9.99/month (recurring)
|
| 48 |
-
- Yearly: $99/year (recurring)
|
| 49 |
-
- Copy the **Price ID** (starts with `price_`)
|
| 50 |
-
|
| 51 |
-
#### Product 2: Authrix Business
|
| 52 |
-
- **Name:** Authrix Business
|
| 53 |
-
- **Description:** 1,000 video analyses per month
|
| 54 |
-
- **Pricing:**
|
| 55 |
-
- Monthly: $49/month (recurring)
|
| 56 |
-
- Yearly: $490/year (recurring)
|
| 57 |
-
- Copy the **Price ID**
|
| 58 |
-
|
| 59 |
-
### Step 4: Update Price IDs (2 min)
|
| 60 |
-
|
| 61 |
-
Edit `backend/stripe_integration.py`:
|
| 62 |
-
|
| 63 |
-
```python
|
| 64 |
-
PRICE_IDS = {
|
| 65 |
-
"pro_monthly": "price_YOUR_PRO_MONTHLY_ID",
|
| 66 |
-
"pro_yearly": "price_YOUR_PRO_YEARLY_ID",
|
| 67 |
-
"business_monthly": "price_YOUR_BUSINESS_MONTHLY_ID",
|
| 68 |
-
"business_yearly": "price_YOUR_BUSINESS_YEARLY_ID",
|
| 69 |
-
}
|
| 70 |
-
```
|
| 71 |
-
|
| 72 |
-
### Step 5: Set Up Webhooks (5 min)
|
| 73 |
-
|
| 74 |
-
1. Go to https://dashboard.stripe.com/webhooks
|
| 75 |
-
2. Click **+ Add endpoint**
|
| 76 |
-
3. **Endpoint URL:** `https://your-domain.com/api/stripe/webhook`
|
| 77 |
-
4. **Events to send:**
|
| 78 |
-
- `checkout.session.completed`
|
| 79 |
-
- `customer.subscription.updated`
|
| 80 |
-
- `customer.subscription.deleted`
|
| 81 |
-
5. Copy the **Signing secret** (starts with `whsec_`)
|
| 82 |
-
6. Add to environment:
|
| 83 |
-
|
| 84 |
-
```bash
|
| 85 |
-
set STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET_HERE
|
| 86 |
-
```
|
| 87 |
-
|
| 88 |
-
### Step 6: Test Payment (5 min)
|
| 89 |
-
|
| 90 |
-
```bash
|
| 91 |
-
# Start backend
|
| 92 |
-
python -m uvicorn main:app --port 8000
|
| 93 |
-
|
| 94 |
-
# Test checkout (in another terminal)
|
| 95 |
-
curl -X POST http://localhost:8000/api/stripe/create-checkout-session \
|
| 96 |
-
-H "Content-Type: application/json" \
|
| 97 |
-
-d '{
|
| 98 |
-
"email": "test@example.com",
|
| 99 |
-
"plan": "pro_monthly",
|
| 100 |
-
"success_url": "http://localhost:8000/success",
|
| 101 |
-
"cancel_url": "http://localhost:8000/pricing"
|
| 102 |
-
}'
|
| 103 |
-
```
|
| 104 |
-
|
| 105 |
-
You'll get a `checkout_url` - open it in your browser!
|
| 106 |
-
|
| 107 |
-
**Test Card Numbers:**
|
| 108 |
-
- Success: `4242 4242 4242 4242`
|
| 109 |
-
- Decline: `4000 0000 0000 0002`
|
| 110 |
-
- Any future expiry date, any CVC
|
| 111 |
-
|
| 112 |
-
---
|
| 113 |
-
|
| 114 |
-
## Integration Examples
|
| 115 |
-
|
| 116 |
-
### Frontend: Create Checkout
|
| 117 |
-
|
| 118 |
-
```javascript
|
| 119 |
-
// When user clicks "Subscribe to Pro"
|
| 120 |
-
async function subscribeToPro() {
|
| 121 |
-
const response = await fetch('http://localhost:8000/api/stripe/create-checkout-session', {
|
| 122 |
-
method: 'POST',
|
| 123 |
-
headers: { 'Content-Type': 'application/json' },
|
| 124 |
-
body: JSON.stringify({
|
| 125 |
-
email: 'user@example.com',
|
| 126 |
-
plan: 'pro_monthly',
|
| 127 |
-
success_url: window.location.origin + '/success',
|
| 128 |
-
cancel_url: window.location.origin + '/pricing'
|
| 129 |
-
})
|
| 130 |
-
});
|
| 131 |
-
|
| 132 |
-
const { checkout_url } = await response.json();
|
| 133 |
-
window.location.href = checkout_url; // Redirect to Stripe
|
| 134 |
-
}
|
| 135 |
-
```
|
| 136 |
-
|
| 137 |
-
### Backend: Use API with Key
|
| 138 |
-
|
| 139 |
-
```python
|
| 140 |
-
import requests
|
| 141 |
-
|
| 142 |
-
response = requests.post(
|
| 143 |
-
'http://localhost:8000/analyze',
|
| 144 |
-
headers={'X-API-Key': 'authrix_YOUR_KEY_HERE'},
|
| 145 |
-
files={'file': open('video.mp4', 'rb')}
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
print(response.json())
|
| 149 |
-
```
|
| 150 |
-
|
| 151 |
-
### Extension: Add API Key
|
| 152 |
-
|
| 153 |
-
Edit `extension/background.js`:
|
| 154 |
-
|
| 155 |
-
```javascript
|
| 156 |
-
const API_KEY = 'authrix_vx5b5HqXIEtAuhUJw92p-aU7Ucz34RtWHzpBCzbKqKE';
|
| 157 |
-
|
| 158 |
-
async function submitBlob(chunks, mimeType, totalSize) {
|
| 159 |
-
// ... existing code ...
|
| 160 |
-
|
| 161 |
-
const res = await fetch(`${API_BASE}/analyze`, {
|
| 162 |
-
method: 'POST',
|
| 163 |
-
headers: {
|
| 164 |
-
'X-API-Key': API_KEY // Add this line
|
| 165 |
-
},
|
| 166 |
-
body: fd
|
| 167 |
-
});
|
| 168 |
-
|
| 169 |
-
// ... rest of code ...
|
| 170 |
-
}
|
| 171 |
-
```
|
| 172 |
-
|
| 173 |
-
---
|
| 174 |
-
|
| 175 |
-
## Pricing Page HTML
|
| 176 |
-
|
| 177 |
-
Create `pricing.html`:
|
| 178 |
-
|
| 179 |
-
```html
|
| 180 |
-
<!DOCTYPE html>
|
| 181 |
-
<html>
|
| 182 |
-
<head>
|
| 183 |
-
<title>Authrix Pricing</title>
|
| 184 |
-
<script src="https://js.stripe.com/v3/"></script>
|
| 185 |
-
</head>
|
| 186 |
-
<body>
|
| 187 |
-
<h1>Choose Your Plan</h1>
|
| 188 |
-
|
| 189 |
-
<div class="pricing-cards">
|
| 190 |
-
<!-- Free -->
|
| 191 |
-
<div class="card">
|
| 192 |
-
<h2>Free</h2>
|
| 193 |
-
<p class="price">$0<span>/month</span></p>
|
| 194 |
-
<ul>
|
| 195 |
-
<li>10 analyses/month</li>
|
| 196 |
-
<li>Browser extension</li>
|
| 197 |
-
<li>2-min videos</li>
|
| 198 |
-
</ul>
|
| 199 |
-
<button onclick="window.location.href='/signup'">Get Started</button>
|
| 200 |
-
</div>
|
| 201 |
-
|
| 202 |
-
<!-- Pro -->
|
| 203 |
-
<div class="card popular">
|
| 204 |
-
<h2>Pro</h2>
|
| 205 |
-
<p class="price">$9.99<span>/month</span></p>
|
| 206 |
-
<ul>
|
| 207 |
-
<li>100 analyses/month</li>
|
| 208 |
-
<li>10-min videos</li>
|
| 209 |
-
<li>API access</li>
|
| 210 |
-
<li>Email support</li>
|
| 211 |
-
</ul>
|
| 212 |
-
<button onclick="subscribe('pro_monthly')">Subscribe</button>
|
| 213 |
-
</div>
|
| 214 |
-
|
| 215 |
-
<!-- Business -->
|
| 216 |
-
<div class="card">
|
| 217 |
-
<h2>Business</h2>
|
| 218 |
-
<p class="price">$49<span>/month</span></p>
|
| 219 |
-
<ul>
|
| 220 |
-
<li>1,000 analyses/month</li>
|
| 221 |
-
<li>Unlimited length</li>
|
| 222 |
-
<li>White-label reports</li>
|
| 223 |
-
<li>Priority support</li>
|
| 224 |
-
</ul>
|
| 225 |
-
<button onclick="subscribe('business_monthly')">Subscribe</button>
|
| 226 |
-
</div>
|
| 227 |
-
</div>
|
| 228 |
-
|
| 229 |
-
<script>
|
| 230 |
-
async function subscribe(plan) {
|
| 231 |
-
const email = prompt('Enter your email:');
|
| 232 |
-
if (!email) return;
|
| 233 |
-
|
| 234 |
-
const response = await fetch('http://localhost:8000/api/stripe/create-checkout-session', {
|
| 235 |
-
method: 'POST',
|
| 236 |
-
headers: { 'Content-Type': 'application/json' },
|
| 237 |
-
body: JSON.stringify({
|
| 238 |
-
email,
|
| 239 |
-
plan,
|
| 240 |
-
success_url: window.location.origin + '/success',
|
| 241 |
-
cancel_url: window.location.origin + '/pricing'
|
| 242 |
-
})
|
| 243 |
-
});
|
| 244 |
-
|
| 245 |
-
const { checkout_url } = await response.json();
|
| 246 |
-
window.location.href = checkout_url;
|
| 247 |
-
}
|
| 248 |
-
</script>
|
| 249 |
-
</body>
|
| 250 |
-
</html>
|
| 251 |
-
```
|
| 252 |
-
|
| 253 |
-
---
|
| 254 |
-
|
| 255 |
-
## Go Live Checklist
|
| 256 |
-
|
| 257 |
-
### Before Launch:
|
| 258 |
-
- [ ] Switch to **Live mode** in Stripe Dashboard
|
| 259 |
-
- [ ] Update `STRIPE_SECRET_KEY` with live key (`sk_live_...`)
|
| 260 |
-
- [ ] Update webhook endpoint to production URL
|
| 261 |
-
- [ ] Test with real card (will charge!)
|
| 262 |
-
- [ ] Set up email notifications (welcome, payment failed, etc.)
|
| 263 |
-
- [ ] Add terms of service & privacy policy
|
| 264 |
-
- [ ] Set up customer support email
|
| 265 |
-
|
| 266 |
-
### After Launch:
|
| 267 |
-
- [ ] Monitor Stripe Dashboard for payments
|
| 268 |
-
- [ ] Set up Stripe Radar for fraud prevention
|
| 269 |
-
- [ ] Enable 3D Secure for EU customers
|
| 270 |
-
- [ ] Set up tax collection (Stripe Tax)
|
| 271 |
-
- [ ] Create refund policy
|
| 272 |
-
|
| 273 |
-
---
|
| 274 |
-
|
| 275 |
-
## Revenue Tracking
|
| 276 |
-
|
| 277 |
-
### Check Earnings:
|
| 278 |
-
```bash
|
| 279 |
-
# Get all API keys and their tiers
|
| 280 |
-
python -c "from auth import load_api_keys; import json; print(json.dumps(load_api_keys(), indent=2))"
|
| 281 |
-
|
| 282 |
-
# Check usage for a key
|
| 283 |
-
python -c "from auth import check_usage_limit; print(check_usage_limit('authrix_YOUR_KEY'))"
|
| 284 |
-
```
|
| 285 |
-
|
| 286 |
-
### Stripe Dashboard:
|
| 287 |
-
- **Revenue:** https://dashboard.stripe.com/revenue
|
| 288 |
-
- **Customers:** https://dashboard.stripe.com/customers
|
| 289 |
-
- **Subscriptions:** https://dashboard.stripe.com/subscriptions
|
| 290 |
-
|
| 291 |
-
---
|
| 292 |
-
|
| 293 |
-
## Support & Resources
|
| 294 |
-
|
| 295 |
-
- **Stripe Docs:** https://stripe.com/docs
|
| 296 |
-
- **Stripe Testing:** https://stripe.com/docs/testing
|
| 297 |
-
- **Webhook Testing:** https://stripe.com/docs/webhooks/test
|
| 298 |
-
- **Stripe CLI:** https://stripe.com/docs/stripe-cli
|
| 299 |
-
|
| 300 |
-
---
|
| 301 |
-
|
| 302 |
-
## Next Steps
|
| 303 |
-
|
| 304 |
-
1. **Deploy to production** (Heroku, AWS, DigitalOcean)
|
| 305 |
-
2. **Get a domain** (authrix.ai, authrix.com)
|
| 306 |
-
3. **Set up SSL** (Let's Encrypt, Cloudflare)
|
| 307 |
-
4. **Create landing page** (Webflow, Carrd, custom)
|
| 308 |
-
5. **Launch on Product Hunt**
|
| 309 |
-
6. **Start marketing!**
|
| 310 |
-
|
| 311 |
-
---
|
| 312 |
-
|
| 313 |
-
**Questions?** Open an issue or email support@authrix.ai
|
| 314 |
-
|
| 315 |
-
**Built with ❤️ by the Authrix Team**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/api_keys.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"c522f03ba084fc1150f9ab5bd78bf23e295dbf15cf75d232f3cf0dc3afad3641": {
|
| 3 |
-
"email": "demo@authrix.ai",
|
| 4 |
-
"tier": "pro",
|
| 5 |
-
"created_at": "2026-04-25T20:13:27.568274",
|
| 6 |
-
"active": true
|
| 7 |
-
},
|
| 8 |
-
"b35e8f8fb3db9ee0f6c4bc3b5a01112861de233b93610e5eeca87b75563ab030": {
|
| 9 |
-
"email": "owner@authrix.ai",
|
| 10 |
-
"tier": "owner",
|
| 11 |
-
"created_at": "2026-04-25T20:16:53.342074",
|
| 12 |
-
"active": true,
|
| 13 |
-
"unlimited": true
|
| 14 |
-
}
|
| 15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/create_owner_key.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Create owner API key with unlimited access
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
from auth import create_api_key, load_api_keys, save_api_keys, hash_key
|
| 6 |
-
|
| 7 |
-
# Create owner key
|
| 8 |
-
owner_email = "owner@authrix.ai"
|
| 9 |
-
owner_key = create_api_key(owner_email, "owner")
|
| 10 |
-
|
| 11 |
-
# Update to unlimited tier
|
| 12 |
-
keys = load_api_keys()
|
| 13 |
-
key_hash = hash_key(owner_key)
|
| 14 |
-
keys[key_hash]["tier"] = "owner"
|
| 15 |
-
keys[key_hash]["unlimited"] = True
|
| 16 |
-
save_api_keys(keys)
|
| 17 |
-
|
| 18 |
-
print("\n" + "="*60)
|
| 19 |
-
print("🎉 OWNER API KEY CREATED")
|
| 20 |
-
print("="*60)
|
| 21 |
-
print(f"\n📧 Email: {owner_email}")
|
| 22 |
-
print(f"🔑 API Key: {owner_key}")
|
| 23 |
-
print(f"⭐ Tier: Owner (Unlimited)")
|
| 24 |
-
print(f"\n💡 Add this to your requests:")
|
| 25 |
-
print(f' X-API-Key: {owner_key}')
|
| 26 |
-
print(f"\n📝 Save this key securely - it won't be shown again!")
|
| 27 |
-
print("="*60 + "\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/stripe_integration.py
DELETED
|
@@ -1,264 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Authrix - Stripe Subscription Integration
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import os
|
| 6 |
-
from typing import Optional
|
| 7 |
-
import stripe
|
| 8 |
-
from fastapi import APIRouter, HTTPException, Request
|
| 9 |
-
from pydantic import BaseModel
|
| 10 |
-
import logging
|
| 11 |
-
|
| 12 |
-
from auth import create_api_key, load_api_keys, save_api_keys, hash_key
|
| 13 |
-
|
| 14 |
-
logger = logging.getLogger(__name__)
|
| 15 |
-
|
| 16 |
-
# Set your Stripe secret key (get from https://dashboard.stripe.com/apikeys)
|
| 17 |
-
stripe.api_key = os.getenv("STRIPE_SECRET_KEY", "sk_test_YOUR_KEY_HERE")
|
| 18 |
-
STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET", "whsec_YOUR_WEBHOOK_SECRET")
|
| 19 |
-
|
| 20 |
-
router = APIRouter(prefix="/api/stripe", tags=["stripe"])
|
| 21 |
-
|
| 22 |
-
# Stripe Price IDs (create these in Stripe Dashboard)
|
| 23 |
-
PRICE_IDS = {
|
| 24 |
-
"pro_monthly": "price_PRO_MONTHLY_ID", # $9.99/month
|
| 25 |
-
"pro_yearly": "price_PRO_YEARLY_ID", # $99/year (2 months free)
|
| 26 |
-
"business_monthly": "price_BUSINESS_MONTHLY_ID", # $49/month
|
| 27 |
-
"business_yearly": "price_BUSINESS_YEARLY_ID", # $490/year
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
class CheckoutRequest(BaseModel):
|
| 31 |
-
email: str
|
| 32 |
-
plan: str # "pro_monthly", "pro_yearly", "business_monthly", "business_yearly"
|
| 33 |
-
success_url: str
|
| 34 |
-
cancel_url: str
|
| 35 |
-
|
| 36 |
-
class PortalRequest(BaseModel):
|
| 37 |
-
customer_id: str
|
| 38 |
-
return_url: str
|
| 39 |
-
|
| 40 |
-
@router.post("/create-checkout-session")
|
| 41 |
-
async def create_checkout_session(request: CheckoutRequest):
|
| 42 |
-
"""
|
| 43 |
-
Create a Stripe Checkout session for subscription.
|
| 44 |
-
|
| 45 |
-
Usage:
|
| 46 |
-
POST /api/stripe/create-checkout-session
|
| 47 |
-
{
|
| 48 |
-
"email": "user@example.com",
|
| 49 |
-
"plan": "pro_monthly",
|
| 50 |
-
"success_url": "https://authrix.ai/success",
|
| 51 |
-
"cancel_url": "https://authrix.ai/pricing"
|
| 52 |
-
}
|
| 53 |
-
"""
|
| 54 |
-
try:
|
| 55 |
-
price_id = PRICE_IDS.get(request.plan)
|
| 56 |
-
if not price_id:
|
| 57 |
-
raise HTTPException(status_code=400, detail=f"Invalid plan: {request.plan}")
|
| 58 |
-
|
| 59 |
-
# Create Stripe Checkout Session
|
| 60 |
-
session = stripe.checkout.Session.create(
|
| 61 |
-
customer_email=request.email,
|
| 62 |
-
payment_method_types=["card"],
|
| 63 |
-
line_items=[{
|
| 64 |
-
"price": price_id,
|
| 65 |
-
"quantity": 1,
|
| 66 |
-
}],
|
| 67 |
-
mode="subscription",
|
| 68 |
-
success_url=request.success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
| 69 |
-
cancel_url=request.cancel_url,
|
| 70 |
-
metadata={
|
| 71 |
-
"email": request.email,
|
| 72 |
-
"plan": request.plan,
|
| 73 |
-
},
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
-
return {"checkout_url": session.url, "session_id": session.id}
|
| 77 |
-
|
| 78 |
-
except stripe.error.StripeError as e:
|
| 79 |
-
logger.error(f"Stripe error: {e}")
|
| 80 |
-
raise HTTPException(status_code=400, detail=str(e))
|
| 81 |
-
|
| 82 |
-
@router.post("/create-portal-session")
|
| 83 |
-
async def create_portal_session(request: PortalRequest):
|
| 84 |
-
"""
|
| 85 |
-
Create a Stripe Customer Portal session for managing subscription.
|
| 86 |
-
|
| 87 |
-
Usage:
|
| 88 |
-
POST /api/stripe/create-portal-session
|
| 89 |
-
{
|
| 90 |
-
"customer_id": "cus_XXXXX",
|
| 91 |
-
"return_url": "https://authrix.ai/account"
|
| 92 |
-
}
|
| 93 |
-
"""
|
| 94 |
-
try:
|
| 95 |
-
session = stripe.billing_portal.Session.create(
|
| 96 |
-
customer=request.customer_id,
|
| 97 |
-
return_url=request.return_url,
|
| 98 |
-
)
|
| 99 |
-
return {"portal_url": session.url}
|
| 100 |
-
|
| 101 |
-
except stripe.error.StripeError as e:
|
| 102 |
-
logger.error(f"Stripe error: {e}")
|
| 103 |
-
raise HTTPException(status_code=400, detail=str(e))
|
| 104 |
-
|
| 105 |
-
@router.post("/webhook")
|
| 106 |
-
async def stripe_webhook(request: Request):
|
| 107 |
-
"""
|
| 108 |
-
Handle Stripe webhook events.
|
| 109 |
-
|
| 110 |
-
Events handled:
|
| 111 |
-
- checkout.session.completed: Create API key when subscription starts
|
| 112 |
-
- customer.subscription.updated: Update tier when subscription changes
|
| 113 |
-
- customer.subscription.deleted: Downgrade to free when subscription cancels
|
| 114 |
-
"""
|
| 115 |
-
payload = await request.body()
|
| 116 |
-
sig_header = request.headers.get("stripe-signature")
|
| 117 |
-
|
| 118 |
-
try:
|
| 119 |
-
event = stripe.Webhook.construct_event(
|
| 120 |
-
payload, sig_header, STRIPE_WEBHOOK_SECRET
|
| 121 |
-
)
|
| 122 |
-
except ValueError:
|
| 123 |
-
raise HTTPException(status_code=400, detail="Invalid payload")
|
| 124 |
-
except stripe.error.SignatureVerificationError:
|
| 125 |
-
raise HTTPException(status_code=400, detail="Invalid signature")
|
| 126 |
-
|
| 127 |
-
# Handle the event
|
| 128 |
-
if event["type"] == "checkout.session.completed":
|
| 129 |
-
session = event["data"]["object"]
|
| 130 |
-
await handle_checkout_completed(session)
|
| 131 |
-
|
| 132 |
-
elif event["type"] == "customer.subscription.updated":
|
| 133 |
-
subscription = event["data"]["object"]
|
| 134 |
-
await handle_subscription_updated(subscription)
|
| 135 |
-
|
| 136 |
-
elif event["type"] == "customer.subscription.deleted":
|
| 137 |
-
subscription = event["data"]["object"]
|
| 138 |
-
await handle_subscription_deleted(subscription)
|
| 139 |
-
|
| 140 |
-
return {"status": "success"}
|
| 141 |
-
|
| 142 |
-
async def handle_checkout_completed(session: dict):
|
| 143 |
-
"""Create API key when checkout completes."""
|
| 144 |
-
email = session["metadata"]["email"]
|
| 145 |
-
plan = session["metadata"]["plan"]
|
| 146 |
-
customer_id = session["customer"]
|
| 147 |
-
subscription_id = session["subscription"]
|
| 148 |
-
|
| 149 |
-
# Determine tier from plan
|
| 150 |
-
tier = "pro" if "pro" in plan else "business"
|
| 151 |
-
|
| 152 |
-
# Create API key
|
| 153 |
-
api_key = create_api_key(email, tier)
|
| 154 |
-
|
| 155 |
-
# Store Stripe customer ID and subscription ID
|
| 156 |
-
keys = load_api_keys()
|
| 157 |
-
key_hash = hash_key(api_key)
|
| 158 |
-
keys[key_hash]["stripe_customer_id"] = customer_id
|
| 159 |
-
keys[key_hash]["stripe_subscription_id"] = subscription_id
|
| 160 |
-
save_api_keys(keys)
|
| 161 |
-
|
| 162 |
-
logger.info(f"Created {tier} API key for {email} (customer: {customer_id})")
|
| 163 |
-
|
| 164 |
-
# TODO: Send welcome email with API key
|
| 165 |
-
# send_email(email, "Welcome to Authrix!", f"Your API key: {api_key}")
|
| 166 |
-
|
| 167 |
-
async def handle_subscription_updated(subscription: dict):
|
| 168 |
-
"""Update tier when subscription changes."""
|
| 169 |
-
customer_id = subscription["customer"]
|
| 170 |
-
status = subscription["status"]
|
| 171 |
-
|
| 172 |
-
# Find API key by customer ID
|
| 173 |
-
keys = load_api_keys()
|
| 174 |
-
for key_hash, key_data in keys.items():
|
| 175 |
-
if key_data.get("stripe_customer_id") == customer_id:
|
| 176 |
-
if status == "active":
|
| 177 |
-
# Subscription is active - ensure tier is correct
|
| 178 |
-
logger.info(f"Subscription active for {key_data['email']}")
|
| 179 |
-
elif status in ("past_due", "unpaid"):
|
| 180 |
-
# Payment failed - send warning
|
| 181 |
-
logger.warning(f"Payment issue for {key_data['email']}")
|
| 182 |
-
break
|
| 183 |
-
|
| 184 |
-
async def handle_subscription_deleted(subscription: dict):
|
| 185 |
-
"""Downgrade to free when subscription cancels."""
|
| 186 |
-
customer_id = subscription["customer"]
|
| 187 |
-
|
| 188 |
-
# Find API key and downgrade to free
|
| 189 |
-
keys = load_api_keys()
|
| 190 |
-
for key_hash, key_data in keys.items():
|
| 191 |
-
if key_data.get("stripe_customer_id") == customer_id:
|
| 192 |
-
keys[key_hash]["tier"] = "free"
|
| 193 |
-
keys[key_hash]["active"] = False # Deactivate key
|
| 194 |
-
save_api_keys(keys)
|
| 195 |
-
logger.info(f"Downgraded {key_data['email']} to free tier")
|
| 196 |
-
break
|
| 197 |
-
|
| 198 |
-
# Pricing plans for frontend
|
| 199 |
-
PRICING_PLANS = {
|
| 200 |
-
"free": {
|
| 201 |
-
"name": "Free",
|
| 202 |
-
"price": 0,
|
| 203 |
-
"interval": "month",
|
| 204 |
-
"analyses": 10,
|
| 205 |
-
"features": [
|
| 206 |
-
"10 video analyses per month",
|
| 207 |
-
"Browser extension",
|
| 208 |
-
"Max 2-minute videos",
|
| 209 |
-
"Community support",
|
| 210 |
-
],
|
| 211 |
-
},
|
| 212 |
-
"pro": {
|
| 213 |
-
"name": "Pro",
|
| 214 |
-
"price_monthly": 9.99,
|
| 215 |
-
"price_yearly": 99,
|
| 216 |
-
"interval": "month",
|
| 217 |
-
"analyses": 100,
|
| 218 |
-
"features": [
|
| 219 |
-
"100 analyses per month",
|
| 220 |
-
"Up to 10-minute videos",
|
| 221 |
-
"API access (100 calls/month)",
|
| 222 |
-
"Priority processing",
|
| 223 |
-
"Email support",
|
| 224 |
-
"Batch upload",
|
| 225 |
-
],
|
| 226 |
-
"popular": True,
|
| 227 |
-
},
|
| 228 |
-
"business": {
|
| 229 |
-
"name": "Business",
|
| 230 |
-
"price_monthly": 49,
|
| 231 |
-
"price_yearly": 490,
|
| 232 |
-
"interval": "month",
|
| 233 |
-
"analyses": 1000,
|
| 234 |
-
"features": [
|
| 235 |
-
"1,000 analyses per month",
|
| 236 |
-
"Unlimited video length",
|
| 237 |
-
"API access (5,000 calls/month)",
|
| 238 |
-
"White-label reports",
|
| 239 |
-
"Slack/Teams integration",
|
| 240 |
-
"Priority support",
|
| 241 |
-
"Custom branding",
|
| 242 |
-
],
|
| 243 |
-
},
|
| 244 |
-
"enterprise": {
|
| 245 |
-
"name": "Enterprise",
|
| 246 |
-
"price": "Custom",
|
| 247 |
-
"interval": "month",
|
| 248 |
-
"analyses": "Unlimited",
|
| 249 |
-
"features": [
|
| 250 |
-
"Unlimited analyses",
|
| 251 |
-
"On-premise deployment",
|
| 252 |
-
"Custom model training",
|
| 253 |
-
"SLA guarantees",
|
| 254 |
-
"Dedicated support",
|
| 255 |
-
"Multi-user accounts",
|
| 256 |
-
],
|
| 257 |
-
"contact": True,
|
| 258 |
-
},
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
@router.get("/pricing")
|
| 262 |
-
async def get_pricing():
|
| 263 |
-
"""Get pricing plans."""
|
| 264 |
-
return PRICING_PLANS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/.gitignore
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
# Logs
|
| 2 |
-
logs
|
| 3 |
-
*.log
|
| 4 |
-
npm-debug.log*
|
| 5 |
-
yarn-debug.log*
|
| 6 |
-
yarn-error.log*
|
| 7 |
-
pnpm-debug.log*
|
| 8 |
-
lerna-debug.log*
|
| 9 |
-
|
| 10 |
-
node_modules
|
| 11 |
-
dist
|
| 12 |
-
dist-ssr
|
| 13 |
-
*.local
|
| 14 |
-
|
| 15 |
-
# Editor directories and files
|
| 16 |
-
.vscode/*
|
| 17 |
-
!.vscode/extensions.json
|
| 18 |
-
.idea
|
| 19 |
-
.DS_Store
|
| 20 |
-
*.suo
|
| 21 |
-
*.ntvs*
|
| 22 |
-
*.njsproj
|
| 23 |
-
*.sln
|
| 24 |
-
*.sw?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/README.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
# React + Vite
|
| 2 |
-
|
| 3 |
-
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
-
|
| 5 |
-
Currently, two official plugins are available:
|
| 6 |
-
|
| 7 |
-
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
| 8 |
-
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
| 9 |
-
|
| 10 |
-
## React Compiler
|
| 11 |
-
|
| 12 |
-
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
| 13 |
-
|
| 14 |
-
## Expanding the ESLint configuration
|
| 15 |
-
|
| 16 |
-
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/eslint.config.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
import js from '@eslint/js'
|
| 2 |
-
import globals from 'globals'
|
| 3 |
-
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
-
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
-
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 6 |
-
|
| 7 |
-
export default defineConfig([
|
| 8 |
-
globalIgnores(['dist']),
|
| 9 |
-
{
|
| 10 |
-
files: ['**/*.{js,jsx}'],
|
| 11 |
-
extends: [
|
| 12 |
-
js.configs.recommended,
|
| 13 |
-
reactHooks.configs.flat.recommended,
|
| 14 |
-
reactRefresh.configs.vite,
|
| 15 |
-
],
|
| 16 |
-
languageOptions: {
|
| 17 |
-
globals: globals.browser,
|
| 18 |
-
parserOptions: { ecmaFeatures: { jsx: true } },
|
| 19 |
-
},
|
| 20 |
-
},
|
| 21 |
-
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/index.html
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<title>Deepfake Authenticator</title>
|
| 7 |
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
-
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
|
| 9 |
-
</head>
|
| 10 |
-
<body>
|
| 11 |
-
<div id="root"></div>
|
| 12 |
-
<script type="module" src="/src/main.tsx"></script>
|
| 13 |
-
</body>
|
| 14 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/package-lock.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "frontend",
|
| 3 |
-
"private": true,
|
| 4 |
-
"version": "0.0.0",
|
| 5 |
-
"type": "module",
|
| 6 |
-
"scripts": {
|
| 7 |
-
"dev": "vite",
|
| 8 |
-
"build": "vite build",
|
| 9 |
-
"lint": "eslint .",
|
| 10 |
-
"preview": "vite preview"
|
| 11 |
-
},
|
| 12 |
-
"dependencies": {
|
| 13 |
-
"@react-three/drei": "^10.7.7",
|
| 14 |
-
"@react-three/fiber": "^9.6.0",
|
| 15 |
-
"@tailwindcss/vite": "^4.2.4",
|
| 16 |
-
"autoprefixer": "^10.5.0",
|
| 17 |
-
"framer-motion": "^12.38.0",
|
| 18 |
-
"postcss": "^8.5.10",
|
| 19 |
-
"react": "^19.2.5",
|
| 20 |
-
"react-dom": "^19.2.5",
|
| 21 |
-
"styled-components": "^6.4.1",
|
| 22 |
-
"tailwindcss": "^4.2.4",
|
| 23 |
-
"three": "^0.184.0",
|
| 24 |
-
"zustand": "^5.0.12"
|
| 25 |
-
},
|
| 26 |
-
"devDependencies": {
|
| 27 |
-
"@eslint/js": "^10.0.1",
|
| 28 |
-
"@types/react": "^19.2.14",
|
| 29 |
-
"@types/react-dom": "^19.2.3",
|
| 30 |
-
"@vitejs/plugin-react": "^6.0.1",
|
| 31 |
-
"eslint": "^10.2.1",
|
| 32 |
-
"eslint-plugin-react-hooks": "^7.1.1",
|
| 33 |
-
"eslint-plugin-react-refresh": "^0.5.2",
|
| 34 |
-
"globals": "^17.5.0",
|
| 35 |
-
"vite": "^8.0.10"
|
| 36 |
-
}
|
| 37 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/public/favicon.svg
DELETED
frontend/public/icons.svg
DELETED
frontend/script.js
DELETED
|
@@ -1,438 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Deepfake Authenticator — Frontend Logic (Cinematic Dark UI)
|
| 3 |
-
*/
|
| 4 |
-
|
| 5 |
-
const API_BASE = (window.location.protocol === 'file:')
|
| 6 |
-
? 'http://localhost:8000'
|
| 7 |
-
: window.location.origin;
|
| 8 |
-
|
| 9 |
-
let selectedFile = null;
|
| 10 |
-
|
| 11 |
-
// ── Boot ──────────────────────────────────────
|
| 12 |
-
window.addEventListener('load', () => {
|
| 13 |
-
initUpload();
|
| 14 |
-
});
|
| 15 |
-
|
| 16 |
-
// ── Upload wiring ─────────────────────────────
|
| 17 |
-
function initUpload() {
|
| 18 |
-
const zone = document.getElementById('dropZone');
|
| 19 |
-
const input = document.getElementById('fileInput');
|
| 20 |
-
const clear = document.getElementById('clearBtn');
|
| 21 |
-
const btn = document.getElementById('analyzeBtn');
|
| 22 |
-
|
| 23 |
-
zone.addEventListener('click', e => {
|
| 24 |
-
if (!e.target.closest('#clearBtn')) input.click();
|
| 25 |
-
});
|
| 26 |
-
|
| 27 |
-
input.addEventListener('change', () => {
|
| 28 |
-
if (input.files?.[0]) applyFile(input.files[0]);
|
| 29 |
-
});
|
| 30 |
-
|
| 31 |
-
clear.addEventListener('click', e => {
|
| 32 |
-
e.stopPropagation();
|
| 33 |
-
clearFile();
|
| 34 |
-
});
|
| 35 |
-
|
| 36 |
-
zone.addEventListener('dragover', e => {
|
| 37 |
-
e.preventDefault(); e.stopPropagation();
|
| 38 |
-
zone.classList.add('drag-over');
|
| 39 |
-
});
|
| 40 |
-
|
| 41 |
-
zone.addEventListener('dragleave', e => {
|
| 42 |
-
e.preventDefault();
|
| 43 |
-
zone.classList.remove('drag-over');
|
| 44 |
-
});
|
| 45 |
-
|
| 46 |
-
zone.addEventListener('drop', e => {
|
| 47 |
-
e.preventDefault(); e.stopPropagation();
|
| 48 |
-
zone.classList.remove('drag-over');
|
| 49 |
-
const f = e.dataTransfer.files[0];
|
| 50 |
-
if (f?.type.startsWith('video/')) applyFile(f);
|
| 51 |
-
else showError('Please drop a valid video file (MP4, AVI, MOV, MKV, WebM).');
|
| 52 |
-
});
|
| 53 |
-
|
| 54 |
-
btn.addEventListener('click', analyzeVideo);
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
function applyFile(file) {
|
| 58 |
-
selectedFile = file;
|
| 59 |
-
document.getElementById('uploadPrompt').classList.add('hidden');
|
| 60 |
-
const fc = document.getElementById('fileChosen');
|
| 61 |
-
fc.classList.remove('hidden');
|
| 62 |
-
document.getElementById('chosenName').textContent = file.name;
|
| 63 |
-
document.getElementById('chosenSize').textContent = fmtBytes(file.size);
|
| 64 |
-
const btn = document.getElementById('analyzeBtn');
|
| 65 |
-
btn.disabled = false;
|
| 66 |
-
btn.classList.add('active');
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
function clearFile() {
|
| 70 |
-
selectedFile = null;
|
| 71 |
-
document.getElementById('fileInput').value = '';
|
| 72 |
-
document.getElementById('fileChosen').classList.add('hidden');
|
| 73 |
-
document.getElementById('uploadPrompt').classList.remove('hidden');
|
| 74 |
-
const btn = document.getElementById('analyzeBtn');
|
| 75 |
-
btn.disabled = true;
|
| 76 |
-
btn.classList.remove('active');
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
function resetAll() {
|
| 80 |
-
clearFile();
|
| 81 |
-
['loadingSection', 'resultSection', 'errorSection'].forEach(hide);
|
| 82 |
-
show('uploadSection');
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
// ── Analyze ───────────────────────────────────
|
| 86 |
-
async function analyzeVideo() {
|
| 87 |
-
if (!selectedFile) return;
|
| 88 |
-
|
| 89 |
-
hide('uploadSection');
|
| 90 |
-
show('loadingSection');
|
| 91 |
-
['resultSection', 'errorSection'].forEach(hide);
|
| 92 |
-
|
| 93 |
-
startAgentAnim();
|
| 94 |
-
|
| 95 |
-
const fd = new FormData();
|
| 96 |
-
fd.append('file', selectedFile);
|
| 97 |
-
|
| 98 |
-
try {
|
| 99 |
-
const res = await fetch(`${API_BASE}/analyze`, { method: 'POST', body: fd });
|
| 100 |
-
if (!res.ok) {
|
| 101 |
-
const e = await res.json().catch(() => ({}));
|
| 102 |
-
throw new Error(e.detail || `Server error ${res.status}`);
|
| 103 |
-
}
|
| 104 |
-
const data = await res.json();
|
| 105 |
-
console.log('[API Response] keys:', Object.keys(data), '| frame_timeline length:', data.frame_timeline?.length);
|
| 106 |
-
renderResult(data);
|
| 107 |
-
} catch (err) {
|
| 108 |
-
showError(err.message || 'Connection to analysis engine failed.');
|
| 109 |
-
} finally {
|
| 110 |
-
hide('loadingSection');
|
| 111 |
-
stopAgentAnim();
|
| 112 |
-
}
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
// ── Loading Animation ─────────────────────────
|
| 116 |
-
let _simTimer = null;
|
| 117 |
-
let _agentTimer = null;
|
| 118 |
-
|
| 119 |
-
function startAgentAnim() {
|
| 120 |
-
const statusEl = document.getElementById('loadingStatus');
|
| 121 |
-
const progBar = document.getElementById('progressBar');
|
| 122 |
-
|
| 123 |
-
const phases = [
|
| 124 |
-
{ p: 12, msg: 'Extracting keyframes...', ag: 0 },
|
| 125 |
-
{ p: 35, msg: 'Isolating facial regions...', ag: 1 },
|
| 126 |
-
{ p: 65, msg: 'Running ViT neural inference...', ag: 2 },
|
| 127 |
-
{ p: 85, msg: 'Cross-referencing metadata...', ag: 2 },
|
| 128 |
-
{ p: 95, msg: 'Compiling authenticity report...', ag: 3 },
|
| 129 |
-
];
|
| 130 |
-
|
| 131 |
-
// Reset agents
|
| 132 |
-
[0,1,2,3].forEach(i => {
|
| 133 |
-
const card = document.getElementById('ag' + i);
|
| 134 |
-
if (card) card.classList.remove('active');
|
| 135 |
-
});
|
| 136 |
-
|
| 137 |
-
statusEl.textContent = 'Initializing sequence...';
|
| 138 |
-
progBar.style.width = '0%';
|
| 139 |
-
|
| 140 |
-
let idx = 0;
|
| 141 |
-
_simTimer = setInterval(() => {
|
| 142 |
-
if (idx < phases.length) {
|
| 143 |
-
const ph = phases[idx];
|
| 144 |
-
statusEl.textContent = ph.msg;
|
| 145 |
-
progBar.style.width = ph.p + '%';
|
| 146 |
-
const card = document.getElementById('ag' + ph.ag);
|
| 147 |
-
if (card) card.classList.add('active');
|
| 148 |
-
idx++;
|
| 149 |
-
}
|
| 150 |
-
}, 1100);
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
function stopAgentAnim() {
|
| 154 |
-
if (_simTimer) { clearInterval(_simTimer); _simTimer = null; }
|
| 155 |
-
document.getElementById('progressBar').style.width = '100%';
|
| 156 |
-
[0,1,2,3].forEach(i => {
|
| 157 |
-
const card = document.getElementById('ag' + i);
|
| 158 |
-
if (card) card.classList.add('active');
|
| 159 |
-
});
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
// ── Render Result ─────────────────────────────
|
| 163 |
-
function renderResult(data) {
|
| 164 |
-
const isFake = data.result === 'FAKE';
|
| 165 |
-
const pct = data.confidence;
|
| 166 |
-
|
| 167 |
-
// Verdict card border glow
|
| 168 |
-
const vc = document.getElementById('verdictCard');
|
| 169 |
-
vc.style.borderColor = isFake ? 'rgba(255,51,85,0.5)' : 'rgba(0,255,136,0.5)';
|
| 170 |
-
vc.style.boxShadow = isFake
|
| 171 |
-
? '0 0 50px rgba(255,51,85,0.15), inset 0 0 30px rgba(255,51,85,0.05)'
|
| 172 |
-
: '0 0 50px rgba(0,255,136,0.15), inset 0 0 30px rgba(0,255,136,0.05)';
|
| 173 |
-
|
| 174 |
-
// Badge
|
| 175 |
-
const badge = document.getElementById('verdictBadge');
|
| 176 |
-
badge.className = 'verdict-badge ' + (isFake ? 'fake' : 'real');
|
| 177 |
-
badge.style.background = isFake ? 'rgba(255,51,85,0.08)' : 'rgba(0,255,136,0.08)';
|
| 178 |
-
|
| 179 |
-
// Emoji
|
| 180 |
-
document.getElementById('verdictEmoji').textContent = isFake ? '⚠' : '✓';
|
| 181 |
-
|
| 182 |
-
// Label
|
| 183 |
-
const lbl = document.getElementById('verdictLabel');
|
| 184 |
-
lbl.textContent = isFake ? 'DEEPFAKE' : 'AUTHENTIC';
|
| 185 |
-
lbl.style.color = isFake ? '#ff3355' : '#00ff88';
|
| 186 |
-
lbl.style.textShadow = isFake
|
| 187 |
-
? '0 0 20px rgba(255,51,85,0.7)'
|
| 188 |
-
: '0 0 20px rgba(0,255,136,0.7)';
|
| 189 |
-
if (isFake) lbl.classList.add('glitch-text');
|
| 190 |
-
else lbl.classList.remove('glitch-text');
|
| 191 |
-
|
| 192 |
-
// Confidence value
|
| 193 |
-
const cv = document.getElementById('confValue');
|
| 194 |
-
cv.textContent = pct + '%';
|
| 195 |
-
cv.style.color = isFake ? '#ff3355' : '#00ff88';
|
| 196 |
-
|
| 197 |
-
// Confidence bar
|
| 198 |
-
const bar = document.getElementById('confBar');
|
| 199 |
-
bar.className = 'conf-fill ' + (isFake ? 'fake' : 'real');
|
| 200 |
-
setTimeout(() => { bar.style.width = pct + '%'; }, 80);
|
| 201 |
-
|
| 202 |
-
// Risk needle
|
| 203 |
-
const needle = document.getElementById('riskNeedle');
|
| 204 |
-
const riskLbl = document.getElementById('riskLabel');
|
| 205 |
-
if (pct < 35) {
|
| 206 |
-
needle.textContent = 'LOW RISK';
|
| 207 |
-
needle.style.color = '#00ff88';
|
| 208 |
-
needle.style.borderColor = 'rgba(0,255,136,0.35)';
|
| 209 |
-
needle.style.background = 'rgba(0,255,136,0.08)';
|
| 210 |
-
if (riskLbl) riskLbl.textContent = 'Minimal manipulation indicators detected';
|
| 211 |
-
} else if (pct < 65) {
|
| 212 |
-
needle.textContent = 'MEDIUM RISK';
|
| 213 |
-
needle.style.color = '#ffaa00';
|
| 214 |
-
needle.style.borderColor = 'rgba(255,170,0,0.35)';
|
| 215 |
-
needle.style.background = 'rgba(255,170,0,0.08)';
|
| 216 |
-
if (riskLbl) riskLbl.textContent = 'Moderate anomalies detected — review advised';
|
| 217 |
-
} else {
|
| 218 |
-
needle.textContent = 'CRITICAL RISK';
|
| 219 |
-
needle.style.color = '#ff3355';
|
| 220 |
-
needle.style.borderColor = 'rgba(255,51,85,0.35)';
|
| 221 |
-
needle.style.background = 'rgba(255,51,85,0.08)';
|
| 222 |
-
if (riskLbl) riskLbl.textContent = 'High-confidence manipulation signatures found';
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
// Insights
|
| 226 |
-
const dl = document.getElementById('detailsList');
|
| 227 |
-
dl.innerHTML = '';
|
| 228 |
-
const details = data.details || ['Analysis completed successfully.'];
|
| 229 |
-
const dotColor = isFake ? '#ff3355' : '#00ff88';
|
| 230 |
-
const dotGlow = isFake ? 'rgba(255,51,85,0.6)' : 'rgba(0,255,136,0.6)';
|
| 231 |
-
details.forEach((txt, i) => {
|
| 232 |
-
const div = document.createElement('div');
|
| 233 |
-
div.className = 'insight-item';
|
| 234 |
-
div.style.animationDelay = (i * 0.08) + 's';
|
| 235 |
-
div.style.borderLeftColor = dotColor;
|
| 236 |
-
div.style.borderLeft = `2px solid ${dotColor}`;
|
| 237 |
-
div.innerHTML = `<span class="insight-dot" style="background:${dotColor};box-shadow:0 0 8px ${dotGlow};"></span><span>${esc(txt)}</span>`;
|
| 238 |
-
dl.appendChild(div);
|
| 239 |
-
});
|
| 240 |
-
|
| 241 |
-
// Metadata
|
| 242 |
-
const meta = data.metadata || {};
|
| 243 |
-
const mg = document.getElementById('metaGrid');
|
| 244 |
-
mg.innerHTML = '';
|
| 245 |
-
const metaItems = [
|
| 246 |
-
['Frames Analyzed', meta.frames_analyzed ?? '—'],
|
| 247 |
-
['Duration', meta.video_duration_sec ? meta.video_duration_sec + 's' : '—'],
|
| 248 |
-
['FPS', meta.video_fps ?? '—'],
|
| 249 |
-
['Resolution', meta.resolution ?? '—'],
|
| 250 |
-
['Processing Time', data.processing_time_sec ? data.processing_time_sec + 's' : '—'],
|
| 251 |
-
];
|
| 252 |
-
metaItems.forEach(([k, v]) => {
|
| 253 |
-
const row = document.createElement('div');
|
| 254 |
-
row.className = 'meta-row';
|
| 255 |
-
row.innerHTML = `<span style="font-size:11px;color:var(--muted);letter-spacing:0.08em;">${k}</span><span style="font-size:13px;font-weight:600;color:#fff;font-family:'JetBrains Mono',monospace;">${v}</span>`;
|
| 256 |
-
mg.appendChild(row);
|
| 257 |
-
});
|
| 258 |
-
|
| 259 |
-
// Frame timeline
|
| 260 |
-
renderTimeline(data, isFake);
|
| 261 |
-
|
| 262 |
-
// Audio result
|
| 263 |
-
renderAudio(data.audio || null);
|
| 264 |
-
|
| 265 |
-
show('resultSection');
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
function renderTimeline(data, isFake) {
|
| 269 |
-
const chart = document.getElementById('timelineChart');
|
| 270 |
-
if (!chart) return;
|
| 271 |
-
chart.innerHTML = '';
|
| 272 |
-
|
| 273 |
-
// Backend sends frame_timeline: [{frame, fake_pct}, ...]
|
| 274 |
-
const frames = data.frame_timeline || data.frame_scores || [];
|
| 275 |
-
console.log('[Timeline] frame_timeline:', data.frame_timeline?.length, 'frames:', frames.length, 'sample:', frames[0]);
|
| 276 |
-
if (!frames.length) {
|
| 277 |
-
chart.innerHTML = '<span style="font-size:11px;color:var(--muted);font-family:\'JetBrains Mono\',monospace;margin:auto;">No per-frame data available</span>';
|
| 278 |
-
return;
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
const maxH = 60; // px
|
| 282 |
-
const barColor = isFake ? '#ff3355' : '#00ff88';
|
| 283 |
-
const barGlow = isFake ? 'rgba(255,51,85,0.5)' : 'rgba(0,255,136,0.5)';
|
| 284 |
-
|
| 285 |
-
frames.forEach((point, i) => {
|
| 286 |
-
// Support both {fake_pct: 72.1} and {fake_probability: 0.721}
|
| 287 |
-
const pct = point.fake_pct != null ? point.fake_pct : (point.fake_probability * 100);
|
| 288 |
-
const score = pct / 100;
|
| 289 |
-
const h = Math.max(4, Math.round(score * maxH));
|
| 290 |
-
const hot = pct >= 60;
|
| 291 |
-
|
| 292 |
-
const wrap = document.createElement('div');
|
| 293 |
-
wrap.className = 'bar-wrap';
|
| 294 |
-
wrap.style.height = maxH + 'px';
|
| 295 |
-
|
| 296 |
-
const outer = document.createElement('div');
|
| 297 |
-
outer.className = 'bar-outer';
|
| 298 |
-
outer.style.height = maxH + 'px';
|
| 299 |
-
|
| 300 |
-
const inner = document.createElement('div');
|
| 301 |
-
inner.className = 'bar-inner';
|
| 302 |
-
inner.style.height = '0px';
|
| 303 |
-
inner.style.background = hot
|
| 304 |
-
? `linear-gradient(to top, ${barColor}, rgba(255,255,255,0.3))`
|
| 305 |
-
: 'rgba(255,255,255,0.12)';
|
| 306 |
-
if (hot) inner.style.boxShadow = `0 0 8px ${barGlow}`;
|
| 307 |
-
|
| 308 |
-
outer.appendChild(inner);
|
| 309 |
-
|
| 310 |
-
const tip = document.createElement('div');
|
| 311 |
-
tip.className = 'bar-tooltip';
|
| 312 |
-
tip.textContent = `Frame ${point.frame != null ? point.frame : i}: ${pct.toFixed(1)}%`;
|
| 313 |
-
|
| 314 |
-
wrap.appendChild(outer);
|
| 315 |
-
wrap.appendChild(tip);
|
| 316 |
-
chart.appendChild(wrap);
|
| 317 |
-
|
| 318 |
-
setTimeout(() => { inner.style.height = h + 'px'; }, 50 + i * 20);
|
| 319 |
-
});
|
| 320 |
-
|
| 321 |
-
// Threshold line at 50%
|
| 322 |
-
const line = document.createElement('div');
|
| 323 |
-
line.style.cssText = `position:absolute;left:0;right:0;bottom:${maxH*0.5}px;height:1px;background:rgba(255,170,0,0.4);pointer-events:none;`;
|
| 324 |
-
const lineLbl = document.createElement('span');
|
| 325 |
-
lineLbl.style.cssText = 'position:absolute;right:4px;top:-14px;font-size:9px;color:#ffaa00;font-family:\'JetBrains Mono\',monospace;letter-spacing:0.1em;';
|
| 326 |
-
lineLbl.textContent = '50%';
|
| 327 |
-
line.appendChild(lineLbl);
|
| 328 |
-
chart.appendChild(line);
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
// ── Helpers ───────────────────────────────────
|
| 332 |
-
function show(id) {
|
| 333 |
-
const el = document.getElementById(id);
|
| 334 |
-
if (!el) return;
|
| 335 |
-
el.classList.remove('hidden');
|
| 336 |
-
if (el.style.display === 'none') el.style.display = '';
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
function hide(id) {
|
| 340 |
-
const el = document.getElementById(id);
|
| 341 |
-
if (el) el.classList.add('hidden');
|
| 342 |
-
}
|
| 343 |
-
|
| 344 |
-
function fmtBytes(b) {
|
| 345 |
-
if (b < 1024) return b + ' B';
|
| 346 |
-
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
|
| 347 |
-
return (b / 1048576).toFixed(1) + ' MB';
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
function esc(s) {
|
| 351 |
-
return String(s)
|
| 352 |
-
.replace(/&/g, '&')
|
| 353 |
-
.replace(/</g, '<')
|
| 354 |
-
.replace(/>/g, '>');
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
function showError(msg) {
|
| 358 |
-
hide('uploadSection');
|
| 359 |
-
hide('loadingSection');
|
| 360 |
-
document.getElementById('errorMsg').textContent = msg;
|
| 361 |
-
show('errorSection');
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
// ── Audio Result ──────────────────────────────
|
| 365 |
-
function renderAudio(audio) {
|
| 366 |
-
const section = document.getElementById('audioSection');
|
| 367 |
-
if (!section) return;
|
| 368 |
-
|
| 369 |
-
if (!audio || !audio.available) {
|
| 370 |
-
section.classList.add('hidden');
|
| 371 |
-
return;
|
| 372 |
-
}
|
| 373 |
-
|
| 374 |
-
section.classList.remove('hidden');
|
| 375 |
-
|
| 376 |
-
const isAI = audio.result === 'AI_VOICE';
|
| 377 |
-
const pct = audio.confidence;
|
| 378 |
-
const color = isAI ? '#ff3355' : '#00ff88';
|
| 379 |
-
const colorDim = isAI ? 'rgba(255,51,85,0.15)' : 'rgba(0,255,136,0.15)';
|
| 380 |
-
|
| 381 |
-
// Header badge
|
| 382 |
-
const badge = document.getElementById('audioBadge');
|
| 383 |
-
if (badge) {
|
| 384 |
-
badge.textContent = isAI ? '🤖 AI VOICE DETECTED' : '🎙️ HUMAN VOICE';
|
| 385 |
-
badge.style.color = color;
|
| 386 |
-
badge.style.borderColor = color + '55';
|
| 387 |
-
badge.style.background = colorDim;
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
// Scores
|
| 391 |
-
const modelScore = document.getElementById('audioModelScore');
|
| 392 |
-
const heurScore = document.getElementById('audioHeurScore');
|
| 393 |
-
if (modelScore) modelScore.textContent = audio.model_score + '%';
|
| 394 |
-
if (heurScore) heurScore.textContent = audio.heuristic_score + '%';
|
| 395 |
-
|
| 396 |
-
// Bar
|
| 397 |
-
const bar = document.getElementById('audioBar');
|
| 398 |
-
if (bar) {
|
| 399 |
-
bar.style.background = isAI
|
| 400 |
-
? 'linear-gradient(90deg,#880022,#ff3355)'
|
| 401 |
-
: 'linear-gradient(90deg,#00aaff,#00ff88)';
|
| 402 |
-
bar.style.boxShadow = `0 0 12px ${color}66`;
|
| 403 |
-
setTimeout(() => { bar.style.width = pct + '%'; }, 80);
|
| 404 |
-
}
|
| 405 |
-
|
| 406 |
-
// Details
|
| 407 |
-
const dl = document.getElementById('audioDetailsList');
|
| 408 |
-
if (dl) {
|
| 409 |
-
dl.innerHTML = '';
|
| 410 |
-
(audio.details || []).forEach((txt, i) => {
|
| 411 |
-
const div = document.createElement('div');
|
| 412 |
-
div.className = 'insight-item';
|
| 413 |
-
div.style.animationDelay = (i * 0.07) + 's';
|
| 414 |
-
div.style.borderLeft = `2px solid ${color}`;
|
| 415 |
-
div.innerHTML = `<span class="insight-dot" style="background:${color};box-shadow:0 0 6px ${color}88;"></span><span>${esc(txt)}</span>`;
|
| 416 |
-
dl.appendChild(div);
|
| 417 |
-
});
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
// Features
|
| 421 |
-
const feat = audio.features || {};
|
| 422 |
-
const featGrid = document.getElementById('audioFeatGrid');
|
| 423 |
-
if (featGrid) {
|
| 424 |
-
featGrid.innerHTML = '';
|
| 425 |
-
const items = [
|
| 426 |
-
['Pitch Std Dev', feat.pitch_std_hz != null ? feat.pitch_std_hz + ' Hz' : '—'],
|
| 427 |
-
['MFCC Δ Variance', feat.mfcc_delta_var != null ? feat.mfcc_delta_var : '—'],
|
| 428 |
-
['Spectral Flatness', feat.spectral_flatness != null ? feat.spectral_flatness : '—'],
|
| 429 |
-
['Silence Ratio', feat.silence_ratio != null ? (feat.silence_ratio * 100).toFixed(1) + '%' : '—'],
|
| 430 |
-
];
|
| 431 |
-
items.forEach(([k, v]) => {
|
| 432 |
-
const row = document.createElement('div');
|
| 433 |
-
row.className = 'meta-row';
|
| 434 |
-
row.innerHTML = `<span style="font-size:11px;color:var(--muted);">${k}</span><span style="font-size:12px;font-weight:600;color:#fff;font-family:'JetBrains Mono',monospace;">${v}</span>`;
|
| 435 |
-
featGrid.appendChild(row);
|
| 436 |
-
});
|
| 437 |
-
}
|
| 438 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/App.css
DELETED
|
@@ -1,184 +0,0 @@
|
|
| 1 |
-
.counter {
|
| 2 |
-
font-size: 16px;
|
| 3 |
-
padding: 5px 10px;
|
| 4 |
-
border-radius: 5px;
|
| 5 |
-
color: var(--accent);
|
| 6 |
-
background: var(--accent-bg);
|
| 7 |
-
border: 2px solid transparent;
|
| 8 |
-
transition: border-color 0.3s;
|
| 9 |
-
margin-bottom: 24px;
|
| 10 |
-
|
| 11 |
-
&:hover {
|
| 12 |
-
border-color: var(--accent-border);
|
| 13 |
-
}
|
| 14 |
-
&:focus-visible {
|
| 15 |
-
outline: 2px solid var(--accent);
|
| 16 |
-
outline-offset: 2px;
|
| 17 |
-
}
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
.hero {
|
| 21 |
-
position: relative;
|
| 22 |
-
|
| 23 |
-
.base,
|
| 24 |
-
.framework,
|
| 25 |
-
.vite {
|
| 26 |
-
inset-inline: 0;
|
| 27 |
-
margin: 0 auto;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
.base {
|
| 31 |
-
width: 170px;
|
| 32 |
-
position: relative;
|
| 33 |
-
z-index: 0;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
.framework,
|
| 37 |
-
.vite {
|
| 38 |
-
position: absolute;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
.framework {
|
| 42 |
-
z-index: 1;
|
| 43 |
-
top: 34px;
|
| 44 |
-
height: 28px;
|
| 45 |
-
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
| 46 |
-
scale(1.4);
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
.vite {
|
| 50 |
-
z-index: 0;
|
| 51 |
-
top: 107px;
|
| 52 |
-
height: 26px;
|
| 53 |
-
width: auto;
|
| 54 |
-
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
| 55 |
-
scale(0.8);
|
| 56 |
-
}
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
#center {
|
| 60 |
-
display: flex;
|
| 61 |
-
flex-direction: column;
|
| 62 |
-
gap: 25px;
|
| 63 |
-
place-content: center;
|
| 64 |
-
place-items: center;
|
| 65 |
-
flex-grow: 1;
|
| 66 |
-
|
| 67 |
-
@media (max-width: 1024px) {
|
| 68 |
-
padding: 32px 20px 24px;
|
| 69 |
-
gap: 18px;
|
| 70 |
-
}
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
#next-steps {
|
| 74 |
-
display: flex;
|
| 75 |
-
border-top: 1px solid var(--border);
|
| 76 |
-
text-align: left;
|
| 77 |
-
|
| 78 |
-
& > div {
|
| 79 |
-
flex: 1 1 0;
|
| 80 |
-
padding: 32px;
|
| 81 |
-
@media (max-width: 1024px) {
|
| 82 |
-
padding: 24px 20px;
|
| 83 |
-
}
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
.icon {
|
| 87 |
-
margin-bottom: 16px;
|
| 88 |
-
width: 22px;
|
| 89 |
-
height: 22px;
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
@media (max-width: 1024px) {
|
| 93 |
-
flex-direction: column;
|
| 94 |
-
text-align: center;
|
| 95 |
-
}
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
#docs {
|
| 99 |
-
border-right: 1px solid var(--border);
|
| 100 |
-
|
| 101 |
-
@media (max-width: 1024px) {
|
| 102 |
-
border-right: none;
|
| 103 |
-
border-bottom: 1px solid var(--border);
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
#next-steps ul {
|
| 108 |
-
list-style: none;
|
| 109 |
-
padding: 0;
|
| 110 |
-
display: flex;
|
| 111 |
-
gap: 8px;
|
| 112 |
-
margin: 32px 0 0;
|
| 113 |
-
|
| 114 |
-
.logo {
|
| 115 |
-
height: 18px;
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
a {
|
| 119 |
-
color: var(--text-h);
|
| 120 |
-
font-size: 16px;
|
| 121 |
-
border-radius: 6px;
|
| 122 |
-
background: var(--social-bg);
|
| 123 |
-
display: flex;
|
| 124 |
-
padding: 6px 12px;
|
| 125 |
-
align-items: center;
|
| 126 |
-
gap: 8px;
|
| 127 |
-
text-decoration: none;
|
| 128 |
-
transition: box-shadow 0.3s;
|
| 129 |
-
|
| 130 |
-
&:hover {
|
| 131 |
-
box-shadow: var(--shadow);
|
| 132 |
-
}
|
| 133 |
-
.button-icon {
|
| 134 |
-
height: 18px;
|
| 135 |
-
width: 18px;
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
@media (max-width: 1024px) {
|
| 140 |
-
margin-top: 20px;
|
| 141 |
-
flex-wrap: wrap;
|
| 142 |
-
justify-content: center;
|
| 143 |
-
|
| 144 |
-
li {
|
| 145 |
-
flex: 1 1 calc(50% - 8px);
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
a {
|
| 149 |
-
width: 100%;
|
| 150 |
-
justify-content: center;
|
| 151 |
-
box-sizing: border-box;
|
| 152 |
-
}
|
| 153 |
-
}
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
#spacer {
|
| 157 |
-
height: 88px;
|
| 158 |
-
border-top: 1px solid var(--border);
|
| 159 |
-
@media (max-width: 1024px) {
|
| 160 |
-
height: 48px;
|
| 161 |
-
}
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
.ticks {
|
| 165 |
-
position: relative;
|
| 166 |
-
width: 100%;
|
| 167 |
-
|
| 168 |
-
&::before,
|
| 169 |
-
&::after {
|
| 170 |
-
content: '';
|
| 171 |
-
position: absolute;
|
| 172 |
-
top: -4.5px;
|
| 173 |
-
border: 5px solid transparent;
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
&::before {
|
| 177 |
-
left: 0;
|
| 178 |
-
border-left-color: var(--border);
|
| 179 |
-
}
|
| 180 |
-
&::after {
|
| 181 |
-
right: 0;
|
| 182 |
-
border-right-color: var(--border);
|
| 183 |
-
}
|
| 184 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/App.jsx
DELETED
|
@@ -1,225 +0,0 @@
|
|
| 1 |
-
import React, { useState, useRef } from 'react';
|
| 2 |
-
import Loader from './components/Loader';
|
| 3 |
-
const API_BASE = 'http://localhost:8000';
|
| 4 |
-
|
| 5 |
-
function App() {
|
| 6 |
-
const [file, setFile] = useState(null);
|
| 7 |
-
const [status, setStatus] = useState('idle'); // 'idle', 'analyzing', 'result', 'error'
|
| 8 |
-
const [resultData, setResultData] = useState(null);
|
| 9 |
-
const [errorMsg, setErrorMsg] = useState('');
|
| 10 |
-
|
| 11 |
-
const fileInputRef = useRef(null);
|
| 12 |
-
|
| 13 |
-
const handleDrop = (e) => {
|
| 14 |
-
e.preventDefault();
|
| 15 |
-
const droppedFile = e.dataTransfer.files[0];
|
| 16 |
-
if (droppedFile && droppedFile.type.startsWith('video/')) {
|
| 17 |
-
setFile(droppedFile);
|
| 18 |
-
} else {
|
| 19 |
-
setErrorMsg('Please drop a valid video file (MP4, AVI, MOV, MKV, WebM).');
|
| 20 |
-
setStatus('error');
|
| 21 |
-
}
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
const handleFileChange = (e) => {
|
| 25 |
-
if (e.target.files && e.target.files[0]) {
|
| 26 |
-
setFile(e.target.files[0]);
|
| 27 |
-
}
|
| 28 |
-
};
|
| 29 |
-
|
| 30 |
-
const handleAnalyze = async () => {
|
| 31 |
-
if (!file) return;
|
| 32 |
-
setStatus('analyzing');
|
| 33 |
-
|
| 34 |
-
const formData = new FormData();
|
| 35 |
-
formData.append('file', file);
|
| 36 |
-
|
| 37 |
-
try {
|
| 38 |
-
const res = await fetch(`${API_BASE}/analyze`, { method: 'POST', body: formData });
|
| 39 |
-
if (!res.ok) {
|
| 40 |
-
const e = await res.json().catch(() => ({}));
|
| 41 |
-
throw new Error(e.detail || `Server error ${res.status}`);
|
| 42 |
-
}
|
| 43 |
-
const data = await res.json();
|
| 44 |
-
setResultData(data);
|
| 45 |
-
setStatus('result');
|
| 46 |
-
} catch (err) {
|
| 47 |
-
setErrorMsg(err.message || 'Connection to analysis engine failed.');
|
| 48 |
-
setStatus('error');
|
| 49 |
-
}
|
| 50 |
-
};
|
| 51 |
-
|
| 52 |
-
const resetAll = () => {
|
| 53 |
-
setFile(null);
|
| 54 |
-
setResultData(null);
|
| 55 |
-
setErrorMsg('');
|
| 56 |
-
setStatus('idle');
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
-
const formatBytes = (bytes) => {
|
| 60 |
-
if (bytes < 1024) return bytes + ' B';
|
| 61 |
-
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
| 62 |
-
return (bytes / 1048576).toFixed(1) + ' MB';
|
| 63 |
-
};
|
| 64 |
-
|
| 65 |
-
return (
|
| 66 |
-
<>
|
| 67 |
-
<div className="scanner"></div>
|
| 68 |
-
|
| 69 |
-
<div className="max-w-5xl mx-auto px-6 py-12 min-h-screen flex flex-col relative z-10">
|
| 70 |
-
|
| 71 |
-
{/* Header */}
|
| 72 |
-
<header className="text-center mb-12 fade-up" style={{ animationDelay: '0.1s' }}>
|
| 73 |
-
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-[rgba(0,255,156,0.3)] bg-[rgba(0,255,156,0.05)] mb-6 shadow-[0_0_15px_rgba(0,255,156,0.1)]">
|
| 74 |
-
<div className="w-2 h-2 rounded-full bg-[#00ff9c] shadow-[0_0_8px_#00ff9c] animate-pulse"></div>
|
| 75 |
-
<span className="text-[10px] font-semibold tracking-[0.2em] text-[#00ff9c] uppercase">AI & Machine Learning</span>
|
| 76 |
-
</div>
|
| 77 |
-
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-white mb-2">Deepfake <span className="text-glow">Authenticator</span></h1>
|
| 78 |
-
<p className="text-[#849ca3] text-sm md:text-base font-light tracking-wide">Advanced video forensics and digital truth verification.</p>
|
| 79 |
-
</header>
|
| 80 |
-
|
| 81 |
-
{/* Main Content */}
|
| 82 |
-
<main className="flex-1 flex flex-col items-center gap-8 w-full">
|
| 83 |
-
|
| 84 |
-
{status === 'idle' && (
|
| 85 |
-
<section className="w-full max-w-3xl glass p-8 fade-up" style={{ animationDelay: '0.2s' }}>
|
| 86 |
-
<div
|
| 87 |
-
className="glass-inner rounded-xl border-2 border-dashed border-[rgba(0,255,156,0.15)] hover:border-[#00ff9c] transition-all cursor-pointer min-h-[200px] flex items-center justify-center relative overflow-hidden"
|
| 88 |
-
onDragOver={(e) => e.preventDefault()}
|
| 89 |
-
onDrop={handleDrop}
|
| 90 |
-
onClick={() => fileInputRef.current?.click()}
|
| 91 |
-
>
|
| 92 |
-
{!file ? (
|
| 93 |
-
<div className="text-center p-8">
|
| 94 |
-
<div className="w-16 h-16 mx-auto mb-4 rounded-full border border-[rgba(0,255,156,0.2)] flex items-center justify-center bg-[rgba(0,255,156,0.02)] shadow-[0_0_20px_rgba(0,255,156,0.05)]">
|
| 95 |
-
<svg className="w-8 h-8 text-[#00ff9c] opacity-70" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
| 96 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
| 97 |
-
</svg>
|
| 98 |
-
</div>
|
| 99 |
-
<h3 className="text-lg font-medium text-white mb-1">Upload Video for Analysis</h3>
|
| 100 |
-
<p className="text-sm text-[#849ca3]">Drag & drop or click to browse</p>
|
| 101 |
-
<p className="text-[11px] text-[#849ca3] mt-4 opacity-60">MP4, AVI, MOV, MKV, WebM — Max 100MB</p>
|
| 102 |
-
</div>
|
| 103 |
-
) : (
|
| 104 |
-
<div className="w-full p-8 flex items-center gap-6" onClick={(e) => e.stopPropagation()}>
|
| 105 |
-
<div className="w-14 h-14 rounded-lg bg-[rgba(0,255,156,0.1)] border border-[rgba(0,255,156,0.3)] flex items-center justify-center flex-shrink-0 shadow-[0_0_15px_rgba(0,255,156,0.15)]">
|
| 106 |
-
<svg className="w-7 h-7 text-[#00ff9c]" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
| 107 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
| 108 |
-
</svg>
|
| 109 |
-
</div>
|
| 110 |
-
<div className="flex-1 min-w-0">
|
| 111 |
-
<p className="text-sm font-medium text-white truncate">{file.name}</p>
|
| 112 |
-
<p className="text-xs text-[#849ca3] mt-1">{formatBytes(file.size)}</p>
|
| 113 |
-
</div>
|
| 114 |
-
<button onClick={() => setFile(null)} className="p-2 text-[#849ca3] hover:text-[#ff4444] transition-colors rounded-lg hover:bg-[rgba(255,68,68,0.1)]">
|
| 115 |
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
| 116 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
| 117 |
-
</svg>
|
| 118 |
-
</button>
|
| 119 |
-
</div>
|
| 120 |
-
)}
|
| 121 |
-
</div>
|
| 122 |
-
<input type="file" ref={fileInputRef} onChange={handleFileChange} accept="video/*" className="hidden" />
|
| 123 |
-
|
| 124 |
-
<div className="mt-8 text-center">
|
| 125 |
-
<button
|
| 126 |
-
onClick={handleAnalyze}
|
| 127 |
-
disabled={!file}
|
| 128 |
-
className={`border border-[rgba(0,255,156,0.15)] rounded-lg px-10 py-3 font-semibold text-sm tracking-widest uppercase transition-all ${
|
| 129 |
-
file
|
| 130 |
-
? 'bg-[#00ff9c] text-[#040906] shadow-[0_0_20px_rgba(0,255,156,0.4)] hover:bg-[#00cc7d] hover:-translate-y-1'
|
| 131 |
-
: 'text-[#849ca3] opacity-50 cursor-not-allowed'
|
| 132 |
-
}`}
|
| 133 |
-
>
|
| 134 |
-
Analyze Video
|
| 135 |
-
</button>
|
| 136 |
-
</div>
|
| 137 |
-
</section>
|
| 138 |
-
)}
|
| 139 |
-
|
| 140 |
-
{status === 'analyzing' && (
|
| 141 |
-
<section className="fade-up w-full flex justify-center py-20">
|
| 142 |
-
<Loader />
|
| 143 |
-
</section>
|
| 144 |
-
)}
|
| 145 |
-
|
| 146 |
-
{status === 'result' && resultData && (
|
| 147 |
-
<section className="w-full flex flex-col gap-10 fade-up">
|
| 148 |
-
|
| 149 |
-
<div className="flex flex-col md:flex-row items-stretch justify-center gap-6 w-full max-w-3xl mx-auto">
|
| 150 |
-
<div className={`flex-1 glass p-8 text-center border-t-4 flex flex-col justify-center ${resultData.result === 'FAKE' ? 'border-t-[#ff4444] shadow-[0_-5px_20px_rgba(255,68,68,0.15)]' : 'border-t-[#00ff9c] shadow-[0_-5px_20px_rgba(0,255,156,0.15)]'}`}>
|
| 151 |
-
<p className="text-[#849ca3] text-xs font-semibold tracking-[0.2em] uppercase mb-2">Verdict</p>
|
| 152 |
-
<h2 className={`text-4xl font-bold tracking-widest ${resultData.result === 'FAKE' ? 'text-[#ff4444] drop-shadow-[0_0_10px_rgba(255,68,68,0.5)]' : 'text-[#00ff9c] drop-shadow-[0_0_10px_rgba(0,255,156,0.5)]'}`}>
|
| 153 |
-
{resultData.result}
|
| 154 |
-
</h2>
|
| 155 |
-
<div className="mt-4 inline-block bg-[rgba(255,255,255,0.05)] px-4 py-1.5 rounded-full border border-[rgba(255,255,255,0.1)]">
|
| 156 |
-
<span className="text-[#849ca3] text-xs uppercase tracking-wider mr-2">Confidence:</span>
|
| 157 |
-
<span className="text-white font-bold">{resultData.confidence}%</span>
|
| 158 |
-
</div>
|
| 159 |
-
</div>
|
| 160 |
-
|
| 161 |
-
<div className="flex-1 glass p-8 text-center border-t-4 border-t-[#00e5ff] shadow-[0_-5px_20px_rgba(0,229,255,0.15)] flex flex-col justify-center">
|
| 162 |
-
<p className="text-[#849ca3] text-xs font-semibold tracking-[0.2em] uppercase mb-2">Metrics</p>
|
| 163 |
-
<div className="flex flex-col gap-3 mt-2">
|
| 164 |
-
<div className="flex justify-between items-center bg-[rgba(255,255,255,0.03)] p-3 rounded border border-[rgba(255,255,255,0.05)]">
|
| 165 |
-
<span className="text-xs text-[#849ca3] uppercase tracking-wider">Frames</span>
|
| 166 |
-
<span className="text-white font-mono font-medium">{resultData.metadata?.frames_analyzed || 0}</span>
|
| 167 |
-
</div>
|
| 168 |
-
<div className="flex justify-between items-center bg-[rgba(255,255,255,0.03)] p-3 rounded border border-[rgba(255,255,255,0.05)]">
|
| 169 |
-
<span className="text-xs text-[#849ca3] uppercase tracking-wider">Time</span>
|
| 170 |
-
<span className="text-[#00e5ff] font-mono font-medium">{resultData.processing_time_sec || 0}s</span>
|
| 171 |
-
</div>
|
| 172 |
-
</div>
|
| 173 |
-
</div>
|
| 174 |
-
</div>
|
| 175 |
-
|
| 176 |
-
<div className="w-full max-w-3xl mx-auto glass p-8 mt-4">
|
| 177 |
-
<h3 className="text-xs font-semibold tracking-[0.15em] text-[#00e5ff] uppercase flex items-center gap-2 mb-4">
|
| 178 |
-
<div className="w-1 h-3 rounded-sm bg-[#00e5ff] shadow-[0_0_8px_#00e5ff]"></div>
|
| 179 |
-
Analysis Insights
|
| 180 |
-
</h3>
|
| 181 |
-
<div className="flex flex-col gap-3">
|
| 182 |
-
{(resultData.details || ['Analysis completed successfully.']).map((txt, i) => (
|
| 183 |
-
<div key={i} className="flex items-start gap-3 p-3 rounded-lg bg-[rgba(255,255,255,0.02)] border border-[rgba(255,255,255,0.05)]">
|
| 184 |
-
<span className={`flex-shrink-0 w-2 h-2 rounded-full mt-1.5 ${resultData.result === 'FAKE' ? 'bg-[#ff4444] shadow-[0_0_8px_rgba(255,68,68,0.6)]' : 'bg-[#00ff9c] shadow-[0_0_8px_rgba(0,255,156,0.6)]'}`}></span>
|
| 185 |
-
<span className="text-sm text-[#a0aec0]">{txt}</span>
|
| 186 |
-
</div>
|
| 187 |
-
))}
|
| 188 |
-
</div>
|
| 189 |
-
</div>
|
| 190 |
-
|
| 191 |
-
<div className="text-center mt-4">
|
| 192 |
-
<button onClick={resetAll} className="px-6 py-3 text-xs font-semibold tracking-widest uppercase text-[#849ca3] border border-[#849ca3]/30 rounded-lg hover:text-[#00ff9c] hover:border-[#00ff9c]/50 hover:bg-[#00ff9c]/5 transition-all">
|
| 193 |
-
<span className="mr-2">↻</span> Analyze Another Video
|
| 194 |
-
</button>
|
| 195 |
-
</div>
|
| 196 |
-
</section>
|
| 197 |
-
)}
|
| 198 |
-
|
| 199 |
-
{status === 'error' && (
|
| 200 |
-
<section className="glass p-6 border-[#ff4444]/30 bg-[#ff4444]/5 fade-up w-full max-w-3xl">
|
| 201 |
-
<div className="flex items-center gap-3 mb-2">
|
| 202 |
-
<svg className="w-6 h-6 text-[#ff4444]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
| 203 |
-
<span className="text-sm font-bold tracking-widest text-[#ff4444] uppercase">Analysis Failed</span>
|
| 204 |
-
</div>
|
| 205 |
-
<p className="text-sm text-[#849ca3] ml-9">{errorMsg}</p>
|
| 206 |
-
<div className="ml-9 mt-4">
|
| 207 |
-
<button onClick={resetAll} className="text-xs text-white/50 hover:text-white uppercase tracking-wider transition-colors">
|
| 208 |
-
↻ Try Again
|
| 209 |
-
</button>
|
| 210 |
-
</div>
|
| 211 |
-
</section>
|
| 212 |
-
)}
|
| 213 |
-
|
| 214 |
-
</main>
|
| 215 |
-
|
| 216 |
-
<footer className="mt-16 text-center text-[10px] text-[#849ca3]/50 tracking-[0.2em] uppercase">
|
| 217 |
-
Deepfake Authenticator • Secure Cybernetic Analysis • Local Inference
|
| 218 |
-
</footer>
|
| 219 |
-
|
| 220 |
-
</div>
|
| 221 |
-
</>
|
| 222 |
-
);
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
export default App;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/App.tsx
DELETED
|
@@ -1,183 +0,0 @@
|
|
| 1 |
-
import { useEffect, Suspense, lazy } from 'react'
|
| 2 |
-
import { motion, AnimatePresence } from 'framer-motion'
|
| 3 |
-
import { useStore } from './store'
|
| 4 |
-
import { checkHealth } from './api'
|
| 5 |
-
import UploadZone from './components/UploadZone'
|
| 6 |
-
import ResultPanel from './components/ResultPanel'
|
| 7 |
-
|
| 8 |
-
// Lazy-load the heavy 3D component
|
| 9 |
-
const DeepfakeOrb = lazy(() => import('./components/DeepfakeOrb'))
|
| 10 |
-
|
| 11 |
-
export default function App() {
|
| 12 |
-
const { state, result, error, reset } = useStore()
|
| 13 |
-
const verdict = result?.result ?? null
|
| 14 |
-
|
| 15 |
-
// Health check on mount
|
| 16 |
-
useEffect(() => {
|
| 17 |
-
const badge = document.getElementById('modelBadge')
|
| 18 |
-
checkHealth()
|
| 19 |
-
.then((d) => {
|
| 20 |
-
if (badge) {
|
| 21 |
-
badge.textContent = d.model.toUpperCase()
|
| 22 |
-
badge.style.color = '#00ff88bb'
|
| 23 |
-
badge.style.borderColor = '#00ff8844'
|
| 24 |
-
}
|
| 25 |
-
})
|
| 26 |
-
.catch(() => {
|
| 27 |
-
if (badge) {
|
| 28 |
-
badge.textContent = 'SERVER OFFLINE'
|
| 29 |
-
badge.style.color = '#ff3355bb'
|
| 30 |
-
badge.style.borderColor = '#ff335544'
|
| 31 |
-
}
|
| 32 |
-
})
|
| 33 |
-
}, [])
|
| 34 |
-
|
| 35 |
-
const showUpload = state === 'idle' || state === 'loading'
|
| 36 |
-
const showResult = state === 'result'
|
| 37 |
-
const showError = state === 'error'
|
| 38 |
-
|
| 39 |
-
return (
|
| 40 |
-
<div className="min-h-screen text-white overflow-x-hidden" style={{ background: '#050508' }}>
|
| 41 |
-
|
| 42 |
-
{/* ── Sticky header ── */}
|
| 43 |
-
<header className="sticky top-0 z-50 border-b border-white/5"
|
| 44 |
-
style={{ background: 'rgba(5,5,8,0.85)', backdropFilter: 'blur(20px)' }}>
|
| 45 |
-
<div className="max-w-5xl mx-auto px-6 h-16 flex items-center justify-between">
|
| 46 |
-
<div className="flex items-center gap-3">
|
| 47 |
-
<motion.div
|
| 48 |
-
className="w-9 h-9 rounded-lg border border-[#00ff8855] flex items-center justify-center"
|
| 49 |
-
animate={{ boxShadow: ['0 0 8px #00ff8820', '0 0 20px #00ff8840', '0 0 8px #00ff8820'] }}
|
| 50 |
-
transition={{ duration: 2.5, repeat: Infinity }}
|
| 51 |
-
>
|
| 52 |
-
<svg width="18" height="18" fill="none" stroke="#00ff88" strokeWidth="1.5" viewBox="0 0 24 24">
|
| 53 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.964-7.178z"/>
|
| 54 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
| 55 |
-
</svg>
|
| 56 |
-
</motion.div>
|
| 57 |
-
<div>
|
| 58 |
-
<p className="text-sm font-bold tracking-widest" style={{ color: '#00ff88', textShadow: '0 0 12px #00ff8855' }}>
|
| 59 |
-
DEEPFAKE AUTHENTICATOR
|
| 60 |
-
</p>
|
| 61 |
-
<p className="text-[9px] tracking-widest text-white/30 font-mono">AI-POWERED VIDEO FORENSICS</p>
|
| 62 |
-
</div>
|
| 63 |
-
</div>
|
| 64 |
-
<div
|
| 65 |
-
id="modelBadge"
|
| 66 |
-
className="text-[10px] px-3 py-1.5 rounded-full border font-mono tracking-wider transition-all duration-500"
|
| 67 |
-
style={{ borderColor: 'rgba(255,255,255,0.1)', color: 'rgba(255,255,255,0.3)' }}
|
| 68 |
-
>
|
| 69 |
-
CONNECTING...
|
| 70 |
-
</div>
|
| 71 |
-
</div>
|
| 72 |
-
</header>
|
| 73 |
-
|
| 74 |
-
{/* ── Main layout ── */}
|
| 75 |
-
<main className="max-w-5xl mx-auto px-6 py-10">
|
| 76 |
-
<div className="flex flex-col lg:flex-row gap-10 items-start">
|
| 77 |
-
|
| 78 |
-
{/* ── Left: 3D orb + hero ── */}
|
| 79 |
-
<div className="w-full lg:w-80 flex-shrink-0 flex flex-col items-center gap-6 lg:sticky lg:top-24">
|
| 80 |
-
{/* 3D Orb */}
|
| 81 |
-
<div className="w-64 h-64 relative">
|
| 82 |
-
<Suspense fallback={
|
| 83 |
-
<div className="w-full h-full rounded-full border border-[#00ff8830] flex items-center justify-center">
|
| 84 |
-
<div className="w-16 h-16 rounded-full border-2 border-[#00ff88] border-t-transparent animate-spin" />
|
| 85 |
-
</div>
|
| 86 |
-
}>
|
| 87 |
-
<DeepfakeOrb state={state} verdict={verdict} />
|
| 88 |
-
</Suspense>
|
| 89 |
-
</div>
|
| 90 |
-
|
| 91 |
-
{/* Hero text */}
|
| 92 |
-
<AnimatePresence mode="wait">
|
| 93 |
-
{showUpload && (
|
| 94 |
-
<motion.div
|
| 95 |
-
key="hero"
|
| 96 |
-
className="text-center"
|
| 97 |
-
initial={{ opacity: 0, y: 10 }}
|
| 98 |
-
animate={{ opacity: 1, y: 0 }}
|
| 99 |
-
exit={{ opacity: 0, y: -10 }}
|
| 100 |
-
>
|
| 101 |
-
<h1 className="text-2xl font-bold mb-2 leading-tight"
|
| 102 |
-
style={{ background: 'linear-gradient(135deg, #fff 30%, #00ff88)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
|
| 103 |
-
Is This Video Real?
|
| 104 |
-
</h1>
|
| 105 |
-
<p className="text-white/40 text-xs leading-relaxed max-w-xs">
|
| 106 |
-
Dual ViT ensemble + Wav2Vec2 audio analysis detects facial manipulation and AI-generated voices.
|
| 107 |
-
</p>
|
| 108 |
-
{/* Stat pills */}
|
| 109 |
-
<div className="flex flex-wrap gap-2 justify-center mt-4">
|
| 110 |
-
{['2 ViT Models', '40 Frames', 'Audio AI', '< 20s'].map((s) => (
|
| 111 |
-
<span key={s} className="text-[10px] px-2.5 py-1 rounded-full border border-white/8 text-white/40 bg-white/3">
|
| 112 |
-
{s}
|
| 113 |
-
</span>
|
| 114 |
-
))}
|
| 115 |
-
</div>
|
| 116 |
-
</motion.div>
|
| 117 |
-
)}
|
| 118 |
-
|
| 119 |
-
{showResult && verdict && (
|
| 120 |
-
<motion.div
|
| 121 |
-
key="verdict-summary"
|
| 122 |
-
className="text-center"
|
| 123 |
-
initial={{ opacity: 0, scale: 0.9 }}
|
| 124 |
-
animate={{ opacity: 1, scale: 1 }}
|
| 125 |
-
exit={{ opacity: 0 }}
|
| 126 |
-
>
|
| 127 |
-
<p className="text-3xl font-bold tracking-widest mb-1"
|
| 128 |
-
style={{ color: verdict === 'FAKE' ? '#ff3355' : '#00ff88', textShadow: `0 0 20px ${verdict === 'FAKE' ? '#ff335566' : '#00ff8866'}` }}>
|
| 129 |
-
{verdict === 'FAKE' ? 'DEEPFAKE' : 'AUTHENTIC'}
|
| 130 |
-
</p>
|
| 131 |
-
<p className="text-white/30 text-xs font-mono">{result?.confidence}% confidence</p>
|
| 132 |
-
</motion.div>
|
| 133 |
-
)}
|
| 134 |
-
</AnimatePresence>
|
| 135 |
-
</div>
|
| 136 |
-
|
| 137 |
-
{/* ── Right: Upload / Result / Error ── */}
|
| 138 |
-
<div className="flex-1 min-w-0">
|
| 139 |
-
<AnimatePresence mode="wait">
|
| 140 |
-
{showUpload && (
|
| 141 |
-
<motion.div key="upload" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }}>
|
| 142 |
-
<UploadZone />
|
| 143 |
-
</motion.div>
|
| 144 |
-
)}
|
| 145 |
-
|
| 146 |
-
{showResult && (
|
| 147 |
-
<motion.div key="result" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }}>
|
| 148 |
-
<ResultPanel />
|
| 149 |
-
</motion.div>
|
| 150 |
-
)}
|
| 151 |
-
|
| 152 |
-
{showError && (
|
| 153 |
-
<motion.div
|
| 154 |
-
key="error"
|
| 155 |
-
className="rounded-2xl border border-red-500/20 bg-red-500/5 p-6"
|
| 156 |
-
initial={{ opacity: 0, scale: 0.95 }}
|
| 157 |
-
animate={{ opacity: 1, scale: 1 }}
|
| 158 |
-
exit={{ opacity: 0 }}
|
| 159 |
-
>
|
| 160 |
-
<div className="flex items-center gap-3 mb-3">
|
| 161 |
-
<span className="text-xl">⚠️</span>
|
| 162 |
-
<span className="text-red-400 font-bold text-sm tracking-wider uppercase">Analysis Failed</span>
|
| 163 |
-
</div>
|
| 164 |
-
<p className="text-white/50 text-sm leading-relaxed">{error}</p>
|
| 165 |
-
<button
|
| 166 |
-
onClick={reset}
|
| 167 |
-
className="mt-4 text-xs text-white/30 hover:text-white transition-colors font-mono tracking-wider"
|
| 168 |
-
>
|
| 169 |
-
↻ Try again
|
| 170 |
-
</button>
|
| 171 |
-
</motion.div>
|
| 172 |
-
)}
|
| 173 |
-
</AnimatePresence>
|
| 174 |
-
</div>
|
| 175 |
-
</div>
|
| 176 |
-
</main>
|
| 177 |
-
|
| 178 |
-
<footer className="text-center py-8 text-[10px] text-white/15 font-mono tracking-widest border-t border-white/3">
|
| 179 |
-
DEEPFAKE AUTHENTICATOR · MEDIAPIPE + VIT ENSEMBLE + WAV2VEC2 · LOCAL INFERENCE
|
| 180 |
-
</footer>
|
| 181 |
-
</div>
|
| 182 |
-
)
|
| 183 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/api.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
import type { AnalysisResult } from './store'
|
| 2 |
-
|
| 3 |
-
const BASE = import.meta.env.DEV ? 'http://localhost:8000' : window.location.origin
|
| 4 |
-
|
| 5 |
-
export async function analyzeVideo(file: File): Promise<AnalysisResult> {
|
| 6 |
-
const fd = new FormData()
|
| 7 |
-
fd.append('file', file)
|
| 8 |
-
const res = await fetch(`${BASE}/analyze`, { method: 'POST', body: fd })
|
| 9 |
-
if (!res.ok) {
|
| 10 |
-
const err = await res.json().catch(() => ({}))
|
| 11 |
-
throw new Error((err as any).detail || `Server error ${res.status}`)
|
| 12 |
-
}
|
| 13 |
-
return res.json()
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export async function checkHealth(): Promise<{ status: string; model: string; ready: boolean }> {
|
| 17 |
-
const res = await fetch(`${BASE}/health`)
|
| 18 |
-
if (!res.ok) throw new Error('Server offline')
|
| 19 |
-
return res.json()
|
| 20 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/assets/hero.png
DELETED
|
Binary file (13.1 kB)
|
|
|
frontend/src/assets/react.svg
DELETED
frontend/src/assets/vite.svg
DELETED
frontend/src/components/CyberCard.jsx
DELETED
|
@@ -1,364 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import styled from 'styled-components';
|
| 3 |
-
|
| 4 |
-
const CyberCard = ({ title, subtitle, highlight, hoverText, isFake }) => {
|
| 5 |
-
return (
|
| 6 |
-
<StyledWrapper $isFake={isFake}>
|
| 7 |
-
<div className="container noselect">
|
| 8 |
-
<div className="canvas">
|
| 9 |
-
{Array.from({ length: 25 }).map((_, i) => (
|
| 10 |
-
<div key={i} className={`tracker tr-${i + 1}`} />
|
| 11 |
-
))}
|
| 12 |
-
<div id="card">
|
| 13 |
-
<div className="card-content">
|
| 14 |
-
<div className="card-glare" />
|
| 15 |
-
<div className="cyber-lines">
|
| 16 |
-
<span /><span /><span /><span />
|
| 17 |
-
</div>
|
| 18 |
-
<p id="prompt">{hoverText || "HOVER ME"}</p>
|
| 19 |
-
<div className="title" dangerouslySetInnerHTML={{ __html: title || "CYBER<br />CARD" }} />
|
| 20 |
-
<div className="glowing-elements">
|
| 21 |
-
<div className="glow-1" />
|
| 22 |
-
<div className="glow-2" />
|
| 23 |
-
<div className="glow-3" />
|
| 24 |
-
</div>
|
| 25 |
-
<div className="subtitle">
|
| 26 |
-
<span>{subtitle || "INTERACTIVE"}</span>
|
| 27 |
-
<span className="highlight">{highlight || "3D EFFECT"}</span>
|
| 28 |
-
</div>
|
| 29 |
-
<div className="card-particles">
|
| 30 |
-
<span /><span /><span /> <span /><span /><span />
|
| 31 |
-
</div>
|
| 32 |
-
<div className="corner-elements">
|
| 33 |
-
<span /><span /><span /><span />
|
| 34 |
-
</div>
|
| 35 |
-
<div className="scan-line" />
|
| 36 |
-
</div>
|
| 37 |
-
</div>
|
| 38 |
-
</div>
|
| 39 |
-
</div>
|
| 40 |
-
</StyledWrapper>
|
| 41 |
-
);
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
const StyledWrapper = styled.div`
|
| 45 |
-
.container {
|
| 46 |
-
position: relative;
|
| 47 |
-
width: 260px;
|
| 48 |
-
height: 340px;
|
| 49 |
-
transition: 200ms;
|
| 50 |
-
margin: 0 auto;
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
.container:active {
|
| 54 |
-
width: 250px;
|
| 55 |
-
height: 330px;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
#card {
|
| 59 |
-
position: absolute;
|
| 60 |
-
inset: 0;
|
| 61 |
-
z-index: 0;
|
| 62 |
-
display: flex;
|
| 63 |
-
justify-content: center;
|
| 64 |
-
align-items: center;
|
| 65 |
-
border-radius: 20px;
|
| 66 |
-
transition: 700ms;
|
| 67 |
-
background: linear-gradient(45deg, #1a1a1a, #262626);
|
| 68 |
-
border: 2px solid ${props => props.$isFake ? 'rgba(255, 68, 68, 0.4)' : 'rgba(0, 255, 170, 0.4)'};
|
| 69 |
-
overflow: hidden;
|
| 70 |
-
box-shadow:
|
| 71 |
-
0 0 20px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.3)' : 'rgba(0, 255, 170, 0.3)'},
|
| 72 |
-
inset 0 0 20px rgba(0, 0, 0, 0.2);
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
.card-content {
|
| 76 |
-
position: relative;
|
| 77 |
-
width: 100%;
|
| 78 |
-
height: 100%;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
#prompt {
|
| 82 |
-
bottom: 120px;
|
| 83 |
-
left: 50%;
|
| 84 |
-
transform: translateX(-50%);
|
| 85 |
-
z-index: 20;
|
| 86 |
-
font-size: 16px;
|
| 87 |
-
font-weight: 600;
|
| 88 |
-
letter-spacing: 2px;
|
| 89 |
-
transition: 300ms ease-in-out;
|
| 90 |
-
position: absolute;
|
| 91 |
-
text-align: center;
|
| 92 |
-
color: rgba(255, 255, 255, 0.7);
|
| 93 |
-
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
.title {
|
| 97 |
-
opacity: 0;
|
| 98 |
-
transition: 300ms ease-in-out;
|
| 99 |
-
position: absolute;
|
| 100 |
-
font-size: 42px;
|
| 101 |
-
font-weight: 800;
|
| 102 |
-
letter-spacing: 6px;
|
| 103 |
-
text-align: center;
|
| 104 |
-
width: 100%;
|
| 105 |
-
padding-top: 40px;
|
| 106 |
-
background: ${props => props.$isFake ? 'linear-gradient(45deg, #ff4444, #ff8888)' : 'linear-gradient(45deg, #00ffaa, #00a2ff)'};
|
| 107 |
-
-webkit-background-clip: text;
|
| 108 |
-
-webkit-text-fill-color: transparent;
|
| 109 |
-
filter: drop-shadow(0 0 15px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.3)' : 'rgba(0, 255, 170, 0.3)'});
|
| 110 |
-
text-shadow:
|
| 111 |
-
0 0 10px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.5)' : 'rgba(92, 103, 255, 0.5)'},
|
| 112 |
-
0 0 20px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.3)' : 'rgba(92, 103, 255, 0.3)'};
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
.subtitle {
|
| 116 |
-
position: absolute;
|
| 117 |
-
bottom: 40px;
|
| 118 |
-
width: 100%;
|
| 119 |
-
text-align: center;
|
| 120 |
-
font-size: 14px;
|
| 121 |
-
letter-spacing: 2px;
|
| 122 |
-
transform: translateY(30px);
|
| 123 |
-
color: rgba(255, 255, 255, 0.6);
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
.highlight {
|
| 127 |
-
color: ${props => props.$isFake ? '#ff4444' : '#00ffaa'};
|
| 128 |
-
margin-left: 5px;
|
| 129 |
-
background: ${props => props.$isFake ? 'linear-gradient(90deg, #ff4444, #ff8888)' : 'linear-gradient(90deg, #5c67ff, #ad51ff)'};
|
| 130 |
-
-webkit-background-clip: text;
|
| 131 |
-
-webkit-text-fill-color: transparent;
|
| 132 |
-
font-weight: bold;
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
.glowing-elements {
|
| 136 |
-
position: absolute;
|
| 137 |
-
inset: 0;
|
| 138 |
-
pointer-events: none;
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
.glow-1,
|
| 142 |
-
.glow-2,
|
| 143 |
-
.glow-3 {
|
| 144 |
-
position: absolute;
|
| 145 |
-
width: 120px;
|
| 146 |
-
height: 120px;
|
| 147 |
-
border-radius: 50%;
|
| 148 |
-
background: radial-gradient(
|
| 149 |
-
circle at center,
|
| 150 |
-
${props => props.$isFake ? 'rgba(255, 68, 68, 0.3)' : 'rgba(0, 255, 170, 0.3)'} 0%,
|
| 151 |
-
transparent 70%
|
| 152 |
-
);
|
| 153 |
-
filter: blur(15px);
|
| 154 |
-
opacity: 0;
|
| 155 |
-
transition: opacity 0.3s ease;
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
.glow-1 { top: -20px; left: -20px; }
|
| 159 |
-
.glow-2 { top: 50%; right: -30px; transform: translateY(-50%); }
|
| 160 |
-
.glow-3 { bottom: -20px; left: 30%; }
|
| 161 |
-
|
| 162 |
-
.card-particles span {
|
| 163 |
-
position: absolute;
|
| 164 |
-
width: 4px;
|
| 165 |
-
height: 4px;
|
| 166 |
-
background: ${props => props.$isFake ? '#ff4444' : '#00ffaa'};
|
| 167 |
-
border-radius: 50%;
|
| 168 |
-
opacity: 0;
|
| 169 |
-
transition: opacity 0.3s ease;
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
/* Hover effects */
|
| 173 |
-
.tracker:hover ~ #card .title {
|
| 174 |
-
opacity: 1;
|
| 175 |
-
transform: translateY(-10px);
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
.tracker:hover ~ #card .glowing-elements div {
|
| 179 |
-
opacity: 1;
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
.tracker:hover ~ #card .card-particles span {
|
| 183 |
-
animation: particleFloat 2s infinite;
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
@keyframes particleFloat {
|
| 187 |
-
0% { transform: translate(0, 0); opacity: 0; }
|
| 188 |
-
50% { opacity: 1; }
|
| 189 |
-
100% { transform: translate(calc(var(--x, 0) * 30px), calc(var(--y, 0) * 30px)); opacity: 0; }
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
/* Particle positions */
|
| 193 |
-
.card-particles span:nth-child(1) { --x: 1; --y: -1; top: 40%; left: 20%; }
|
| 194 |
-
.card-particles span:nth-child(2) { --x: -1; --y: -1; top: 60%; right: 20%; }
|
| 195 |
-
.card-particles span:nth-child(3) { --x: 0.5; --y: 1; top: 20%; left: 40%; }
|
| 196 |
-
.card-particles span:nth-child(4) { --x: -0.5; --y: 1; top: 80%; right: 40%; }
|
| 197 |
-
.card-particles span:nth-child(5) { --x: 1; --y: 0.5; top: 30%; left: 60%; }
|
| 198 |
-
.card-particles span:nth-child(6) { --x: -1; --y: 0.5; top: 70%; right: 60%; }
|
| 199 |
-
|
| 200 |
-
#card::before {
|
| 201 |
-
content: "";
|
| 202 |
-
background: radial-gradient(
|
| 203 |
-
circle at center,
|
| 204 |
-
${props => props.$isFake ? 'rgba(255, 68, 68, 0.1)' : 'rgba(0, 255, 170, 0.1)'} 0%,
|
| 205 |
-
${props => props.$isFake ? 'rgba(255, 136, 136, 0.05)' : 'rgba(0, 162, 255, 0.05)'} 50%,
|
| 206 |
-
transparent 100%
|
| 207 |
-
);
|
| 208 |
-
filter: blur(20px);
|
| 209 |
-
opacity: 0;
|
| 210 |
-
width: 150%;
|
| 211 |
-
height: 150%;
|
| 212 |
-
position: absolute;
|
| 213 |
-
left: 50%;
|
| 214 |
-
top: 50%;
|
| 215 |
-
transform: translate(-50%, -50%);
|
| 216 |
-
transition: opacity 0.3s ease;
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
.tracker:hover ~ #card::before { opacity: 1; }
|
| 220 |
-
|
| 221 |
-
.tracker {
|
| 222 |
-
position: absolute;
|
| 223 |
-
z-index: 200;
|
| 224 |
-
width: 100%;
|
| 225 |
-
height: 100%;
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
.tracker:hover { cursor: pointer; }
|
| 229 |
-
.tracker:hover ~ #card #prompt { opacity: 0; }
|
| 230 |
-
.tracker:hover ~ #card { transition: 300ms; filter: brightness(1.1); }
|
| 231 |
-
.container:hover #card::before { transition: 200ms; content: ""; opacity: 80%; }
|
| 232 |
-
|
| 233 |
-
.canvas {
|
| 234 |
-
perspective: 800px;
|
| 235 |
-
inset: 0;
|
| 236 |
-
z-index: 200;
|
| 237 |
-
position: absolute;
|
| 238 |
-
display: grid;
|
| 239 |
-
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
| 240 |
-
grid-template-rows: 1fr 1fr 1fr 1fr 1fr;
|
| 241 |
-
gap: 0px 0px;
|
| 242 |
-
grid-template-areas:
|
| 243 |
-
"tr-1 tr-2 tr-3 tr-4 tr-5"
|
| 244 |
-
"tr-6 tr-7 tr-8 tr-9 tr-10"
|
| 245 |
-
"tr-11 tr-12 tr-13 tr-14 tr-15"
|
| 246 |
-
"tr-16 tr-17 tr-18 tr-19 tr-20"
|
| 247 |
-
"tr-21 tr-22 tr-23 tr-24 tr-25";
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
.tr-1 { grid-area: tr-1; } .tr-2 { grid-area: tr-2; } .tr-3 { grid-area: tr-3; } .tr-4 { grid-area: tr-4; } .tr-5 { grid-area: tr-5; }
|
| 251 |
-
.tr-6 { grid-area: tr-6; } .tr-7 { grid-area: tr-7; } .tr-8 { grid-area: tr-8; } .tr-9 { grid-area: tr-9; } .tr-10 { grid-area: tr-10; }
|
| 252 |
-
.tr-11 { grid-area: tr-11; } .tr-12 { grid-area: tr-12; } .tr-13 { grid-area: tr-13; } .tr-14 { grid-area: tr-14; } .tr-15 { grid-area: tr-15; }
|
| 253 |
-
.tr-16 { grid-area: tr-16; } .tr-17 { grid-area: tr-17; } .tr-18 { grid-area: tr-18; } .tr-19 { grid-area: tr-19; } .tr-20 { grid-area: tr-20; }
|
| 254 |
-
.tr-21 { grid-area: tr-21; } .tr-22 { grid-area: tr-22; } .tr-23 { grid-area: tr-23; } .tr-24 { grid-area: tr-24; } .tr-25 { grid-area: tr-25; }
|
| 255 |
-
|
| 256 |
-
.tr-1:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(-10deg) rotateZ(0deg); }
|
| 257 |
-
.tr-2:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(-5deg) rotateZ(0deg); }
|
| 258 |
-
.tr-3:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(0deg) rotateZ(0deg); }
|
| 259 |
-
.tr-4:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(5deg) rotateZ(0deg); }
|
| 260 |
-
.tr-5:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(10deg) rotateZ(0deg); }
|
| 261 |
-
.tr-6:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(-10deg) rotateZ(0deg); }
|
| 262 |
-
.tr-7:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(-5deg) rotateZ(0deg); }
|
| 263 |
-
.tr-8:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(0deg) rotateZ(0deg); }
|
| 264 |
-
.tr-9:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(5deg) rotateZ(0deg); }
|
| 265 |
-
.tr-10:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(10deg) rotateZ(0deg); }
|
| 266 |
-
.tr-11:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(-10deg) rotateZ(0deg); }
|
| 267 |
-
.tr-12:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(-5deg) rotateZ(0deg); }
|
| 268 |
-
.tr-13:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
|
| 269 |
-
.tr-14:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(5deg) rotateZ(0deg); }
|
| 270 |
-
.tr-15:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(10deg) rotateZ(0deg); }
|
| 271 |
-
.tr-16:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(-10deg) rotateZ(0deg); }
|
| 272 |
-
.tr-17:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(-5deg) rotateZ(0deg); }
|
| 273 |
-
.tr-18:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(0deg) rotateZ(0deg); }
|
| 274 |
-
.tr-19:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(5deg) rotateZ(0deg); }
|
| 275 |
-
.tr-20:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(10deg) rotateZ(0deg); }
|
| 276 |
-
.tr-21:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(-10deg) rotateZ(0deg); }
|
| 277 |
-
.tr-22:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(-5deg) rotateZ(0deg); }
|
| 278 |
-
.tr-23:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(0deg) rotateZ(0deg); }
|
| 279 |
-
.tr-24:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(5deg) rotateZ(0deg); }
|
| 280 |
-
.tr-25:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(10deg) rotateZ(0deg); }
|
| 281 |
-
|
| 282 |
-
.noselect {
|
| 283 |
-
-webkit-touch-callout: none;
|
| 284 |
-
-webkit-user-select: none;
|
| 285 |
-
-moz-user-select: none;
|
| 286 |
-
-ms-user-select: none;
|
| 287 |
-
user-select: none;
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
.card-glare {
|
| 291 |
-
position: absolute;
|
| 292 |
-
inset: 0;
|
| 293 |
-
background: linear-gradient(
|
| 294 |
-
125deg,
|
| 295 |
-
rgba(255, 255, 255, 0) 0%,
|
| 296 |
-
rgba(255, 255, 255, 0.05) 45%,
|
| 297 |
-
rgba(255, 255, 255, 0.1) 50%,
|
| 298 |
-
rgba(255, 255, 255, 0.05) 55%,
|
| 299 |
-
rgba(255, 255, 255, 0) 100%
|
| 300 |
-
);
|
| 301 |
-
opacity: 0;
|
| 302 |
-
transition: opacity 300ms;
|
| 303 |
-
}
|
| 304 |
-
|
| 305 |
-
.cyber-lines span {
|
| 306 |
-
position: absolute;
|
| 307 |
-
background: linear-gradient(
|
| 308 |
-
90deg,
|
| 309 |
-
transparent,
|
| 310 |
-
${props => props.$isFake ? 'rgba(255, 68, 68, 0.4)' : 'rgba(92, 103, 255, 0.2)'},
|
| 311 |
-
transparent
|
| 312 |
-
);
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
.cyber-lines span:nth-child(1) { top: 20%; left: 0; width: 100%; height: 1px; transform: scaleX(0); transform-origin: left; animation: lineGrow 3s linear infinite; }
|
| 316 |
-
.cyber-lines span:nth-child(2) { top: 40%; right: 0; width: 100%; height: 1px; transform: scaleX(0); transform-origin: right; animation: lineGrow 3s linear infinite 1s; }
|
| 317 |
-
.cyber-lines span:nth-child(3) { top: 60%; left: 0; width: 100%; height: 1px; transform: scaleX(0); transform-origin: left; animation: lineGrow 3s linear infinite 2s; }
|
| 318 |
-
.cyber-lines span:nth-child(4) { top: 80%; right: 0; width: 100%; height: 1px; transform: scaleX(0); transform-origin: right; animation: lineGrow 3s linear infinite 1.5s; }
|
| 319 |
-
|
| 320 |
-
.corner-elements span {
|
| 321 |
-
position: absolute;
|
| 322 |
-
width: 15px;
|
| 323 |
-
height: 15px;
|
| 324 |
-
border: 2px solid ${props => props.$isFake ? 'rgba(255, 68, 68, 0.5)' : 'rgba(92, 103, 255, 0.3)'};
|
| 325 |
-
transition: all 0.3s ease;
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
.corner-elements span:nth-child(1) { top: 10px; left: 10px; border-right: 0; border-bottom: 0; }
|
| 329 |
-
.corner-elements span:nth-child(2) { top: 10px; right: 10px; border-left: 0; border-bottom: 0; }
|
| 330 |
-
.corner-elements span:nth-child(3) { bottom: 10px; left: 10px; border-right: 0; border-top: 0; }
|
| 331 |
-
.corner-elements span:nth-child(4) { bottom: 10px; right: 10px; border-left: 0; border-top: 0; }
|
| 332 |
-
|
| 333 |
-
.scan-line {
|
| 334 |
-
position: absolute;
|
| 335 |
-
inset: 0;
|
| 336 |
-
background: linear-gradient(
|
| 337 |
-
to bottom,
|
| 338 |
-
transparent,
|
| 339 |
-
${props => props.$isFake ? 'rgba(255, 68, 68, 0.2)' : 'rgba(92, 103, 255, 0.1)'},
|
| 340 |
-
transparent
|
| 341 |
-
);
|
| 342 |
-
transform: translateY(-100%);
|
| 343 |
-
animation: scanMove 2s linear infinite;
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
@keyframes lineGrow {
|
| 347 |
-
0% { transform: scaleX(0); opacity: 0; }
|
| 348 |
-
50% { transform: scaleX(1); opacity: 1; }
|
| 349 |
-
100% { transform: scaleX(0); opacity: 0; }
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
@keyframes scanMove {
|
| 353 |
-
0% { transform: translateY(-100%); }
|
| 354 |
-
100% { transform: translateY(100%); }
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
#card:hover .card-glare { opacity: 1; }
|
| 358 |
-
#card:hover .corner-elements span {
|
| 359 |
-
border-color: ${props => props.$isFake ? 'rgba(255, 68, 68, 0.8)' : 'rgba(92, 103, 255, 0.8)'};
|
| 360 |
-
box-shadow: 0 0 10px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.5)' : 'rgba(92, 103, 255, 0.5)'};
|
| 361 |
-
}
|
| 362 |
-
`;
|
| 363 |
-
|
| 364 |
-
export default CyberCard;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/DeepfakeOrb.tsx
DELETED
|
@@ -1,103 +0,0 @@
|
|
| 1 |
-
import { useRef, useMemo } from 'react'
|
| 2 |
-
import { Canvas, useFrame } from '@react-three/fiber'
|
| 3 |
-
import { Sphere, MeshDistortMaterial, Float, Stars } from '@react-three/drei'
|
| 4 |
-
import * as THREE from 'three'
|
| 5 |
-
import type { AppState, Verdict } from '../store'
|
| 6 |
-
|
| 7 |
-
// ── Inner animated orb ────────────────────────────────────────────────────────
|
| 8 |
-
function Orb({ state, verdict }: { state: AppState; verdict: Verdict }) {
|
| 9 |
-
const meshRef = useRef<THREE.Mesh>(null)
|
| 10 |
-
const ringRef = useRef<THREE.Mesh>(null)
|
| 11 |
-
const ring2Ref = useRef<THREE.Mesh>(null)
|
| 12 |
-
|
| 13 |
-
const isFake = verdict === 'FAKE'
|
| 14 |
-
const isReal = verdict === 'REAL'
|
| 15 |
-
const isLoading = state === 'loading'
|
| 16 |
-
|
| 17 |
-
const color = isFake ? '#ff3355' : isReal ? '#00ff88' : '#00aaff'
|
| 18 |
-
const emissive = isFake ? '#ff0033' : isReal ? '#00cc66' : '#0066ff'
|
| 19 |
-
|
| 20 |
-
useFrame((_, delta) => {
|
| 21 |
-
if (!meshRef.current) return
|
| 22 |
-
meshRef.current.rotation.y += delta * (isLoading ? 1.5 : 0.4)
|
| 23 |
-
meshRef.current.rotation.x += delta * (isLoading ? 0.8 : 0.15)
|
| 24 |
-
if (ringRef.current) ringRef.current.rotation.z += delta * 0.6
|
| 25 |
-
if (ring2Ref.current) ring2Ref.current.rotation.x += delta * 0.4
|
| 26 |
-
})
|
| 27 |
-
|
| 28 |
-
// Particle ring
|
| 29 |
-
const particles = useMemo(() => {
|
| 30 |
-
const count = 120
|
| 31 |
-
const positions = new Float32Array(count * 3)
|
| 32 |
-
for (let i = 0; i < count; i++) {
|
| 33 |
-
const angle = (i / count) * Math.PI * 2
|
| 34 |
-
const radius = 1.8 + Math.random() * 0.4
|
| 35 |
-
positions[i * 3] = Math.cos(angle) * radius
|
| 36 |
-
positions[i * 3 + 1] = (Math.random() - 0.5) * 0.3
|
| 37 |
-
positions[i * 3 + 2] = Math.sin(angle) * radius
|
| 38 |
-
}
|
| 39 |
-
return positions
|
| 40 |
-
}, [])
|
| 41 |
-
|
| 42 |
-
return (
|
| 43 |
-
<group>
|
| 44 |
-
{/* Stars background */}
|
| 45 |
-
<Stars radius={8} depth={4} count={300} factor={1} fade speed={isLoading ? 3 : 0.5} />
|
| 46 |
-
|
| 47 |
-
{/* Main orb */}
|
| 48 |
-
<Float speed={isLoading ? 4 : 1.5} rotationIntensity={isLoading ? 1 : 0.3} floatIntensity={isLoading ? 1.5 : 0.5}>
|
| 49 |
-
<Sphere ref={meshRef} args={[1, 64, 64]}>
|
| 50 |
-
<MeshDistortMaterial
|
| 51 |
-
color={color}
|
| 52 |
-
emissive={emissive}
|
| 53 |
-
emissiveIntensity={isLoading ? 0.8 : 0.4}
|
| 54 |
-
distort={isLoading ? 0.6 : isFake ? 0.45 : 0.25}
|
| 55 |
-
speed={isLoading ? 4 : 2}
|
| 56 |
-
roughness={0.1}
|
| 57 |
-
metalness={0.8}
|
| 58 |
-
transparent
|
| 59 |
-
opacity={0.9}
|
| 60 |
-
/>
|
| 61 |
-
</Sphere>
|
| 62 |
-
</Float>
|
| 63 |
-
|
| 64 |
-
{/* Orbit ring 1 */}
|
| 65 |
-
<mesh ref={ringRef} rotation={[Math.PI / 2, 0, 0]}>
|
| 66 |
-
<torusGeometry args={[1.6, 0.015, 8, 100]} />
|
| 67 |
-
<meshBasicMaterial color={color} transparent opacity={0.5} />
|
| 68 |
-
</mesh>
|
| 69 |
-
|
| 70 |
-
{/* Orbit ring 2 */}
|
| 71 |
-
<mesh ref={ring2Ref} rotation={[Math.PI / 3, Math.PI / 4, 0]}>
|
| 72 |
-
<torusGeometry args={[1.9, 0.008, 8, 100]} />
|
| 73 |
-
<meshBasicMaterial color={color} transparent opacity={0.25} />
|
| 74 |
-
</mesh>
|
| 75 |
-
|
| 76 |
-
{/* Particle ring */}
|
| 77 |
-
<points>
|
| 78 |
-
<bufferGeometry>
|
| 79 |
-
<bufferAttribute attach="attributes-position" args={[particles, 3]} />
|
| 80 |
-
</bufferGeometry>
|
| 81 |
-
<pointsMaterial color={color} size={0.025} transparent opacity={0.6} />
|
| 82 |
-
</points>
|
| 83 |
-
|
| 84 |
-
{/* Ambient + point lights */}
|
| 85 |
-
<ambientLight intensity={0.2} />
|
| 86 |
-
<pointLight position={[3, 3, 3]} intensity={2} color={color} />
|
| 87 |
-
<pointLight position={[-3, -3, -3]} intensity={1} color={emissive} />
|
| 88 |
-
</group>
|
| 89 |
-
)
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
// ── Exported canvas wrapper ───────────────────────────────────────────────────
|
| 93 |
-
export default function DeepfakeOrb({ state, verdict }: { state: AppState; verdict: Verdict }) {
|
| 94 |
-
return (
|
| 95 |
-
<Canvas
|
| 96 |
-
camera={{ position: [0, 0, 4], fov: 50 }}
|
| 97 |
-
style={{ width: '100%', height: '100%' }}
|
| 98 |
-
gl={{ antialias: true, alpha: true }}
|
| 99 |
-
>
|
| 100 |
-
<Orb state={state} verdict={verdict} />
|
| 101 |
-
</Canvas>
|
| 102 |
-
)
|
| 103 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/Loader.jsx
DELETED
|
@@ -1,97 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import styled from 'styled-components';
|
| 3 |
-
|
| 4 |
-
const Loader = () => {
|
| 5 |
-
return (
|
| 6 |
-
<StyledWrapper>
|
| 7 |
-
<div className="liquid-loader">
|
| 8 |
-
<div className="loading-text">
|
| 9 |
-
Analyzing<span className="dot">.</span><span className="dot">.</span><span className="dot">.</span>
|
| 10 |
-
</div>
|
| 11 |
-
<div className="loader-track">
|
| 12 |
-
<div className="liquid-fill" />
|
| 13 |
-
</div>
|
| 14 |
-
</div>
|
| 15 |
-
</StyledWrapper>
|
| 16 |
-
);
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
const StyledWrapper = styled.div`
|
| 20 |
-
.liquid-loader {
|
| 21 |
-
display: flex;
|
| 22 |
-
flex-direction: column;
|
| 23 |
-
align-items: center;
|
| 24 |
-
gap: 15px;
|
| 25 |
-
padding: 20px;
|
| 26 |
-
font-family: system-ui, sans-serif;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
.loader-track {
|
| 30 |
-
position: relative;
|
| 31 |
-
width: 250px;
|
| 32 |
-
height: 32px;
|
| 33 |
-
background: linear-gradient(135deg, #2a2a2a, #1a1a1a);
|
| 34 |
-
border-radius: 16px;
|
| 35 |
-
overflow: hidden;
|
| 36 |
-
box-shadow:
|
| 37 |
-
inset 0 2px 4px rgba(0, 0, 0, 0.6),
|
| 38 |
-
0 1px 3px rgba(255, 255, 255, 0.1);
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
.liquid-fill {
|
| 42 |
-
position: absolute;
|
| 43 |
-
top: 2px;
|
| 44 |
-
left: 2px;
|
| 45 |
-
height: calc(100% - 4px);
|
| 46 |
-
background: linear-gradient(90deg, #00ffaa, #00a2ff, #5c67ff, #00ffaa);
|
| 47 |
-
border-radius: 14px;
|
| 48 |
-
animation:
|
| 49 |
-
fillProgress 8s ease-out forwards,
|
| 50 |
-
colorShift 3s linear infinite;
|
| 51 |
-
box-shadow:
|
| 52 |
-
0 0 12px rgba(0, 255, 170, 0.4),
|
| 53 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.2);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
.loading-text {
|
| 57 |
-
color: white;
|
| 58 |
-
font-size: 18px;
|
| 59 |
-
font-weight: 600;
|
| 60 |
-
letter-spacing: 2px;
|
| 61 |
-
text-transform: uppercase;
|
| 62 |
-
animation: textGlow 1s ease-in-out infinite;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
.dot {
|
| 66 |
-
margin-left: 3px;
|
| 67 |
-
animation: blink 1.5s infinite;
|
| 68 |
-
}
|
| 69 |
-
.dot:nth-of-type(1) { animation-delay: 0s; }
|
| 70 |
-
.dot:nth-of-type(2) { animation-delay: 0.3s; }
|
| 71 |
-
.dot:nth-of-type(3) { animation-delay: 0.6s; }
|
| 72 |
-
|
| 73 |
-
@keyframes fillProgress {
|
| 74 |
-
0% { width: 4px; }
|
| 75 |
-
25% { width: 35%; }
|
| 76 |
-
50% { width: 65%; }
|
| 77 |
-
75% { width: 85%; }
|
| 78 |
-
100% { width: calc(100% - 4px); }
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
@keyframes colorShift {
|
| 82 |
-
0% { filter: hue-rotate(0deg) brightness(1); }
|
| 83 |
-
100% { filter: hue-rotate(360deg) brightness(1); }
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
@keyframes textGlow {
|
| 87 |
-
0%, 100% { opacity: 0.7; text-shadow: 0 0 8px rgba(0, 255, 170, 0.3); }
|
| 88 |
-
50% { opacity: 1; text-shadow: 0 0 16px rgba(0, 255, 170, 0.6); }
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
@keyframes blink {
|
| 92 |
-
0%, 50% { opacity: 1; }
|
| 93 |
-
51%, 100% { opacity: 0; }
|
| 94 |
-
}
|
| 95 |
-
`;
|
| 96 |
-
|
| 97 |
-
export default Loader;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/ResultPanel.tsx
DELETED
|
@@ -1,346 +0,0 @@
|
|
| 1 |
-
import { motion, AnimatePresence } from 'framer-motion'
|
| 2 |
-
import { useStore } from '../store'
|
| 3 |
-
import type { FramePoint, AudioResult } from '../store'
|
| 4 |
-
|
| 5 |
-
// ── Animated confidence arc ───────────────────────────────────────────────────
|
| 6 |
-
function ConfidenceArc({ pct, isFake }: { pct: number; isFake: boolean }) {
|
| 7 |
-
const r = 54
|
| 8 |
-
const circ = 2 * Math.PI * r
|
| 9 |
-
const half = circ / 2
|
| 10 |
-
const fill = (pct / 100) * half
|
| 11 |
-
const color = isFake ? '#ff3355' : '#00ff88'
|
| 12 |
-
|
| 13 |
-
return (
|
| 14 |
-
<div className="relative w-40 h-20 mx-auto">
|
| 15 |
-
<svg width="160" height="80" viewBox="0 0 160 80">
|
| 16 |
-
{/* Track */}
|
| 17 |
-
<path
|
| 18 |
-
d={`M 16 80 A ${r} ${r} 0 0 1 144 80`}
|
| 19 |
-
fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="10" strokeLinecap="round"
|
| 20 |
-
/>
|
| 21 |
-
{/* Fill */}
|
| 22 |
-
<motion.path
|
| 23 |
-
d={`M 16 80 A ${r} ${r} 0 0 1 144 80`}
|
| 24 |
-
fill="none"
|
| 25 |
-
stroke={color}
|
| 26 |
-
strokeWidth="10"
|
| 27 |
-
strokeLinecap="round"
|
| 28 |
-
strokeDasharray={`${half} ${half}`}
|
| 29 |
-
initial={{ strokeDashoffset: half }}
|
| 30 |
-
animate={{ strokeDashoffset: half - fill }}
|
| 31 |
-
transition={{ duration: 1.4, ease: [0.22, 1, 0.36, 1] }}
|
| 32 |
-
style={{ filter: `drop-shadow(0 0 8px ${color})` }}
|
| 33 |
-
/>
|
| 34 |
-
</svg>
|
| 35 |
-
<div className="absolute inset-0 flex flex-col items-center justify-end pb-1">
|
| 36 |
-
<motion.span
|
| 37 |
-
className="text-2xl font-bold font-mono"
|
| 38 |
-
style={{ color }}
|
| 39 |
-
initial={{ opacity: 0 }}
|
| 40 |
-
animate={{ opacity: 1 }}
|
| 41 |
-
transition={{ delay: 0.5 }}
|
| 42 |
-
>
|
| 43 |
-
{pct}%
|
| 44 |
-
</motion.span>
|
| 45 |
-
</div>
|
| 46 |
-
</div>
|
| 47 |
-
)
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
// ── Frame timeline ────────────────────────────────────────────────────────────
|
| 51 |
-
function Timeline({ frames, isFake }: { frames: FramePoint[]; isFake: boolean }) {
|
| 52 |
-
if (!frames.length) return (
|
| 53 |
-
<p className="text-white/30 text-xs font-mono text-center py-4">No per-frame data</p>
|
| 54 |
-
)
|
| 55 |
-
|
| 56 |
-
const color = isFake ? '#ff3355' : '#00ff88'
|
| 57 |
-
const maxH = 60
|
| 58 |
-
|
| 59 |
-
return (
|
| 60 |
-
<div className="flex items-end gap-1 overflow-x-auto pb-1" style={{ height: maxH + 24 + 'px', position: 'relative' }}>
|
| 61 |
-
{/* 60% threshold line */}
|
| 62 |
-
<div
|
| 63 |
-
className="absolute left-0 right-0 pointer-events-none"
|
| 64 |
-
style={{ bottom: 24 + maxH * 0.6, height: 1, background: 'rgba(255,170,0,0.35)' }}
|
| 65 |
-
>
|
| 66 |
-
<span className="absolute right-0 -top-4 text-[9px] text-yellow-400/60 font-mono">60%</span>
|
| 67 |
-
</div>
|
| 68 |
-
|
| 69 |
-
{frames.map((pt, i) => {
|
| 70 |
-
const h = Math.max(3, (pt.fake_pct / 100) * maxH)
|
| 71 |
-
const hot = pt.fake_pct >= 60
|
| 72 |
-
return (
|
| 73 |
-
<div key={i} className="group relative flex-shrink-0" style={{ width: 10, height: maxH }}>
|
| 74 |
-
{/* Tooltip */}
|
| 75 |
-
<div className="absolute bottom-full mb-1 left-1/2 -translate-x-1/2 bg-[#0a0a14] border border-white/10 rounded px-1.5 py-0.5 text-[9px] font-mono whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none">
|
| 76 |
-
F{pt.frame}: {pt.fake_pct}%
|
| 77 |
-
</div>
|
| 78 |
-
<motion.div
|
| 79 |
-
className="absolute bottom-0 w-full rounded-t"
|
| 80 |
-
style={{
|
| 81 |
-
background: hot
|
| 82 |
-
? `linear-gradient(to top, ${color}, rgba(255,255,255,0.3))`
|
| 83 |
-
: 'rgba(255,255,255,0.1)',
|
| 84 |
-
boxShadow: hot ? `0 0 6px ${color}66` : 'none',
|
| 85 |
-
}}
|
| 86 |
-
initial={{ height: 0 }}
|
| 87 |
-
animate={{ height: h }}
|
| 88 |
-
transition={{ duration: 0.6, delay: i * 0.02, ease: [0.22, 1, 0.36, 1] }}
|
| 89 |
-
/>
|
| 90 |
-
</div>
|
| 91 |
-
)
|
| 92 |
-
})}
|
| 93 |
-
</div>
|
| 94 |
-
)
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
// ── Audio card ────────────────────────────────────────────────────────────────
|
| 98 |
-
function AudioCard({ audio }: { audio: AudioResult }) {
|
| 99 |
-
if (!audio?.available) return null
|
| 100 |
-
|
| 101 |
-
const isAI = audio.result === 'AI_VOICE'
|
| 102 |
-
const color = isAI ? '#ff3355' : '#00ff88'
|
| 103 |
-
const colorDim = isAI ? 'rgba(255,51,85,0.12)' : 'rgba(0,255,136,0.12)'
|
| 104 |
-
|
| 105 |
-
return (
|
| 106 |
-
<motion.div
|
| 107 |
-
className="rounded-2xl border p-5"
|
| 108 |
-
style={{ borderColor: `${color}33`, background: colorDim }}
|
| 109 |
-
initial={{ opacity: 0, y: 16 }}
|
| 110 |
-
animate={{ opacity: 1, y: 0 }}
|
| 111 |
-
transition={{ delay: 0.4 }}
|
| 112 |
-
>
|
| 113 |
-
<div className="flex items-center gap-3 mb-4">
|
| 114 |
-
<div className="w-1 h-5 rounded-full" style={{ background: '#00aaff', boxShadow: '0 0 8px #00aaff' }} />
|
| 115 |
-
<span className="text-[10px] font-bold tracking-widest text-[#00aaff] uppercase">Voice Authenticity</span>
|
| 116 |
-
</div>
|
| 117 |
-
|
| 118 |
-
<div className="flex items-center gap-3 mb-4">
|
| 119 |
-
<span
|
| 120 |
-
className="px-3 py-1.5 rounded-full text-xs font-bold tracking-wider border"
|
| 121 |
-
style={{ color, borderColor: `${color}55`, background: `${color}12` }}
|
| 122 |
-
>
|
| 123 |
-
{isAI ? '🤖 AI VOICE DETECTED' : '🎙️ HUMAN VOICE'}
|
| 124 |
-
</span>
|
| 125 |
-
<span className="text-2xl font-bold font-mono ml-auto" style={{ color }}>
|
| 126 |
-
{audio.confidence}%
|
| 127 |
-
</span>
|
| 128 |
-
</div>
|
| 129 |
-
|
| 130 |
-
{/* Bar */}
|
| 131 |
-
<div className="h-2 rounded-full bg-white/5 overflow-hidden mb-4">
|
| 132 |
-
<motion.div
|
| 133 |
-
className="h-full rounded-full"
|
| 134 |
-
style={{
|
| 135 |
-
background: isAI ? 'linear-gradient(90deg,#880022,#ff3355)' : 'linear-gradient(90deg,#00aaff,#00ff88)',
|
| 136 |
-
boxShadow: `0 0 10px ${color}66`,
|
| 137 |
-
}}
|
| 138 |
-
initial={{ width: 0 }}
|
| 139 |
-
animate={{ width: `${audio.confidence}%` }}
|
| 140 |
-
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
|
| 141 |
-
/>
|
| 142 |
-
</div>
|
| 143 |
-
|
| 144 |
-
{/* Scores */}
|
| 145 |
-
<div className="grid grid-cols-2 gap-3 mb-4">
|
| 146 |
-
{[
|
| 147 |
-
{ label: 'Wav2Vec2 Model', val: `${audio.model_score}%` },
|
| 148 |
-
{ label: 'Signal Heuristics', val: `${audio.heuristic_score}%` },
|
| 149 |
-
].map(({ label, val }) => (
|
| 150 |
-
<div key={label} className="rounded-xl bg-white/3 border border-white/5 p-3 text-center">
|
| 151 |
-
<p className="text-[10px] text-white/40 tracking-wider mb-1">{label}</p>
|
| 152 |
-
<p className="text-lg font-bold font-mono text-white">{val}</p>
|
| 153 |
-
</div>
|
| 154 |
-
))}
|
| 155 |
-
</div>
|
| 156 |
-
|
| 157 |
-
{/* Details */}
|
| 158 |
-
<div className="space-y-2">
|
| 159 |
-
{audio.details.map((d, i) => (
|
| 160 |
-
<motion.div
|
| 161 |
-
key={i}
|
| 162 |
-
className="flex items-start gap-2.5 text-xs text-white/60 bg-white/2 rounded-lg px-3 py-2 border-l-2"
|
| 163 |
-
style={{ borderLeftColor: color }}
|
| 164 |
-
initial={{ opacity: 0, x: -8 }}
|
| 165 |
-
animate={{ opacity: 1, x: 0 }}
|
| 166 |
-
transition={{ delay: 0.5 + i * 0.07 }}
|
| 167 |
-
>
|
| 168 |
-
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0 mt-1" style={{ background: color, boxShadow: `0 0 6px ${color}` }} />
|
| 169 |
-
{d}
|
| 170 |
-
</motion.div>
|
| 171 |
-
))}
|
| 172 |
-
</div>
|
| 173 |
-
|
| 174 |
-
{/* Features */}
|
| 175 |
-
{audio.features && Object.keys(audio.features).length > 0 && (
|
| 176 |
-
<div className="mt-4 pt-4 border-t border-white/5">
|
| 177 |
-
<p className="text-[10px] text-white/30 tracking-widest uppercase mb-2">Signal Features</p>
|
| 178 |
-
<div className="grid grid-cols-2 gap-2">
|
| 179 |
-
{Object.entries(audio.features).map(([k, v]) => (
|
| 180 |
-
<div key={k} className="flex justify-between text-[10px]">
|
| 181 |
-
<span className="text-white/30 capitalize">{k.replace(/_/g, ' ')}</span>
|
| 182 |
-
<span className="text-white/70 font-mono">{typeof v === 'number' ? v.toFixed(3) : v}</span>
|
| 183 |
-
</div>
|
| 184 |
-
))}
|
| 185 |
-
</div>
|
| 186 |
-
</div>
|
| 187 |
-
)}
|
| 188 |
-
</motion.div>
|
| 189 |
-
)
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
// ── Main result panel ─────────────────────────────────────────────────────────
|
| 193 |
-
export default function ResultPanel() {
|
| 194 |
-
const { result, reset } = useStore()
|
| 195 |
-
if (!result) return null
|
| 196 |
-
|
| 197 |
-
const isFake = result.result === 'FAKE'
|
| 198 |
-
const color = isFake ? '#ff3355' : '#00ff88'
|
| 199 |
-
const colorDim = isFake ? 'rgba(255,51,85,0.08)' : 'rgba(0,255,136,0.08)'
|
| 200 |
-
|
| 201 |
-
return (
|
| 202 |
-
<AnimatePresence>
|
| 203 |
-
<motion.div
|
| 204 |
-
className="w-full max-w-2xl mx-auto space-y-4"
|
| 205 |
-
initial={{ opacity: 0 }}
|
| 206 |
-
animate={{ opacity: 1 }}
|
| 207 |
-
exit={{ opacity: 0 }}
|
| 208 |
-
>
|
| 209 |
-
{/* Verdict card */}
|
| 210 |
-
<motion.div
|
| 211 |
-
className="rounded-2xl border-2 p-6"
|
| 212 |
-
style={{ borderColor: `${color}55`, background: colorDim, boxShadow: `0 0 50px ${color}15` }}
|
| 213 |
-
initial={{ scale: 0.95, opacity: 0 }}
|
| 214 |
-
animate={{ scale: 1, opacity: 1 }}
|
| 215 |
-
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
|
| 216 |
-
>
|
| 217 |
-
<div className="flex flex-wrap items-center gap-6">
|
| 218 |
-
{/* Badge */}
|
| 219 |
-
<div className="text-center flex-shrink-0">
|
| 220 |
-
<motion.div
|
| 221 |
-
className="w-24 h-24 rounded-full border-2 flex items-center justify-center mx-auto mb-3 text-4xl"
|
| 222 |
-
style={{ borderColor: color, boxShadow: `0 0 30px ${color}44`, background: `${color}10` }}
|
| 223 |
-
animate={{ boxShadow: [`0 0 20px ${color}30`, `0 0 50px ${color}60`, `0 0 20px ${color}30`] }}
|
| 224 |
-
transition={{ duration: 2, repeat: Infinity }}
|
| 225 |
-
>
|
| 226 |
-
{isFake ? '⚠️' : '✅'}
|
| 227 |
-
</motion.div>
|
| 228 |
-
<motion.p
|
| 229 |
-
className="text-2xl font-bold tracking-widest"
|
| 230 |
-
style={{ color, textShadow: `0 0 20px ${color}66` }}
|
| 231 |
-
animate={isFake ? { textShadow: [`0 0 10px ${color}44`, `0 0 25px ${color}88`, `0 0 10px ${color}44`] } : {}}
|
| 232 |
-
transition={{ duration: 2.5, repeat: Infinity }}
|
| 233 |
-
>
|
| 234 |
-
{isFake ? 'DEEPFAKE' : 'AUTHENTIC'}
|
| 235 |
-
</motion.p>
|
| 236 |
-
<p className="text-[10px] text-white/30 tracking-widest mt-1">VERDICT</p>
|
| 237 |
-
</div>
|
| 238 |
-
|
| 239 |
-
{/* Confidence */}
|
| 240 |
-
<div className="flex-1 min-w-[200px]">
|
| 241 |
-
<p className="text-[10px] text-white/40 tracking-widest uppercase mb-3">Confidence Score</p>
|
| 242 |
-
<ConfidenceArc pct={result.confidence} isFake={isFake} />
|
| 243 |
-
|
| 244 |
-
{/* Risk pill */}
|
| 245 |
-
<div className="mt-4 flex items-center justify-between bg-white/3 rounded-xl px-4 py-3 border border-white/5">
|
| 246 |
-
<span className="text-[10px] text-white/40 tracking-wider uppercase">Risk Level</span>
|
| 247 |
-
<span
|
| 248 |
-
className="text-xs font-bold tracking-wider px-3 py-1 rounded-full border"
|
| 249 |
-
style={{
|
| 250 |
-
color: result.confidence < 35 ? '#00ff88' : result.confidence < 65 ? '#ffaa00' : '#ff3355',
|
| 251 |
-
borderColor: result.confidence < 35 ? '#00ff8844' : result.confidence < 65 ? '#ffaa0044' : '#ff335544',
|
| 252 |
-
background: result.confidence < 35 ? '#00ff8810' : result.confidence < 65 ? '#ffaa0010' : '#ff335510',
|
| 253 |
-
}}
|
| 254 |
-
>
|
| 255 |
-
{result.confidence < 35 ? 'LOW' : result.confidence < 65 ? 'MEDIUM' : result.confidence < 80 ? 'HIGH' : 'CRITICAL'}
|
| 256 |
-
</span>
|
| 257 |
-
</div>
|
| 258 |
-
</div>
|
| 259 |
-
</div>
|
| 260 |
-
</motion.div>
|
| 261 |
-
|
| 262 |
-
{/* Insights + Meta */}
|
| 263 |
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 264 |
-
{/* Insights */}
|
| 265 |
-
<div className="rounded-2xl border border-white/6 bg-white/2 p-5">
|
| 266 |
-
<div className="flex items-center gap-2 mb-4">
|
| 267 |
-
<div className="w-0.5 h-4 rounded-full bg-[#00aaff] shadow-[0_0_8px_#00aaff]" />
|
| 268 |
-
<span className="text-[10px] font-bold tracking-widest text-[#00aaff] uppercase">Analysis Insights</span>
|
| 269 |
-
</div>
|
| 270 |
-
<div className="space-y-2">
|
| 271 |
-
{result.details.map((d, i) => (
|
| 272 |
-
<motion.div
|
| 273 |
-
key={i}
|
| 274 |
-
className="flex items-start gap-2.5 text-xs text-white/60 bg-white/2 rounded-lg px-3 py-2.5 border-l-2"
|
| 275 |
-
style={{ borderLeftColor: color }}
|
| 276 |
-
initial={{ opacity: 0, x: -10 }}
|
| 277 |
-
animate={{ opacity: 1, x: 0 }}
|
| 278 |
-
transition={{ delay: 0.1 + i * 0.07 }}
|
| 279 |
-
>
|
| 280 |
-
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0 mt-1" style={{ background: color }} />
|
| 281 |
-
{d}
|
| 282 |
-
</motion.div>
|
| 283 |
-
))}
|
| 284 |
-
</div>
|
| 285 |
-
</div>
|
| 286 |
-
|
| 287 |
-
{/* Metadata */}
|
| 288 |
-
<div className="rounded-2xl border border-white/6 bg-white/2 p-5">
|
| 289 |
-
<div className="flex items-center gap-2 mb-4">
|
| 290 |
-
<div className="w-0.5 h-4 rounded-full bg-[#00aaff] shadow-[0_0_8px_#00aaff]" />
|
| 291 |
-
<span className="text-[10px] font-bold tracking-widest text-[#00aaff] uppercase">Video Metadata</span>
|
| 292 |
-
</div>
|
| 293 |
-
<div className="space-y-2.5">
|
| 294 |
-
{[
|
| 295 |
-
['Frames Analyzed', result.metadata.frames_analyzed],
|
| 296 |
-
['Faces Detected', result.metadata.frames_with_faces],
|
| 297 |
-
['Duration', `${result.metadata.video_duration_sec}s`],
|
| 298 |
-
['FPS', result.metadata.video_fps],
|
| 299 |
-
['Resolution', result.metadata.resolution],
|
| 300 |
-
['Processing Time', `${result.processing_time_sec}s`],
|
| 301 |
-
].map(([k, v]) => (
|
| 302 |
-
<div key={String(k)} className="flex justify-between items-center border-b border-white/4 pb-2 last:border-0 last:pb-0">
|
| 303 |
-
<span className="text-[11px] text-white/40">{k}</span>
|
| 304 |
-
<span className="text-[12px] font-bold font-mono text-white">{v ?? '—'}</span>
|
| 305 |
-
</div>
|
| 306 |
-
))}
|
| 307 |
-
</div>
|
| 308 |
-
<div className="mt-4 pt-3 border-t border-white/5 flex items-center gap-2">
|
| 309 |
-
<div className="w-1.5 h-1.5 rounded-full bg-[#00ff88] shadow-[0_0_6px_#00ff88]" />
|
| 310 |
-
<span className="text-[10px] text-white/30 font-mono tracking-wider">ViT Ensemble · 2 Models</span>
|
| 311 |
-
</div>
|
| 312 |
-
</div>
|
| 313 |
-
</div>
|
| 314 |
-
|
| 315 |
-
{/* Frame Timeline */}
|
| 316 |
-
<div className="rounded-2xl border border-white/6 bg-white/2 p-5">
|
| 317 |
-
<div className="flex items-center gap-2 mb-4">
|
| 318 |
-
<div className="w-0.5 h-4 rounded-full bg-[#00aaff] shadow-[0_0_8px_#00aaff]" />
|
| 319 |
-
<span className="text-[10px] font-bold tracking-widest text-[#00aaff] uppercase">Frame Analysis Timeline</span>
|
| 320 |
-
<span className="text-[10px] text-white/20 font-mono ml-1">— {result.frame_timeline.length} frames</span>
|
| 321 |
-
</div>
|
| 322 |
-
<Timeline frames={result.frame_timeline} isFake={isFake} />
|
| 323 |
-
<div className="flex justify-between mt-2">
|
| 324 |
-
<span className="text-[10px] text-white/20 font-mono">Frame 0</span>
|
| 325 |
-
<span className="text-[10px] text-white/20 font-mono">Frame {result.frame_timeline.length}</span>
|
| 326 |
-
</div>
|
| 327 |
-
</div>
|
| 328 |
-
|
| 329 |
-
{/* Audio */}
|
| 330 |
-
{result.audio && <AudioCard audio={result.audio} />}
|
| 331 |
-
|
| 332 |
-
{/* Reset */}
|
| 333 |
-
<div className="text-center pt-2">
|
| 334 |
-
<motion.button
|
| 335 |
-
onClick={reset}
|
| 336 |
-
className="px-6 py-2.5 text-xs font-bold tracking-widest uppercase text-white/40 border border-white/10 rounded-xl hover:text-[#00ff88] hover:border-[#00ff8840] hover:bg-[#00ff8808] transition-all duration-200"
|
| 337 |
-
whileHover={{ scale: 1.02 }}
|
| 338 |
-
whileTap={{ scale: 0.98 }}
|
| 339 |
-
>
|
| 340 |
-
↻ Analyze Another Video
|
| 341 |
-
</motion.button>
|
| 342 |
-
</div>
|
| 343 |
-
</motion.div>
|
| 344 |
-
</AnimatePresence>
|
| 345 |
-
)
|
| 346 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/UploadZone.tsx
DELETED
|
@@ -1,190 +0,0 @@
|
|
| 1 |
-
import { useRef, useState } from 'react'
|
| 2 |
-
import { motion } from 'framer-motion'
|
| 3 |
-
import { useStore } from '../store'
|
| 4 |
-
import { analyzeVideo } from '../api'
|
| 5 |
-
|
| 6 |
-
const AGENTS = [
|
| 7 |
-
{ icon: '🎬', label: 'Frame Extractor', sub: 'Sampling keyframes' },
|
| 8 |
-
{ icon: '👤', label: 'Face Detector', sub: 'Isolating regions' },
|
| 9 |
-
{ icon: '🧠', label: 'ViT Ensemble', sub: 'Neural inference' },
|
| 10 |
-
{ icon: '📊', label: 'Report Builder', sub: 'Compiling results' },
|
| 11 |
-
]
|
| 12 |
-
|
| 13 |
-
function fmtBytes(b: number) {
|
| 14 |
-
if (b < 1024) return `${b} B`
|
| 15 |
-
if (b < 1048576) return `${(b / 1024).toFixed(1)} KB`
|
| 16 |
-
return `${(b / 1048576).toFixed(1)} MB`
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
export default function UploadZone() {
|
| 20 |
-
const { file, state, setFile, setState, setResult, setError, setAgentStep, reset } = useStore()
|
| 21 |
-
const inputRef = useRef<HTMLInputElement>(null)
|
| 22 |
-
const [dragging, setDragging] = useState(false)
|
| 23 |
-
|
| 24 |
-
const isLoading = state === 'loading'
|
| 25 |
-
|
| 26 |
-
function applyFile(f: File) {
|
| 27 |
-
if (f.type.startsWith('video/')) setFile(f)
|
| 28 |
-
else setError('Please select a valid video file (MP4, AVI, MOV, MKV, WebM).')
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
async function handleAnalyze() {
|
| 32 |
-
if (!file) return
|
| 33 |
-
setState('loading')
|
| 34 |
-
setAgentStep(0)
|
| 35 |
-
|
| 36 |
-
// Simulate agent steps
|
| 37 |
-
const delays = [0, 1800, 4200, 7500]
|
| 38 |
-
delays.forEach((d, i) => setTimeout(() => setAgentStep(i), d))
|
| 39 |
-
|
| 40 |
-
try {
|
| 41 |
-
const result = await analyzeVideo(file)
|
| 42 |
-
setResult(result)
|
| 43 |
-
} catch (e: any) {
|
| 44 |
-
setError(e.message || 'Analysis failed')
|
| 45 |
-
}
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
return (
|
| 49 |
-
<div className="w-full max-w-xl mx-auto flex flex-col gap-5">
|
| 50 |
-
{/* Drop zone */}
|
| 51 |
-
<motion.div
|
| 52 |
-
className={`relative rounded-2xl border-2 border-dashed cursor-pointer transition-all duration-300 overflow-hidden
|
| 53 |
-
${dragging ? 'border-[#00ff88] bg-[#00ff8808] shadow-[0_0_40px_#00ff8820]' : 'border-[#00ff8830] hover:border-[#00ff8866] hover:bg-[#00ff8806]'}
|
| 54 |
-
${file ? 'py-6 px-8' : 'py-12 px-8'}`}
|
| 55 |
-
onClick={() => !file && inputRef.current?.click()}
|
| 56 |
-
onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
|
| 57 |
-
onDragLeave={() => setDragging(false)}
|
| 58 |
-
onDrop={(e) => {
|
| 59 |
-
e.preventDefault(); setDragging(false)
|
| 60 |
-
const f = e.dataTransfer.files[0]
|
| 61 |
-
if (f) applyFile(f)
|
| 62 |
-
}}
|
| 63 |
-
whileHover={{ scale: file ? 1 : 1.01 }}
|
| 64 |
-
whileTap={{ scale: 0.99 }}
|
| 65 |
-
>
|
| 66 |
-
<input
|
| 67 |
-
ref={inputRef}
|
| 68 |
-
type="file"
|
| 69 |
-
accept="video/*"
|
| 70 |
-
className="hidden"
|
| 71 |
-
onChange={(e) => e.target.files?.[0] && applyFile(e.target.files[0])}
|
| 72 |
-
/>
|
| 73 |
-
|
| 74 |
-
{!file ? (
|
| 75 |
-
<motion.div className="text-center" initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
|
| 76 |
-
{/* Orbit upload icon */}
|
| 77 |
-
<div className="relative w-20 h-20 mx-auto mb-5">
|
| 78 |
-
<motion.div
|
| 79 |
-
className="absolute inset-0 rounded-full border border-dashed border-[#00ff8840]"
|
| 80 |
-
animate={{ rotate: 360 }}
|
| 81 |
-
transition={{ duration: 8, repeat: Infinity, ease: 'linear' }}
|
| 82 |
-
/>
|
| 83 |
-
<motion.div
|
| 84 |
-
className="absolute inset-3 rounded-full border border-[#00ff8820]"
|
| 85 |
-
animate={{ rotate: -360 }}
|
| 86 |
-
transition={{ duration: 5, repeat: Infinity, ease: 'linear' }}
|
| 87 |
-
/>
|
| 88 |
-
<div className="absolute inset-0 flex items-center justify-center">
|
| 89 |
-
<svg width="28" height="28" fill="none" stroke="#00ff8899" strokeWidth="1.5" viewBox="0 0 24 24">
|
| 90 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/>
|
| 91 |
-
</svg>
|
| 92 |
-
</div>
|
| 93 |
-
</div>
|
| 94 |
-
<p className="text-white/80 font-semibold text-base mb-1">Drop video for forensic analysis</p>
|
| 95 |
-
<p className="text-white/40 text-sm">Drag & drop or click to browse</p>
|
| 96 |
-
<p className="text-white/20 text-xs mt-3 font-mono">MP4 · AVI · MOV · MKV · WebM — Max 100MB</p>
|
| 97 |
-
</motion.div>
|
| 98 |
-
) : (
|
| 99 |
-
<motion.div
|
| 100 |
-
className="flex items-center gap-4"
|
| 101 |
-
initial={{ opacity: 0, y: 8 }}
|
| 102 |
-
animate={{ opacity: 1, y: 0 }}
|
| 103 |
-
>
|
| 104 |
-
<div className="w-12 h-12 rounded-xl bg-[#00ff8812] border border-[#00ff8830] flex items-center justify-center flex-shrink-0 shadow-[0_0_20px_#00ff8820]">
|
| 105 |
-
<svg width="22" height="22" fill="none" stroke="#00ff88" strokeWidth="1.5" viewBox="0 0 24 24">
|
| 106 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"/>
|
| 107 |
-
</svg>
|
| 108 |
-
</div>
|
| 109 |
-
<div className="flex-1 min-w-0">
|
| 110 |
-
<p className="text-white font-semibold text-sm truncate">{file.name}</p>
|
| 111 |
-
<p className="text-white/40 text-xs mt-0.5 font-mono">{fmtBytes(file.size)}</p>
|
| 112 |
-
</div>
|
| 113 |
-
<button
|
| 114 |
-
onClick={(e) => { e.stopPropagation(); reset() }}
|
| 115 |
-
className="p-2 text-white/30 hover:text-red-400 transition-colors rounded-lg hover:bg-red-400/10"
|
| 116 |
-
>
|
| 117 |
-
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
| 118 |
-
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
| 119 |
-
</svg>
|
| 120 |
-
</button>
|
| 121 |
-
</motion.div>
|
| 122 |
-
)}
|
| 123 |
-
</motion.div>
|
| 124 |
-
|
| 125 |
-
{/* Analyze button */}
|
| 126 |
-
<motion.button
|
| 127 |
-
disabled={!file || isLoading}
|
| 128 |
-
onClick={handleAnalyze}
|
| 129 |
-
className={`w-full py-4 rounded-xl font-bold text-sm tracking-widest uppercase transition-all duration-300 relative overflow-hidden
|
| 130 |
-
${file && !isLoading
|
| 131 |
-
? 'bg-gradient-to-r from-[#00ff88] to-[#00cc66] text-[#050508] shadow-[0_0_30px_#00ff8840] cursor-pointer'
|
| 132 |
-
: 'bg-white/5 text-white/20 cursor-not-allowed border border-white/10'}`}
|
| 133 |
-
whileHover={file && !isLoading ? { scale: 1.02, boxShadow: '0 0 50px #00ff8860' } : {}}
|
| 134 |
-
whileTap={file && !isLoading ? { scale: 0.98 } : {}}
|
| 135 |
-
>
|
| 136 |
-
{/* Shimmer */}
|
| 137 |
-
{file && !isLoading && (
|
| 138 |
-
<motion.div
|
| 139 |
-
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent skew-x-12"
|
| 140 |
-
animate={{ x: ['-100%', '200%'] }}
|
| 141 |
-
transition={{ duration: 2.5, repeat: Infinity, ease: 'linear' }}
|
| 142 |
-
/>
|
| 143 |
-
)}
|
| 144 |
-
<span className="relative z-10">
|
| 145 |
-
{isLoading ? '⏳ Analyzing...' : '▶ Analyze Video'}
|
| 146 |
-
</span>
|
| 147 |
-
</motion.button>
|
| 148 |
-
|
| 149 |
-
{/* Agent pipeline (loading state) */}
|
| 150 |
-
{isLoading && <AgentPipeline />}
|
| 151 |
-
</div>
|
| 152 |
-
)
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
function AgentPipeline() {
|
| 156 |
-
const agentStep = useStore((s) => s.agentStep)
|
| 157 |
-
|
| 158 |
-
return (
|
| 159 |
-
<motion.div
|
| 160 |
-
initial={{ opacity: 0, y: 12 }}
|
| 161 |
-
animate={{ opacity: 1, y: 0 }}
|
| 162 |
-
className="grid grid-cols-2 gap-3"
|
| 163 |
-
>
|
| 164 |
-
{AGENTS.map((agent, i) => {
|
| 165 |
-
const active = i <= agentStep
|
| 166 |
-
const current = i === agentStep
|
| 167 |
-
return (
|
| 168 |
-
<motion.div
|
| 169 |
-
key={i}
|
| 170 |
-
className={`rounded-xl p-3 border transition-all duration-500 flex items-center gap-3
|
| 171 |
-
${active
|
| 172 |
-
? 'border-[#00ff8844] bg-[#00ff8808] shadow-[0_0_15px_#00ff8815]'
|
| 173 |
-
: 'border-white/5 bg-white/2'}`}
|
| 174 |
-
animate={current ? { scale: [1, 1.02, 1] } : {}}
|
| 175 |
-
transition={{ duration: 0.6, repeat: current ? Infinity : 0 }}
|
| 176 |
-
>
|
| 177 |
-
<div className={`w-2 h-2 rounded-full flex-shrink-0 transition-all duration-500
|
| 178 |
-
${active ? 'bg-[#00ff88] shadow-[0_0_8px_#00ff88]' : 'bg-white/15'}`} />
|
| 179 |
-
<div>
|
| 180 |
-
<p className="text-xs font-semibold text-white/80">{agent.label}</p>
|
| 181 |
-
<p className={`text-[10px] font-mono transition-colors duration-300 ${active ? 'text-[#00ff88]' : 'text-white/30'}`}>
|
| 182 |
-
{active ? (current ? agent.sub : 'Done ✓') : 'Waiting...'}
|
| 183 |
-
</p>
|
| 184 |
-
</div>
|
| 185 |
-
</motion.div>
|
| 186 |
-
)
|
| 187 |
-
})}
|
| 188 |
-
</motion.div>
|
| 189 |
-
)
|
| 190 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/index.css
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
@import "tailwindcss";
|
| 2 |
-
|
| 3 |
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 4 |
-
|
| 5 |
-
body {
|
| 6 |
-
background: #050508;
|
| 7 |
-
color: #e2e8f0;
|
| 8 |
-
font-family: 'Space Grotesk', 'Inter', sans-serif;
|
| 9 |
-
-webkit-font-smoothing: antialiased;
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
::-webkit-scrollbar { width: 4px; height: 4px; }
|
| 13 |
-
::-webkit-scrollbar-track { background: #050508; }
|
| 14 |
-
::-webkit-scrollbar-thumb { background: rgba(0,255,136,0.2); border-radius: 4px; }
|
| 15 |
-
|
| 16 |
-
/* Animated grid background */
|
| 17 |
-
body::before {
|
| 18 |
-
content: '';
|
| 19 |
-
position: fixed;
|
| 20 |
-
inset: 0;
|
| 21 |
-
z-index: 0;
|
| 22 |
-
pointer-events: none;
|
| 23 |
-
background-image:
|
| 24 |
-
linear-gradient(rgba(0,255,136,0.025) 1px, transparent 1px),
|
| 25 |
-
linear-gradient(90deg, rgba(0,255,136,0.025) 1px, transparent 1px);
|
| 26 |
-
background-size: 60px 60px;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
/* Radial glow at top */
|
| 30 |
-
body::after {
|
| 31 |
-
content: '';
|
| 32 |
-
position: fixed;
|
| 33 |
-
top: -200px;
|
| 34 |
-
left: 50%;
|
| 35 |
-
transform: translateX(-50%);
|
| 36 |
-
width: 800px;
|
| 37 |
-
height: 600px;
|
| 38 |
-
background: radial-gradient(ellipse, rgba(0,255,136,0.06) 0%, transparent 70%);
|
| 39 |
-
pointer-events: none;
|
| 40 |
-
z-index: 0;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
main, header, footer { position: relative; z-index: 1; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/main.jsx
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
import React from 'react'
|
| 2 |
-
import ReactDOM from 'react-dom/client'
|
| 3 |
-
import App from './App.jsx'
|
| 4 |
-
import './index.css'
|
| 5 |
-
|
| 6 |
-
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 7 |
-
<React.StrictMode>
|
| 8 |
-
<App />
|
| 9 |
-
</React.StrictMode>,
|
| 10 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/main.tsx
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
import { StrictMode } from 'react'
|
| 2 |
-
import { createRoot } from 'react-dom/client'
|
| 3 |
-
import './index.css'
|
| 4 |
-
import App from './App.tsx'
|
| 5 |
-
|
| 6 |
-
createRoot(document.getElementById('root')!).render(
|
| 7 |
-
<StrictMode>
|
| 8 |
-
<App />
|
| 9 |
-
</StrictMode>,
|
| 10 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/store.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
import { create } from 'zustand'
|
| 2 |
-
|
| 3 |
-
export type AppState = 'idle' | 'loading' | 'result' | 'error'
|
| 4 |
-
export type Verdict = 'FAKE' | 'REAL' | null
|
| 5 |
-
|
| 6 |
-
export interface FramePoint { frame: number; fake_pct: number }
|
| 7 |
-
export interface AudioResult {
|
| 8 |
-
available: boolean
|
| 9 |
-
result: string
|
| 10 |
-
confidence: number
|
| 11 |
-
fake_probability: number
|
| 12 |
-
model_score: number
|
| 13 |
-
heuristic_score: number
|
| 14 |
-
details: string[]
|
| 15 |
-
features: Record<string, number>
|
| 16 |
-
}
|
| 17 |
-
export interface AnalysisResult {
|
| 18 |
-
result: 'FAKE' | 'REAL'
|
| 19 |
-
confidence: number
|
| 20 |
-
details: string[]
|
| 21 |
-
frame_timeline: FramePoint[]
|
| 22 |
-
metadata: {
|
| 23 |
-
frames_analyzed: number
|
| 24 |
-
frames_with_faces: number
|
| 25 |
-
video_duration_sec: number
|
| 26 |
-
video_fps: number
|
| 27 |
-
resolution: string
|
| 28 |
-
}
|
| 29 |
-
processing_time_sec: number
|
| 30 |
-
audio: AudioResult
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
interface Store {
|
| 34 |
-
state: AppState
|
| 35 |
-
file: File | null
|
| 36 |
-
result: AnalysisResult | null
|
| 37 |
-
error: string | null
|
| 38 |
-
agentStep: number
|
| 39 |
-
setState: (s: AppState) => void
|
| 40 |
-
setFile: (f: File | null) => void
|
| 41 |
-
setResult: (r: AnalysisResult) => void
|
| 42 |
-
setError: (e: string) => void
|
| 43 |
-
setAgentStep: (n: number) => void
|
| 44 |
-
reset: () => void
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
export const useStore = create<Store>((set) => ({
|
| 48 |
-
state: 'idle',
|
| 49 |
-
file: null,
|
| 50 |
-
result: null,
|
| 51 |
-
error: null,
|
| 52 |
-
agentStep: -1,
|
| 53 |
-
setState: (state) => set({ state }),
|
| 54 |
-
setFile: (file) => set({ file }),
|
| 55 |
-
setResult: (result) => set({ result, state: 'result' }),
|
| 56 |
-
setError: (error) => set({ error, state: 'error' }),
|
| 57 |
-
setAgentStep: (agentStep) => set({ agentStep }),
|
| 58 |
-
reset: () => set({ state: 'idle', file: null, result: null, error: null, agentStep: -1 }),
|
| 59 |
-
}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/vite.config.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
| 1 |
-
import { defineConfig } from 'vite'
|
| 2 |
-
import react from '@vitejs/plugin-react'
|
| 3 |
-
import tailwindcss from '@tailwindcss/vite'
|
| 4 |
-
|
| 5 |
-
export default defineConfig({
|
| 6 |
-
plugins: [react(), tailwindcss()],
|
| 7 |
-
server: {
|
| 8 |
-
port: 5173,
|
| 9 |
-
proxy: {
|
| 10 |
-
'/analyze': 'http://localhost:8000',
|
| 11 |
-
'/health': 'http://localhost:8000',
|
| 12 |
-
},
|
| 13 |
-
},
|
| 14 |
-
build: {
|
| 15 |
-
outDir: '../frontend-dist',
|
| 16 |
-
emptyOutDir: true,
|
| 17 |
-
},
|
| 18 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
render.yaml
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
services:
|
| 2 |
-
- type: web
|
| 3 |
-
name: authrix-api
|
| 4 |
-
runtime: docker
|
| 5 |
-
dockerfilePath: ./Dockerfile
|
| 6 |
-
plan: standard
|
| 7 |
-
healthCheckPath: /health
|
| 8 |
-
envVars:
|
| 9 |
-
- key: PORT
|
| 10 |
-
value: 8000
|
| 11 |
-
- key: PYTHONUNBUFFERED
|
| 12 |
-
value: "1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setup.bat
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
@echo off
|
| 2 |
-
echo.
|
| 3 |
-
echo ==========================================
|
| 4 |
-
echo DEEPFAKE AUTHENTICATOR -- SETUP
|
| 5 |
-
echo ==========================================
|
| 6 |
-
echo.
|
| 7 |
-
|
| 8 |
-
where python >nul 2>&1
|
| 9 |
-
if %errorlevel% neq 0 (
|
| 10 |
-
echo ERROR: Python not found. Please install Python 3.9+
|
| 11 |
-
pause
|
| 12 |
-
exit /b 1
|
| 13 |
-
)
|
| 14 |
-
|
| 15 |
-
echo Creating virtual environment...
|
| 16 |
-
python -m venv venv
|
| 17 |
-
call venv\Scripts\activate.bat
|
| 18 |
-
|
| 19 |
-
echo Upgrading pip...
|
| 20 |
-
python -m pip install --upgrade pip -q
|
| 21 |
-
|
| 22 |
-
echo Installing core dependencies...
|
| 23 |
-
pip install fastapi==0.111.0 "uvicorn[standard]==0.29.0" python-multipart==0.0.9 -q
|
| 24 |
-
pip install opencv-python==4.9.0.80 mediapipe==0.10.14 numpy==1.26.4 Pillow==10.3.0 -q
|
| 25 |
-
|
| 26 |
-
echo Installing HuggingFace dependencies (optional)...
|
| 27 |
-
pip install transformers==4.41.0 torch==2.3.0 --index-url https://download.pytorch.org/whl/cpu -q
|
| 28 |
-
if %errorlevel% neq 0 (
|
| 29 |
-
echo WARNING: torch/transformers install failed -- will use heuristic fallback
|
| 30 |
-
)
|
| 31 |
-
|
| 32 |
-
echo.
|
| 33 |
-
echo ==========================================
|
| 34 |
-
echo SETUP COMPLETE
|
| 35 |
-
echo ==========================================
|
| 36 |
-
echo.
|
| 37 |
-
echo To start the server:
|
| 38 |
-
echo venv\Scripts\activate.bat
|
| 39 |
-
echo cd backend
|
| 40 |
-
echo python main.py
|
| 41 |
-
echo.
|
| 42 |
-
echo Then open: http://localhost:8000
|
| 43 |
-
echo.
|
| 44 |
-
pause
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setup.sh
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
# Deepfake Authenticator — Setup Script
|
| 3 |
-
|
| 4 |
-
set -e
|
| 5 |
-
|
| 6 |
-
echo ""
|
| 7 |
-
echo "╔══════════════════════════════════════════╗"
|
| 8 |
-
echo "║ DEEPFAKE AUTHENTICATOR — SETUP ║"
|
| 9 |
-
echo "╚══════════════════════════════════════════╝"
|
| 10 |
-
echo ""
|
| 11 |
-
|
| 12 |
-
# Check Python
|
| 13 |
-
if ! command -v python3 &>/dev/null; then
|
| 14 |
-
echo "❌ Python 3 not found. Please install Python 3.9+"
|
| 15 |
-
exit 1
|
| 16 |
-
fi
|
| 17 |
-
|
| 18 |
-
PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
| 19 |
-
echo "✅ Python $PYTHON_VERSION detected"
|
| 20 |
-
|
| 21 |
-
# Create virtual environment
|
| 22 |
-
echo ""
|
| 23 |
-
echo "📦 Creating virtual environment..."
|
| 24 |
-
python3 -m venv venv
|
| 25 |
-
source venv/bin/activate
|
| 26 |
-
|
| 27 |
-
# Upgrade pip
|
| 28 |
-
pip install --upgrade pip -q
|
| 29 |
-
|
| 30 |
-
# Install dependencies
|
| 31 |
-
echo ""
|
| 32 |
-
echo "📥 Installing dependencies..."
|
| 33 |
-
echo " (This may take a few minutes for torch/transformers)"
|
| 34 |
-
echo ""
|
| 35 |
-
|
| 36 |
-
pip install fastapi==0.111.0 uvicorn[standard]==0.29.0 python-multipart==0.0.9 -q
|
| 37 |
-
pip install opencv-python==4.9.0.80 mediapipe==0.10.14 numpy==1.26.4 Pillow==10.3.0 -q
|
| 38 |
-
|
| 39 |
-
echo ""
|
| 40 |
-
echo "🤖 Installing HuggingFace model dependencies..."
|
| 41 |
-
echo " (Optional — skip with Ctrl+C if you want heuristic-only mode)"
|
| 42 |
-
pip install transformers==4.41.0 torch==2.3.0 --index-url https://download.pytorch.org/whl/cpu -q || \
|
| 43 |
-
echo "⚠️ torch/transformers install failed — will use heuristic fallback"
|
| 44 |
-
|
| 45 |
-
echo ""
|
| 46 |
-
echo "╔══════════════════════════════════════════╗"
|
| 47 |
-
echo "║ SETUP COMPLETE ✅ ║"
|
| 48 |
-
echo "╚══════════════════════════════════════════╝"
|
| 49 |
-
echo ""
|
| 50 |
-
echo "To start the server:"
|
| 51 |
-
echo " source venv/bin/activate"
|
| 52 |
-
echo " cd backend && python main.py"
|
| 53 |
-
echo ""
|
| 54 |
-
echo "Then open: http://localhost:8000"
|
| 55 |
-
echo ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start.bat
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
@echo off
|
| 2 |
-
title Deepfake Authenticator
|
| 3 |
-
color 0A
|
| 4 |
-
|
| 5 |
-
echo.
|
| 6 |
-
echo ============================================
|
| 7 |
-
echo DEEPFAKE AUTHENTICATOR - Starting...
|
| 8 |
-
echo ============================================
|
| 9 |
-
echo.
|
| 10 |
-
|
| 11 |
-
:: Check if venv exists
|
| 12 |
-
if not exist "venv\Scripts\activate.bat" (
|
| 13 |
-
echo [!] Virtual environment not found.
|
| 14 |
-
echo [*] Run setup.bat first to install dependencies.
|
| 15 |
-
echo.
|
| 16 |
-
pause
|
| 17 |
-
exit /b 1
|
| 18 |
-
)
|
| 19 |
-
|
| 20 |
-
:: Activate venv
|
| 21 |
-
call venv\Scripts\activate.bat
|
| 22 |
-
|
| 23 |
-
:: Kill anything on port 8000
|
| 24 |
-
echo [*] Clearing port 8000...
|
| 25 |
-
for /f "tokens=5" %%a in ('netstat -aon ^| findstr ":8000 " 2^>nul') do (
|
| 26 |
-
taskkill /PID %%a /F >nul 2>&1
|
| 27 |
-
)
|
| 28 |
-
|
| 29 |
-
echo [*] Starting server...
|
| 30 |
-
echo [*] Open your browser at: http://localhost:8000
|
| 31 |
-
echo.
|
| 32 |
-
echo Press Ctrl+C to stop the server.
|
| 33 |
-
echo ============================================
|
| 34 |
-
echo.
|
| 35 |
-
|
| 36 |
-
:: Open browser after 5 seconds in background
|
| 37 |
-
start /b cmd /c "timeout /t 8 /nobreak >nul && start http://localhost:8000"
|
| 38 |
-
|
| 39 |
-
:: Start server
|
| 40 |
-
cd backend
|
| 41 |
-
python -m uvicorn main:app --host 0.0.0.0 --port 8000 --log-level warning
|
| 42 |
-
|
| 43 |
-
echo.
|
| 44 |
-
echo Server stopped.
|
| 45 |
-
pause
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start.sh
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
clear
|
| 3 |
-
echo ""
|
| 4 |
-
echo " ============================================"
|
| 5 |
-
echo " DEEPFAKE AUTHENTICATOR - Starting..."
|
| 6 |
-
echo " ============================================"
|
| 7 |
-
echo ""
|
| 8 |
-
|
| 9 |
-
# Check venv
|
| 10 |
-
if [ ! -f "venv/bin/activate" ]; then
|
| 11 |
-
echo " [!] Virtual environment not found."
|
| 12 |
-
echo " [*] Run ./setup.sh first to install dependencies."
|
| 13 |
-
echo ""
|
| 14 |
-
exit 1
|
| 15 |
-
fi
|
| 16 |
-
|
| 17 |
-
source venv/bin/activate
|
| 18 |
-
|
| 19 |
-
# Kill anything on port 8000
|
| 20 |
-
echo " [*] Clearing port 8000..."
|
| 21 |
-
lsof -ti:8000 | xargs kill -9 2>/dev/null
|
| 22 |
-
|
| 23 |
-
echo " [*] Starting server..."
|
| 24 |
-
echo " [*] Open your browser at: http://localhost:8000"
|
| 25 |
-
echo ""
|
| 26 |
-
echo " Press Ctrl+C to stop."
|
| 27 |
-
echo " ============================================"
|
| 28 |
-
echo ""
|
| 29 |
-
|
| 30 |
-
# Open browser after 8s in background
|
| 31 |
-
(sleep 8 && open "http://localhost:8000" 2>/dev/null || xdg-open "http://localhost:8000" 2>/dev/null) &
|
| 32 |
-
|
| 33 |
-
cd backend
|
| 34 |
-
python -m uvicorn main:app --host 0.0.0.0 --port 8000 --log-level warning
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|