Spaces:
Sleeping
Sleeping
File size: 5,133 Bytes
3ba5b62 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | 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/<filename>')
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)
|