File size: 11,039 Bytes
701628b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
279
280
281
282
283
284
285
286
287
"""
One Step Greener – Face recognition attendance (waste management).
Flask application entry point.
"""

import os
import logging
from flask import Flask, render_template, request, jsonify, redirect

from database.db import init_db, add_employee, get_employee, get_all_employees, mark_attendance, get_today_attendance
from models.embeddings_store import EmbeddingStore
from models.face_engine import (
    decode_image,
    get_face_embedding,
    check_face_in_frame,
    get_embeddings_from_frames,
    get_embeddings_and_crops_from_frames,
    get_face_crops_from_frames,
)
from models.anti_spoof import check_liveness, check_liveness_sequence

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", "constable-secret-2025")

REGISTER_PIN = os.environ.get("REGISTER_PIN", "3620")

# ── Initialise database and embedding store ─────────────────────────────────
init_db()
store = EmbeddingStore()

# ══════════════════════════════════════════════════════════════════════════════
# Page routes
# ══════════════════════════════════════════════════════════════════════════════

@app.route("/")
@app.route("/dashboard")
def dashboard():
    return render_template("dashboard.html")


@app.route("/register")
def register_page():
    return render_template("register.html")


@app.route("/manage")
def manage_redirect():
    return redirect("/dashboard", code=302)


@app.route("/attendance")
def attendance_page():
    return render_template("attendance.html")


# ══════════════════════════════════════════════════════════════════════════════
# API routes
# ══════════════════════════════════════════════════════════════════════════════

@app.route("/api/face-check", methods=["POST"])
def api_face_check():
    """
    Lightweight face-in-frame check for registration flow.
    Body: { frame: base64DataUrl }
    Returns: { face_detected, centered, big_enough, ready } (ready = all true).
    """
    data = request.get_json(force=True)
    frame = data.get("frame", "")
    if not frame:
        return jsonify({"face_detected": False, "centered": False, "big_enough": False, "ready": False})
    try:
        img = decode_image(frame)
    except Exception:
        return jsonify({"face_detected": False, "centered": False, "big_enough": False, "ready": False})
    r = check_face_in_frame(img)
    r["ready"] = r["face_detected"] and r["centered"] and r["big_enough"]
    return jsonify(r)


@app.route("/api/register", methods=["POST"])
def api_register():
    """
    Body JSON:
      Manforce: { user_type: 'manforce', aadhaar, name, mobile, frames }
      Employee: { user_type: 'employee', employee_id, frames }
    """
    data = request.get_json(force=True)
    user_type = (data.get("user_type") or "employee").strip().lower()
    frames = data.get("frames", [])

    if not frames:
        return jsonify({"status": "error", "message": "No frames provided."}), 400

    if user_type == "manforce":
        aadhaar = data.get("aadhaar", "").strip()
        name = data.get("name", "").strip()
        mobile = data.get("mobile", "").strip()
        if not aadhaar or not name or not mobile:
            return jsonify({"status": "error", "message": "Aadhaar number, full name and mobile number are required for Manforce."}), 400
        employee_id = aadhaar
    else:
        employee_id = data.get("employee_id", "").strip()
        if not employee_id:
            return jsonify({"status": "error", "message": "Employee code is required."}), 400
        name = data.get("name", "").strip() or employee_id
        aadhaar = ""
        mobile = ""

    logger.info(f"Registering {employee_id} ({name}, type={user_type}) with {len(frames)} frames …")
    embeddings, face_crops = get_embeddings_and_crops_from_frames(frames)

    if not embeddings:
        return jsonify({
            "status": "error",
            "message": "No face detected in the provided frames. "
                       "Please ensure good lighting and that your face is clearly visible."
        }), 400

    # Anti-spoofing: reject photo/screen/video (motion + blink + texture)
    if len(face_crops) >= 2:
        liveness = check_liveness_sequence([c for c in face_crops if c is not None and c.size > 0])
    else:
        liveness = check_liveness(face_crops[0]) if face_crops and face_crops[0] is not None else {"is_live": False}
    if not liveness.get("is_live", True):
        logger.warning(f"Registration rejected (spoof): {liveness.get('reason', 'liveness failed')}")
        return jsonify({
            "status": "spoof",
            "message": liveness.get("reason", "Liveness check failed. Use a live face, not a photo or screen."),
            "reason": liveness.get("reason", "Liveness check failed"),
            "composite": liveness.get("score", 0.0),
        }), 400

    # Check for duplicate face registration
    for emb in embeddings:
        match_id, score = store.search(emb)
        if match_id:
            match_emp = get_employee(match_id)
            match_name = match_emp["name"] if match_emp else match_id
            logger.warning(f"Registration rejected: face already registered to {match_name} ({match_id})")
            return jsonify({
                "status": "error",
                "message": f"This face is already registered to {match_name} ({match_id})."
            }), 400

    # Persist employee in DB and embeddings in FAISS
    add_employee(employee_id, name, user_type=user_type, aadhaar=aadhaar, mobile=mobile)
    store.add(employee_id, embeddings)

    logger.info(f"Registered {employee_id} with {len(embeddings)} embedding(s).")
    return jsonify({
        "status": "registered",
        "employee_id": employee_id,
        "name": name,
        "user_type": user_type,
        "embeddings_stored": len(embeddings),
    })


