From 7db8710fea5446afad08f818d57f266a8777f197 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 15:55:03 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20repo=20improvements=20=E2=80=93=20L?= =?UTF-8?q?ICENSE,=20CI,=20tests,=20type=20hints,=20templates,=20Makefile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/icecold009/face-attendance-opencv-python/sessions/63322def-5ea7-4632-9328-d1a374b29e1d Co-authored-by: icecold009 <184486122+icecold009@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.md | 28 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 17 +++ .github/pull_request_template.md | 23 ++++ .github/workflows/ci.yml | 44 ++++++++ CHANGELOG.md | 37 +++++++ CONTRIBUTING.md | 79 ++++++++++++++ LICENSE | 21 ++++ Makefile | 20 ++++ README.md | 35 +++---- requirements.txt | 1 + src/attendance.py | 24 +++-- src/attendance_project.py | 94 ----------------- src/face_attendance_app.py | 17 +-- tests/conftest.py | 5 + tests/test_attendance.py | 122 ++++++++++++++++++++++ tests/test_face_attendance_app.py | 67 ++++++++++++ web_app.py | 28 +++-- 17 files changed, 524 insertions(+), 138 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile delete mode 100644 src/attendance_project.py create mode 100644 tests/conftest.py create mode 100644 tests/test_attendance.py create mode 100644 tests/test_face_attendance_app.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..a2847b1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug Report +about: Report a bug or unexpected behaviour +labels: bug +--- + +## Describe the Bug + + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. See error + +## Expected Behaviour + + +## Actual Behaviour + + +## Environment +- **OS**: +- **Python version**: +- **Camera**: +- **Browser (web UI only)**: + +## Additional Context + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f224958 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature Request +about: Suggest a new feature or improvement +labels: enhancement +--- + +## Problem / Motivation + + +## Proposed Solution + + +## Alternatives Considered + + +## Additional Context + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..e3c012a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,23 @@ +## Summary + + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor / code quality +- [ ] Documentation update +- [ ] Other: + +## Changes Made + +- + +## Testing +- [ ] Existing tests pass (`pytest tests/ -v`) +- [ ] New tests added for new functionality +- [ ] Manually tested with webcam + +## Checklist +- [ ] Code follows the project style (PEP 8, max line length 120) +- [ ] README updated if behaviour changed +- [ ] CHANGELOG.md updated under `[Unreleased]` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f4dde87 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + name: Lint (flake8) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install flake8 + run: pip install flake8 + + - name: Lint with flake8 + run: | + # Fail only on syntax errors and undefined names; style warnings are informational + flake8 src/ web_app.py --max-line-length=120 --select=E9,F63,F7,F82 + + test: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install test dependencies + run: pip install pytest pandas numpy opencv-python-headless Pillow + + - name: Run tests + run: pytest tests/ -v diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..86a92fc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +--- + +## [1.0.0] - 2026-01-22 + +### Added +- Real-time face recognition and attendance marking via webcam +- Flask web dashboard with live video preview (5 fps) +- Face enrollment via webcam capture directly in the browser +- Daily attendance records exported to CSV (`data/Attendance/`) +- CLI interface (`src/main.py`) for server or terminal-only environments +- OpenCV-based face recognition engine (`src/face_recognition.py`) — runs 100% locally, no cloud APIs +- Support for per-person image directories under `ImagesAttendance/` +- `GET /attendance` API endpoint for today's attendance list +- `GET /enrolled-persons` API endpoint for enrolled person names +- `POST /recognize` API endpoint for single-frame recognition +- `POST /enroll` API endpoint for face enrollment +- `GET /health` health-check endpoint +- `--host` and `--port` CLI flags for `web_app.py` + +--- + +## [Unreleased] + +### Planned +- Database backend (PostgreSQL / MongoDB) +- Multi-camera support +- Liveness detection (anti-spoofing) +- REST API for external integration +- Docker containerisation +- Email / SMS notifications +- PDF / Excel report generation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7fa0072 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# Contributing to Face Attendance System + +Thank you for your interest in contributing! This guide will help you get set up quickly. + +--- + +## 🚀 Getting Started + +### 1. Fork & Clone + +```bash +git clone https://github.com//face-attendance-opencv-python.git +cd face-attendance-opencv-python +``` + +### 2. Create a Virtual Environment + +```bash +python -m venv .venv +# Windows +.venv\Scripts\activate +# macOS/Linux +source .venv/bin/activate +``` + +### 3. Install Dependencies + +```bash +pip install -r requirements.txt +pip install pytest flake8 # dev/test tools +``` + +### 4. Verify Your Setup + +```bash +# Run the test suite +pytest tests/ -v + +# Run the linter +flake8 src/ web_app.py --max-line-length=120 --select=E9,F63,F7,F82 +``` + +--- + +## 🌿 Branching + +- Branch from `main` using a descriptive name: + - `feature/multi-camera-support` + - `fix/attendance-duplicate-entry` + - `docs/update-installation` + +--- + +## ✅ Pull Request Checklist + +Before opening a PR, please make sure: + +- [ ] The existing tests still pass (`pytest tests/ -v`) +- [ ] New functionality includes tests where applicable +- [ ] Code follows the existing style (PEP 8, max line length 120) +- [ ] The README is updated if you changed behaviour or added features + +--- + +## 🐛 Reporting Issues + +Use the [GitHub issue tracker](https://github.com/icecold009/face-attendance-opencv-python/issues). +Please include: + +- Python version and OS +- Steps to reproduce +- Expected vs. actual behaviour +- Any relevant error output or screenshots + +--- + +## 📄 License + +By contributing you agree that your contributions will be licensed under the project's [MIT License](LICENSE). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b18db87 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 icecold009 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a08a576 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY: install run run-cli test lint clean + +install: + pip install -r requirements.txt + +run: + python web_app.py + +run-cli: + cd src && python main.py + +test: + python -m pytest tests/ -v + +lint: + flake8 src/ web_app.py --max-line-length=120 --select=E9,F63,F7,F82 + +clean: + find . -type d -name __pycache__ -exec rm -rf {} + + find . -type f -name "*.pyc" -delete diff --git a/README.md b/README.md index afc40aa..b4ee877 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![Flask](https://img.shields.io/badge/Flask-2.0+-green?style=flat&logo=flask&logoColor=white)](https://flask.palletsprojects.com) [![License](https://img.shields.io/badge/License-MIT-yellow?style=flat)](LICENSE) [![Status](https://img.shields.io/badge/Status-Active-brightgreen?style=flat)]() +[![CI](https://github.com/icecold009/face-attendance-opencv-python/actions/workflows/ci.yml/badge.svg)](https://github.com/icecold009/face-attendance-opencv-python/actions/workflows/ci.yml) **A modern, local-first face recognition system for automatic attendance marking** @@ -90,12 +91,15 @@ python main.py - **Cost**: $0 (runs entirely on your machine) ### Screenshots -``` -Dashboard: Start/Stop Recognition, Enroll, View Attendance -Live Feed: Video + Annotated Results side-by-side -Detection: Green boxes for recognized faces, red for unknown -Enrollment: Capture → Submit → Automatic encoding -``` + +| Screen | Description | +|--------|-------------| +| **Dashboard** | Start/Stop recognition, enroll new people, view live attendance stats | +| **Live Feed** | Side-by-side: raw webcam video + annotated recognition results | +| **Detection** | Green bounding boxes for recognised faces, red for unknown visitors | +| **Enrollment** | Step-by-step: capture → enter name → submit → encoding saved automatically | + +> 💡 Run the app locally and capture your own screenshots to add here! --- @@ -318,7 +322,7 @@ pip install face-recognition ### Local Network Access ```bash -# Run with host='0.0.0.0' +# Run with --host 0.0.0.0 python web_app.py --host 0.0.0.0 # Access from: http://YOUR_IP:5000 ``` @@ -376,7 +380,8 @@ Jane,09:35:42,Present ## 🤝 Contributing -Contributions are welcome! Please feel free to: +Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines. + 1. Fork the repository 2. Create a feature branch 3. Make your changes @@ -392,6 +397,9 @@ Contributions are welcome! Please feel free to: - [ ] Docker containerization - [ ] Mobile app companion - [ ] Real-time notifications +- [ ] Email/SMS notifications +- [ ] Report generation (PDF/Excel) +- [ ] Age and gender detection --- @@ -402,14 +410,3 @@ Contributions are welcome! Please feel free to: ⭐ If you found this helpful, please consider starring the repository! -- [ ] Email/SMS notifications -- [ ] Report generation (PDF/Excel) -- [ ] Multi-face recognition in single frame -- [ ] Age and gender detection -- [ ] Liveness detection to prevent spoofing -- [ ] Mobile app integration - ---- - -**Created**: January 2026 -**Last Updated**: January 22, 2026 diff --git a/requirements.txt b/requirements.txt index c405d95..37951cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ cmake dlib>=19.7 click face-recognition-models +face-recognition>=1.3.0 flask>=2.0.0 diff --git a/src/attendance.py b/src/attendance.py index a766806..e1b1439 100644 --- a/src/attendance.py +++ b/src/attendance.py @@ -1,15 +1,21 @@ -import pandas as pd +from __future__ import annotations + import os +from typing import Optional, Set + +import pandas as pd + from datetime import datetime from utils import get_date, get_timestamp, create_directory + class AttendanceSystem: - def __init__(self, attendance_path='data/Attendance'): + def __init__(self, attendance_path: str = 'data/Attendance') -> None: self.attendance_path = attendance_path create_directory(attendance_path) - self.marked_today = set() - - def get_attendance_file(self): + self.marked_today: Set[str] = set() + + def get_attendance_file(self) -> str: """Get or create attendance file for today""" date = get_date() filename = f"{self.attendance_path}/Attendance_{date}.csv" @@ -20,7 +26,7 @@ def get_attendance_file(self): return filename - def mark_attendance(self, name, status='Present'): + def mark_attendance(self, name: str, status: str = 'Present') -> bool: """Mark attendance for a person""" if name in self.marked_today or name == "Unknown": return False @@ -43,7 +49,7 @@ def mark_attendance(self, name, status='Present'): print(f"Error marking attendance: {e}") return False - def get_attendance_summary(self): + def get_attendance_summary(self) -> Optional[pd.DataFrame]: """Get attendance summary for today""" filename = self.get_attendance_file() @@ -54,11 +60,11 @@ def get_attendance_summary(self): print(f"Error reading attendance: {e}") return None - def reset_daily_marked(self): + def reset_daily_marked(self) -> None: """Reset marked attendance for the day""" self.marked_today = set() - def get_person_attendance_history(self, name, days=30): + def get_person_attendance_history(self, name: str, days: int = 30) -> Optional[pd.DataFrame]: """Get attendance history for a person""" records = [] diff --git a/src/attendance_project.py b/src/attendance_project.py deleted file mode 100644 index 52beb08..0000000 --- a/src/attendance_project.py +++ /dev/null @@ -1,94 +0,0 @@ -import cv2 -import numpy as np -import face_recognition -import os -from datetime import datetime - -# b. Load images and class names -path = 'ImagesAttendance' -images = [] -classNames = [] -myList = os.listdir(path) -for cls in myList: - curImg = cv2.imread(os.path.join(path, cls)) - images.append(curImg) - classNames.append(os.path.splitext(cls)[0]) - - -# c. Define encoding function -def findEncodings(images): - encodeList = [] - for img in images: - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - encode = face_recognition.face_encodings(img)[0] - encodeList.append(encode) - return encodeList - - -# d. Encode known faces -encodeListKnown = findEncodings(images) -print("Encoding Complete") - - -# e. Attendance CSV function -attendance_dir = os.path.join('data', 'Attendance') -os.makedirs(attendance_dir, exist_ok=True) - - -def markAttendance(name): - date_str = datetime.now().strftime('%Y-%m-%d') - attendance_file = os.path.join(attendance_dir, f'Attendance_{date_str}.csv') - - with open(attendance_file, 'a+', encoding='utf-8') as f: - f.seek(0) - myDataList = f.readlines() - nameList = [] - for line in myDataList: - entry = line.split(',') - if entry: - nameList.append(entry[0]) - if name not in nameList: - time_str = datetime.now().strftime('%H:%M:%S') - f.write(f'{name},{time_str}\n') - - -# f. Initialize webcam -cap = cv2.VideoCapture(0) - -# g. Real-time loop -while True: - success, img = cap.read() - imgS = cv2.resize(img, (0, 0), None, 0.25, 0.25) - imgS = cv2.cvtColor(imgS, cv2.COLOR_BGR2RGB) - - facesCurFrame = face_recognition.face_locations(imgS) - encodesCurFrame = face_recognition.face_encodings(imgS, facesCurFrame) - - for encodeFace, faceLoc in zip(encodesCurFrame, facesCurFrame): - matches = face_recognition.compare_faces(encodeListKnown, encodeFace) - faceDis = face_recognition.face_distance(encodeListKnown, encodeFace) - matchIndex = np.argmin(faceDis) - - if matches[matchIndex]: - name = classNames[matchIndex].upper() - - top, right, bottom, left = faceLoc - top *= 4 - right *= 4 - bottom *= 4 - left *= 4 - - cv2.rectangle(img, (left, top), (right, bottom), (0, 255, 0), 2) - cv2.rectangle(img, (left, bottom - 35), (right, bottom), (0, 255, 0), cv2.FILLED) - cv2.putText(img, name, (left + 6, bottom - 6), cv2.FONT_HERSHEY_COMPLEX, 1, (255, 255, 255), 2) - - markAttendance(name) - - cv2.imshow('Webcam', img) - if cv2.waitKey(1) & 0xFF == ord('q'): - break - -cap.release() -cv2.destroyAllWindows() - - diff --git a/src/face_attendance_app.py b/src/face_attendance_app.py index 02c6cf2..ad722eb 100644 --- a/src/face_attendance_app.py +++ b/src/face_attendance_app.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import cv2 import numpy as np import face_recognition import os +from typing import List, Tuple from datetime import datetime from attendance import AttendanceSystem from utils import get_date @@ -10,14 +13,14 @@ class FaceAttendanceApp: """Face recognition and attendance system without UI dependencies""" - def __init__(self, enrollment_path='ImagesAttendance', attendance_path='data/Attendance'): + def __init__(self, enrollment_path: str = 'ImagesAttendance', attendance_path: str = 'data/Attendance') -> None: self.enrollment_path = enrollment_path self.attendance_system = AttendanceSystem(attendance_path) self.known_face_encodings = [] self.known_face_names = [] self.load_and_encode_faces() - def load_and_encode_faces(self): + def load_and_encode_faces(self) -> None: """Load all enrolled faces and generate their encodings""" self.known_face_encodings = [] self.known_face_names = [] @@ -57,7 +60,7 @@ def load_and_encode_faces(self): except Exception as e: print(f"Error encoding {img_path}: {e}") - def recognize_frame(self, frame, scale=0.25): + def recognize_frame(self, frame: np.ndarray, scale: float = 0.25) -> Tuple[np.ndarray, List[str]]: """ Recognize faces in a frame and mark attendance @@ -129,7 +132,7 @@ def recognize_frame(self, frame, scale=0.25): return annotated_frame, recognized_names - def enroll_person(self, name, image_path): + def enroll_person(self, name: str, image_path: str) -> bool: """ Enroll a new person from an image file @@ -172,7 +175,7 @@ def enroll_person(self, name, image_path): print(f"Error enrolling {name}: {e}") return False - def enroll_from_array(self, name, image_array): + def enroll_from_array(self, name: str, image_array: np.ndarray) -> bool: """ Enroll a new person from a numpy array (BGR format) @@ -211,7 +214,7 @@ def enroll_from_array(self, name, image_array): print(f"Error enrolling {name}: {e}") return False - def get_attendance_today(self): + def get_attendance_today(self) -> List[dict]: """ Get today's attendance list @@ -227,7 +230,7 @@ def get_attendance_today(self): print(f"Error reading attendance: {e}") return [] - def get_enrolled_persons(self): + def get_enrolled_persons(self) -> List[str]: """ Get list of all enrolled persons diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..cc1a378 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +import sys +import os + +# Add src/ to the path so tests can import project modules directly +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) diff --git a/tests/test_attendance.py b/tests/test_attendance.py new file mode 100644 index 0000000..0f02fb8 --- /dev/null +++ b/tests/test_attendance.py @@ -0,0 +1,122 @@ +"""Unit tests for AttendanceSystem.""" + +import os +import pytest +import pandas as pd + +from attendance import AttendanceSystem + + +@pytest.fixture +def system(tmp_path): + """Return a fresh AttendanceSystem backed by a temporary directory.""" + return AttendanceSystem(str(tmp_path)) + + +# --------------------------------------------------------------------------- +# get_attendance_file +# --------------------------------------------------------------------------- + +def test_get_attendance_file_creates_csv(system, tmp_path): + filepath = system.get_attendance_file() + assert os.path.exists(filepath) + assert filepath.endswith('.csv') + + +def test_get_attendance_file_has_correct_columns(system): + filepath = system.get_attendance_file() + df = pd.read_csv(filepath) + assert list(df.columns) == ['Name', 'Time', 'Status'] + + +def test_get_attendance_file_idempotent(system): + """Calling twice should return the same path and not overwrite data.""" + path1 = system.get_attendance_file() + system.mark_attendance('Alice') + path2 = system.get_attendance_file() + assert path1 == path2 + df = pd.read_csv(path1) + assert len(df) == 1 + + +# --------------------------------------------------------------------------- +# mark_attendance +# --------------------------------------------------------------------------- + +def test_mark_attendance_returns_true_first_time(system): + assert system.mark_attendance('Alice') is True + + +def test_mark_attendance_records_name(system): + system.mark_attendance('Bob') + df = pd.read_csv(system.get_attendance_file()) + assert 'Bob' in df['Name'].values + + +def test_mark_attendance_records_status(system): + system.mark_attendance('Carol', status='Late') + df = pd.read_csv(system.get_attendance_file()) + assert df.loc[df['Name'] == 'Carol', 'Status'].iloc[0] == 'Late' + + +def test_mark_attendance_duplicate_returns_false(system): + system.mark_attendance('Dave') + assert system.mark_attendance('Dave') is False + + +def test_mark_attendance_duplicate_not_written_twice(system): + system.mark_attendance('Eve') + system.mark_attendance('Eve') + df = pd.read_csv(system.get_attendance_file()) + assert len(df[df['Name'] == 'Eve']) == 1 + + +def test_mark_attendance_unknown_returns_false(system): + assert system.mark_attendance('Unknown') is False + + +def test_mark_attendance_unknown_not_written(system): + system.mark_attendance('Unknown') + df = pd.read_csv(system.get_attendance_file()) + assert 'Unknown' not in df['Name'].values + + +def test_mark_attendance_multiple_people(system): + system.mark_attendance('Alice') + system.mark_attendance('Bob') + df = pd.read_csv(system.get_attendance_file()) + assert set(df['Name'].values) == {'Alice', 'Bob'} + + +# --------------------------------------------------------------------------- +# reset_daily_marked +# --------------------------------------------------------------------------- + +def test_reset_daily_marked_clears_set(system): + system.mark_attendance('Alice') + assert 'Alice' in system.marked_today + system.reset_daily_marked() + assert len(system.marked_today) == 0 + + +def test_mark_after_reset_succeeds(system): + system.mark_attendance('Alice') + system.reset_daily_marked() + assert system.mark_attendance('Alice') is True + + +# --------------------------------------------------------------------------- +# get_attendance_summary +# --------------------------------------------------------------------------- + +def test_get_attendance_summary_returns_dataframe(system): + system.mark_attendance('Alice') + summary = system.get_attendance_summary() + assert isinstance(summary, pd.DataFrame) + assert 'Alice' in summary['Name'].values + + +def test_get_attendance_summary_empty_file(system): + summary = system.get_attendance_summary() + assert isinstance(summary, pd.DataFrame) + assert summary.empty diff --git a/tests/test_face_attendance_app.py b/tests/test_face_attendance_app.py new file mode 100644 index 0000000..4280818 --- /dev/null +++ b/tests/test_face_attendance_app.py @@ -0,0 +1,67 @@ +"""Unit tests for FaceAttendanceApp (no camera / face_recognition required).""" + +import os +import sys +import pytest + +# Ensure src/ is importable (conftest.py does this, but be explicit for clarity) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from face_attendance_app import FaceAttendanceApp + + +@pytest.fixture +def app(tmp_path): + """Return a FaceAttendanceApp with empty temporary directories.""" + enrollment_dir = tmp_path / 'images' + attendance_dir = tmp_path / 'attendance' + return FaceAttendanceApp( + enrollment_path=str(enrollment_dir), + attendance_path=str(attendance_dir), + ) + + +# --------------------------------------------------------------------------- +# get_enrolled_persons +# --------------------------------------------------------------------------- + +def test_get_enrolled_persons_empty(app): + persons = app.get_enrolled_persons() + assert persons == [] + + +def test_get_enrolled_persons_returns_list(app): + assert isinstance(app.get_enrolled_persons(), list) + + +# --------------------------------------------------------------------------- +# get_attendance_today +# --------------------------------------------------------------------------- + +def test_get_attendance_today_empty(app): + records = app.get_attendance_today() + assert records == [] + + +def test_get_attendance_today_returns_list(app): + assert isinstance(app.get_attendance_today(), list) + + +# --------------------------------------------------------------------------- +# load_and_encode_faces — directory creation side-effects +# --------------------------------------------------------------------------- + +def test_load_creates_enrollment_directory(tmp_path): + enrollment_dir = tmp_path / 'new_enrollment' + app = FaceAttendanceApp( + enrollment_path=str(enrollment_dir), + attendance_path=str(tmp_path / 'attendance'), + ) + assert enrollment_dir.exists() + + +def test_reload_does_not_raise_on_empty_dir(app): + """Calling load_and_encode_faces on an empty dir should not raise.""" + app.load_and_encode_faces() + assert app.known_face_encodings == [] + assert app.known_face_names == [] diff --git a/web_app.py b/web_app.py index 779363d..9b9f7a5 100644 --- a/web_app.py +++ b/web_app.py @@ -1,10 +1,14 @@ +from __future__ import annotations + +import argparse import os import sys import base64 import cv2 import numpy as np from io import BytesIO -from flask import Flask, render_template, request, jsonify +from typing import Any +from flask import Flask, render_template, request, jsonify, Response from datetime import datetime # Add src directory to path to import our modules @@ -22,13 +26,13 @@ @app.route('/') -def index(): +def index() -> str: """Serve the main HTML page""" return render_template('index.html') @app.route('/recognize', methods=['POST']) -def recognize(): +def recognize() -> Response: """ Handle face recognition on a frame. Expects: @@ -72,7 +76,7 @@ def recognize(): @app.route('/enroll', methods=['POST']) -def enroll(): +def enroll() -> Response: """ Enroll a new person. Expects: @@ -120,7 +124,7 @@ def enroll(): @app.route('/attendance', methods=['GET']) -def get_attendance(): +def get_attendance() -> Response: """ Get today's attendance list. Returns: @@ -140,7 +144,7 @@ def get_attendance(): @app.route('/enrolled-persons', methods=['GET']) -def get_enrolled_persons(): +def get_enrolled_persons() -> Response: """ Get list of all enrolled persons. Returns: @@ -158,12 +162,18 @@ def get_enrolled_persons(): @app.route('/health', methods=['GET']) -def health(): +def health() -> Response: """Health check endpoint""" return jsonify({'status': 'ok', 'timestamp': datetime.now().isoformat()}) if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Face Attendance Web App') + parser.add_argument('--host', default='127.0.0.1', help='Host to bind (default: 127.0.0.1)') + parser.add_argument('--port', type=int, default=5000, help='Port to bind (default: 5000)') + parser.add_argument('--debug', action='store_true', help='Enable debug mode') + args = parser.parse_args() + print("Starting Face Attendance Web App...") - print("Open http://localhost:5000 in your browser") - app.run(debug=True, host='127.0.0.1', port=5000) + print(f"Open http://{args.host}:{args.port} in your browser") + app.run(debug=args.debug, host=args.host, port=args.port) From 062fdb50324949061d75d9d030ec2d4132fa09e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 15:56:10 +0000 Subject: [PATCH 2/2] fix: add explicit permissions to CI workflow Agent-Logs-Url: https://github.com/icecold009/face-attendance-opencv-python/sessions/63322def-5ea7-4632-9328-d1a374b29e1d Co-authored-by: icecold009 <184486122+icecold009@users.noreply.github.com> --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4dde87..1b97fe4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: lint: name: Lint (flake8)