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)