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}") @app.route("/") def home(): return "Backend running" # ---------------- AUTH ---------------- @app.route("/auth/login", methods=["POST", "OPTIONS"]) 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}) @app.route("/auth/logout", methods=["POST", "OPTIONS"]) def logout(): if request.method == "OPTIONS": return ("", 204) session.clear() return jsonify({"ok": True}) @app.route("/auth/me", methods=["GET", "OPTIONS"]) def me(): if request.method == "OPTIONS": return ("", 204) return jsonify({"user": current_user()}) # ---------------- DIFF ---------------- @app.route("/compare", methods=["POST", "OPTIONS"]) 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, }) @app.route('/uploads/') 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)