File size: 9,919 Bytes
df53738
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
One Step Greener – Face recognition attendance (waste management).
Flask application entry point.
"""

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

from database.db import init_db, add_employee, get_employee, get_all_employees, mark_attendance, get_today_attendance, delete_employee
from models.embeddings_store import EmbeddingStore
from models.face_engine import (
    decode_image,
    get_face_embedding,
    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_page():
    return render_template("manage.html")


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


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

@app.route("/api/register", methods=["POST"])
def api_register():
    """
    Body JSON:
      { employee_id, name, frames: [base64DataUrl, ...] }
    """
    data = request.get_json(force=True)
    employee_id = data.get("employee_id", "").strip()
    name        = data.get("name", "").strip()
    frames      = data.get("frames", [])

    if not employee_id or not name:
        return jsonify({"status": "error", "message": "Employee ID and name are required."}), 400

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

    logger.info(f"Registering {employee_id} ({name}) 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)
    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,
        "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 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"] == "already_marked":
        return jsonify({"status": "already_marked", "name": name})

    logger.info(f"Attendance marked: {employee_id} ({name}) at {result['timestamp']}")
    return jsonify({
        "status": "success",
        "name": name,
        "employee_id": employee_id,
        "timestamp": result["timestamp"],
        "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/employees/<employee_id>", methods=["DELETE"])
def api_employee_delete(employee_id):
    """Delete a registered employee (DB + face index)."""
    if not get_employee(employee_id):
        return jsonify({"status": "error", "message": "Employee not found"}), 404
    store.remove_employee(employee_id)
    if not delete_employee(employee_id):
        return jsonify({"status": "error", "message": "Delete failed"}), 500
    logger.info(f"Deleted employee {employee_id}")
    return jsonify({"status": "ok", "message": "Deleted"})


@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)