File size: 9,081 Bytes
ccf33d6
 
 
 
 
 
965041e
ccf33d6
a11a231
 
ccf33d6
 
 
 
 
 
 
 
 
 
 
 
 
965041e
a11a231
965041e
ccf33d6
965041e
ccf33d6
a11a231
965041e
 
 
 
 
 
 
 
 
 
 
ccf33d6
 
a11a231
965041e
 
 
a11a231
965041e
 
ccf33d6
 
 
 
965041e
ccf33d6
 
 
 
 
 
965041e
ccf33d6
 
 
 
a11a231
ccf33d6
 
 
 
 
 
 
 
965041e
ccf33d6
 
 
 
 
 
965041e
ccf33d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965041e
ccf33d6
 
 
 
 
 
 
 
 
 
 
 
 
965041e
ccf33d6
 
 
 
 
 
965041e
ccf33d6
 
 
965041e
ccf33d6
 
 
 
 
 
965041e
ccf33d6
 
 
 
 
 
965041e
ccf33d6
 
 
 
 
 
 
 
 
 
 
 
965041e
 
ccf33d6
965041e
ccf33d6
 
 
 
a11a231
 
 
ccf33d6
 
 
 
 
 
 
 
965041e
ccf33d6
 
 
965041e
 
ccf33d6
965041e
ccf33d6
 
 
965041e
 
 
 
 
 
 
 
 
a11a231
965041e
ccf33d6
965041e
a11a231
965041e
 
 
 
 
 
a11a231
ccf33d6
965041e
ccf33d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965041e
ccf33d6
 
 
 
 
 
 
 
965041e
 
ccf33d6
965041e
ccf33d6
 
 
a11a231
 
965041e
ccf33d6
 
 
 
 
 
 
965041e
ccf33d6
965041e
 
 
 
 
 
 
 
 
 
ccf33d6
965041e
fd00127
 
 
 
 
 
 
 
 
 
 
 
 
965041e
 
 
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
from flask import Flask, request, jsonify
import os
import pickle
import uuid
import json
from datetime import datetime
import threading

# 🛑 NO HEAVY IMPORTS AT TOP LEVEL
# We import them inside functions to prevent "Memory Limit" crashes on startup.

app = Flask(__name__)

# Global Cache
model_cache = {
    "lucid": None,
    "mouse": None,
    "fusion": None,
    "loaded": False,
    "error": None,
    "logs": []
}

# --- CONFIGURATION ---
# We write to /tmp because the root folder is Read-Only on HF Spaces
LOG_FILE_PATH = "/tmp/predictions.log"

# ------------------ LOGGING HELPERS ------------------
def log_prediction(req_id, payload, output):
    """Safely logs predictions to a temp file."""
    try:
        record = {
            "request_id": req_id,
            "time": datetime.utcnow().isoformat(),
            "input": payload,
            "output": output
        }
        with open(LOG_FILE_PATH, "a") as f:
            f.write(json.dumps(record) + "\n")
    except Exception as e:
        print(f"⚠️ LOGGING FAILED (Non-Fatal): {e}")

def log_feedback(feedback):
    """Safely logs user feedback."""
    try:
        feedback["time"] = datetime.utcnow().isoformat()
        with open(LOG_FILE_PATH, 'a') as f:
            f.write(json.dumps(feedback) + "\n")
    except Exception as e:
        print(f"⚠️ FEEDBACK LOGGING FAILED: {e}")

# ------------------ MODEL LOADING ------------------
def load_heavy_brains():
    if model_cache["loaded"]:
        return model_cache["logs"]

    log = []
    try:
        log.append("⏳ Importing TensorFlow...")
        import tensorflow as tf
        log.append("✅ TensorFlow Imported")
        
        log.append("⏳ Importing XGBoost...")
        import xgboost as xgb
        log.append("✅ XGBoost Imported")

        # Define Architecture locally
        Sequential = tf.keras.models.Sequential
        Input = tf.keras.layers.Input
        LSTM = tf.keras.layers.LSTM
        Dense = tf.keras.layers.Dense
        Dropout = tf.keras.layers.Dropout
        BatchNormalization = tf.keras.layers.BatchNormalization
        LeakyReLU = tf.keras.layers.LeakyReLU

        # Load LUCID
        if os.path.exists("lucid_cnn.h5"):
            model_cache["lucid"] = tf.keras.models.load_model("lucid_cnn.h5")
            log.append("✅ LUCID Model Loaded")
        else:
            log.append("⚠️ lucid_cnn.h5 missing")

        # Load MOUSE
        if os.path.exists("delbot_rnn.h5"):
            mouse_model = Sequential([
                Input(shape=(None, 10)),
                LSTM(128, return_sequences=True),
                BatchNormalization(),
                LeakyReLU(alpha=0.1),
                Dropout(0.3),
                LSTM(64),
                LeakyReLU(alpha=0.1),
                Dropout(0.1),
                Dense(2, activation='softmax')
            ])
            mouse_model.load_weights("delbot_rnn.h5")
            model_cache["mouse"] = mouse_model
            log.append("✅ Mouse Model Loaded")
        else:
            log.append("⚠️ delbot_rnn.h5 missing")

        # Load FUSION
        if os.path.exists("fusion_xgboost.pkl"):
            with open("fusion_xgboost.pkl", "rb") as f:
                model_cache["fusion"] = pickle.load(f)
            log.append("✅ Fusion Model Loaded")
        else:
            log.append("⚠️ fusion_xgboost.pkl missing")

        model_cache["loaded"] = True
        model_cache["logs"] = log
        return log

    except Exception as e:
        err = f"❌ CRITICAL LOAD ERROR: {str(e)}"
        print(err)
        model_cache["error"] = err
        return log + [err]

