Jubaer43434 commited on
Commit
df9b356
·
1 Parent(s): a7342ae

Add initial project files

Browse files
Files changed (7) hide show
  1. .DS_Store +0 -0
  2. Dockerfile +29 -0
  3. app.py +126 -0
  4. assets/face.jpg +0 -0
  5. requirements.txt +9 -0
  6. src/inswapper_128.onnx +3 -0
  7. templates/index.html +323 -0
.DS_Store ADDED
Binary file (6.15 kB). View file
 
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Install system dependencies for building PyQt5 and OpenCV
4
+ RUN apt-get update && apt-get install -y \
5
+ build-essential \
6
+ libgl1-mesa-glx \
7
+ libglib2.0-0 \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Set the working directory
11
+ WORKDIR /app
12
+
13
+ # Copy requirements and install Python dependencies
14
+ COPY requirements.txt ./
15
+ RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Copy the rest of the application code
18
+ COPY . .
19
+
20
+ # Expose the port (adjust if needed)
21
+ EXPOSE 7890
22
+
23
+ # Set environment variables for a Flask app
24
+ ENV FLASK_APP=app.py
25
+ ENV FLASK_RUN_HOST=0.0.0.0
26
+
27
+ # Command to run the application
28
+ CMD ["flask", "run", "--port=7890"]
29
+
app.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, Response, request, jsonify, send_file
2
+ import cv2
3
+ import numpy as np
4
+ import insightface
5
+ import base64
6
+ from insightface.app import FaceAnalysis
7
+ import os
8
+ from werkzeug.utils import secure_filename
9
+ import shutil
10
+
11
+ app = Flask(__name__)
12
+
13
+ # Initialize the face detection and swapping models
14
+ app_model = FaceAnalysis(name='buffalo_l')
15
+ app_model.prepare(ctx_id=0, det_size=(640, 640))
16
+
17
+ swapper_model_path = 'src/inswapper_128.onnx'
18
+ swapper = insightface.model_zoo.get_model(swapper_model_path, download=False, download_zip=False)
19
+
20
+ # Load the default image for face swapping
21
+ default_img_path = 'assets/face.jpg'
22
+ default_img = cv2.imread(default_img_path)
23
+
24
+ # Make sure the assets directory exists
25
+ if not os.path.exists('assets'):
26
+ os.makedirs('assets')
27
+
28
+ # Check if a face is detected in the default image
29
+ faces_in_default_img = app_model.get(default_img)
30
+ if len(faces_in_default_img) == 0:
31
+ raise Exception("No face detected in the default image.")
32
+
33
+ default_face = faces_in_default_img[0]
34
+
35
+
36
+ @app.route('/')
37
+ def index():
38
+ return render_template('index.html')
39
+
40
+
41
+ @app.route('/process_frame', methods=['POST'])
42
+ def process_frame():
43
+ try:
44
+ data = request.json['image']
45
+ img_data = base64.b64decode(data.split(',')[1])
46
+ np_img = np.frombuffer(img_data, np.uint8)
47
+ frame = cv2.imdecode(np_img, cv2.IMREAD_COLOR)
48
+
49
+ faces_in_frame = app_model.get(frame)
50
+ if len(faces_in_frame) > 0:
51
+ face_in_frame = faces_in_frame[0]
52
+ try:
53
+ # Perform face swap
54
+ swapped_frame = swapper.get(frame, face_in_frame, default_face, paste_back=True)
55
+ except Exception as e:
56
+ print(f"Error during face swap: {e}")
57
+ swapped_frame = frame
58
+ else:
59
+ swapped_frame = frame
60
+
61
+ _, buffer = cv2.imencode('.jpg', swapped_frame)
62
+ processed_img_data = base64.b64encode(buffer).decode('utf-8')
63
+ return {'image': 'data:image/jpeg;base64,' + processed_img_data}
64
+ except Exception as e:
65
+ print(f"Error processing frame: {e}")
66
+ return {'error': str(e)}, 500
67
+
68
+
69
+ @app.route('/get_current_face')
70
+ def get_current_face():
71
+ return send_file(default_img_path, mimetype='image/jpeg')
72
+
73
+
74
+ @app.route('/upload_face', methods=['POST'])
75
+ def upload_face():
76
+ if 'face' not in request.files:
77
+ return jsonify({'success': False, 'message': 'No file part'})
78
+
79
+ file = request.files['face']
80
+ if file.filename == '':
81
+ return jsonify({'success': False, 'message': 'No selected file'})
82
+
83
+ # Create a backup of the original default face if it doesn't exist
84
+ backup_path = 'assets/face.jpg'
85
+ if not os.path.exists(backup_path):
86
+ shutil.copy2(default_img_path, backup_path)
87
+
88
+ # Save the new face (always as face.jpg)
89
+ file_path = os.path.join('assets', 'face.jpg')
90
+ file.save(file_path)
91
+
92
+ # Reload the face for face swapping
93
+ global default_img, default_face
94
+ default_img = cv2.imread(default_img_path)
95
+ faces_in_default_img = app_model.get(default_img)
96
+
97
+ if len(faces_in_default_img) == 0:
98
+ # If no face detected, restore the backup
99
+ shutil.copy2(backup_path, default_img_path)
100
+ default_img = cv2.imread(default_img_path)
101
+ faces_in_default_img = app_model.get(default_img)
102
+ return jsonify({'success': False, 'message': 'No face detected in the uploaded image'})
103
+
104
+ default_face = faces_in_default_img[0]
105
+ return jsonify({'success': True})
106
+
107
+
108
+ @app.route('/reset_face', methods=['POST'])
109
+ def reset_face():
110
+ backup_path = 'assets/face.jpg'
111
+ if os.path.exists(backup_path):
112
+ # Restore the original face
113
+ shutil.copy2(backup_path, default_img_path)
114
+
115
+ # Reload the face for face swapping
116
+ global default_img, default_face
117
+ default_img = cv2.imread(default_img_path)
118
+ faces_in_default_img = app_model.get(default_img)
119
+ default_face = faces_in_default_img[0]
120
+ return jsonify({'success': True})
121
+ else:
122
+ return jsonify({'success': False, 'message': 'No backup found'})
123
+
124
+
125
+ if __name__ == "__main__":
126
+ app.run(host="0.0.0.0", port=7890)
assets/face.jpg ADDED
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ insightface==0.7.3
2
+ onnxruntime==1.19.2
3
+ torch==2.5.1
4
+ transformers==4.46.1
5
+ opencv-python==4.10.0.84
6
+ opencv-python-headless==4.10.0.84
7
+ Flask==3.0.3
8
+ numpy==1.22.0
9
+ requests==2.32.3
src/inswapper_128.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e4a3f08c753cb72d04e10aa0f7dbe3deebbf39567d4ead6dce08e98aa49e16af
3
+ size 554253681
templates/index.html ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>LetsPhish Morphis</title>
7
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --primary: #4f46e5;
11
+ --primary-hover: #4338ca;
12
+ --background: #f9fafb;
13
+ --card-bg: #ffffff;
14
+ --text: #1f2937;
15
+ --text-light: #6b7280;
16
+ --border: #e5e7eb;
17
+ --radius: 8px;
18
+ --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
19
+ }
20
+ * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
21
+ body { background-color: var(--background); color: var(--text); padding: 0; margin: 0; min-height: 100vh; }
22
+ .navbar { background-color: var(--card-bg); padding: 1rem 2rem; box-shadow: var(--shadow); display: flex; align-items: center; }
23
+ .logo { font-size: 1.5rem; font-weight: 700; color: var(--primary); display: flex; align-items: center; gap: 0.5rem; }
24
+ .container { max-width: 1200px; margin: 2rem auto; padding: 0 1rem; }
25
+ .app-header { text-align: center; margin-bottom: 2rem; }
26
+ .app-title { font-size: 2rem; margin-bottom: 0.5rem; color: var(--text); }
27
+ .app-description { color: var(--text-light); max-width: 600px; margin: 0 auto; }
28
+ .card { background-color: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; margin-bottom: 2rem; }
29
+ .card-header { padding: 1rem; border-bottom: 1px solid var(--border); font-weight: 600; }
30
+ .card-body { padding: 1rem; }
31
+ .flex { display: flex; }
32
+ .flex-col { flex-direction: column; }
33
+ .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 2rem; }
34
+ @media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
35
+ .video-container { position: relative; width: 100%; aspect-ratio: 16/9; overflow: hidden; border-radius: var(--radius); background-color: #000; }
36
+ video, #output { position: absolute; width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1); }
37
+ #video { z-index: 1; }
38
+ #output { z-index: 2; }
39
+ #canvas { display: none; }
40
+ .controls { margin-top: 1rem; display: flex; gap: 1rem; flex-wrap: wrap; }
41
+ .btn { padding: 0.5rem 1rem; border-radius: var(--radius); border: none; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s; }
42
+ .btn-primary { background-color: var(--primary); color: white; }
43
+ .btn-primary:hover { background-color: var(--primary-hover); }
44
+ .btn-outline { border: 1px solid var(--primary); color: var(--primary); background-color: transparent; }
45
+ .btn-outline:hover { background-color: var(--primary); color: white; }
46
+ .file-input-container { display: flex; flex-direction: column; gap: 1rem; }
47
+ .file-input-label { border: 2px dashed var(--border); border-radius: var(--radius); padding: 2rem; text-align: center; cursor: pointer; transition: all 0.2s; }
48
+ .file-input-label:hover { border-color: var(--primary); }
49
+ .file-input { display: none; }
50
+ .preview-container { width: 128px; height: 128px; border-radius: 50%; overflow: hidden; margin: 1rem auto; border: 4px solid var(--primary); }
51
+ .preview-image { width: 100%; height: 100%; object-fit: cover; }
52
+ .status-container { display: flex; align-items: center; gap: 0.5rem; color: var(--text-light); margin-top: 1rem; }
53
+ .status-dot { width: 10px; height: 10px; border-radius: 50%; background-color: #ccc; }
54
+ .status-dot.active { background-color: #10b981; }
55
+ .timer { margin-top: 1rem; font-size: 1.2rem; font-weight: bold; text-align: center; }
56
+ .footer { padding: 2rem; text-align: center; color: var(--text-light); border-top: 1px solid var(--border); margin-top: 3rem; }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <nav class="navbar">
61
+ <div class="logo">
62
+ <i class="fas fa-mask"></i>
63
+ <span>LetsPhish Morphis</span>
64
+ </div>
65
+ </nav>
66
+ <div class="container">
67
+ <div class="app-header">
68
+ <h1 class="app-title">Real-Time Face Swapping</h1>
69
+ <p class="app-description">Experience real-time face swapping powered by AI. Upload your own face or use the default one.</p>
70
+ </div>
71
+ <div class="grid">
72
+ <!-- Live preview card spans both columns to be bigger -->
73
+ <div class="flex flex-col" style="grid-column: span 2;">
74
+ <div class="card">
75
+ <div class="card-header">
76
+ <i class="fas fa-camera"></i> Live Preview
77
+ </div>
78
+ <div class="card-body">
79
+ <div class="video-container">
80
+ <video id="video" autoplay></video>
81
+ <img id="output">
82
+ </div>
83
+ <canvas id="canvas"></canvas>
84
+ <div class="controls">
85
+ <button id="toggleBtn" class="btn btn-primary">
86
+ <i class="fas fa-pause"></i> Pause Face Swap
87
+ </button>
88
+ <button id="screenshotBtn" class="btn btn-outline">
89
+ <i class="fas fa-camera"></i> Take Screenshot
90
+ </button>
91
+ </div>
92
+ <div class="status-container">
93
+ <div id="statusDot" class="status-dot active"></div>
94
+ <span id="statusText">Face swap active</span>
95
+ </div>
96
+ <!-- Timer display -->
97
+ <div id="timerDisplay" class="timer">Time remaining: 02:00</div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ <!-- Upload card remains in one column -->
102
+ <div class="flex flex-col">
103
+ <div class="card">
104
+ <div class="card-header">
105
+ <i class="fas fa-upload"></i> Upload Reference Face
106
+ </div>
107
+ <div class="card-body">
108
+ <div class="preview-container">
109
+ <img id="facePreview" class="preview-image" src="/get_current_face" alt="Current face">
110
+ </div>
111
+ <div class="file-input-container">
112
+ <label for="faceUpload" class="file-input-label">
113
+ <i class="fas fa-cloud-upload-alt fa-2x"></i>
114
+ <p>Drop your image here or click to browse</p>
115
+ <p class="text-light">JPG or PNG, max 5MB</p>
116
+ </label>
117
+ <input type="file" id="faceUpload" class="file-input" accept="image/*">
118
+ </div>
119
+ <div class="controls">
120
+ <button id="uploadBtn" class="btn btn-primary" disabled>
121
+ <i class="fas fa-check"></i> Use This Face
122
+ </button>
123
+ <button id="resetBtn" class="btn btn-outline">
124
+ <i class="fas fa-undo"></i> Reset to Default
125
+ </button>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ <footer class="footer">
133
+ <p>Powered By LetsPhish Morphis</p>
134
+ </footer>
135
+ <script>
136
+ const video = document.getElementById('video');
137
+ const canvas = document.getElementById('canvas');
138
+ const output = document.getElementById('output');
139
+ const context = canvas.getContext('2d');
140
+ const toggleBtn = document.getElementById('toggleBtn');
141
+ const screenshotBtn = document.getElementById('screenshotBtn');
142
+ const faceUpload = document.getElementById('faceUpload');
143
+ const facePreview = document.getElementById('facePreview');
144
+ const uploadBtn = document.getElementById('uploadBtn');
145
+ const resetBtn = document.getElementById('resetBtn');
146
+ const statusDot = document.getElementById('statusDot');
147
+ const statusText = document.getElementById('statusText');
148
+ const timerDisplay = document.getElementById('timerDisplay');
149
+
150
+ let isProcessing = true;
151
+ let processingInterval = 100; // interval in ms
152
+ let intervalId = null;
153
+ let selectedFile = null;
154
+ let frameCounter = 0;
155
+ const skipFrames = 2; // Process every 2nd frame (adjust as needed)
156
+ let processingInProgress = false;
157
+
158
+ // Total time in seconds for live preview
159
+ let totalTime = 120;
160
+ let timerIntervalId = null;
161
+
162
+ // Function to update timer display in mm:ss format
163
+ function updateTimerDisplay() {
164
+ let minutes = Math.floor(totalTime / 60);
165
+ let seconds = totalTime % 60;
166
+ timerDisplay.textContent = `Time remaining: ${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
167
+ }
168
+
169
+ // Start the countdown timer
170
+ function startTimer() {
171
+ updateTimerDisplay();
172
+ timerIntervalId = setInterval(() => {
173
+ totalTime--;
174
+ updateTimerDisplay();
175
+ if (totalTime <= 0) {
176
+ clearInterval(timerIntervalId);
177
+ if (isProcessing) {
178
+ clearInterval(intervalId);
179
+ isProcessing = false;
180
+ toggleBtn.innerHTML = '<i class="fas fa-play"></i> Resume Face Swap';
181
+ statusDot.classList.remove('active');
182
+ statusText.textContent = 'Live preview turned off after 2 minutes';
183
+ alert("Live preview has been turned off after 2 minutes.");
184
+ }
185
+ }
186
+ }, 1000);
187
+ }
188
+
189
+ // Toggle face swapping
190
+ toggleBtn.addEventListener('click', () => {
191
+ isProcessing = !isProcessing;
192
+ if (isProcessing) {
193
+ toggleBtn.innerHTML = '<i class="fas fa-pause"></i> Pause Face Swap';
194
+ intervalId = setInterval(captureFrame, processingInterval);
195
+ statusDot.classList.add('active');
196
+ statusText.textContent = 'Face swap active';
197
+ // Restart timer if it had stopped
198
+ if (totalTime <= 0) {
199
+ totalTime = 120;
200
+ startTimer();
201
+ }
202
+ } else {
203
+ toggleBtn.innerHTML = '<i class="fas fa-play"></i> Resume Face Swap';
204
+ clearInterval(intervalId);
205
+ statusDot.classList.remove('active');
206
+ statusText.textContent = 'Face swap paused';
207
+ }
208
+ });
209
+
210
+ // Take screenshot
211
+ screenshotBtn.addEventListener('click', () => {
212
+ const link = document.createElement('a');
213
+ link.download = 'faceswap-screenshot.jpg';
214
+ link.href = output.src;
215
+ link.click();
216
+ });
217
+
218
+ // Handle file upload preview
219
+ faceUpload.addEventListener('change', (e) => {
220
+ selectedFile = e.target.files[0];
221
+ if (selectedFile) {
222
+ const reader = new FileReader();
223
+ reader.onload = (event) => {
224
+ facePreview.src = event.target.result;
225
+ uploadBtn.disabled = false;
226
+ };
227
+ reader.readAsDataURL(selectedFile);
228
+ }
229
+ });
230
+
231
+ // Upload new face
232
+ uploadBtn.addEventListener('click', () => {
233
+ if (!selectedFile) return;
234
+ const formData = new FormData();
235
+ formData.append('face', selectedFile);
236
+ fetch('/upload_face', {
237
+ method: 'POST',
238
+ body: formData
239
+ })
240
+ .then(response => response.json())
241
+ .then(data => {
242
+ if (data.success) {
243
+ alert('Face updated successfully!');
244
+ facePreview.src = `/get_current_face?t=${new Date().getTime()}`;
245
+ uploadBtn.disabled = true;
246
+ } else {
247
+ alert('Error: ' + data.message);
248
+ }
249
+ })
250
+ .catch(error => {
251
+ console.error('Error:', error);
252
+ alert('An error occurred while uploading the face.');
253
+ });
254
+ });
255
+
256
+ // Reset to default face
257
+ resetBtn.addEventListener('click', () => {
258
+ fetch('/reset_face', { method: 'POST' })
259
+ .then(response => response.json())
260
+ .then(data => {
261
+ if (data.success) {
262
+ alert('Reset to default face successfully!');
263
+ facePreview.src = `/get_current_face?t=${new Date().getTime()}`;
264
+ } else {
265
+ alert('Error: ' + data.message);
266
+ }
267
+ })
268
+ .catch(error => {
269
+ console.error('Error:', error);
270
+ alert('An error occurred while resetting the face.');
271
+ });
272
+ });
273
+
274
+ // Access the webcam and stream the video to the video element
275
+ navigator.mediaDevices.getUserMedia({ video: true })
276
+ .then((stream) => { video.srcObject = stream; })
277
+ .catch((err) => {
278
+ console.error("Error accessing webcam: " + err);
279
+ statusText.textContent = 'Error: Cannot access webcam';
280
+ statusDot.classList.remove('active');
281
+ statusDot.style.backgroundColor = 'red';
282
+ });
283
+
284
+ // Asynchronous function to capture and process the frame
285
+ async function captureFrame() {
286
+ if (processingInProgress) return;
287
+ frameCounter++;
288
+ if (frameCounter % skipFrames !== 0) return;
289
+ if (!video.videoWidth) return;
290
+
291
+ canvas.width = video.videoWidth;
292
+ canvas.height = video.videoHeight;
293
+
294
+ // Mirror the video: save and restore context
295
+ context.save();
296
+ context.scale(-1, 1);
297
+ context.drawImage(video, -canvas.width, 0, canvas.width, canvas.height);
298
+ context.restore();
299
+
300
+ // Reduce image quality by using a lower quality parameter (0.5 here)
301
+ const dataURL = canvas.toDataURL('image/jpeg', 0.5);
302
+ processingInProgress = true;
303
+ try {
304
+ const response = await fetch('/process_frame', {
305
+ method: 'POST',
306
+ headers: { 'Content-Type': 'application/json' },
307
+ body: JSON.stringify({ image: dataURL })
308
+ });
309
+ const data = await response.json();
310
+ output.src = data.image;
311
+ } catch (error) {
312
+ console.error('Error:', error);
313
+ } finally {
314
+ processingInProgress = false;
315
+ }
316
+ }
317
+
318
+ // Start capturing frames and start the timer
319
+ intervalId = setInterval(captureFrame, processingInterval);
320
+ startTimer();
321
+ </script>
322
+ </body>
323
+ </html>