Spaces:
Build error
Build error
Upload 16 files
Browse files- Dockerfile.dockerfile +11 -0
- analytics_service.py +66 -0
- app.py +65 -0
- attendance_manager.py +50 -0
- capture_screenshots.py +19 -0
- create_demo.py +26 -0
- demo_live.py +56 -0
- demo_simulated.py +90 -0
- demo_video.py +80 -0
- face_detection.py +27 -0
- face_recognition.py +42 -0
- interview_demo.py +95 -0
- mock_dashboard.py +66 -0
- sql.sql +35 -0
- video_processor.py +55 -0
- web_socket.py +58 -0
Dockerfile.dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dockerfile.backend
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
COPY . .
|
| 10 |
+
|
| 11 |
+
CMD ["uvicorn", "backend.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
analytics_service.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/services/analytics_service.py
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
from sqlalchemy import func, and_
|
| 4 |
+
from database.models import Attendance, Employee
|
| 5 |
+
import pandas as pd
|
| 6 |
+
|
| 7 |
+
class AnalyticsService:
|
| 8 |
+
def __init__(self, db_session):
|
| 9 |
+
self.db = db_session
|
| 10 |
+
|
| 11 |
+
def get_personnel_present(self, date=None):
|
| 12 |
+
"""Get number of personnel present today"""
|
| 13 |
+
if date is None:
|
| 14 |
+
date = datetime.now().date()
|
| 15 |
+
|
| 16 |
+
count = self.db.query(Attendance).filter(
|
| 17 |
+
Attendance.date == date,
|
| 18 |
+
Attendance.status.in_(['present', 'early', 'on_time', 'late'])
|
| 19 |
+
).count()
|
| 20 |
+
return count
|
| 21 |
+
|
| 22 |
+
def get_shift_coverage(self):
|
| 23 |
+
"""Calculate shift coverage percentage"""
|
| 24 |
+
total_employees = self.db.query(Employee).count()
|
| 25 |
+
present_today = self.get_personnel_present()
|
| 26 |
+
|
| 27 |
+
if total_employees == 0:
|
| 28 |
+
return 0
|
| 29 |
+
return (present_today / total_employees) * 100
|
| 30 |
+
|
| 31 |
+
def get_staffing_trends(self, days=30):
|
| 32 |
+
"""Get staffing trends for last N days"""
|
| 33 |
+
end_date = datetime.now().date()
|
| 34 |
+
start_date = end_date - timedelta(days=days)
|
| 35 |
+
|
| 36 |
+
results = self.db.query(
|
| 37 |
+
Attendance.date,
|
| 38 |
+
func.count(Attendance.id).label('present_count')
|
| 39 |
+
).filter(
|
| 40 |
+
Attendance.date.between(start_date, end_date),
|
| 41 |
+
Attendance.status.in_(['present', 'early', 'on_time', 'late'])
|
| 42 |
+
).group_by(Attendance.date).all()
|
| 43 |
+
|
| 44 |
+
return pd.DataFrame(results, columns=['date', 'present_count'])
|
| 45 |
+
|
| 46 |
+
def get_missing_personnel(self):
|
| 47 |
+
"""Get list of employees who haven't checked in today"""
|
| 48 |
+
today = datetime.now().date()
|
| 49 |
+
|
| 50 |
+
# Get all employees
|
| 51 |
+
all_employees = self.db.query(Employee).all()
|
| 52 |
+
all_ids = [e.id for e in all_employees]
|
| 53 |
+
|
| 54 |
+
# Get present employees today
|
| 55 |
+
present = self.db.query(Attendance.employee_id).filter(
|
| 56 |
+
Attendance.date == today,
|
| 57 |
+
Attendance.status.in_(['present', 'early', 'on_time', 'late'])
|
| 58 |
+
).distinct().all()
|
| 59 |
+
present_ids = [p[0] for p in present]
|
| 60 |
+
|
| 61 |
+
# Find missing
|
| 62 |
+
missing_ids = set(all_ids) - set(present_ids)
|
| 63 |
+
|
| 64 |
+
return self.db.query(Employee).filter(
|
| 65 |
+
Employee.id.in_(missing_ids)
|
| 66 |
+
).all()
|
app.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# frontend/app.py (Streamlit version)
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import requests
|
| 4 |
+
import websocket
|
| 5 |
+
import json
|
| 6 |
+
import base64
|
| 7 |
+
from PIL import Image
|
| 8 |
+
import io
|
| 9 |
+
import numpy as np
|
| 10 |
+
import cv2
|
| 11 |
+
|
| 12 |
+
st.set_page_config(
|
| 13 |
+
page_title="AI Personnel Dashboard",
|
| 14 |
+
page_icon="👥",
|
| 15 |
+
layout="wide"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Sidebar
|
| 19 |
+
st.sidebar.title("🔍 Navigation")
|
| 20 |
+
page = st.sidebar.selectbox(
|
| 21 |
+
"Select Page",
|
| 22 |
+
["Live Feed", "Dashboard", "Analytics", "Employee Management"]
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
if page == "Live Feed":
|
| 26 |
+
st.title("📹 Live Personnel Monitoring")
|
| 27 |
+
|
| 28 |
+
# Video feed placeholder
|
| 29 |
+
video_placeholder = st.empty()
|
| 30 |
+
metrics_placeholder = st.empty()
|
| 31 |
+
|
| 32 |
+
# Connect to WebSocket
|
| 33 |
+
# ... WebSocket implementation for real-time feed
|
| 34 |
+
|
| 35 |
+
elif page == "Dashboard":
|
| 36 |
+
st.title("📊 Dashboard")
|
| 37 |
+
|
| 38 |
+
# Fetch analytics
|
| 39 |
+
response = requests.get("http://localhost:8000/api/analytics/dashboard")
|
| 40 |
+
data = response.json()
|
| 41 |
+
|
| 42 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 43 |
+
|
| 44 |
+
with col1:
|
| 45 |
+
st.metric("👥 Personnel Present", data['personnel_present'])
|
| 46 |
+
|
| 47 |
+
with col2:
|
| 48 |
+
st.metric("📈 Shift Coverage", f"{data['shift_coverage']:.1f}%")
|
| 49 |
+
|
| 50 |
+
with col3:
|
| 51 |
+
st.metric("📅 Total Employees", 50) # Fetch from DB
|
| 52 |
+
|
| 53 |
+
with col4:
|
| 54 |
+
missing_count = len(data['missing_personnel'])
|
| 55 |
+
st.metric("⚠️ Missing Personnel", missing_count)
|
| 56 |
+
|
| 57 |
+
# Staffing trends chart
|
| 58 |
+
st.subheader("Staffing Trends (Last 30 Days)")
|
| 59 |
+
# ... Plot trends using matplotlib/plotly
|
| 60 |
+
|
| 61 |
+
# Missing personnel alerts
|
| 62 |
+
if missing_count > 0:
|
| 63 |
+
st.warning(f"⚠️ {missing_count} employees haven't checked in today!")
|
| 64 |
+
for employee in data['missing_personnel']:
|
| 65 |
+
st.write(f"- {employee['name']}")
|
attendance_manager.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/core/attendance_manager.py
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from database.models import Attendance, Employee
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
class AttendanceManager:
|
| 8 |
+
def __init__(self, db_session: Session):
|
| 9 |
+
self.db = db_session
|
| 10 |
+
self.attendance_cache = {}
|
| 11 |
+
self.grace_period_minutes = 15
|
| 12 |
+
|
| 13 |
+
def mark_attendance(self, employee_id, timestamp=None):
|
| 14 |
+
"""Mark attendance for an employee"""
|
| 15 |
+
if timestamp is None:
|
| 16 |
+
timestamp = datetime.now()
|
| 17 |
+
|
| 18 |
+
# Check if already marked today
|
| 19 |
+
today = timestamp.date()
|
| 20 |
+
existing = self.db.query(Attendance).filter(
|
| 21 |
+
Attendance.employee_id == employee_id,
|
| 22 |
+
Attendance.date == today
|
| 23 |
+
).first()
|
| 24 |
+
|
| 25 |
+
if existing:
|
| 26 |
+
existing.check_out = timestamp
|
| 27 |
+
existing.status = 'present'
|
| 28 |
+
else:
|
| 29 |
+
# Determine if early/late/on-time
|
| 30 |
+
status = self._determine_status(timestamp)
|
| 31 |
+
attendance = Attendance(
|
| 32 |
+
employee_id=employee_id,
|
| 33 |
+
date=today,
|
| 34 |
+
check_in=timestamp,
|
| 35 |
+
status=status
|
| 36 |
+
)
|
| 37 |
+
self.db.add(attendance)
|
| 38 |
+
|
| 39 |
+
self.db.commit()
|
| 40 |
+
return True
|
| 41 |
+
|
| 42 |
+
def _determine_status(self, timestamp):
|
| 43 |
+
"""Determine if employee is on-time, late, or early"""
|
| 44 |
+
shift_start = timestamp.replace(hour=9, minute=0, second=0)
|
| 45 |
+
if timestamp < shift_start:
|
| 46 |
+
return 'early'
|
| 47 |
+
elif timestamp <= shift_start + timedelta(minutes=self.grace_period_minutes):
|
| 48 |
+
return 'on_time'
|
| 49 |
+
else:
|
| 50 |
+
return 'late'
|
capture_screenshots.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# capture_screenshots.py
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
def create_demo_screenshot():
|
| 6 |
+
"""Generate visual screenshots for portfolio"""
|
| 7 |
+
|
| 8 |
+
# Create simulated dashboard images
|
| 9 |
+
dashboard = np.zeros((800, 1200, 3), dtype=np.uint8)
|
| 10 |
+
|
| 11 |
+
# Add simulated metrics
|
| 12 |
+
cv2.rectangle(dashboard, (50, 50), (250, 150), (30, 30, 30), -1)
|
| 13 |
+
cv2.putText(dashboard, "👥 8 Personnel Present", (70, 110),
|
| 14 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
| 15 |
+
|
| 16 |
+
# Save
|
| 17 |
+
cv2.imwrite('dashboard_screenshot.jpg', dashboard)
|
| 18 |
+
|
| 19 |
+
create_demo_screenshot()
|
create_demo.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# create_demo_gif.py
|
| 2 |
+
import imageio
|
| 3 |
+
import cv2
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
def create_demo_gif(video_path, output_path='demo.gif', duration=0.5):
|
| 7 |
+
"""Create GIF from video for portfolio"""
|
| 8 |
+
reader = imageio.get_reader(video_path)
|
| 9 |
+
fps = reader.get_meta_data()['fps']
|
| 10 |
+
|
| 11 |
+
# Extract frames (every 3rd frame)
|
| 12 |
+
frames = []
|
| 13 |
+
for i, frame in enumerate(reader):
|
| 14 |
+
if i % 3 == 0:
|
| 15 |
+
# Convert to RGB
|
| 16 |
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 17 |
+
frames.append(frame_rgb)
|
| 18 |
+
if len(frames) >= 20: # Limit GIF length
|
| 19 |
+
break
|
| 20 |
+
|
| 21 |
+
# Save as GIF
|
| 22 |
+
imageio.mimsave(output_path, frames, duration=duration)
|
| 23 |
+
print(f"✅ GIF saved to {output_path}")
|
| 24 |
+
|
| 25 |
+
# Usage
|
| 26 |
+
create_demo_gif('demo_video.mp4')
|
demo_live.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# demo_live.py - Simple script to run a live demo
|
| 2 |
+
import cv2
|
| 3 |
+
from core.face_detection import FaceDetector
|
| 4 |
+
from core.face_recognition import FaceRecognizer
|
| 5 |
+
from core.attendance_manager import AttendanceManager
|
| 6 |
+
import time
|
| 7 |
+
|
| 8 |
+
def run_live_demo():
|
| 9 |
+
"""Run live demo with webcam"""
|
| 10 |
+
# Initialize components
|
| 11 |
+
detector = FaceDetector()
|
| 12 |
+
recognizer = FaceRecognizer()
|
| 13 |
+
|
| 14 |
+
# Open webcam
|
| 15 |
+
cap = cv2.VideoCapture(0)
|
| 16 |
+
|
| 17 |
+
print("🎥 Live Demo Started - Press 'q' to quit")
|
| 18 |
+
print("👤 Show your face to the camera")
|
| 19 |
+
|
| 20 |
+
while True:
|
| 21 |
+
ret, frame = cap.read()
|
| 22 |
+
if not ret:
|
| 23 |
+
break
|
| 24 |
+
|
| 25 |
+
# Detect and recognize
|
| 26 |
+
faces = detector.detect_faces(frame)
|
| 27 |
+
for face_data in faces:
|
| 28 |
+
name = recognizer.recognize_face(face_data['face_img'])
|
| 29 |
+
|
| 30 |
+
# Draw results
|
| 31 |
+
x1, y1, x2, y2 = face_data['bbox']
|
| 32 |
+
color = (0, 255, 0) if name else (0, 0, 255)
|
| 33 |
+
label = name if name else "Unknown"
|
| 34 |
+
|
| 35 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
|
| 36 |
+
cv2.putText(frame, label, (x1, y1-10),
|
| 37 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.9, color, 2)
|
| 38 |
+
|
| 39 |
+
# Show confidence
|
| 40 |
+
cv2.putText(frame, f"Conf: {face_data['confidence']:.2f}",
|
| 41 |
+
(x1, y2+20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
|
| 42 |
+
|
| 43 |
+
# Display stats
|
| 44 |
+
cv2.putText(frame, f"Faces Detected: {len(faces)}", (10, 30),
|
| 45 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
| 46 |
+
|
| 47 |
+
cv2.imshow('AI Attendance Demo', frame)
|
| 48 |
+
|
| 49 |
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
| 50 |
+
break
|
| 51 |
+
|
| 52 |
+
cap.release()
|
| 53 |
+
cv2.destroyAllWindows()
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
run_live_demo()
|
demo_simulated.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# demo_simulated.py - Generate realistic demo data
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
import random
|
| 6 |
+
|
| 7 |
+
class SimulatedDemo:
|
| 8 |
+
"""Generate simulated attendance data for dashboard demo"""
|
| 9 |
+
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.employees = self._create_employees()
|
| 12 |
+
self.attendance_log = []
|
| 13 |
+
|
| 14 |
+
def _create_employees(self):
|
| 15 |
+
"""Create employee database"""
|
| 16 |
+
names = [
|
| 17 |
+
"Alice Johnson", "Bob Smith", "Carol White",
|
| 18 |
+
"David Brown", "Eve Davis", "Frank Wilson",
|
| 19 |
+
"Grace Lee", "Henry Kim", "Ivy Chen", "Jack Taylor"
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
employees = []
|
| 23 |
+
for i, name in enumerate(names, 1):
|
| 24 |
+
employees.append({
|
| 25 |
+
'id': i,
|
| 26 |
+
'name': name,
|
| 27 |
+
'department': random.choice(['Engineering', 'Sales', 'HR', 'Marketing']),
|
| 28 |
+
'shift_start': '09:00',
|
| 29 |
+
'shift_end': '18:00'
|
| 30 |
+
})
|
| 31 |
+
return employees
|
| 32 |
+
|
| 33 |
+
def generate_attendance(self, days=30):
|
| 34 |
+
"""Generate simulated attendance data"""
|
| 35 |
+
end_date = datetime.now()
|
| 36 |
+
start_date = end_date - timedelta(days=days)
|
| 37 |
+
|
| 38 |
+
attendance = []
|
| 39 |
+
|
| 40 |
+
for day in range(days):
|
| 41 |
+
current_date = start_date + timedelta(days=day)
|
| 42 |
+
# Skip weekends
|
| 43 |
+
if current_date.weekday() >= 5:
|
| 44 |
+
continue
|
| 45 |
+
|
| 46 |
+
for employee in self.employees:
|
| 47 |
+
# 90% attendance rate
|
| 48 |
+
if random.random() < 0.9:
|
| 49 |
+
check_in = self._random_time(current_date, 8, 10)
|
| 50 |
+
check_out = self._random_time(current_date, 17, 19)
|
| 51 |
+
|
| 52 |
+
status = self._determine_status(check_in, '09:00')
|
| 53 |
+
|
| 54 |
+
attendance.append({
|
| 55 |
+
'employee_name': employee['name'],
|
| 56 |
+
'date': current_date.strftime('%Y-%m-%d'),
|
| 57 |
+
'check_in': check_in.strftime('%H:%M'),
|
| 58 |
+
'check_out': check_out.strftime('%H:%M'),
|
| 59 |
+
'status': status,
|
| 60 |
+
'department': employee['department']
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
self.attendance_log = pd.DataFrame(attendance)
|
| 64 |
+
return self.attendance_log
|
| 65 |
+
|
| 66 |
+
def _random_time(self, date, hour_start, hour_end):
|
| 67 |
+
"""Generate random time within range"""
|
| 68 |
+
hour = random.randint(hour_start, hour_end)
|
| 69 |
+
minute = random.randint(0, 59)
|
| 70 |
+
return date.replace(hour=hour, minute=minute)
|
| 71 |
+
|
| 72 |
+
def _determine_status(self, check_in, shift_start):
|
| 73 |
+
"""Determine attendance status"""
|
| 74 |
+
shift_hour, shift_min = map(int, shift_start.split(':'))
|
| 75 |
+
shift_start_time = check_in.replace(hour=shift_hour, minute=shift_min)
|
| 76 |
+
|
| 77 |
+
if check_in < shift_start_time:
|
| 78 |
+
return 'early'
|
| 79 |
+
elif check_in <= shift_start_time + timedelta(minutes=15):
|
| 80 |
+
return 'on_time'
|
| 81 |
+
else:
|
| 82 |
+
return 'late'
|
| 83 |
+
|
| 84 |
+
# Generate and save demo data
|
| 85 |
+
demo = SimulatedDemo()
|
| 86 |
+
attendance_data = demo.generate_attendance()
|
| 87 |
+
|
| 88 |
+
# Save as CSV for dashboard
|
| 89 |
+
attendance_data.to_csv('demo_attendance_data.csv', index=False)
|
| 90 |
+
print("✅ Demo data generated and saved to 'demo_attendance_data.csv'")
|
demo_video.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# demo_video.py - Process a video file
|
| 2 |
+
def process_video_demo(video_path='sample_video.mp4'):
|
| 3 |
+
"""Process a pre-recorded video for demo"""
|
| 4 |
+
cap = cv2.VideoCapture(video_path)
|
| 5 |
+
|
| 6 |
+
# Setup video writer for output
|
| 7 |
+
output_path = 'demo_output.mp4'
|
| 8 |
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
| 9 |
+
out = cv2.VideoWriter(output_path, fourcc, 30.0,
|
| 10 |
+
(int(cap.get(3)), int(cap.get(4))))
|
| 11 |
+
|
| 12 |
+
detector = FaceDetector()
|
| 13 |
+
recognizer = FaceRecognizer()
|
| 14 |
+
|
| 15 |
+
frame_count = 0
|
| 16 |
+
recognition_results = []
|
| 17 |
+
|
| 18 |
+
while cap.isOpened():
|
| 19 |
+
ret, frame = cap.read()
|
| 20 |
+
if not ret:
|
| 21 |
+
break
|
| 22 |
+
|
| 23 |
+
frame_count += 1
|
| 24 |
+
|
| 25 |
+
# Process every 3rd frame for speed
|
| 26 |
+
if frame_count % 3 == 0:
|
| 27 |
+
faces = detector.detect_faces(frame)
|
| 28 |
+
|
| 29 |
+
for face_data in faces:
|
| 30 |
+
name = recognizer.recognize_face(face_data['face_img'])
|
| 31 |
+
recognition_results.append({
|
| 32 |
+
'frame': frame_count,
|
| 33 |
+
'name': name,
|
| 34 |
+
'confidence': float(face_data['confidence'])
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
# Draw on frame
|
| 38 |
+
x1, y1, x2, y2 = face_data['bbox']
|
| 39 |
+
if name:
|
| 40 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 3)
|
| 41 |
+
cv2.putText(frame, f"✅ {name}", (x1, y1-10),
|
| 42 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
|
| 43 |
+
else:
|
| 44 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2)
|
| 45 |
+
cv2.putText(frame, "❌ Unknown", (x1, y1-10),
|
| 46 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
|
| 47 |
+
|
| 48 |
+
out.write(frame)
|
| 49 |
+
|
| 50 |
+
cap.release()
|
| 51 |
+
out.release()
|
| 52 |
+
|
| 53 |
+
# Generate demo report
|
| 54 |
+
generate_demo_report(recognition_results)
|
| 55 |
+
print(f"✅ Demo video saved to {output_path}")
|
| 56 |
+
|
| 57 |
+
def generate_demo_report(results):
|
| 58 |
+
"""Create a report showing recognition accuracy"""
|
| 59 |
+
recognized = [r for r in results if r['name']]
|
| 60 |
+
unknown = [r for r in results if not r['name']]
|
| 61 |
+
|
| 62 |
+
report = f"""
|
| 63 |
+
📊 Demo Results Report
|
| 64 |
+
{'='*40}
|
| 65 |
+
Total Faces Processed: {len(results)}
|
| 66 |
+
Successfully Recognized: {len(recognized)} ({len(recognized)/len(results)*100:.1f}%)
|
| 67 |
+
Unknown Faces: {len(unknown)}
|
| 68 |
+
Average Confidence: {sum(r['confidence'] for r in recognized)/len(recognized):.2f}
|
| 69 |
+
|
| 70 |
+
Recognized Personnel:
|
| 71 |
+
"""
|
| 72 |
+
# Group by name
|
| 73 |
+
from collections import Counter
|
| 74 |
+
name_counts = Counter(r['name'] for r in recognized)
|
| 75 |
+
for name, count in name_counts.items():
|
| 76 |
+
report += f" - {name}: {count} detections\n"
|
| 77 |
+
|
| 78 |
+
with open('demo_report.txt', 'w') as f:
|
| 79 |
+
f.write(report)
|
| 80 |
+
print(report)
|
face_detection.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/core/face_detection.py
|
| 2 |
+
import cv2
|
| 3 |
+
from ultralytics import YOLO
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
class FaceDetector:
|
| 7 |
+
def __init__(self, model_path='yolov8n-face.pt'):
|
| 8 |
+
self.model = YOLO(model_path)
|
| 9 |
+
self.confidence_threshold = 0.5
|
| 10 |
+
|
| 11 |
+
def detect_faces(self, frame):
|
| 12 |
+
"""Detect faces in a frame using YOLO"""
|
| 13 |
+
results = self.model(frame)
|
| 14 |
+
faces = []
|
| 15 |
+
|
| 16 |
+
for result in results:
|
| 17 |
+
boxes = result.boxes
|
| 18 |
+
for box in boxes:
|
| 19 |
+
if box.conf > self.confidence_threshold:
|
| 20 |
+
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
|
| 21 |
+
face = frame[y1:y2, x1:x2]
|
| 22 |
+
faces.append({
|
| 23 |
+
'bbox': (x1, y1, x2, y2),
|
| 24 |
+
'face_img': face,
|
| 25 |
+
'confidence': box.conf
|
| 26 |
+
})
|
| 27 |
+
return faces
|
face_recognition.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/core/face_recognition.py
|
| 2 |
+
import face_recognition
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pickle
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
class FaceRecognizer:
|
| 8 |
+
def __init__(self, encodings_path='models/face_encodings.pkl'):
|
| 9 |
+
self.known_face_encodings = []
|
| 10 |
+
self.known_face_names = []
|
| 11 |
+
self.load_encodings(encodings_path)
|
| 12 |
+
|
| 13 |
+
def load_encodings(self, path):
|
| 14 |
+
"""Load pre-computed face encodings"""
|
| 15 |
+
if os.path.exists(path):
|
| 16 |
+
with open(path, 'rb') as f:
|
| 17 |
+
data = pickle.load(f)
|
| 18 |
+
self.known_face_encodings = data['encodings']
|
| 19 |
+
self.known_face_names = data['names']
|
| 20 |
+
|
| 21 |
+
def recognize_face(self, face_img):
|
| 22 |
+
"""Recognize a single face"""
|
| 23 |
+
if face_img is None or face_img.size == 0:
|
| 24 |
+
return None
|
| 25 |
+
|
| 26 |
+
# Get face encoding
|
| 27 |
+
face_locations = face_recognition.face_locations(face_img)
|
| 28 |
+
if not face_locations:
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
face_encoding = face_recognition.face_encodings(face_img, face_locations)[0]
|
| 32 |
+
|
| 33 |
+
# Compare with known faces
|
| 34 |
+
matches = face_recognition.compare_faces(
|
| 35 |
+
self.known_face_encodings,
|
| 36 |
+
face_encoding
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
if True in matches:
|
| 40 |
+
matched_index = matches.index(True)
|
| 41 |
+
return self.known_face_names[matched_index]
|
| 42 |
+
return None
|
interview_demo.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# interview_demo.py - Streamlit app for interviews
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import cv2
|
| 4 |
+
import tempfile
|
| 5 |
+
import numpy as np
|
| 6 |
+
from PIL import Image
|
| 7 |
+
|
| 8 |
+
def interview_demo_app():
|
| 9 |
+
st.set_page_config(page_title="Live Demo", layout="wide")
|
| 10 |
+
|
| 11 |
+
st.title("🎯 Live Demo - AI Attendance System")
|
| 12 |
+
|
| 13 |
+
st.info("""
|
| 14 |
+
**How to demonstrate:**
|
| 15 |
+
1. Click 'Start Camera' below
|
| 16 |
+
2. Show your face to the camera
|
| 17 |
+
3. See real-time detection and recognition
|
| 18 |
+
4. Watch attendance log update automatically
|
| 19 |
+
""")
|
| 20 |
+
|
| 21 |
+
# Sidebar with status
|
| 22 |
+
with st.sidebar:
|
| 23 |
+
st.header("🔴 System Status")
|
| 24 |
+
st.success("✅ Camera Ready")
|
| 25 |
+
st.success("✅ Model Loaded")
|
| 26 |
+
st.success("✅ Database Connected")
|
| 27 |
+
|
| 28 |
+
st.header("📊 Session Stats")
|
| 29 |
+
if 'detections' not in st.session_state:
|
| 30 |
+
st.session_state.detections = []
|
| 31 |
+
|
| 32 |
+
st.metric("Faces Detected", len(st.session_state.detections))
|
| 33 |
+
|
| 34 |
+
# Camera feed
|
| 35 |
+
col1, col2 = st.columns([2, 1])
|
| 36 |
+
|
| 37 |
+
with col1:
|
| 38 |
+
st.subheader("📹 Live Feed")
|
| 39 |
+
camera_placeholder = st.empty()
|
| 40 |
+
|
| 41 |
+
# Start camera button
|
| 42 |
+
if st.button("🚀 Start Camera", use_container_width=True):
|
| 43 |
+
cap = cv2.VideoCapture(0)
|
| 44 |
+
|
| 45 |
+
if not cap.isOpened():
|
| 46 |
+
st.error("⚠️ Could not access camera")
|
| 47 |
+
else:
|
| 48 |
+
# Process frames
|
| 49 |
+
frame_count = 0
|
| 50 |
+
while True:
|
| 51 |
+
ret, frame = cap.read()
|
| 52 |
+
if not ret:
|
| 53 |
+
break
|
| 54 |
+
|
| 55 |
+
# Your detection code here
|
| 56 |
+
# For demo, just show frame with overlay
|
| 57 |
+
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 58 |
+
frame = cv2.putText(frame, "🟢 Live Demo", (10, 30),
|
| 59 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
| 60 |
+
|
| 61 |
+
camera_placeholder.image(frame, use_container_width=True)
|
| 62 |
+
|
| 63 |
+
# Simulate detections
|
| 64 |
+
if frame_count % 30 == 0:
|
| 65 |
+
st.session_state.detections.append({
|
| 66 |
+
'time': datetime.now(),
|
| 67 |
+
'name': 'Demo User'
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
frame_count += 1
|
| 71 |
+
|
| 72 |
+
with col2:
|
| 73 |
+
st.subheader("📋 Live Attendance")
|
| 74 |
+
attendance_table = st.empty()
|
| 75 |
+
|
| 76 |
+
# Show attendance log
|
| 77 |
+
if st.session_state.detections:
|
| 78 |
+
df = pd.DataFrame(st.session_state.detections)
|
| 79 |
+
attendance_table.dataframe(df.tail(10))
|
| 80 |
+
else:
|
| 81 |
+
st.info("Waiting for detections...")
|
| 82 |
+
|
| 83 |
+
# Demo controls
|
| 84 |
+
col3, col4 = st.columns(2)
|
| 85 |
+
with col3:
|
| 86 |
+
if st.button("🧹 Clear Log", use_container_width=True):
|
| 87 |
+
st.session_state.detections = []
|
| 88 |
+
st.rerun()
|
| 89 |
+
with col4:
|
| 90 |
+
if st.button("📥 Export Demo Data", use_container_width=True):
|
| 91 |
+
# Export functionality
|
| 92 |
+
st.success("✅ Demo data exported!")
|
| 93 |
+
|
| 94 |
+
if __name__ == "__main__":
|
| 95 |
+
interview_demo_app()
|
mock_dashboard.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mock_dashboard.py - Show dashboard working without webcam
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
import plotly.express as px
|
| 6 |
+
|
| 7 |
+
# Load demo data
|
| 8 |
+
df = pd.read_csv('demo_attendance_data.csv')
|
| 9 |
+
|
| 10 |
+
def show_mock_dashboard():
|
| 11 |
+
"""Display dashboard with mock data"""
|
| 12 |
+
st.title("👥 AI Personnel Dashboard - Demo")
|
| 13 |
+
|
| 14 |
+
# Add watermark
|
| 15 |
+
st.markdown("""
|
| 16 |
+
<div style='position: fixed; bottom: 0; right: 0;
|
| 17 |
+
background: rgba(255,255,255,0.8); padding: 10px;'>
|
| 18 |
+
🎯 Demo Data - Not Live
|
| 19 |
+
</div>
|
| 20 |
+
""", unsafe_allow_html=True)
|
| 21 |
+
|
| 22 |
+
# Metrics
|
| 23 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 24 |
+
|
| 25 |
+
today = datetime.now().strftime('%Y-%m-%d')
|
| 26 |
+
today_data = df[df['date'] == today]
|
| 27 |
+
|
| 28 |
+
with col1:
|
| 29 |
+
st.metric("👥 Present Today", len(today_data))
|
| 30 |
+
|
| 31 |
+
with col2:
|
| 32 |
+
total_employees = 50
|
| 33 |
+
coverage = (len(today_data) / total_employees) * 100
|
| 34 |
+
st.metric("📈 Coverage", f"{coverage:.1f}%")
|
| 35 |
+
|
| 36 |
+
with col3:
|
| 37 |
+
late = len(today_data[today_data['status'] == 'late'])
|
| 38 |
+
st.metric("⏰ Late Arrivals", late)
|
| 39 |
+
|
| 40 |
+
with col4:
|
| 41 |
+
missing = total_employees - len(today_data)
|
| 42 |
+
st.metric("⚠️ Missing", missing)
|
| 43 |
+
|
| 44 |
+
# Charts
|
| 45 |
+
st.subheader("📊 Attendance Trends")
|
| 46 |
+
|
| 47 |
+
daily_attendance = df.groupby('date').size().reset_index(name='count')
|
| 48 |
+
fig = px.line(daily_attendance, x='date', y='count',
|
| 49 |
+
title='Daily Attendance Trend')
|
| 50 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 51 |
+
|
| 52 |
+
# Department breakdown
|
| 53 |
+
st.subheader("🏢 Department-wise Attendance")
|
| 54 |
+
dept_data = today_data.groupby('department').size().reset_index(name='count')
|
| 55 |
+
fig = px.bar(dept_data, x='department', y='count',
|
| 56 |
+
title='Today\'s Attendance by Department')
|
| 57 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 58 |
+
|
| 59 |
+
# Missing personnel
|
| 60 |
+
st.subheader("⚠️ Missing Personnel Alert")
|
| 61 |
+
missing_emps = ['Alice Johnson', 'Bob Smith', 'Carol White']
|
| 62 |
+
for emp in missing_emps:
|
| 63 |
+
st.warning(f"🔴 {emp} has not checked in today")
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
show_mock_dashboard()
|
sql.sql
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- PostgreSQL Schema
|
| 2 |
+
CREATE TABLE employees (
|
| 3 |
+
id SERIAL PRIMARY KEY,
|
| 4 |
+
employee_id VARCHAR(50) UNIQUE NOT NULL,
|
| 5 |
+
name VARCHAR(100) NOT NULL,
|
| 6 |
+
department VARCHAR(100),
|
| 7 |
+
position VARCHAR(100),
|
| 8 |
+
shift_start TIME,
|
| 9 |
+
shift_end TIME,
|
| 10 |
+
email VARCHAR(100),
|
| 11 |
+
face_encoding BYTEA,
|
| 12 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 13 |
+
);
|
| 14 |
+
|
| 15 |
+
CREATE TABLE attendance (
|
| 16 |
+
id SERIAL PRIMARY KEY,
|
| 17 |
+
employee_id INTEGER REFERENCES employees(id),
|
| 18 |
+
date DATE NOT NULL,
|
| 19 |
+
check_in TIMESTAMP,
|
| 20 |
+
check_out TIMESTAMP,
|
| 21 |
+
status VARCHAR(20),
|
| 22 |
+
location VARCHAR(100),
|
| 23 |
+
device_id VARCHAR(50),
|
| 24 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
CREATE TABLE alerts (
|
| 28 |
+
id SERIAL PRIMARY KEY,
|
| 29 |
+
type VARCHAR(50),
|
| 30 |
+
employee_id INTEGER REFERENCES employees(id),
|
| 31 |
+
message TEXT,
|
| 32 |
+
severity VARCHAR(20),
|
| 33 |
+
read BOOLEAN DEFAULT FALSE,
|
| 34 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 35 |
+
);
|
video_processor.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/core/video_processor.py
|
| 2 |
+
import cv2
|
| 3 |
+
import asyncio
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from core.face_detection import FaceDetector
|
| 6 |
+
from core.face_recognition import FaceRecognizer
|
| 7 |
+
from core.attendance_manager import AttendanceManager
|
| 8 |
+
|
| 9 |
+
class VideoProcessor:
|
| 10 |
+
def __init__(self, detector, recognizer, attendance_manager):
|
| 11 |
+
self.detector = detector
|
| 12 |
+
self.recognizer = recognizer
|
| 13 |
+
self.attendance_manager = attendance_manager
|
| 14 |
+
self.frame_buffer = []
|
| 15 |
+
self.processing_interval = 0.5 # Process every 0.5 seconds
|
| 16 |
+
|
| 17 |
+
async def process_video(self, video_source=0):
|
| 18 |
+
"""Process video stream from camera or file"""
|
| 19 |
+
cap = cv2.VideoCapture(video_source)
|
| 20 |
+
|
| 21 |
+
if not cap.isOpened():
|
| 22 |
+
raise ValueError("Could not open video source")
|
| 23 |
+
|
| 24 |
+
last_process_time = datetime.now()
|
| 25 |
+
|
| 26 |
+
while True:
|
| 27 |
+
ret, frame = cap.read()
|
| 28 |
+
if not ret:
|
| 29 |
+
break
|
| 30 |
+
|
| 31 |
+
# Process at intervals for efficiency
|
| 32 |
+
current_time = datetime.now()
|
| 33 |
+
if (current_time - last_process_time).total_seconds() >= self.processing_interval:
|
| 34 |
+
# Detect faces
|
| 35 |
+
faces = self.detector.detect_faces(frame)
|
| 36 |
+
|
| 37 |
+
for face_data in faces:
|
| 38 |
+
# Recognize face
|
| 39 |
+
name = self.recognizer.recognize_face(face_data['face_img'])
|
| 40 |
+
if name:
|
| 41 |
+
# Mark attendance
|
| 42 |
+
employee_id = self._get_employee_id(name)
|
| 43 |
+
if employee_id:
|
| 44 |
+
self.attendance_manager.mark_attendance(employee_id)
|
| 45 |
+
# Draw bounding box with name
|
| 46 |
+
x1, y1, x2, y2 = face_data['bbox']
|
| 47 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
| 48 |
+
cv2.putText(frame, name, (x1, y1-10),
|
| 49 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0,255,0), 2)
|
| 50 |
+
|
| 51 |
+
last_process_time = current_time
|
| 52 |
+
|
| 53 |
+
yield frame
|
| 54 |
+
|
| 55 |
+
cap.release()
|
web_socket.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/api/websocket.py
|
| 2 |
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
+
import asyncio
|
| 5 |
+
import json
|
| 6 |
+
import cv2
|
| 7 |
+
import base64
|
| 8 |
+
|
| 9 |
+
app = FastAPI()
|
| 10 |
+
|
| 11 |
+
app.add_middleware(
|
| 12 |
+
CORSMiddleware,
|
| 13 |
+
allow_origins=["*"],
|
| 14 |
+
allow_credentials=True,
|
| 15 |
+
allow_methods=["*"],
|
| 16 |
+
allow_headers=["*"],
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
@app.websocket("/ws/video")
|
| 20 |
+
async def video_websocket(websocket: WebSocket):
|
| 21 |
+
await websocket.accept()
|
| 22 |
+
|
| 23 |
+
# Initialize components
|
| 24 |
+
detector = FaceDetector()
|
| 25 |
+
recognizer = FaceRecognizer()
|
| 26 |
+
attendance_manager = AttendanceManager(db_session)
|
| 27 |
+
processor = VideoProcessor(detector, recognizer, attendance_manager)
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
async for frame in processor.process_video():
|
| 31 |
+
# Encode frame to JPEG
|
| 32 |
+
_, buffer = cv2.imencode('.jpg', frame)
|
| 33 |
+
frame_b64 = base64.b64encode(buffer).decode('utf-8')
|
| 34 |
+
|
| 35 |
+
# Send to client
|
| 36 |
+
await websocket.send_json({
|
| 37 |
+
'frame': frame_b64,
|
| 38 |
+
'timestamp': datetime.now().isoformat()
|
| 39 |
+
})
|
| 40 |
+
|
| 41 |
+
except WebSocketDisconnect:
|
| 42 |
+
print("Client disconnected")
|
| 43 |
+
|
| 44 |
+
@app.get("/api/analytics/dashboard")
|
| 45 |
+
async def get_dashboard_data():
|
| 46 |
+
"""Get all dashboard metrics"""
|
| 47 |
+
analytics = AnalyticsService(db_session)
|
| 48 |
+
|
| 49 |
+
return {
|
| 50 |
+
'personnel_present': analytics.get_personnel_present(),
|
| 51 |
+
'shift_coverage': analytics.get_shift_coverage(),
|
| 52 |
+
'staffing_trends': analytics.get_staffing_trends().to_dict(),
|
| 53 |
+
'missing_personnel': [
|
| 54 |
+
{'id': e.id, 'name': e.name}
|
| 55 |
+
for e in analytics.get_missing_personnel()
|
| 56 |
+
],
|
| 57 |
+
'timestamp': datetime.now().isoformat()
|
| 58 |
+
}
|