Goldstandard777 commited on
Commit
c08bdd7
Β·
verified Β·
1 Parent(s): b5421f1

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +325 -0
app.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Rebel Face Swap β€” AI Face Swap for Images & Video
3
+ Token-gated, login-based face swap app using open-source AI.
4
+ """
5
+
6
+ import os, json, uuid, hashlib, time, threading, shutil, tempfile
7
+ from datetime import datetime
8
+ from flask import Flask, request, jsonify, render_template, send_from_directory, session, redirect, url_for
9
+ from gradio_client import Client, handle_file
10
+ from functools import wraps
11
+
12
+ app = Flask(__name__)
13
+ app.secret_key = os.environ.get("SECRET_KEY", "rebel-face-swap-secret-2026")
14
+
15
+ # ── Config ──────────────────────────────────────────────────────────
16
+ UPLOAD_FOLDER = "/tmp/rebel_uploads"
17
+ OUTPUT_FOLDER = "/tmp/rebel_outputs"
18
+ DB_FILE = "/tmp/rebel_faceswap_db.json"
19
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
20
+ os.makedirs(OUTPUT_FOLDER, exist_ok=True)
21
+
22
+ FREE_CREDITS = 5
23
+ IMAGE_SWAP_COST = 1
24
+ VIDEO_SWAP_COST = 3
25
+
26
+ IMAGE_SPACE = "tonyassi/face-swap"
27
+ VIDEO_SPACE = "tonyassi/video-face-swap"
28
+
29
+ # ── Database (JSON file) ────────────────────────────────────────────
30
+ db_lock = threading.Lock()
31
+
32
+ def load_db():
33
+ if os.path.exists(DB_FILE):
34
+ with open(DB_FILE, "r") as f:
35
+ return json.load(f)
36
+ return {"users": {}, "jobs": {}, "purchases": []}
37
+
38
+ def save_db(db):
39
+ with open(DB_FILE, "w") as f:
40
+ json.dump(db, f, indent=2)
41
+
42
+ def hash_pw(pw):
43
+ return hashlib.sha256(pw.encode()).hexdigest()
44
+
45
+ # ── Auth decorator ──────────────────────────────────────────────────
46
+ def login_required(f):
47
+ @wraps(f)
48
+ def decorated(*args, **kwargs):
49
+ if "user_id" not in session:
50
+ if request.is_json or request.path.startswith("/api/"):
51
+ return jsonify({"error": "Not logged in"}), 401
52
+ return redirect(url_for("login_page"))
53
+ return f(*args, **kwargs)
54
+ return decorated
55
+
56
+ # ── Job queue ───────────────────────────────────────────────────────
57
+ jobs = {}
58
+ jobs_lock = threading.Lock()
59
+
60
+ def process_image_swap(job_id, source_path, target_path):
61
+ """Background thread: image face swap via HF Space."""
62
+ try:
63
+ with jobs_lock:
64
+ jobs[job_id]["status"] = "processing"
65
+ client = Client(IMAGE_SPACE)
66
+ result = client.predict(
67
+ src_img=handle_file(source_path),
68
+ dest_img=handle_file(target_path),
69
+ api_name="/swap_faces"
70
+ )
71
+ # result is the output image path
72
+ out_name = f"{job_id}.png"
73
+ out_path = os.path.join(OUTPUT_FOLDER, out_name)
74
+ shutil.copy(result, out_path)
75
+ with jobs_lock:
76
+ jobs[job_id]["status"] = "done"
77
+ jobs[job_id]["result"] = out_name
78
+ jobs[job_id]["result_type"] = "image"
79
+ except Exception as e:
80
+ with jobs_lock:
81
+ jobs[job_id]["status"] = "error"
82
+ jobs[job_id]["error"] = str(e)
83
+
84
+ def process_video_swap(job_id, source_path, video_path, gender="all"):
85
+ """Background thread: video face swap via HF Space."""
86
+ try:
87
+ with jobs_lock:
88
+ jobs[job_id]["status"] = "processing"
89
+ client = Client(VIDEO_SPACE)
90
+ result = client.predict(
91
+ input_image=handle_file(source_path),
92
+ input_video=handle_file(video_path),
93
+ gender=gender,
94
+ api_name="/generate"
95
+ )
96
+ out_name = f"{job_id}.mp4"
97
+ out_path = os.path.join(OUTPUT_FOLDER, out_name)
98
+ shutil.copy(result, out_path)
99
+ with jobs_lock:
100
+ jobs[job_id]["status"] = "done"
101
+ jobs[job_id]["result"] = out_name
102
+ jobs[job_id]["result_type"] = "video"
103
+ except Exception as e:
104
+ with jobs_lock:
105
+ jobs[job_id]["status"] = "error"
106
+ jobs[job_id]["error"] = str(e)
107
+
108
+ # ── Pages ───────────────────────────────────────────────────────────
109
+ @app.route("/")
110
+ def index():
111
+ if "user_id" in session:
112
+ return redirect(url_for("app_page"))
113
+ return redirect(url_for("login_page"))
114
+
115
+ @app.route("/login")
116
+ def login_page():
117
+ return render_template("login.html")
118
+
119
+ @app.route("/signup")
120
+ def signup_page():
121
+ return render_template("signup.html")
122
+
123
+ @app.route("/app")
124
+ @login_required
125
+ def app_page():
126
+ return render_template("app.html")
127
+
128
+ @app.route("/pricing")
129
+ def pricing_page():
130
+ return render_template("pricing.html", logged_in="user_id" in session)
131
+
132
+ @app.route("/admin")
133
+ def admin_page():
134
+ key = request.args.get("key", "")
135
+ if key != "rebel-admin-2026":
136
+ return "Unauthorized", 403
137
+ return render_template("admin.html")
138
+
139
+ # ── Auth API ────────────────────────────────────────────────────────
140
+ @app.route("/api/signup", methods=["POST"])
141
+ def api_signup():
142
+ data = request.json
143
+ email = data.get("email", "").strip().lower()
144
+ password = data.get("password", "")
145
+ username = data.get("username", "").strip()
146
+ if not email or not password or not username:
147
+ return jsonify({"error": "All fields required"}), 400
148
+ with db_lock:
149
+ db = load_db()
150
+ if email in db["users"]:
151
+ return jsonify({"error": "Email already registered"}), 400
152
+ user_id = str(uuid.uuid4())[:8]
153
+ db["users"][email] = {
154
+ "id": user_id,
155
+ "username": username,
156
+ "email": email,
157
+ "password": hash_pw(password),
158
+ "credits": FREE_CREDITS,
159
+ "created": datetime.utcnow().isoformat(),
160
+ "swaps_done": 0
161
+ }
162
+ save_db(db)
163
+ session["user_id"] = user_id
164
+ session["email"] = email
165
+ return jsonify({"success": True, "credits": FREE_CREDITS})
166
+
167
+ @app.route("/api/login", methods=["POST"])
168
+ def api_login():
169
+ data = request.json
170
+ email = data.get("email", "").strip().lower()
171
+ password = data.get("password", "")
172
+ with db_lock:
173
+ db = load_db()
174
+ user = db["users"].get(email)
175
+ if not user or user["password"] != hash_pw(password):
176
+ return jsonify({"error": "Invalid credentials"}), 401
177
+ session["user_id"] = user["id"]
178
+ session["email"] = email
179
+ return jsonify({"success": True, "credits": user["credits"]})
180
+
181
+ @app.route("/api/logout", methods=["POST"])
182
+ def api_logout():
183
+ session.clear()
184
+ return jsonify({"success": True})
185
+
186
+ @app.route("/api/me")
187
+ @login_required
188
+ def api_me():
189
+ with db_lock:
190
+ db = load_db()
191
+ user = db["users"].get(session["email"], {})
192
+ return jsonify({
193
+ "username": user.get("username", ""),
194
+ "email": user.get("email", ""),
195
+ "credits": user.get("credits", 0),
196
+ "swaps_done": user.get("swaps_done", 0)
197
+ })
198
+
199
+ # ── Swap API ────────────────────────────────────────────────────────
200
+ @app.route("/api/swap", methods=["POST"])
201
+ @login_required
202
+ def api_swap():
203
+ swap_type = request.form.get("type", "image") # image or video
204
+ gender = request.form.get("gender", "all")
205
+ cost = IMAGE_SWAP_COST if swap_type == "image" else VIDEO_SWAP_COST
206
+
207
+ # Check credits
208
+ with db_lock:
209
+ db = load_db()
210
+ user = db["users"].get(session["email"])
211
+ if not user:
212
+ return jsonify({"error": "User not found"}), 404
213
+ if user["credits"] < cost:
214
+ return jsonify({"error": f"Not enough credits. Need {cost}, have {user['credits']}"}), 402
215
+
216
+ # Get files
217
+ source = request.files.get("source")
218
+ target = request.files.get("target")
219
+ if not source or not target:
220
+ return jsonify({"error": "Source face image and target file required"}), 400
221
+
222
+ # Save uploads
223
+ job_id = str(uuid.uuid4())[:12]
224
+ source_ext = os.path.splitext(source.filename)[1] or ".png"
225
+ target_ext = os.path.splitext(target.filename)[1] or (".png" if swap_type == "image" else ".mp4")
226
+ source_path = os.path.join(UPLOAD_FOLDER, f"{job_id}_source{source_ext}")
227
+ target_path = os.path.join(UPLOAD_FOLDER, f"{job_id}_target{target_ext}")
228
+ source.save(source_path)
229
+ target.save(target_path)
230
+
231
+ # Deduct credits
232
+ with db_lock:
233
+ db = load_db()
234
+ db["users"][session["email"]]["credits"] -= cost
235
+ db["users"][session["email"]]["swaps_done"] += 1
236
+ save_db(db)
237
+
238
+ # Start job
239
+ with jobs_lock:
240
+ jobs[job_id] = {
241
+ "status": "queued",
242
+ "type": swap_type,
243
+ "user": session["email"],
244
+ "created": time.time()
245
+ }
246
+
247
+ if swap_type == "image":
248
+ t = threading.Thread(target=process_image_swap, args=(job_id, source_path, target_path))
249
+ else:
250
+ t = threading.Thread(target=process_video_swap, args=(job_id, source_path, target_path, gender))
251
+ t.daemon = True
252
+ t.start()
253
+
254
+ return jsonify({"job_id": job_id, "credits_remaining": db["users"][session["email"]]["credits"]})
255
+
256
+ @app.route("/api/job/<job_id>")
257
+ @login_required
258
+ def api_job_status(job_id):
259
+ with jobs_lock:
260
+ job = jobs.get(job_id)
261
+ if not job:
262
+ return jsonify({"error": "Job not found"}), 404
263
+ resp = {"status": job["status"], "type": job.get("type", "image")}
264
+ if job["status"] == "done":
265
+ resp["result_url"] = f"/output/{job['result']}"
266
+ resp["result_type"] = job.get("result_type", "image")
267
+ elif job["status"] == "error":
268
+ resp["error"] = job.get("error", "Unknown error")
269
+ return jsonify(resp)
270
+
271
+ @app.route("/output/<filename>")
272
+ @login_required
273
+ def serve_output(filename):
274
+ return send_from_directory(OUTPUT_FOLDER, filename)
275
+
276
+ # ── Credits / Payment API ──────────────────────────────────────────
277
+ @app.route("/api/add_credits", methods=["POST"])
278
+ @login_required
279
+ def api_add_credits():
280
+ """Manual credit add (for PayPal verification or admin)."""
281
+ data = request.json
282
+ amount = data.get("amount", 0)
283
+ with db_lock:
284
+ db = load_db()
285
+ if session["email"] in db["users"]:
286
+ db["users"][session["email"]]["credits"] += amount
287
+ db["purchases"].append({
288
+ "email": session["email"],
289
+ "amount": amount,
290
+ "timestamp": datetime.utcnow().isoformat()
291
+ })
292
+ save_db(db)
293
+ return jsonify({"success": True, "credits": db["users"][session["email"]]["credits"]})
294
+
295
+ # ── Admin API ───────────────────────────────────────────────────────
296
+ @app.route("/api/admin/stats")
297
+ def api_admin_stats():
298
+ key = request.args.get("key", "")
299
+ if key != "rebel-admin-2026":
300
+ return jsonify({"error": "Unauthorized"}), 403
301
+ with db_lock:
302
+ db = load_db()
303
+ total_users = len(db["users"])
304
+ total_swaps = sum(u.get("swaps_done", 0) for u in db["users"].values())
305
+ total_purchases = len(db["purchases"])
306
+ users_list = [
307
+ {"username": u["username"], "email": u["email"], "credits": u["credits"],
308
+ "swaps_done": u.get("swaps_done", 0), "created": u.get("created", "")}
309
+ for u in db["users"].values()
310
+ ]
311
+ return jsonify({
312
+ "total_users": total_users,
313
+ "total_swaps": total_swaps,
314
+ "total_purchases": total_purchases,
315
+ "users": users_list,
316
+ "purchases": db["purchases"]
317
+ })
318
+
319
+ # ── Static ──────────────────────────────────────────────────────────
320
+ @app.route("/static/<path:filename>")
321
+ def serve_static(filename):
322
+ return send_from_directory("static", filename)
323
+
324
+ if __name__ == "__main__":
325
+ app.run(host="0.0.0.0", port=7860, debug=False)