| import logging |
| import requests |
| import threading |
| import time |
| import os |
| from pydantic import Field, AliasChoices |
| from pydantic_settings import BaseSettings, SettingsConfigDict |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
| ) |
| logger = logging.getLogger("face-service-config") |
|
|
| class Settings(BaseSettings): |
| model_config = SettingsConfigDict( |
| env_file=".env", |
| env_file_encoding="utf-8", |
| extra="ignore" |
| ) |
|
|
| PROJECT_NAME: str = "FaceAttendanceAPI" |
| MONGODB_URL: str = Field("mongodb://localhost:27017", validation_alias=AliasChoices("MONGODB_URL", "MONGODB_URI")) |
| DB_NAME: str = "EduSync" |
| COLLECTION_NAME: str = "students" |
| |
| |
| FACE_SIMILARITY_THRESHOLD: float = 0.48 |
| LIVENESS_THRESHOLD: float = 0.20 |
| BLINK_EAR_THRESHOLD: float = 0.18 |
| |
| |
| LIGHTING_BRIGHTNESS_MIN: float = 60 |
| LIGHTING_BRIGHTNESS_FAIR_LOW: float = 90 |
| LIGHTING_BRIGHTNESS_FAIR_HIGH: float = 210 |
| LIGHTING_BRIGHTNESS_MAX: float = 180 |
| LIGHTING_VARIANCE_THRESHOLD: float = 5000 |
| |
| |
| CORE_SERVER_URL: str = Field("https://edusyncserver.onrender.com", validation_alias=AliasChoices("CORE_SERVER_URL")) |
| MODEL_PATH: str = "./models/w600k_mbf.onnx" |
| |
| |
| |
| |
| FACE_API_KEY: str = "" |
|
|
| def update_from_server(self): |
| """Fetch latest config from Core Server.""" |
| try: |
| logger.info(f"🔄 Syncing system config from {self.CORE_SERVER_URL}") |
| |
| headers = {"x-api-key": self.FACE_API_KEY} if self.FACE_API_KEY else {} |
| |
| response = requests.get(f"{self.CORE_SERVER_URL}/api/v1/config/system", headers=headers, timeout=15) |
| if response.status_code == 200: |
| data = response.json() |
| if data.get("success") and "config" in data: |
| remote_config = data["config"] |
| |
| mapping = { |
| "faceSimilarityThreshold": "FACE_SIMILARITY_THRESHOLD", |
| "livenessThreshold": "LIVENESS_THRESHOLD", |
| "blinkEarThreshold": "BLINK_EAR_THRESHOLD", |
| "lightingBrightnessMin": "LIGHTING_BRIGHTNESS_MIN", |
| "lightingBrightnessFairLow": "LIGHTING_BRIGHTNESS_FAIR_LOW", |
| "lightingBrightnessFairHigh": "LIGHTING_BRIGHTNESS_FAIR_HIGH", |
| "lightingBrightnessMax": "LIGHTING_BRIGHTNESS_MAX", |
| "lightingVarianceThreshold": "LIGHTING_VARIANCE_THRESHOLD" |
| } |
| |
| for remote_key, local_key in mapping.items(): |
| if remote_key in remote_config: |
| setattr(self, local_key, remote_config[remote_key]) |
| logger.info("✅ System config updated from server") |
| else: |
| logger.warning(f"⚠️ Failed to sync config: HTTP {response.status_code}") |
| except Exception as e: |
| logger.error(f"❌ Config sync failure: {e}. Using local defaults.") |
|
|
| def start_config_sync(self): |
| """ |
| Starts a background thread to sync config every 10 minutes. |
| Uses a file-based lock to ensure only ONE sync thread runs across all workers. |
| """ |
| lock_file = "config_sync.lock" |
| |
| try: |
| |
| fd = os.open(lock_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY) |
| os.write(fd, str(os.getpid()).encode()) |
| |
| def sync_worker(): |
| while True: |
| time.sleep(600) |
| self.update_from_server() |
|
|
| thread = threading.Thread(target=sync_worker, daemon=True) |
| thread.start() |
| logger.info(f"🚀 [Worker {os.getpid()}] Config sync thread started.") |
| |
| except FileExistsError: |
| |
| try: |
| if time.time() - os.path.getmtime(lock_file) > 900: |
| os.remove(lock_file) |
| return self.start_config_sync() |
| except Exception: |
| pass |
| logger.info("ℹ️ Config sync already active in another worker.") |
| except Exception as e: |
| logger.error(f"⚠️ Could not start config sync: {e}") |
|
|
| settings = Settings() |
|
|