@app.route("/api/recognize", methods=["POST"])
def api_recognize():
    """
    Body JSON:
      { frame: base64DataUrl }  or  { frames: [base64DataUrl, ...] }
    When frames is provided, uses sequence liveness (motion + blink).

    Response JSON (one of):
      { status: 'success',        name, timestamp }
      { status: 'already_marked', name }
      { status: 'spoof', reason, composite }
      { status: 'unknown' }
      { status: 'no_face' }
    """
    data = request.get_json(force=True)
    frame = data.get("frame", "")
    frames = data.get("frames", [])

    # Prefer frames for sequence liveness (motion + blink) when available
    if frames and len(frames) >= 2:
        try:
            face_crops = get_face_crops_from_frames(frames)
        except Exception:
            face_crops = []
        if not face_crops:
            return jsonify({"status": "no_face"})
        # Use latest frame for identity
        try:
            img = decode_image(frames[-1])
        except Exception:
            return jsonify({"status": "no_face"})
        embedding, _ = get_face_embedding(img)
        if embedding is None:
            return jsonify({"status": "no_face"})
        liveness = check_liveness_sequence(face_crops)
    else:
        if not frame:
            return jsonify({"status": "no_face"})
        try:
            img = decode_image(frame)
        except Exception:
            return jsonify({"status": "no_face"})
        embedding, face_crop = get_face_embedding(img)
        if embedding is None:
            return jsonify({"status": "no_face"})
        if face_crop is not None:
            liveness = check_liveness(face_crop)
        else:
            liveness = {"is_live": True}

    if not liveness.get("is_live", True):
        logger.info(f"Spoof detected (score={liveness.get('score', 0):.4f}, reason={liveness.get('reason', '')})")
        return jsonify({
            "status": "spoof",
            "reason": liveness.get("reason", "Liveness check failed"),
            "scores": liveness.get("scores", {}),
            "composite": liveness.get("score", 0.0),
        })

    # Identity search
    employee_id, score = store.search(embedding)
    if employee_id is None:
        return jsonify({"status": "unknown"})

    employee = get_employee(employee_id)
    name = employee["name"] if employee else employee_id

    result = mark_attendance(employee_id)

    if result["status"] == "cooldown":
        return jsonify({
            "status": "cooldown",
            "name": name,
            "message": "Please wait 1 minute before punching again.",
        })

    punch_type = result.get("punch_type", "in")
    logger.info(f"Punch {punch_type}: {employee_id} ({name}) at {result['timestamp']}")
    return jsonify({
        "status": "success",
        "name": name,
        "employee_id": employee_id,
        "timestamp": result["timestamp"],
        "punch_type": punch_type,
        "confidence": round(score, 4),
    })


@app.route("/api/verify-pin", methods=["POST"])
def api_verify_pin():
    """Verify PIN to unlock Register form for this page. PIN must match REGISTER_PIN (default 3620)."""
    data = request.get_json(force=True)
    pin = (data.get("pin") or "").strip()
    if pin == REGISTER_PIN:
        return jsonify({"status": "ok", "message": "Verified"})
    return jsonify({"status": "error", "message": "Incorrect PIN"}), 403


@app.route("/api/employees", methods=["GET"])
def api_employees_list():
    employees = get_all_employees()
    return jsonify({"status": "ok", "employees": employees, "count": len(employees)})


@app.route("/api/attendance/today", methods=["GET"])
def api_today_attendance():
    records = get_today_attendance()
    return jsonify({"status": "ok", "records": records, "count": len(records)})


@app.route("/api/health", methods=["GET"])
def health():
    return jsonify({
        "status": "ok",
        "total_employees_indexed": store.total_vectors,
    })


# ══════════════════════════════════════════════════════════════════════════════

if __name__ == "__main__":
    port = int(os.environ.get("PORT", 5000))
    debug = os.environ.get("FLASK_DEBUG", "0") == "1"
    logger.info(f"One Step Greener starting on http://localhost:{port}")
    app.run(host="0.0.0.0", port=port, debug=debug, threaded=True)