# ------------------ DATA PROCESSING ------------------
def process_mouse_data(trace):
    try:
        import numpy as np
        MAX_STEPS = 60
        if not trace or len(trace) < 2:
            return None
        
        vectors = []
        for i in range(1, len(trace)):
            dt = (trace[i]['t'] - trace[i-1]['t']) or 1
            dx = trace[i]['x'] - trace[i-1]['x']
            dy = trace[i]['y'] - trace[i-1]['y']
            angle = np.arctan2(dy, dx)
            vectors.append([dx, dy, dt, dx/dt, dy/dt, angle, 0.0, 0.0, 0.0, 0.0])

        data = np.array(vectors)
        if len(data) > MAX_STEPS:
            data = data[:MAX_STEPS]
        else:
            data = np.vstack([data, np.zeros((MAX_STEPS - len(data), 10))])
            
        return np.expand_dims(data, axis=0)
    except:
        return None

# ------------------ ROUTES ------------------
@app.route("/")
def home():
    return "<h3>Bot Detection Server</h3>Status: 🟢 Running"

@app.route("/detect", methods=["POST"])
def detect():
    req_id = str(uuid.uuid4())
    
    # 1. Load Brains (Lazy)
    load_logs = load_heavy_brains()
    
    if model_cache["error"]:
        return jsonify({"success": False, "error": model_cache["error"]})

    try:
        # CRITICAL FIX: Import numpy HERE so it exists even if 'lucid' block is skipped
        import numpy as np 

        data = request.json or {}
        botd = float(data.get("botd_score", 0.0))
        mouse_trace = data.get("mouse_trace", [])
        ts = data.get("request_timestamps", [])

        mouse_score = None
        net_score = 0.0

        # A. Mouse Prediction
        if model_cache["mouse"]:
            inp = process_mouse_data(mouse_trace)
            if inp is not None:
                raw_mouse = model_cache["mouse"].predict(inp, verbose=0)[0][1]
                mouse_score = float(raw_mouse)

        # B. Net Prediction
        if model_cache["lucid"] and len(ts) > 2:
            iat = np.diff(sorted(ts))[:10] / 1000.0
            mat = np.zeros((1, 10, 11, 1))
            l = min(len(iat), 10)
            mat[0, :l, 0, 0] = iat[:l]
            raw_net = model_cache["lucid"].predict(mat, verbose=0)[0][0]
            net_score = float(raw_net)

        # C. Fusion Prediction
        safe_mouse = mouse_score if mouse_score is not None else 0.5
        features = [botd, safe_mouse, net_score]
        
        final_prob = max(features) # Fallback
        
        if model_cache["fusion"]:
            try:
                # XGBoost might warn about feature names, but it won't crash
                raw_fusion = model_cache["fusion"].predict_proba([features])[0][1]
                final_prob = float(raw_fusion)
            except Exception as e:
                print(f"Fusion Pred Error: {e}")

        # D. Decision Logic
        # Now 'np' is guaranteed to be defined
        pct = float(np.clip(final_prob, 0.0, 1.0) * 100)
        
        if pct > 85:
            decision, action, is_bot = "BOT", "BLOCK", True
        elif pct > 50:
            decision, action, is_bot = "SUSPICIOUS", "CAPTCHA", True
        else:
            decision, action, is_bot = "HUMAN", "ALLOW", False

        response = {
            "success": True,
            "request_id": req_id,
            "is_bot": is_bot,
            "action": action,
            "decision": decision,
            "confidence": round(pct, 2),
            "forensics": {
                "botd": round(botd, 2),
                "mouse": round(safe_mouse, 2),
                "net": round(net_score, 2)
            },
            "signals": {
                "mouse_available": mouse_score is not None,
                "net_available": net_score > 0
            },
            "internal_logs": load_logs
        }
        
        # Log to file (Non-blocking)
        log_prediction(req_id, data, response)
        
        return jsonify(response)

    except Exception as e:
        # Print actual error to server logs for debugging
        print(f"RUNTIME ERROR: {e}")
        return jsonify({"success": False, "error": f"Runtime Error: {str(e)}"})

@app.route("/feedback", methods=["POST"])
def feedback():
    fb = request.json
    log_feedback(fb)
    return jsonify({"success": True})

# ------------------ BACKGROUND TASKS ------------------
def start_auto_retrain():
    try:
        if os.path.exists("auto_retrain.py"):
            from auto_retrain import retrain_loop
            t = threading.Thread(target=retrain_loop, daemon=True)
            t.start()
            print("🔄 Auto-retrain thread started.")
        else:
            print("⚠️ auto_retrain.py not found. Skipping background training.")
    except Exception as e:
        print(f"⚠️ Failed to start retrain thread: {e}")

# ------------------ ENTRY ------------------
@app.route("/admin/logs")
def view_logs():
    # ⚠️ SECURITY WARNING: In a real app, protect this with a password!
    try:
        if os.path.exists(LOG_FILE_PATH):
            with open(LOG_FILE_PATH, "r") as f:
                content = f.read()
            # Wrap in <pre> so it looks like code in the browser
            return f"<h3>Prediction Logs</h3><pre>{content}</pre>"
        else:
            return "<h3>Log file is empty (No requests yet).</h3>"
    except Exception as e:
        return f"Error reading logs: {e}"
if __name__ == "__main__":
    start_auto_retrain()
    app.run(host="0.0.0.0", port=7860)