Spaces:
Sleeping
Sleeping
| """ | |
| Database models and configuration for SCA CV Module | |
| Uses SQLAlchemy with SQLite | |
| """ | |
| from sqlalchemy import create_engine, Column, Integer, String, Float, Boolean, DateTime, ForeignKey, JSON, CheckConstraint, text | |
| from sqlalchemy.ext.declarative import declarative_base | |
| from sqlalchemy.orm import relationship, sessionmaker, scoped_session | |
| from datetime import datetime | |
| import json | |
| from pathlib import Path | |
| import os | |
| import sys | |
| import hashlib | |
| from config import config | |
| Base = declarative_base() | |
| class Person(Base): | |
| """Person tracking table - Campus optimized""" | |
| __tablename__ = 'persons' | |
| __table_args__ = () | |
| person_id = Column(String(50), primary_key=True) # e.g., "STU_CS_2024_001" or "person_000" | |
| student_id = Column(String(50), nullable=True, index=True) # Actual student/faculty ID | |
| department = Column(String(50), nullable=True, index=True) # CS, IT, MECH, etc. | |
| user_type = Column(String(20), default='student') # 'student', 'faculty', 'staff' | |
| first_seen = Column(DateTime, default=datetime.now) | |
| last_seen = Column(DateTime, default=datetime.now, onupdate=datetime.now) | |
| total_detections = Column(Integer, default=0) | |
| face_image_path = Column(String(255), nullable=True) | |
| detection_method = Column(String(50), default='appearance') # 'face' or 'appearance' | |
| total_credits_earned = Column(Float, default=0.0) # Cumulative blockchain credits | |
| wallet_address = Column(String(42), nullable=True) # Ethereum/Polygon address | |
| # Relationship to events | |
| events = relationship('Event', back_populates='person', cascade='all, delete-orphan') | |
| activities = relationship('PersonActivity', back_populates='person', cascade='all, delete-orphan') | |
| def to_dict(self): | |
| """Convert person to dictionary""" | |
| return { | |
| 'person_id': self.person_id, | |
| 'student_id': self.student_id, | |
| 'department': self.department, | |
| 'user_type': self.user_type, | |
| 'first_seen': self.first_seen.isoformat() if self.first_seen else None, | |
| 'last_seen': self.last_seen.isoformat() if self.last_seen else None, | |
| 'total_detections': self.total_detections, | |
| 'face_image_path': self.face_image_path, | |
| 'detection_method': self.detection_method, | |
| 'total_credits': self.total_credits_earned, | |
| 'wallet_address': self.wallet_address | |
| } | |
| def __repr__(self): | |
| return f"<Person(person_id='{self.person_id}', dept='{self.department}', credits={self.total_credits_earned})>" | |
| class Event(Base): | |
| """Event detection table - Campus optimized""" | |
| __tablename__ = 'events' | |
| __table_args__ = ( | |
| CheckConstraint('person_count >= 0', name='check_person_count_positive'), | |
| CheckConstraint('confidence >= 0.0 AND confidence <= 1.0', name='check_confidence_range'), | |
| CheckConstraint('device_count >= 0', name='check_device_count_positive'), | |
| ) | |
| event_id = Column(Integer, primary_key=True, autoincrement=True) | |
| timestamp = Column(DateTime, default=datetime.now, index=True) | |
| room_id = Column(String(50), index=True) # e.g., "CS_LAB_101", "IT_CLASS_202" | |
| department = Column(String(50), nullable=True, index=True) # Extracted from room_id | |
| # Detection details | |
| occupancy = Column(Boolean, default=False) | |
| person_count = Column(Integer, default=0) | |
| # Person reference (nullable for events without persons) | |
| person_id = Column(String(50), ForeignKey('persons.person_id'), nullable=True, index=True) | |
| # Bounding box data (stored as JSON) | |
| bbox = Column(JSON, nullable=True) # {"x1": int, "y1": int, "x2": int, "y2": int} | |
| face_bbox = Column(JSON, nullable=True) | |
| # Detection metadata | |
| confidence = Column(Float, default=0.0) | |
| overall_confidence = Column(Float, default=0.0) | |
| action_confidence = Column(Float, default=0.0) | |
| detection_method = Column(String(50), nullable=True) # 'face' or 'appearance' | |
| # Device information | |
| devices_detected = Column(JSON, default='[]') # [{"type": "laptop", "confidence": 0.85}] | |
| device_count = Column(Integer, default=0) | |
| # Video metadata | |
| video_file = Column(String(255), nullable=True) | |
| frame_number = Column(Integer, nullable=True) | |
| # Action detection | |
| action_detected = Column(String(100), nullable=True) | |
| action_type = Column(String(50), nullable=True) # 'sustainable', 'unsustainable', 'neutral' | |
| # Energy tracking | |
| energy_saved_estimate = Column(Float, default=0.0) # Watts or kWh | |
| blockchain_credits = Column(Float, default=0.0) # ₹ value | |
| status = Column(String(20), default='pending', index=True) # 'pending', 'verified', 'rejected' | |
| # Device state tracking | |
| devices_on = Column(JSON, default='[]') # List of devices in ON state | |
| devices_off = Column(JSON, default='[]') # List of devices in OFF state | |
| lights_on = Column(Boolean, default=False) | |
| # NEW: Multi-User Analytics Persistence | |
| impact_analytics = Column(JSON, default='[]') # Detailed breakdown for multi-user events | |
| # Relationship to person | |
| person = relationship('Person', back_populates='events') | |
| def __repr__(self): | |
| return f"<Event(event_id={self.event_id}, person_id='{self.person_id}', timestamp='{self.timestamp}')>" | |
| def to_dict(self): | |
| """Convert event to dictionary""" | |
| return { | |
| 'event_id': self.event_id, | |
| 'timestamp': self.timestamp.isoformat() if self.timestamp else None, | |
| 'room_id': self.room_id, | |
| 'department': self.department, | |
| 'occupancy': self.occupancy, | |
| 'person_count': self.person_count, | |
| 'person_id': self.person_id, | |
| 'bbox': self.bbox, | |
| 'face_bbox': self.face_bbox, | |
| 'confidence': self.confidence, | |
| 'overall_confidence': self.overall_confidence or self.confidence, | |
| 'action_confidence': self.action_confidence or self.confidence, | |
| 'detection_method': self.detection_method, | |
| 'devices_detected': self.devices_detected or [], | |
| 'device_count': self.device_count, | |
| 'video_file': self.video_file, | |
| 'frame_number': self.frame_number, | |
| 'action_detected': self.action_detected, | |
| 'action_type': self.action_type, | |
| 'energy_saved_estimate': self.energy_saved_estimate, | |
| 'blockchain_credits': self.blockchain_credits, | |
| 'status': self.status, | |
| 'devices_on': self.devices_on or [], | |
| 'devices_off': self.devices_off or [], | |
| 'lights_on': self.lights_on, | |
| 'impact_analytics': self.impact_analytics or [] | |
| } | |
| class PersonActivity(Base): | |
| """Person activity log table for incentive tracking""" | |
| __tablename__ = 'person_activities' | |
| activity_id = Column(Integer, primary_key=True, autoincrement=True) | |
| person_id = Column(String(50), ForeignKey('persons.person_id'), index=True) | |
| timestamp = Column(DateTime, default=datetime.now, index=True) | |
| room_id = Column(String(50)) | |
| activity_type = Column(String(50), index=True) # 'presence', 'entry', 'exit', 'device_usage', 'violation' | |
| details = Column(JSON, nullable=True) # Additional metadata | |
| # Incentive tracking | |
| incentive_points = Column(Float, default=0.0) # Positive or negative | |
| incentive_reason = Column(String(255), nullable=True) | |
| # Relationship to person | |
| person = relationship('Person', back_populates='activities') | |
| def __repr__(self): | |
| return f"<PersonActivity(activity_id={self.activity_id}, person_id='{self.person_id}', type='{self.activity_type}')>" | |
| def to_dict(self): | |
| """Convert activity to dictionary""" | |
| return { | |
| 'activity_id': self.activity_id, | |
| 'person_id': self.person_id, | |
| 'timestamp': self.timestamp.isoformat(), | |
| 'room_id': self.room_id, | |
| 'activity_type': self.activity_type, | |
| 'details': self.details, | |
| 'incentive_points': self.incentive_points, | |
| 'incentive_reason': self.incentive_reason | |
| } | |
| class ProcessingTask(Base): | |
| """Background processing tasks persistent storage""" | |
| __tablename__ = 'processing_tasks' | |
| task_id = Column(String(36), primary_key=True) # UUID | |
| status = Column(String(20), default='queued') # queued, processing, completed, failed | |
| progress = Column(Float, default=0.0) | |
| filename = Column(String(255), nullable=False) | |
| created_at = Column(DateTime, default=datetime.now) | |
| updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) | |
| completion_time = Column(DateTime, nullable=True) | |
| # Task metadata/results | |
| user_email = Column(String(100), nullable=True) | |
| user_department = Column(String(100), nullable=True) | |
| # Store results/errors as JSON | |
| results = Column(JSON, nullable=True) | |
| error = Column(String, nullable=True) | |
| # Execution stats | |
| current_frame = Column(Integer, default=0) | |
| total_frames = Column(Integer, default=0) | |
| def to_dict(self): | |
| return { | |
| 'task_id': self.task_id, | |
| 'status': self.status, | |
| 'progress': self.progress, | |
| 'filename': self.filename, | |
| 'created_at': self.created_at.isoformat() if self.created_at else None, | |
| 'updated_at': self.updated_at.isoformat() if self.updated_at else None, | |
| 'completion_time': self.completion_time.isoformat() if self.completion_time else None, | |
| 'results': self.results, | |
| 'error': self.error, | |
| 'current_frame': self.current_frame, | |
| 'total_frames': self.total_frames | |
| } | |
| class User(Base): | |
| """User authentication table for role-based access control""" | |
| __tablename__ = 'users' | |
| user_id = Column(Integer, primary_key=True, autoincrement=True) | |
| email = Column(String(255), unique=True, nullable=False, index=True) | |
| password_hash = Column(String(255), nullable=False) # In production, use bcrypt/argon2 | |
| name = Column(String(255), nullable=True) | |
| role = Column(String(20), default='student', index=True) # 'student', 'faculty', 'admin' | |
| department = Column(String(100), nullable=True) | |
| created_at = Column(DateTime, default=datetime.now) | |
| last_login = Column(DateTime, nullable=True) | |
| is_active = Column(Boolean, default=True) | |
| def __repr__(self): | |
| return f"<User(user_id={self.user_id}, email='{self.email}', role='{self.role}')>" | |
| def to_dict(self): | |
| """Convert user to dictionary (excluding password)""" | |
| return { | |
| 'user_id': self.user_id, | |
| 'email': self.email, | |
| 'name': self.name, | |
| 'role': self.role, | |
| 'department': self.department, | |
| 'created_at': self.created_at.isoformat() if self.created_at else None, | |
| 'is_active': self.is_active | |
| } | |
| class ContactInquiry(Base): | |
| """Contact form submissions from homepage""" | |
| __tablename__ = 'contact_inquiries' | |
| inquiry_id = Column(Integer, primary_key=True, autoincrement=True) | |
| name = Column(String(255), nullable=False) | |
| email = Column(String(255), nullable=False, index=True) | |
| message = Column(String(2000), nullable=False) | |
| submitted_at = Column(DateTime, default=datetime.now, index=True) | |
| status = Column(String(20), default='new', index=True) # 'new', 'read', 'responded', 'archived' | |
| ip_address = Column(String(45), nullable=True) # Support IPv6 | |
| user_agent = Column(String(500), nullable=True) | |
| def __repr__(self): | |
| return f"<ContactInquiry(inquiry_id={self.inquiry_id}, email='{self.email}', status='{self.status}')>" | |
| def to_dict(self): | |
| """Convert contact inquiry to dictionary""" | |
| return { | |
| 'inquiry_id': self.inquiry_id, | |
| 'name': self.name, | |
| 'email': self.email, | |
| 'message': self.message, | |
| 'submitted_at': self.submitted_at.isoformat() if self.submitted_at else None, | |
| 'status': self.status | |
| } | |
| class Database: | |
| """Database management class - Supports SQLite and PostgreSQL (Neon)""" | |
| def __init__(self, db_url=None, auto_create_admin=True): | |
| """ | |
| Initialize database connection | |
| Args: | |
| db_url: SQLAlchemy database URL | |
| auto_create_admin: Whether to auto-create admin user if none exist | |
| """ | |
| if db_url is None: | |
| # Check environment variable first (Production/Neon) | |
| db_url = os.environ.get('DATABASE_URL') | |
| # Strict mode: Warn but allow fallback in production (Prevents Boot Loop) | |
| if db_url is None: | |
| if config.is_production(): | |
| print("\n" + "!"*80) | |
| print("⚠️ WARNING: 'DATABASE_URL' is missing in Production environment.") | |
| print(" The node will fall back to local SQLite persistence.") | |
| print(" DATA WILL NOT PERSIST ACROSS CONTAINER REBOOTS on ephemeral platforms like HF.") | |
| print(" To solve this, set the DATABASE_URL secret (e.g. Neon/Postgres).") | |
| print("!"*80 + "\n") | |
| # Fallback to local SQLite only in development/sandbox | |
| is_vercel = os.environ.get('VERCEL') == '1' | |
| base_dir = config.BASE_DIR | |
| if is_vercel: | |
| # Vercel only allows writing to /tmp | |
| db_path = Path('/tmp') / 'sca_events.db' | |
| else: | |
| db_path = base_dir / 'outputs' / 'sca_events.db' | |
| db_path.parent.mkdir(parents=True, exist_ok=True) | |
| db_url = f'sqlite:///{db_path}' | |
| # Protocol normalization for SQLAlchemy (Heroku/Neon often use postgres://) | |
| if db_url.startswith('postgres://'): | |
| db_url = db_url.replace('postgres://', 'postgresql://', 1) | |
| is_sqlite = db_url.startswith('sqlite') | |
| # Connection parameters | |
| connect_args = {} | |
| if is_sqlite: | |
| connect_args = { | |
| 'check_same_thread': False, | |
| 'timeout': 30 | |
| } | |
| # Create engine | |
| self.engine = create_engine( | |
| db_url, | |
| echo=False, | |
| pool_pre_ping=True, | |
| connect_args=connect_args | |
| ) | |
| self.Session = scoped_session(sessionmaker(bind=self.engine)) | |
| # Create tables if they don't exist | |
| Base.metadata.create_all(self.engine) | |
| # Cleanup: Mark any 'processing' tasks as 'failed' (Server Restart detected) | |
| self._cleanup_stale_tasks() | |
| # SQLite-specific optimizations | |
| if is_sqlite: | |
| try: | |
| self._ensure_schema_up_to_date() | |
| with self.engine.connect() as conn: | |
| conn.execute(text('PRAGMA journal_mode=WAL')) | |
| conn.execute(text('PRAGMA synchronous=NORMAL')) | |
| conn.execute(text('PRAGMA cache_size=-64000')) | |
| conn.execute(text('PRAGMA busy_timeout=30000')) | |
| conn.commit() | |
| except Exception as e: | |
| print(f"⚠️ SQLite optimization warning: {e}") | |
| # Auto-create admin user if no users exist | |
| if auto_create_admin: | |
| self._ensure_admin_exists() | |
| # Seed Sandbox Data logic: | |
| # STRICT RULE: NEVER seed mock data into a Remote/Production database (Postgres/Neon) or Production Environment. | |
| # Sandbox seeding is ONLY for Local Development using SQLite. | |
| is_sqlite = 'sqlite' in str(self.engine.url) | |
| if config.is_production(): | |
| print("🔒 Production Mode Active: Strict integrity enforced. No sandbox data will be seeded.") | |
| elif config.is_local() and is_sqlite: | |
| self._seed_sandbox_data() | |
| elif config.is_local() and not is_sqlite: | |
| print("ℹ️ Local Environment detected, but connected to Remote Database (Mainnet).") | |
| print(" Skipping Sandbox Seeding to preserve Mainnet integrity.") | |
| def _seed_sandbox_data(self): | |
| """Seed database with mock data for Sandbox/Demo mode""" | |
| if config.is_production(): | |
| print("⚠️ Attempted to seed sandbox data in PRODUCTION. Operation blocked.") | |
| return | |
| session = self.get_session() | |
| try: | |
| event_count = session.query(Event).count() | |
| if event_count > 0: | |
| print("ℹ️ Sandbox events already exist. Skipping seed.") | |
| return | |
| print("🚀 Initializing Sandbox Population (Restored)...") | |
| import random | |
| from datetime import timedelta | |
| from sqlalchemy import func | |
| # Use deterministic seed for consistent demos | |
| random.seed(42) | |
| # Get person IDs | |
| persons = session.query(Person).all() | |
| if not persons: | |
| print("⚠️ No persons found for seeding.") | |
| return | |
| person_ids = [p.person_id for p in persons] | |
| # Rooms and Actions | |
| rooms = ['CS_LAB_101', 'IT_HUB_202', 'MECH_WORKSHOP', 'MAIN_LIBRARY', 'FACULTY_LOUNGE', 'AUDITORIUM_A'] | |
| sust_actions = [('light_off', 5, 20), ('fan_off', 3, 15), ('ac_off', 15, 60), ('laptop_sleep', 1, 8)] | |
| unsust_actions = [('light_on_empty', -2, 0), ('fan_on_empty', -1, 0), ('ac_on_empty', -10, 0)] | |
| # Generate 150 Events | |
| for i in range(150): | |
| is_sust = random.random() < 0.75 | |
| action, credits, energy = random.choice(sust_actions if is_sust else unsust_actions) | |
| room = random.choice(rooms) | |
| pid = random.choice(person_ids) | |
| # Random time in last 30 days | |
| ts = datetime.now() - timedelta(days=random.randint(0, 30), minutes=random.randint(0, 1440)) | |
| event = Event( | |
| timestamp=ts, | |
| room_id=room, | |
| department=room.split('_')[0], | |
| person_id=pid, | |
| confidence=random.uniform(0.9, 0.99), | |
| action_detected=action, | |
| action_type='sustainable' if is_sust else 'unsustainable', | |
| energy_saved_estimate=float(energy), | |
| blockchain_credits=float(abs(credits)), | |
| status='verified', | |
| overall_confidence=random.uniform(0.9, 0.99), | |
| action_confidence=random.uniform(0.85, 0.99) | |
| ) | |
| session.add(event) | |
| activity = PersonActivity( | |
| person_id=pid, | |
| timestamp=ts, | |
| room_id=room, | |
| activity_type='disbursement' if is_sust else 'violation', | |
| incentive_points=credits, | |
| incentive_reason=f'{"Reward" if is_sust else "Penalty"}: {action}' | |
| ) | |
| session.add(activity) | |
| # Sync Credits | |
| session.commit() | |
| print("✓ Generated 150+ mock events") | |
| # Recalculate balances | |
| for p in persons: | |
| total = session.query(func.sum(PersonActivity.incentive_points)).filter_by(person_id=p.person_id).scalar() or 0 | |
| p.total_credits_earned = float(max(0, total)) | |
| session.commit() | |
| print("✅ Sandbox population complete!") | |
| except Exception as e: | |
| session.rollback() | |
| print(f"❌ Sandbox seeding failed: {e}") | |
| finally: | |
| session.close() | |
| def _ensure_schema_up_to_date(self): | |
| """Add missing columns to existing tables and handle constraint resets""" | |
| db_path = str(self.engine.url).replace('sqlite:///', '') | |
| needs_reset = False | |
| try: | |
| with self.engine.connect() as conn: | |
| # 1. Check for Event table columns | |
| existing_event_cols = [c['name'] for c in self.engine.dialect.get_columns(conn, 'events')] | |
| # Columns to add if missing | |
| event_updates = [ | |
| ('overall_confidence', 'FLOAT DEFAULT 0.0'), | |
| ('action_confidence', 'FLOAT DEFAULT 0.0'), | |
| ('impact_analytics', 'JSON DEFAULT "[]"') | |
| ] | |
| for col_name, col_type in event_updates: | |
| if col_name not in existing_event_cols: | |
| print(f"🔧 Database Sync: Adding missing column '{col_name}' to 'events' table") | |
| conn.execute(text(f"ALTER TABLE events ADD COLUMN {col_name} {col_type}")) | |
| # 2. Check for outdated constraints (require full reset in SQLite) | |
| event_sql = conn.execute(text("SELECT sql FROM sqlite_master WHERE type='table' AND name='events'")).scalar() | |
| person_sql = conn.execute(text("SELECT sql FROM sqlite_master WHERE type='table' AND name='persons'")).scalar() | |
| if (event_sql and 'check_energy_positive' in event_sql) or \ | |
| (person_sql and 'check_credits_positive' in person_sql): | |
| print("🔧 Database Sync: Outdated constraints detected. Ledger reset required.") | |
| needs_reset = True | |
| conn.commit() | |
| except Exception as e: | |
| # Table might not exist yet, which is fine | |
| pass | |
| if needs_reset: | |
| try: | |
| import os | |
| print(f"🧹 Clearing legacy ledger: {db_path}") | |
| # Dispose engine to release file locks | |
| self.engine.dispose() | |
| timestamp = int(datetime.now().timestamp()) | |
| os.rename(db_path, f"{db_path}.old.{timestamp}") | |
| print("✅ Legacy ledger archived.") | |
| # Re-create tables in the new database file | |
| Base.metadata.create_all(self.engine) | |
| print("✅ New ledger initialized with updated constraints.") | |
| except Exception as e: | |
| print(f"❌ Reset failed: {e}") | |
| print("💡 Please manually delete 'outputs/sca_events.db' if it persists.") | |
| def _ensure_admin_exists(self): | |
| """Create default admin user if no users exist in the system""" | |
| session = self.get_session() | |
| try: | |
| user_count = session.query(User).count() | |
| if user_count == 0: | |
| # Define unified users (Admin always exists) | |
| admin_password = None | |
| # STRICT PRODUCTION POLICY: | |
| # If running in Production (Cloud), we require a secure password. | |
| if config.is_production(): | |
| import secrets | |
| import string | |
| env_pass = os.environ.get('INITIAL_ADMIN_PASSWORD') | |
| if env_pass: | |
| admin_password = env_pass | |
| else: | |
| # Generate strong random password for Production security | |
| chars = string.ascii_letters + string.digits + "!@#$%^&*" | |
| admin_password = ''.join(secrets.choice(chars) for i in range(24)) | |
| print(f"\n{'!'*60}") | |
| print(f"🔒 SECURE PRODUCTION ADMIN GENERATED: {admin_password}") | |
| print(f" PLEASE SAVE THIS PASSWORD IMMEDIATELY!") | |
| print(f"{'!'*60}\n") | |
| else: | |
| # LOCAL DEV POLICY: | |
| # For convenience in Local (Mainnet or Demo), we default to 'admin123'. | |
| admin_password = os.environ.get('INITIAL_ADMIN_PASSWORD', 'admin123') | |
| demo_users = [ | |
| { | |
| 'email': 'admin@sca.campus', | |
| 'name': 'System Administrator', | |
| 'role': 'admin', | |
| 'dept': 'Administration', | |
| 'pass': admin_password | |
| }, | |
| { | |
| 'email': 'student@sca.campus', | |
| 'name': 'Demo Student', | |
| 'role': 'student', | |
| 'dept': 'Computer Science', | |
| 'pass': 'user123' | |
| }, | |
| { | |
| 'email': 'faculty@sca.campus', | |
| 'name': 'Demo Faculty', | |
| 'role': 'faculty', | |
| 'dept': 'Electrical Engineering', | |
| 'pass': 'user123' | |
| }, | |
| ] | |
| # In production, we ONLY want the admin user, not the demo accounts | |
| # Also, if we are connecting to Mainnet (Remote DB) from Local, we should NOT create junk users. | |
| is_sqlite = 'sqlite' in str(self.engine.url) | |
| should_create_demo_users = config.is_local() and is_sqlite | |
| if not should_create_demo_users: | |
| # Filter to only include admin | |
| demo_users = [u for u in demo_users if u['role'] == 'admin'] | |
| print(f"🔒 Mainnet/Production Init: Skipping creation of {len(demo_users) - 1 if len(demo_users)>1 else 0} demo accounts.") | |
| # Import hash_password cleanly | |
| try: | |
| from jwt_auth import hash_password | |
| except ImportError: | |
| # Fallback: Try to use bcrypt directly if jwt_auth module is not reachable | |
| try: | |
| import bcrypt | |
| def hash_password(password): | |
| salt = bcrypt.gensalt() | |
| return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8') | |
| except ImportError: | |
| print("CRITICAL: bcrypt not found. Cannot create secure admin user.") | |
| raise | |
| for user_data in demo_users: | |
| try: | |
| # Re-hash password for each user | |
| pwd_hash = hash_password(user_data['pass']) | |
| # 1. Create User | |
| user = User( | |
| email=user_data['email'], | |
| password_hash=pwd_hash, | |
| name=user_data['name'], | |
| role=user_data['role'], | |
| department=user_data['dept'], | |
| is_active=True | |
| ) | |
| session.add(user) | |
| # 2. Create corresponding Person record for wallet/credits tracking | |
| person = Person( | |
| person_id=user_data['email'], | |
| student_id=hashlib.sha256(user_data['email'].encode()).hexdigest()[:10].upper() if 'student' in user_data['role'] else None, | |
| department=user_data['dept'], | |
| user_type=user_data['role'], | |
| total_credits_earned=0.0 | |
| ) | |
| session.add(person) | |
| # Add User-specific Persons (e.g. Pratham) for Demo richness | |
| # Only in Local SQLite environments | |
| if should_create_demo_users: | |
| if user_data['email'] == 'student@sca.campus': | |
| # Extra detailed student | |
| extra_student = Person( | |
| person_id='pratham@sca.campus', | |
| student_id='STU_CS_001', | |
| department='Computer Science', | |
| user_type='student', | |
| total_credits_earned=120.5 | |
| ) | |
| session.add(extra_student) | |
| # Create User for him too | |
| p_user = User( | |
| email='pratham@sca.campus', | |
| password_hash=pwd_hash, # Same demo pass | |
| name='Pratham Amritkar', | |
| role='student', | |
| department='Computer Science' | |
| ) | |
| session.add(p_user) | |
| except Exception as e: | |
| print(f"Error creating demo user/person {user_data['email']}: {e}") | |
| session.commit() | |
| print(f"✓ Auto-created {len(demo_users)} initial users (Admin{' & Demo Accounts' if should_create_demo_users else ''})") | |
| except Exception as e: | |
| session.rollback() | |
| print(f"Warning: Could not auto-create admin: {e}") | |
| finally: | |
| session.close() | |
| def get_session(self): | |
| """Get a new database session""" | |
| return self.Session() | |
| def close(self): | |
| """Close database connection""" | |
| self.Session.remove() | |
| def add_person(self, person_id, detection_method='appearance', face_image_path=None): | |
| """Add or update a person""" | |
| session = self.get_session() | |
| try: | |
| person = session.query(Person).filter_by(person_id=person_id).first() | |
| if person: | |
| # Update existing person | |
| person.last_seen = datetime.now() | |
| person.total_detections += 1 | |
| if detection_method == 'face' and person.detection_method == 'appearance': | |
| person.detection_method = 'face' | |
| if face_image_path: | |
| person.face_image_path = face_image_path | |
| else: | |
| # Create new person | |
| person = Person( | |
| person_id=person_id, | |
| detection_method=detection_method, | |
| face_image_path=face_image_path, | |
| total_detections=1 | |
| ) | |
| session.add(person) | |
| session.commit() | |
| # Refresh to get updated values, then expunge | |
| session.refresh(person) | |
| session.expunge(person) | |
| return person | |
| except Exception as e: | |
| session.rollback() | |
| raise e | |
| finally: | |
| session.close() | |
| def add_event(self, event_data): | |
| """ | |
| Add an event to the database | |
| Args: | |
| event_data: Dictionary with event information | |
| """ | |
| if not event_data: | |
| return None | |
| session = self.get_session() | |
| try: | |
| # EDGE CASE FIX: Handle malformed timestamps or None values gracefully | |
| ts_val = event_data.get('timestamp') | |
| timestamp = datetime.now() | |
| if ts_val and isinstance(ts_val, str): | |
| try: | |
| timestamp = datetime.fromisoformat(ts_val) | |
| except ValueError: | |
| pass # Keep default now() | |
| # EDGE CASE FIX: Ensure list fields are never None (handle JSON null) | |
| devices_detected = event_data.get('devices_detected') or [] | |
| devices_on = event_data.get('devices_on') or [] | |
| devices_off = event_data.get('devices_off') or [] | |
| event = Event( | |
| timestamp=timestamp, | |
| room_id=event_data.get('room_id') or 'UNKNOWN_ROOM', | |
| occupancy=event_data.get('occupancy', False), | |
| person_count=event_data.get('person_count', 0), | |
| person_id=event_data.get('person_id'), | |
| bbox=event_data.get('bbox'), | |
| face_bbox=event_data.get('face_bbox'), | |
| confidence=event_data.get('confidence', 0.0), | |
| detection_method=event_data.get('detection_method'), | |
| devices_detected=devices_detected, | |
| device_count=len(devices_detected), | |
| video_file=event_data.get('video_file'), | |
| frame_number=event_data.get('frame_number'), | |
| action_detected=event_data.get('action_detected'), | |
| action_type=event_data.get('action_type'), | |
| energy_saved_estimate=event_data.get('energy_saved_estimate', 0.0), | |
| blockchain_credits=event_data.get('blockchain_credits', 0.0), | |
| overall_confidence=event_data.get('overall_confidence', 0.0), | |
| action_confidence=event_data.get('action_confidence', 0.0), | |
| devices_on=devices_on, | |
| devices_off=devices_off, | |
| lights_on=event_data.get('lights_on', False), | |
| impact_analytics=event_data.get('impact_analytics') or [], | |
| status=event_data.get('status', 'pending') # Default to 'pending' for admin review | |
| ) | |
| session.add(event) | |
| session.commit() | |
| # Refresh to get auto-generated ID, then expunge | |
| session.refresh(event) | |
| session.expunge(event) | |
| return event | |
| except Exception as e: | |
| session.rollback() | |
| raise e | |
| finally: | |
| session.close() | |
| def add_activity(self, person_id, activity_type, details=None, incentive_points=0, incentive_reason=None, room_id=None): | |
| """Add a person activity""" | |
| session = self.get_session() | |
| try: | |
| activity = PersonActivity( | |
| person_id=person_id, | |
| activity_type=activity_type, | |
| details=details, | |
| incentive_points=incentive_points, | |
| incentive_reason=incentive_reason, | |
| room_id=room_id or 'CS_Lab_5' | |
| ) | |
| session.add(activity) | |
| # Update the Person's cached total_credits_earned | |
| person = session.query(Person).filter_by(person_id=person_id).first() | |
| if person: | |
| person.total_credits_earned = (person.total_credits_earned or 0) + incentive_points | |
| person.last_seen = datetime.now() | |
| person.total_detections += 1 | |
| else: | |
| # If person doesn't exist, create one | |
| new_person = Person( | |
| person_id=person_id, | |
| total_credits_earned=float(incentive_points), | |
| total_detections=1, | |
| first_seen=datetime.now(), | |
| last_seen=datetime.now(), | |
| department=room_id.split('_')[0] if room_id and '_' in room_id else 'Universal' | |
| ) | |
| session.add(new_person) | |
| session.commit() | |
| # Refresh to get auto-generated ID, then expunge | |
| session.refresh(activity) | |
| session.expunge(activity) | |
| return activity | |
| except Exception as e: | |
| session.rollback() | |
| raise e | |
| finally: | |
| session.close() | |
| def get_all_persons(self): | |
| """Get all persons""" | |
| session = self.get_session() | |
| try: | |
| persons = session.query(Person).all() | |
| # Expunge objects from session to prevent DetachedInstanceError | |
| for person in persons: | |
| session.expunge(person) | |
| return persons | |
| finally: | |
| session.close() | |
| def get_person_events(self, person_id): | |
| """Get all events for a person""" | |
| session = self.get_session() | |
| try: | |
| events = session.query(Event).filter_by(person_id=person_id).all() | |
| # Expunge objects from session to prevent DetachedInstanceError | |
| for event in events: | |
| session.expunge(event) | |
| return events | |
| finally: | |
| session.close() | |
| def get_recent_events(self, limit=100): | |
| """Get recent events""" | |
| session = self.get_session() | |
| try: | |
| events = session.query(Event).order_by(Event.timestamp.desc()).limit(limit).all() | |
| # Expunge objects from session to prevent DetachedInstanceError | |
| for event in events: | |
| session.expunge(event) | |
| return events | |
| finally: | |
| session.close() | |
| def get_person_score(self, person_id): | |
| """Calculate total incentive score for a person""" | |
| session = self.get_session() | |
| try: | |
| activities = session.query(PersonActivity).filter_by(person_id=person_id).all() | |
| total_score = sum(activity.incentive_points for activity in activities) | |
| return total_score | |
| finally: | |
| session.close() | |
| def get_leaderboard(self): | |
| """Get person leaderboard with scores and energy impact - Optimized""" | |
| print("Interrogating database for node census...") | |
| session = self.get_session() | |
| try: | |
| from sqlalchemy import func, case | |
| from datetime import timedelta | |
| # Weekly threshold | |
| one_week_ago = datetime.now() - timedelta(days=7) | |
| # Get energy impact and trust per person from Events - Only VERIFIED records | |
| energy_stats = session.query( | |
| Event.person_id, | |
| func.sum(Event.energy_saved_estimate).label('energy_saved'), | |
| func.sum(Event.blockchain_credits).label('credits_earned'), | |
| func.count(Event.event_id).label('event_count'), | |
| func.avg(Event.confidence).label('avg_conf') | |
| ).filter( | |
| Event.person_id.isnot(None), | |
| Event.status == 'verified' | |
| ).group_by(Event.person_id).all() | |
| energy_map = {row.person_id: (float(row.energy_saved or 0), float(row.credits_earned or 0), row.event_count, float(row.avg_conf or 0.95)) for row in energy_stats} | |
| # Get weekly gain per person (from verified activities if possible, but activities don't have status yet) | |
| # We filter by persons who have at least one verified event to ensure no mock data leaks | |
| activity_stats = session.query( | |
| PersonActivity.person_id, | |
| func.count(PersonActivity.activity_id).label('act_count'), | |
| func.sum(case((PersonActivity.timestamp >= one_week_ago, PersonActivity.incentive_points), else_=0)).label('weekly_gain') | |
| ).group_by(PersonActivity.person_id).all() | |
| activity_map = {row.person_id: (row.act_count, int(row.weekly_gain or 0)) for row in activity_stats} | |
| # Get all persons | |
| persons = session.query(Person).all() | |
| # Cache all users for faster lookup | |
| users = session.query(User).all() | |
| user_map = {user.email: user for user in users} | |
| leaderboard = [] | |
| for person in persons: | |
| person_id = person.person_id | |
| # Do NOT exclude persons with no verified events - show them with 0 score | |
| # if person_id not in energy_map: | |
| # continue | |
| user = user_map.get(person_id) | |
| # Exclude faculty from leaderboard as per request | |
| # Check both User role (auth) and Person user_type (data) | |
| is_faculty_user = user and user.role == 'faculty' | |
| is_faculty_person = person.user_type == 'faculty' | |
| if is_faculty_user or is_faculty_person: | |
| continue | |
| energy_saved, credits_earned, event_count, avg_conf = energy_map.get(person_id, (0.0, 0.0, 0, 0.95)) | |
| total_activities, weekly_gain = activity_map.get(person_id, (event_count, 0)) | |
| leaderboard.append({ | |
| 'person_id': person_id, | |
| 'name': user.name if user and user.name else (person.student_id or person_id), | |
| 'total_credits': round(credits_earned, 2), | |
| 'total_activities': total_activities, | |
| 'weekly_gain': weekly_gain, | |
| 'trust_score': avg_conf, | |
| 'department': person.department or (user.department if user else "Universal"), | |
| 'total_energy_saved': round(energy_saved, 2), | |
| 'role': user.role if user else 'student', | |
| 'last_seen': person.last_seen.isoformat() if person.last_seen else None | |
| }) | |
| leaderboard.sort(key=lambda x: x['total_credits'], reverse=True) | |
| return leaderboard | |
| finally: | |
| session.close() | |
| def get_admin_stats(self, department=None): | |
| """Get statistics for the Admin dashboard (Supports departmental filtering)""" | |
| session = self.get_session() | |
| try: | |
| from sqlalchemy import func | |
| # Base queries | |
| pending_q = session.query(Event).filter_by(status='pending') | |
| hc_q = session.query(Event).filter(Event.status == 'pending', Event.confidence >= 0.8) | |
| verified_q = session.query(Event).filter_by(status='verified') | |
| # Apply department filter if provided | |
| if department: | |
| pending_q = pending_q.filter(Event.department == department) | |
| hc_q = hc_q.filter(Event.department == department) | |
| verified_q = verified_q.filter(Event.department == department) | |
| pending_count = pending_q.count() | |
| hc_count = hc_q.count() | |
| total_verified = verified_q.count() | |
| # System Accuracy is the average overall confidence of verified events | |
| avg_conf = 0.0 | |
| if total_verified > 0: | |
| avg_conf = verified_q.with_entities(func.avg(Event.overall_confidence)).scalar() or 0 | |
| else: | |
| # Fallback to general system confidence if no verified events yet | |
| avg_q = session.query(func.avg(Event.overall_confidence)) | |
| if department: | |
| avg_q = avg_q.filter(Event.department == department) | |
| avg_conf = avg_q.scalar() or 0.85 | |
| # Get database file size | |
| import os | |
| db_size_kb = 0 | |
| try: # EDGE CASE FIX: Only check file size if using SQLite | |
| db_url_str = str(self.engine.url) | |
| if 'sqlite' in db_url_str: | |
| # Extract path from sqlite URL | |
| db_path = db_url_str.replace('sqlite:///', '') | |
| # Handle potential relative paths or special chars | |
| if os.path.exists(db_path): | |
| db_size_kb = os.path.getsize(db_path) / 1024 | |
| except: | |
| pass | |
| return { | |
| 'pending_count': pending_count, | |
| 'hc_count': hc_count, | |
| 'avg_fidelity': round(float(avg_conf) * 100, 1), | |
| 'total_verified': total_verified, | |
| 'db_size_kb': round(db_size_kb, 1) | |
| } | |
| finally: | |
| session.close() | |
| # ========================================== | |
| # Task Management (Persistence) | |
| # ========================================== | |
| def _cleanup_stale_tasks(self): | |
| """Mark tasks that were 'processing' during a restart as 'failed'""" | |
| session = self.get_session() | |
| try: | |
| stale_tasks = session.query(ProcessingTask).filter(ProcessingTask.status.in_(['processing', 'queued'])).all() | |
| if stale_tasks: | |
| print(f"🔧 Maintenance: Marking {len(stale_tasks)} stale/interrupted tasks as failed.") | |
| for task in stale_tasks: | |
| task.status = 'failed' | |
| task.error = 'Server restarted during processing (Interrupted)' | |
| task.completion_time = datetime.now() | |
| session.commit() | |
| except Exception as e: | |
| print(f"⚠️ Task cleanup warning: {e}") | |
| finally: | |
| session.close() | |
| def create_task(self, task_id, filename, user_email=None, user_dept=None): | |
| """Create a new processing task""" | |
| session = self.get_session() | |
| try: | |
| task = ProcessingTask( | |
| task_id=task_id, | |
| filename=filename, | |
| user_email=user_email, | |
| user_department=user_dept, | |
| status='queued' | |
| ) | |
| session.add(task) | |
| session.commit() | |
| return task.to_dict() | |
| except Exception as e: | |
| session.rollback() | |
| print(f"Failed to create task: {e}") | |
| return None | |
| finally: | |
| session.close() | |
| def update_task_status(self, task_id, status=None, progress=None, result=None, error=None, current_frame=None, total_frames=None): | |
| """Update persistent task status""" | |
| session = self.get_session() | |
| try: | |
| task = session.query(ProcessingTask).filter_by(task_id=task_id).first() | |
| if not task: | |
| return False | |
| if status: task.status = status | |
| if progress is not None: task.progress = progress | |
| if result: task.results = result | |
| if error: task.error = error | |
| if current_frame: task.current_frame = current_frame | |
| if total_frames: task.total_frames = total_frames | |
| if status in ['completed', 'failed']: | |
| task.completion_time = datetime.now() | |
| session.commit() | |
| return True | |
| except Exception as e: | |
| session.rollback() | |
| print(f"Failed to update task {task_id}: {e}") | |
| return False | |
| finally: | |
| session.close() | |
| def get_task(self, task_id): | |
| """Get task details""" | |
| session = self.get_session() | |
| try: | |
| task = session.query(ProcessingTask).filter_by(task_id=task_id).first() | |
| if task: | |
| return task.to_dict() | |
| return None | |
| finally: | |
| session.close() | |
| if __name__ == "__main__": | |
| # Test database creation when run directly | |
| print("Initializing database (standalone)...") | |
| db = Database() | |
| # Ensure outputs/face_database exists | |
| from pathlib import Path | |
| Path('outputs/face_database').mkdir(parents=True, exist_ok=True) | |
| # Add test person | |
| db.add_person('person_000', 'face', 'outputs/face_database/person_000_face.jpg') | |
| print("✓ Added test person") | |
| # Add test event | |
| test_event = { | |
| 'timestamp': datetime.now().isoformat(), | |
| 'room_id': 'CS_Lab_5', | |
| 'occupancy': True, | |
| 'person_count': 1, | |
| 'person_id': 'person_000', | |
| 'bbox': [100, 200, 300, 400], | |
| 'confidence': 0.9, | |
| 'detection_method': 'face', | |
| 'devices_detected': [{'type': 'laptop', 'confidence': 0.85}], | |
| 'video_file': 'test_video.mp4', | |
| 'frame_number': 30 | |
| } | |
| db.add_event(test_event) | |
| print("✓ Added test event") | |
| # Add test activity | |
| db.add_activity('person_000', 'presence', {'devices_nearby': 1}, incentive_points=1, incentive_reason='room_presence') | |
| print("✓ Added test activity") | |
| # Query data | |
| persons = db.get_all_persons() | |
| print(f"\n✓ Total persons in database: {len(persons)}") | |
| events = db.get_recent_events(10) | |
| print(f"✓ Recent events: {len(events)}") | |
| leaderboard = db.get_leaderboard() | |
| print(f"✓ Leaderboard entries: {len(leaderboard)}") | |
| print("\n✓ Database initialized successfully!") | |
| print("Database file: outputs/sca_events.db") | |
| # ========================================== | |
| # Task Management (Persistence) | |
| # ========================================== | |
| def _cleanup_stale_tasks(self): | |
| """Mark tasks that were 'processing' during a restart as 'failed'""" | |
| session = self.get_session() | |
| try: | |
| stale_tasks = session.query(ProcessingTask).filter(ProcessingTask.status.in_(['processing', 'queued'])).all() | |
| if stale_tasks: | |
| print(f"🔧 Maintenance: Marking {len(stale_tasks)} stale/interrupted tasks as failed.") | |
| for task in stale_tasks: | |
| task.status = 'failed' | |
| task.error = 'Server restarted during processing (Interrupted)' | |
| task.completion_time = datetime.now() | |
| session.commit() | |
| except Exception as e: | |
| print(f"⚠️ Task cleanup warning: {e}") | |
| finally: | |
| session.close() | |
| def create_task(self, task_id, filename, user_email=None, user_dept=None): | |
| """Create a new processing task""" | |
| session = self.get_session() | |
| try: | |
| task = ProcessingTask( | |
| task_id=task_id, | |
| filename=filename, | |
| user_email=user_email, | |
| user_department=user_dept, | |
| status='queued' | |
| ) | |
| session.add(task) | |
| session.commit() | |
| return task.to_dict() | |
| except Exception as e: | |
| session.rollback() | |
| print(f"Failed to create task: {e}") | |
| return None | |
| finally: | |
| session.close() | |
| def update_task_status(self, task_id, status=None, progress=None, result=None, error=None, current_frame=None, total_frames=None): | |
| """Update persistent task status""" | |
| session = self.get_session() | |
| try: | |
| task = session.query(ProcessingTask).filter_by(task_id=task_id).first() | |
| if not task: | |
| return False | |
| if status: task.status = status | |
| if progress is not None: task.progress = progress | |
| if result: task.results = result | |
| if error: task.error = error | |
| if current_frame: task.current_frame = current_frame | |
| if total_frames: task.total_frames = total_frames | |
| if status in ['completed', 'failed']: | |
| task.completion_time = datetime.now() | |
| session.commit() | |
| return True | |
| except Exception as e: | |
| session.rollback() | |
| print(f"Failed to update task {task_id}: {e}") | |
| return False | |
| finally: | |
| session.close() | |
| def get_task(self, task_id): | |
| """Get task details""" | |
| session = self.get_session() | |
| try: | |
| task = session.query(ProcessingTask).filter_by(task_id=task_id).first() | |
| if task: | |
| return task.to_dict() | |
| return None | |
| finally: | |
| session.close() | |