Spaces:
Runtime error
Runtime error
Upload 3 files
Browse files- Dockerfile +22 -0
- app.py +128 -0
- face_swap.py +72 -0
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
# System dependencies
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
ffmpeg \
|
| 6 |
+
libgl1-mesa-glx \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# Copy all files
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# Install Python dependencies
|
| 15 |
+
RUN pip install --upgrade pip
|
| 16 |
+
RUN pip install -r requirements.txt
|
| 17 |
+
|
| 18 |
+
# Expose port
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
# Run your Flask app
|
| 22 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
| 3 |
+
from flask import Flask, render_template, request, jsonify, Response, send_file
|
| 4 |
+
import cv2
|
| 5 |
+
import uuid
|
| 6 |
+
import numpy as np
|
| 7 |
+
from face_swap import face_swap
|
| 8 |
+
from moviepy.editor import VideoFileClip
|
| 9 |
+
|
| 10 |
+
app = Flask(__name__)
|
| 11 |
+
app.config['UPLOAD_FOLDER'] = 'uploads'
|
| 12 |
+
app.config['OUTPUT_FOLDER'] = 'outputs'
|
| 13 |
+
|
| 14 |
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
| 15 |
+
os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True)
|
| 16 |
+
|
| 17 |
+
source_face = None
|
| 18 |
+
anonymize = False
|
| 19 |
+
save_faces = False
|
| 20 |
+
webcam_active = False
|
| 21 |
+
cap = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@app.route('/')
|
| 25 |
+
def landing():
|
| 26 |
+
return render_template('landing.html')
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@app.route('/app')
|
| 30 |
+
def main_app():
|
| 31 |
+
return render_template('app.html')
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@app.route('/upload-face', methods=['POST'])
|
| 35 |
+
def upload_face():
|
| 36 |
+
global source_face
|
| 37 |
+
file = request.files['file']
|
| 38 |
+
path = os.path.join(app.config['UPLOAD_FOLDER'], f"face_{uuid.uuid4().hex}.jpg")
|
| 39 |
+
file.save(path)
|
| 40 |
+
source_face = cv2.imread(path)
|
| 41 |
+
return jsonify({'status': 'success'})
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@app.route('/upload-video', methods=['POST'])
|
| 45 |
+
def upload_video():
|
| 46 |
+
global source_face
|
| 47 |
+
file = request.files['file']
|
| 48 |
+
path = os.path.join(app.config['UPLOAD_FOLDER'], f"video_{uuid.uuid4().hex}.mp4")
|
| 49 |
+
file.save(path)
|
| 50 |
+
|
| 51 |
+
cap = cv2.VideoCapture(path)
|
| 52 |
+
width, height = int(cap.get(3)), int(cap.get(4))
|
| 53 |
+
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
|
| 54 |
+
|
| 55 |
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
| 56 |
+
output_path = os.path.join(app.config['OUTPUT_FOLDER'], f"output_{uuid.uuid4().hex}.mp4")
|
| 57 |
+
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
| 58 |
+
|
| 59 |
+
while cap.isOpened():
|
| 60 |
+
ret, frame = cap.read()
|
| 61 |
+
if not ret:
|
| 62 |
+
break
|
| 63 |
+
result = face_swap(source_face, frame)
|
| 64 |
+
out.write(result)
|
| 65 |
+
|
| 66 |
+
cap.release()
|
| 67 |
+
out.release()
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
original = VideoFileClip(path)
|
| 71 |
+
edited = VideoFileClip(output_path)
|
| 72 |
+
final = edited.set_audio(original.audio)
|
| 73 |
+
final_path = output_path.replace(".mp4", "_with_audio.mp4")
|
| 74 |
+
final.write_videofile(final_path, codec='libx264', audio_codec='aac')
|
| 75 |
+
return send_file(final_path, as_attachment=True)
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print("Audio merge error:", e)
|
| 78 |
+
return send_file(output_path, as_attachment=True)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@app.route('/toggle-anonymize', methods=['POST'])
|
| 82 |
+
def toggle_anonymize():
|
| 83 |
+
global anonymize
|
| 84 |
+
anonymize = not anonymize
|
| 85 |
+
return jsonify({'status': 'success', 'anonymize': anonymize})
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@app.route('/toggle-save-faces', methods=['POST'])
|
| 89 |
+
def toggle_save_faces():
|
| 90 |
+
global save_faces
|
| 91 |
+
save_faces = not save_faces
|
| 92 |
+
return jsonify({'status': 'success', 'save_faces': save_faces})
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@app.route('/start-webcam')
|
| 96 |
+
def start_webcam():
|
| 97 |
+
global webcam_active, cap
|
| 98 |
+
if not webcam_active:
|
| 99 |
+
cap = cv2.VideoCapture(0)
|
| 100 |
+
webcam_active = True
|
| 101 |
+
return Response(gen_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
@app.route('/stop-webcam')
|
| 105 |
+
def stop_webcam():
|
| 106 |
+
global webcam_active, cap
|
| 107 |
+
webcam_active = False
|
| 108 |
+
if cap:
|
| 109 |
+
cap.release()
|
| 110 |
+
return jsonify({'status': 'stopped'})
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def gen_frames():
|
| 114 |
+
global cap, webcam_active, source_face
|
| 115 |
+
while webcam_active and cap.isOpened():
|
| 116 |
+
ret, frame = cap.read()
|
| 117 |
+
if not ret:
|
| 118 |
+
break
|
| 119 |
+
frame = face_swap(source_face, frame)
|
| 120 |
+
_, buffer = cv2.imencode('.jpg', frame)
|
| 121 |
+
frame_bytes = buffer.tobytes()
|
| 122 |
+
yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
|
| 123 |
+
if cap:
|
| 124 |
+
cap.release()
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
if __name__ == '__main__':
|
| 128 |
+
app.run(debug=True)
|
face_swap.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
import warnings
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from insightface.app import FaceAnalysis
|
| 7 |
+
from insightface.model_zoo import get_model
|
| 8 |
+
|
| 9 |
+
warnings.filterwarnings("ignore", category=FutureWarning)
|
| 10 |
+
|
| 11 |
+
# Try GPU, fallback to CPU
|
| 12 |
+
try:
|
| 13 |
+
face_analyzer = FaceAnalysis(name='buffalo_l', providers=['CUDAExecutionProvider'])
|
| 14 |
+
face_analyzer.prepare(ctx_id=0)
|
| 15 |
+
except Exception:
|
| 16 |
+
print("⚠️ CUDA not available for FaceAnalysis. Falling back to CPU.")
|
| 17 |
+
face_analyzer = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider'])
|
| 18 |
+
face_analyzer.prepare(ctx_id=0)
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
swapper = get_model('model/insightface/inswapper_128.onnx', providers=['CUDAExecutionProvider'])
|
| 22 |
+
except Exception:
|
| 23 |
+
print("⚠️ CUDA not available for Swapper. Falling back to CPU.")
|
| 24 |
+
swapper = get_model('inswapper_128.onnx', providers=['CPUExecutionProvider'])
|
| 25 |
+
|
| 26 |
+
def face_swap(source_img, target_img, anonymize=False, save_faces=False):
|
| 27 |
+
if source_img is None:
|
| 28 |
+
return target_img
|
| 29 |
+
|
| 30 |
+
source_img_rgb = cv2.cvtColor(source_img, cv2.COLOR_BGR2RGB)
|
| 31 |
+
target_img_rgb = cv2.cvtColor(target_img, cv2.COLOR_BGR2RGB)
|
| 32 |
+
|
| 33 |
+
# Resize source to target if needed
|
| 34 |
+
if source_img_rgb.shape != target_img_rgb.shape:
|
| 35 |
+
source_img_rgb = cv2.resize(source_img_rgb, (target_img_rgb.shape[1], target_img_rgb.shape[0]))
|
| 36 |
+
|
| 37 |
+
src_faces = face_analyzer.get(source_img_rgb)
|
| 38 |
+
tgt_faces = face_analyzer.get(target_img_rgb)
|
| 39 |
+
|
| 40 |
+
if len(tgt_faces) == 0 or len(src_faces) == 0:
|
| 41 |
+
return cv2.cvtColor(target_img_rgb, cv2.COLOR_RGB2BGR)
|
| 42 |
+
|
| 43 |
+
src_face = src_faces[0]
|
| 44 |
+
|
| 45 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 46 |
+
output_dir = f"saved_faces/{timestamp}"
|
| 47 |
+
if save_faces:
|
| 48 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 49 |
+
|
| 50 |
+
for i, tgt_face in enumerate(tgt_faces):
|
| 51 |
+
x1, y1, x2, y2 = tgt_face.bbox.astype(int)
|
| 52 |
+
|
| 53 |
+
if anonymize:
|
| 54 |
+
target_img_rgb[y1:y2, x1:x2] = cv2.GaussianBlur(target_img_rgb[y1:y2, x1:x2], (99, 99), 30)
|
| 55 |
+
else:
|
| 56 |
+
try:
|
| 57 |
+
target_img_rgb = swapper.get(target_img_rgb, tgt_face, src_face, paste_back=True)
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"Swapper error on face {i}: {e}")
|
| 60 |
+
|
| 61 |
+
if save_faces:
|
| 62 |
+
face_crop = target_img_rgb[y1:y2, x1:x2]
|
| 63 |
+
face_bgr = cv2.cvtColor(face_crop, cv2.COLOR_RGB2BGR)
|
| 64 |
+
cv2.imwrite(f"{output_dir}/swapped_face_{i}.jpg", face_bgr)
|
| 65 |
+
|
| 66 |
+
side_by_side = np.hstack([
|
| 67 |
+
cv2.resize(cv2.cvtColor(source_img_rgb, cv2.COLOR_RGB2BGR), (200, 200)),
|
| 68 |
+
cv2.resize(face_bgr, (200, 200))
|
| 69 |
+
])
|
| 70 |
+
cv2.imwrite(f"{output_dir}/source_target_face_{i}.jpg", side_by_side)
|
| 71 |
+
|
| 72 |
+
return cv2.cvtColor(target_img_rgb, cv2.COLOR_RGB2BGR)
|