Spaces:
Sleeping
Sleeping
| import os | |
| import time | |
| import uuid | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from flask import Flask, request, send_from_directory, jsonify, session | |
| from flask_cors import CORS | |
| from diff import compare_images | |
| from diff_ai import compare_images_ai | |
| from describe import describe_change | |
| from auth import ( | |
| authenticate, login_required, get_secret_key, current_user, | |
| ) | |
| app = Flask(__name__) | |
| app.config["SECRET_KEY"] = get_secret_key() | |
| app.config["SESSION_COOKIE_HTTPONLY"] = True | |
| app.config["SESSION_COOKIE_SAMESITE"] = "Lax" | |
| # Allow the frontend origin to send cookies. For local dev we list the dev | |
| # origins; in production set DIFFLENS_ORIGINS env var to your domain. | |
| _origins_env = os.environ.get("DIFFLENS_ORIGINS") | |
| if _origins_env: | |
| _origins = [o.strip() for o in _origins_env.split(",") if o.strip()] | |
| else: | |
| _origins = [ | |
| "http://localhost:8000", "http://127.0.0.1:8000", | |
| "http://localhost:5000", "http://127.0.0.1:5000", | |
| "http://localhost:3000", "http://127.0.0.1:3000" | |
| ] | |
| CORS(app, resources={r"/*": {"origins": _origins}}, supports_credentials=True) | |
| # Use /tmp/uploads on Linux/production environments (e.g. Hugging Face Spaces) | |
| if os.name != "nt" or os.environ.get("HF_HUB_DISABLE_SYMLINKS_WARNING"): | |
| UPLOAD_FOLDER = "/tmp/uploads" | |
| else: | |
| UPLOAD_FOLDER = "../uploads" | |
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) | |
| def cleanup_old_files(folder, max_age_seconds=900): | |
| """Clean up files in the given folder that are older than max_age_seconds.""" | |
| try: | |
| now = time.time() | |
| for filename in os.listdir(folder): | |
| filepath = os.path.join(folder, filename) | |
| if os.path.isfile(filepath): | |
| stat = os.stat(filepath) | |
| if now - stat.st_mtime > max_age_seconds: | |
| try: | |
| os.remove(filepath) | |
| except Exception as e: | |
| print(f"[app] Failed to remove {filename}: {e}") | |
| except Exception as e: | |
| print(f"[app] Cleanup failed: {e}") | |
| def home(): | |
| return "Backend running" | |
| # ---------------- AUTH ---------------- | |
| def login(): | |
| if request.method == "OPTIONS": | |
| return ("", 204) | |
| data = request.get_json(silent=True) or {} | |
| username = (data.get("username") or "").strip() | |
| password = data.get("password") or "" | |
| if not username or not password: | |
| return jsonify({"error": "missing credentials"}), 400 | |
| if not authenticate(username, password): | |
| return jsonify({"error": "invalid credentials"}), 401 | |
| session["user"] = username | |
| session.permanent = True | |
| return jsonify({"user": username}) | |
| def logout(): | |
| if request.method == "OPTIONS": | |
| return ("", 204) | |
| session.clear() | |
| return jsonify({"ok": True}) | |
| def me(): | |
| if request.method == "OPTIONS": | |
| return ("", 204) | |
| return jsonify({"user": current_user()}) | |
| # ---------------- DIFF ---------------- | |
| def compare(): | |
| if request.method == "OPTIONS": | |
| return jsonify({"status": "ok"}), 200 | |
| # Clean up old temp uploads | |
| cleanup_old_files(UPLOAD_FOLDER) | |
| if "image1" not in request.files or "image2" not in request.files: | |
| return jsonify({"error": "Missing images"}), 400 | |
| img1 = request.files["image1"] | |
| img2 = request.files["image2"] | |
| # Use UUID to prevent image name collisions from concurrent users | |
| req_id = str(uuid.uuid4()) | |
| path1 = os.path.join(UPLOAD_FOLDER, f"{req_id}_img1.jpg") | |
| path2 = os.path.join(UPLOAD_FOLDER, f"{req_id}_img2.jpg") | |
| img1.save(path1) | |
| img2.save(path2) | |
| output_path = os.path.join(UPLOAD_FOLDER, f"{req_id}_result.png") | |
| heatmap_path = os.path.join(UPLOAD_FOLDER, f"{req_id}_heatmap.png") | |
| try: | |
| result_data = compare_images_ai(path1, path2, output_path, heatmap_path) | |
| except Exception as e: | |
| print(f"[app] AI pipeline failed ({e}); falling back to classical") | |
| result_data = compare_images(path1, path2, output_path, heatmap_path) | |
| result_data["pipeline"] = "classical" | |
| description = describe_change(path1, path2, result_data) | |
| # Construct the base URL dynamically so Hugging Face Space works without hardcoded domains | |
| public_base = os.environ.get("DIFFLENS_PUBLIC_URL") | |
| if not public_base: | |
| proto = request.headers.get("X-Forwarded-Proto", request.scheme) | |
| public_base = f"{proto}://{request.host}" | |
| public_base = public_base.rstrip("/") | |
| return jsonify({ | |
| "result": f"{public_base}/uploads/{req_id}_result.png", | |
| "heatmap": f"{public_base}/uploads/{req_id}_heatmap.png", | |
| "metrics": result_data, | |
| "description": description, | |
| }) | |
| def uploaded_file(filename): | |
| return send_from_directory(UPLOAD_FOLDER, filename) | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 5050)) | |
| app.run(debug=True, host="0.0.0.0", port=port) | |