Spaces:
Running
Running
| """ | |
| Orionus AI -- Behavioral Intelligence for Security | |
| Hugging Face Spaces Demo | |
| Self-contained Streamlit app demonstrating: | |
| - Multimodal security emotion analysis (text + simulated movement) | |
| - 10 security intent categories | |
| - 5 threat level assessment | |
| - Zone-based monitoring | |
| - Fuzzy inference visualization | |
| - Cross-modal conflict detection | |
| - Alert generation | |
| Gated behind an email-verified trial system (1 use per email, 60-second session). | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import time | |
| import uuid | |
| import random | |
| from dataclasses import dataclass, field | |
| from enum import Enum | |
| from typing import Any | |
| import numpy as np | |
| import plotly.graph_objects as go | |
| import streamlit as st | |
| from auth import ( | |
| CODE_EXPIRY_SEC, | |
| LIVE_SESSION_MAX_SEC, | |
| TRIAL_DURATION_SEC, | |
| VIDEO_MAX_DURATION_SEC, | |
| generate_code, | |
| is_email_used, | |
| mark_email_used, | |
| remaining_seconds, | |
| send_verification_email, | |
| session_expired, | |
| smtp_configured, | |
| ) | |
| # ============================================================================ | |
| # PAGE CONFIG & CSS | |
| # ============================================================================ | |
| st.set_page_config( | |
| page_title="Orionus AI -- Behavioral Intelligence for Security", | |
| page_icon="shield", | |
| layout="wide", | |
| initial_sidebar_state="collapsed", | |
| ) | |
| BRAND_CSS = """ | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| .stApp { | |
| background: linear-gradient(135deg, #0a0f1a 0%, #111927 100%); | |
| color: #e0e0e0; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| [data-testid="stSidebar"] { | |
| background-color: #0d1117; | |
| border-right: 1px solid #1a3a1a; | |
| } | |
| .orionus-header { | |
| text-align: center; | |
| padding: 2rem 0 1rem; | |
| } | |
| .orionus-header h1 { | |
| font-size: 2.6rem; | |
| background: linear-gradient(90deg, #00E676, #00C853); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 0.2rem; | |
| letter-spacing: 4px; | |
| } | |
| .orionus-header p { | |
| color: #81C784; | |
| font-size: 1.05rem; | |
| } | |
| .auth-card { | |
| max-width: 460px; | |
| margin: 2rem auto; | |
| background: rgba(30, 41, 59, 0.85); | |
| border: 1px solid #2E7D32; | |
| border-radius: 14px; | |
| padding: 2rem 2.5rem; | |
| } | |
| .auth-card h3 { color: #e2e8f0; margin-bottom: 0.6rem; } | |
| .auth-card p { color: #94a3b8; font-size: 0.92rem; } | |
| .trial-ended { | |
| text-align: center; | |
| padding: 4rem 2rem; | |
| background: rgba(30, 41, 59, 0.9); | |
| border: 1px solid #334155; | |
| border-radius: 14px; | |
| margin: 2rem auto; | |
| max-width: 600px; | |
| } | |
| .trial-ended h2 { color: #f87171; } | |
| .trial-ended p { color: #94a3b8; } | |
| .trial-ended a { color: #00E676; } | |
| .timer-bar { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 0.6rem; | |
| padding: 0.5rem 1rem; | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| } | |
| .timer-green { background: rgba(0, 230, 118, 0.15); color: #00E676; } | |
| .timer-yellow { background: rgba(250, 204, 21, 0.15); color: #facc15; } | |
| .timer-red { background: rgba(248, 113, 113, 0.2); color: #f87171; } | |
| .threat-card { | |
| border-radius: 12px; | |
| padding: 1.2rem; | |
| margin: 0.5rem 0; | |
| border: 1px solid #333; | |
| } | |
| .threat-none { background: linear-gradient(135deg, #0a1a0a, #112211); border-color: #2E7D32; } | |
| .threat-low { background: linear-gradient(135deg, #0a1a0a, #132613); border-color: #4CAF50; } | |
| .threat-elevated { background: linear-gradient(135deg, #1a1a0a, #2a2510); border-color: #FF9800; } | |
| .threat-high { background: linear-gradient(135deg, #1a0a0a, #2a1010); border-color: #f44336; } | |
| .threat-critical { background: linear-gradient(135deg, #2a0505, #3a0808); border-color: #d50000; } | |
| .alert-box { | |
| border-radius: 10px; | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| border-left: 4px solid; | |
| } | |
| .alert-warning { background: #1a1a0a; border-color: #FF9800; } | |
| .alert-urgent { background: #1a0a0a; border-color: #f44336; } | |
| .alert-critical { background: #2a0505; border-color: #d50000; } | |
| div[data-testid="stMetric"] { | |
| background: #111927; | |
| border: 1px solid #2E7D32; | |
| border-radius: 10px; | |
| padding: 0.8rem; | |
| } | |
| div[data-testid="stMetric"] label { color: #81C784 !important; } | |
| div[data-testid="stMetric"] [data-testid="stMetricValue"] { color: #00E676 !important; } | |
| .section-header { | |
| color: #00E676; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| border-bottom: 1px solid #2E7D32; | |
| padding-bottom: 0.4rem; | |
| margin: 1rem 0 0.5rem 0; | |
| } | |
| .factor-item { | |
| background: #0d1117; | |
| border-left: 3px solid #00C853; | |
| padding: 0.4rem 0.8rem; | |
| margin: 0.3rem 0; | |
| font-size: 0.85rem; | |
| border-radius: 0 6px 6px 0; | |
| } | |
| @keyframes float { | |
| 0% { transform: translateY(0px); } | |
| 50% { transform: translateY(-20px); } | |
| 100% { transform: translateY(0px); } | |
| } | |
| .floating-logo { | |
| animation: float 6s ease-in-out infinite; | |
| display: block; | |
| margin: 0 auto; | |
| } | |
| .stTextArea textarea { | |
| background-color: #111927; | |
| color: #e0e0e0; | |
| border: 1px solid #2E7D32; | |
| } | |
| </style> | |
| <script> | |
| window.focus(); | |
| document.addEventListener('DOMContentLoaded', function() { window.focus(); }); | |
| document.addEventListener('mouseover', function() { window.focus(); }, {once: true}); | |
| </script> | |
| """ | |
| st.markdown(BRAND_CSS, unsafe_allow_html=True) | |
| # ============================================================================ | |
| # CONSTANTS | |
| # ============================================================================ | |
| GOEMOTIONS_LABELS = [ | |
| "admiration", "amusement", "anger", "annoyance", "approval", "caring", | |
| "confusion", "curiosity", "desire", "disappointment", "disapproval", | |
| "disgust", "embarrassment", "excitement", "fear", "gratitude", "grief", | |
| "joy", "love", "nervousness", "neutral", "optimism", "pride", | |
| "realization", "relief", "remorse", "sadness", "surprise", | |
| ] | |
| NUM_GOEMOTIONS = len(GOEMOTIONS_LABELS) | |
| INTENT_LABELS = [ | |
| "normal", "agitated", "aggressive", "attack_intent", "concealment", | |
| "evasion", "loitering", "panic", "deceptive", "erratic", | |
| ] | |
| THREAT_LEVELS_LIST = ["none", "low", "elevated", "high", "critical"] | |
| SECURITY_EMOTION_CLUSTERS = { | |
| "aggression": ["anger", "annoyance", "disapproval", "disgust"], | |
| "fear_anxiety": ["fear", "nervousness"], | |
| "distress": ["sadness", "grief", "disappointment"], | |
| "deception": ["amusement", "joy", "neutral"], | |
| "agitation": ["anger", "nervousness", "confusion", "surprise"], | |
| "calm_normal": ["neutral", "approval", "optimism", "relief"], | |
| } | |
| ZONE_TYPES = [ | |
| "checkpoint", "boarding", "transit", "queue", "retail", | |
| "restricted", "entrance", "gathering", "perimeter", "parking", | |
| ] | |
| # ============================================================================ | |
| # SCHEMAS | |
| # ============================================================================ | |
| class ThreatLevel(str, Enum): | |
| NONE = "none" | |
| LOW = "low" | |
| ELEVATED = "elevated" | |
| HIGH = "high" | |
| CRITICAL = "critical" | |
| class AlertPriority(str, Enum): | |
| INFO = "info" | |
| WARNING = "warning" | |
| URGENT = "urgent" | |
| CRITICAL = "critical" | |
| class MovementFeatures: | |
| velocity: float = 0.0 | |
| acceleration: float = 0.0 | |
| direction_changes: float = 0.0 | |
| proximity_approach: float = 0.0 | |
| proximity_avoid: float = 0.0 | |
| hand_activity: float = 0.0 | |
| body_tension: float = 0.0 | |
| gait_regularity: float = 0.0 | |
| loiter_score: float = 0.0 | |
| crowd_interaction: float = 0.0 | |
| def as_dict(self) -> dict[str, float]: | |
| return { | |
| "velocity": self.velocity, "acceleration": self.acceleration, | |
| "direction_changes": self.direction_changes, | |
| "proximity_approach": self.proximity_approach, | |
| "proximity_avoid": self.proximity_avoid, | |
| "hand_activity": self.hand_activity, "body_tension": self.body_tension, | |
| "gait_regularity": self.gait_regularity, "loiter_score": self.loiter_score, | |
| "crowd_interaction": self.crowd_interaction, | |
| } | |
| class MovementResult: | |
| features: MovementFeatures | |
| intent_scores: dict[str, float] | |
| dominant_intent: str = "normal" | |
| confidence: float = 1.0 | |
| timestamp: float = 0.0 | |
| class FuzzyEmotionState: | |
| emotion: str | |
| memberships: dict[str, float] | |
| dominant_level: str | |
| crisp_value: float | |
| class CrossModalConflict: | |
| emotion: str | |
| modality_a: str | |
| modality_b: str | |
| level_a: str | |
| level_b: str | |
| severity: float | |
| interpretation: str | |
| class ThreatAssessmentResult: | |
| threat_level: ThreatLevel | |
| threat_score: float | |
| primary_intent: str | |
| intent_scores: dict[str, float] | |
| emotion_summary: dict[str, float] | |
| movement_summary: dict[str, float] | |
| conflicts: list[CrossModalConflict] | |
| fired_rules: list[str] | |
| contributing_factors: list[str] | |
| recommended_action: str | |
| class Alert: | |
| alert_id: str | |
| zone_id: str | |
| subject_id: str | None | |
| priority: AlertPriority | |
| threat_level: ThreatLevel | |
| title: str | |
| description: str | |
| timestamp: float | |
| contributing_signals: list[str] | |
| recommended_action: str | |
| # ============================================================================ | |
| # FUZZY MEMBERSHIP FUNCTIONS | |
| # ============================================================================ | |
| FUZZY_LEVELS = ["absent", "low", "moderate", "high", "very_high"] | |
| LEVEL_CENTROIDS = { | |
| "absent": 0.0, "low": 0.15, "moderate": 0.35, "high": 0.60, "very_high": 0.85, | |
| } | |
| def _trapezoid(x: float, a: float, b: float, c: float, d: float) -> float: | |
| if x <= a or x >= d: | |
| return 0.0 | |
| if b <= x <= c: | |
| return 1.0 | |
| if a < x < b: | |
| return (x - a) / (b - a) | |
| return (d - x) / (d - c) | |
| def _left_shoulder(x: float, a: float, b: float) -> float: | |
| if x <= a: return 1.0 | |
| if x >= b: return 0.0 | |
| return (b - x) / (b - a) | |
| def _right_shoulder(x: float, a: float, b: float) -> float: | |
| if x <= a: return 0.0 | |
| if x >= b: return 1.0 | |
| return (x - a) / (b - a) | |
| def mf_absent(x): return _left_shoulder(x, 0.05, 0.12) | |
| def mf_low(x): return _trapezoid(x, 0.05, 0.12, 0.20, 0.30) | |
| def mf_moderate(x): return _trapezoid(x, 0.20, 0.30, 0.45, 0.55) | |
| def mf_high(x): return _trapezoid(x, 0.45, 0.55, 0.70, 0.80) | |
| def mf_very_high(x): return _right_shoulder(x, 0.70, 0.80) | |
| def fuzzify(crisp_value: float) -> dict[str, float]: | |
| return { | |
| "absent": mf_absent(crisp_value), "low": mf_low(crisp_value), | |
| "moderate": mf_moderate(crisp_value), "high": mf_high(crisp_value), | |
| "very_high": mf_very_high(crisp_value), | |
| } | |
| # ============================================================================ | |
| # FUZZY RULE BASE | |
| # ============================================================================ | |
| class FuzzyRule: | |
| rule_id: str | |
| category: str | |
| conditions: list[dict] | |
| outputs: dict[str, str] | |
| threat_boost: float | |
| intent_signal: str | |
| description: str | |
| def build_security_rules(zone_type: str | None = None) -> list[FuzzyRule]: | |
| rules = [] | |
| # Aggression | |
| rules.append(FuzzyRule("S01_rising_aggression", "aggression", | |
| [{"modality": "face", "emotion": "angry", "level": "moderate", "op": ">="}, | |
| {"modality": "voice", "emotion": "angry", "level": "moderate", "op": ">="}, | |
| {"modality": "movement", "emotion": "body_tension", "level": "moderate", "op": ">="}], | |
| {"anger": "very_high", "nervousness": "low"}, 0.6, "aggressive", | |
| "Multi-modal aggression: face angry + voice angry + tense body")) | |
| rules.append(FuzzyRule("S02_pre_attack_posture", "aggression", | |
| [{"modality": "movement", "emotion": "body_tension", "level": "high", "op": ">="}, | |
| {"modality": "movement", "emotion": "acceleration", "level": "moderate", "op": ">="}, | |
| {"modality": "face", "emotion": "angry", "level": "low", "op": ">="}], | |
| {"anger": "high", "fear": "low"}, 0.8, "attack_intent", | |
| "Pre-attack posture: high body tension + sudden acceleration + angry face")) | |
| rules.append(FuzzyRule("S03_verbal_threat", "aggression", | |
| [{"modality": "text", "emotion": "anger", "level": "high", "op": ">="}, | |
| {"modality": "voice", "emotion": "angry", "level": "moderate", "op": ">="}], | |
| {"anger": "very_high"}, 0.5, "aggressive", | |
| "Verbal threat: angry text + angry voice")) | |
| rules.append(FuzzyRule("S04_approach_with_anger", "aggression", | |
| [{"modality": "movement", "emotion": "proximity_approach", "level": "high", "op": ">="}, | |
| {"modality": "face", "emotion": "angry", "level": "moderate", "op": ">="}], | |
| {"anger": "high"}, 0.7, "attack_intent", | |
| "Aggressive approach: rapid approach + angry face")) | |
| # Deception | |
| rules.append(FuzzyRule("S05_calm_face_stressed_voice", "deception", | |
| [{"modality": "face", "emotion": "neutral", "level": "high", "op": ">="}, | |
| {"modality": "voice", "emotion": "fearful", "level": "moderate", "op": ">="}], | |
| {"nervousness": "high", "neutral": "low"}, 0.4, "deceptive", | |
| "Deception: calm face but stressed voice -- concealing fear")) | |
| rules.append(FuzzyRule("S06_forced_calm", "deception", | |
| [{"modality": "face", "emotion": "happy", "level": "moderate", "op": ">="}, | |
| {"modality": "movement", "emotion": "body_tension", "level": "moderate", "op": ">="}, | |
| {"modality": "movement", "emotion": "hand_activity", "level": "moderate", "op": ">="}], | |
| {"nervousness": "high", "joy": "low"}, 0.35, "deceptive", | |
| "Forced calm: smiling but tense body + fidgety hands")) | |
| rules.append(FuzzyRule("S07_social_engineering", "deception", | |
| [{"modality": "face", "emotion": "happy", "level": "high", "op": ">="}, | |
| {"modality": "voice", "emotion": "happy", "level": "moderate", "op": ">="}, | |
| {"modality": "text", "emotion": "nervousness", "level": "moderate", "op": ">="}], | |
| {"nervousness": "high", "joy": "absent"}, 0.45, "deceptive", | |
| "Social engineering: overly friendly but nervous text content")) | |
| # Panic | |
| rules.append(FuzzyRule("S08_genuine_panic", "panic", | |
| [{"modality": "face", "emotion": "fear", "level": "high", "op": ">="}, | |
| {"modality": "voice", "emotion": "fearful", "level": "moderate", "op": ">="}, | |
| {"modality": "movement", "emotion": "velocity", "level": "high", "op": ">="}], | |
| {"fear": "very_high"}, 0.5, "panic", | |
| "Genuine panic: fearful face + fearful voice + running")) | |
| rules.append(FuzzyRule("S10_crowd_panic", "panic", | |
| [{"modality": "movement", "emotion": "velocity", "level": "high", "op": ">="}, | |
| {"modality": "movement", "emotion": "crowd_interaction", "level": "high", "op": ">="}], | |
| {"fear": "high", "surprise": "high"}, 0.6, "panic", | |
| "Mass movement: high velocity + crowd interaction")) | |
| # Surveillance | |
| rules.append(FuzzyRule("S11_suspicious_loitering", "surveillance", | |
| [{"modality": "movement", "emotion": "loiter_score", "level": "moderate", "op": ">="}, | |
| {"modality": "face", "emotion": "neutral", "level": "high", "op": ">="}], | |
| {"neutral": "moderate"}, 0.25, "loitering", | |
| "Suspicious loitering: lingering + watchful face")) | |
| rules.append(FuzzyRule("S12_evasive_movement", "surveillance", | |
| [{"modality": "movement", "emotion": "direction_changes", "level": "high", "op": ">="}, | |
| {"modality": "movement", "emotion": "proximity_avoid", "level": "moderate", "op": ">="}], | |
| {"nervousness": "high"}, 0.4, "evasion", | |
| "Evasive movement: direction changes + avoiding security areas")) | |
| rules.append(FuzzyRule("S13_concealment_behavior", "surveillance", | |
| [{"modality": "movement", "emotion": "hand_activity", "level": "high", "op": ">="}, | |
| {"modality": "movement", "emotion": "velocity", "level": "low", "op": "<="}, | |
| {"modality": "face", "emotion": "neutral", "level": "moderate", "op": ">="}], | |
| {"nervousness": "moderate"}, 0.35, "concealment", | |
| "Concealment: high hand activity + slow movement + controlled face")) | |
| rules.append(FuzzyRule("S14_erratic_behavior", "surveillance", | |
| [{"modality": "movement", "emotion": "gait_regularity", "level": "high", "op": ">="}, | |
| {"modality": "movement", "emotion": "direction_changes", "level": "moderate", "op": ">="}], | |
| {"confusion": "high"}, 0.3, "erratic", | |
| "Erratic behavior: irregular gait + frequent direction changes")) | |
| # De-escalation | |
| rules.append(FuzzyRule("S15_genuine_calm", "deescalation", | |
| [{"modality": "face", "emotion": "neutral", "level": "high", "op": ">="}, | |
| {"modality": "voice", "emotion": "calm", "level": "high", "op": ">="}, | |
| {"modality": "movement", "emotion": "body_tension", "level": "absent", "op": "<="}], | |
| {"neutral": "very_high"}, -0.3, "normal", | |
| "Genuine calm: neutral face + calm voice + relaxed body")) | |
| # Zone-specific | |
| if zone_type == "checkpoint": | |
| rules.append(FuzzyRule("Z02_checkpoint_evasion", "zone", | |
| [{"modality": "movement", "emotion": "direction_changes", "level": "high", "op": ">="}, | |
| {"modality": "movement", "emotion": "velocity", "level": "moderate", "op": ">="}], | |
| {"nervousness": "very_high"}, 0.6, "evasion", | |
| "Checkpoint evasion: sudden direction change near checkpoint")) | |
| elif zone_type == "restricted": | |
| rules.append(FuzzyRule("Z03_restricted_presence", "zone", | |
| [{"modality": "movement", "emotion": "loiter_score", "level": "low", "op": ">="}], | |
| {"nervousness": "moderate"}, 0.4, "evasion", | |
| "Restricted zone presence: any loitering in restricted area")) | |
| elif zone_type == "entrance": | |
| rules.append(FuzzyRule("Z04_entrance_reversal", "zone", | |
| [{"modality": "movement", "emotion": "direction_changes", "level": "high", "op": ">="}, | |
| {"modality": "face", "emotion": "fear", "level": "moderate", "op": ">="}], | |
| {"nervousness": "high"}, 0.5, "evasion", | |
| "Entrance reversal: turning back at entrance with fear")) | |
| elif zone_type == "gathering": | |
| rules.append(FuzzyRule("Z05_crowd_agitator", "zone", | |
| [{"modality": "voice", "emotion": "angry", "level": "high", "op": ">="}, | |
| {"modality": "movement", "emotion": "hand_activity", "level": "high", "op": ">="}], | |
| {"anger": "very_high"}, 0.5, "aggressive", | |
| "Crowd agitator: loud angry voice + aggressive gestures")) | |
| return rules | |
| # ============================================================================ | |
| # TEXT EMOTION SIMULATOR | |
| # ============================================================================ | |
| _TEXT_EMOTION_KEYWORDS = { | |
| "anger": ["angry", "furious", "hate", "rage", "kill", "destroy", "attack", "fight", "threat", "damn", "hell"], | |
| "annoyance": ["annoyed", "irritated", "frustrated", "bothered", "stupid", "idiot", "ridiculous", "unacceptable"], | |
| "fear": ["afraid", "scared", "terrified", "panic", "danger", "help", "run", "bomb", "gun", "weapon"], | |
| "nervousness": ["nervous", "anxious", "worried", "uneasy", "tense", "shaking", "sweating"], | |
| "joy": ["happy", "glad", "wonderful", "great", "love", "beautiful", "amazing", "fantastic"], | |
| "sadness": ["sad", "depressed", "crying", "miserable", "lonely", "grief", "mourn"], | |
| "surprise": ["surprised", "shocked", "unexpected", "wow", "unbelievable", "sudden"], | |
| "disgust": ["disgusting", "revolting", "vile", "sick", "horrible", "gross"], | |
| "confusion": ["confused", "lost", "unclear", "don't understand", "what", "spinning"], | |
| "neutral": ["ok", "fine", "normal", "nothing", "usual", "regular", "just"], | |
| "disapproval": ["wrong", "disagree", "bad", "terrible", "no"], | |
| "curiosity": ["curious", "wonder", "interested"], | |
| "admiration": ["admire", "respect", "impressive", "brilliant"], | |
| "approval": ["agree", "yes", "correct", "right", "good"], | |
| "caring": ["care", "worry about", "hope", "please", "safe"], | |
| "excitement": ["excited", "thrilled", "can't wait", "pumped"], | |
| "gratitude": ["thank", "grateful", "appreciate"], | |
| "optimism": ["hope", "optimistic", "better", "improve"], | |
| "embarrassment": ["embarrassed", "ashamed", "awkward"], | |
| "desire": ["want", "wish", "need"], | |
| "disappointment": ["disappointed", "letdown", "expected more", "waiting"], | |
| "love": ["love", "adore", "cherish", "kind"], | |
| "pride": ["proud", "accomplished"], | |
| "realization": ["realize", "understand now", "see", "dawned"], | |
| "relief": ["relieved", "finally", "phew"], | |
| "remorse": ["sorry", "regret", "apologize"], | |
| "grief": ["death", "gone forever"], | |
| "amusement": ["funny", "laugh", "hilarious", "joke", "haha"], | |
| } | |
| def simulate_text_emotions(text: str) -> dict[str, float]: | |
| text_lower = text.lower() | |
| scores = {label: 0.01 for label in GOEMOTIONS_LABELS} | |
| for emotion, keywords in _TEXT_EMOTION_KEYWORDS.items(): | |
| for kw in keywords: | |
| if kw in text_lower: | |
| scores[emotion] = min(scores[emotion] + 0.25, 1.0) | |
| total = sum(scores.values()) | |
| if total > 0: | |
| scores = {k: v / total for k, v in scores.items()} | |
| return scores | |
| def simulate_face_emotions(text_emotions: dict[str, float]) -> dict[str, float]: | |
| face_map = { | |
| "angry": text_emotions.get("anger", 0) * 0.7 + text_emotions.get("annoyance", 0) * 0.3, | |
| "disgust": text_emotions.get("disgust", 0) * 0.8 + text_emotions.get("disapproval", 0) * 0.2, | |
| "fear": text_emotions.get("fear", 0) * 0.7 + text_emotions.get("nervousness", 0) * 0.3, | |
| "happy": text_emotions.get("joy", 0) * 0.6 + text_emotions.get("amusement", 0) * 0.2 + text_emotions.get("excitement", 0) * 0.2, | |
| "sad": text_emotions.get("sadness", 0) * 0.7 + text_emotions.get("grief", 0) * 0.2 + text_emotions.get("disappointment", 0) * 0.1, | |
| "surprise": text_emotions.get("surprise", 0) * 0.8 + text_emotions.get("realization", 0) * 0.2, | |
| "neutral": text_emotions.get("neutral", 0) * 0.8 + text_emotions.get("approval", 0) * 0.2, | |
| } | |
| for k in face_map: | |
| face_map[k] = max(0, face_map[k] + random.gauss(0, 0.02)) | |
| total = sum(face_map.values()) | |
| if total > 0: | |
| face_map = {k: v / total for k, v in face_map.items()} | |
| return face_map | |
| def simulate_voice_emotions(text_emotions: dict[str, float]) -> dict[str, float]: | |
| voice_map = { | |
| "angry": text_emotions.get("anger", 0) * 0.6 + text_emotions.get("annoyance", 0) * 0.3, | |
| "calm": text_emotions.get("neutral", 0) * 0.5 + text_emotions.get("relief", 0) * 0.3, | |
| "disgust": text_emotions.get("disgust", 0) * 0.7, | |
| "fearful": text_emotions.get("fear", 0) * 0.7 + text_emotions.get("nervousness", 0) * 0.4, | |
| "happy": text_emotions.get("joy", 0) * 0.6 + text_emotions.get("excitement", 0) * 0.3, | |
| "neutral": text_emotions.get("neutral", 0) * 0.6, | |
| "sad": text_emotions.get("sadness", 0) * 0.7 + text_emotions.get("grief", 0) * 0.2, | |
| "surprised": text_emotions.get("surprise", 0) * 0.8, | |
| } | |
| for k in voice_map: | |
| voice_map[k] = max(0, voice_map[k] + random.gauss(0, 0.02)) | |
| total = sum(voice_map.values()) | |
| if total > 0: | |
| voice_map = {k: v / total for k, v in voice_map.items()} | |
| return voice_map | |
| # ============================================================================ | |
| # EMOTION PROJECTOR | |
| # ============================================================================ | |
| FACE_TO_GOEMOTIONS_MAP = { | |
| "angry": ["anger", "annoyance"], "disgust": ["disgust", "disapproval"], | |
| "fear": ["fear", "nervousness"], "happy": ["joy", "amusement", "excitement"], | |
| "sad": ["sadness", "grief", "disappointment"], "surprise": ["surprise", "realization"], | |
| "neutral": ["neutral"], | |
| } | |
| VOICE_TO_GOEMOTIONS_MAP = { | |
| "angry": ["anger", "annoyance"], "calm": ["neutral", "relief"], | |
| "disgust": ["disgust", "disapproval"], "fearful": ["fear", "nervousness"], | |
| "happy": ["joy", "amusement", "excitement"], "neutral": ["neutral"], | |
| "sad": ["sadness", "grief", "disappointment"], "surprised": ["surprise", "realization"], | |
| } | |
| def project_to_goemotions(source_probs: dict[str, float], mapping: dict[str, list[str]]) -> np.ndarray: | |
| vec = np.zeros(NUM_GOEMOTIONS) | |
| go_idx = {label: i for i, label in enumerate(GOEMOTIONS_LABELS)} | |
| for src_label, prob in source_probs.items(): | |
| targets = mapping.get(src_label, []) | |
| if targets: | |
| weight = prob / len(targets) | |
| for t in targets: | |
| if t in go_idx: | |
| vec[go_idx[t]] += weight | |
| total = vec.sum() | |
| if total > 0: | |
| vec = vec / total | |
| return vec | |
| def movement_to_emotion_space(intent_scores: dict[str, float]) -> np.ndarray: | |
| vec = np.zeros(NUM_GOEMOTIONS) | |
| idx = {label: i for i, label in enumerate(GOEMOTIONS_LABELS)} | |
| attack = intent_scores.get("attack_intent", 0) + intent_scores.get("aggressive", 0) * 0.8 | |
| vec[idx["anger"]] += attack * 0.6; vec[idx["disgust"]] += attack * 0.2; vec[idx["annoyance"]] += attack * 0.2 | |
| p = intent_scores.get("panic", 0) | |
| vec[idx["fear"]] += p * 0.5; vec[idx["nervousness"]] += p * 0.3; vec[idx["surprise"]] += p * 0.2 | |
| e = intent_scores.get("evasion", 0) | |
| vec[idx["nervousness"]] += e * 0.6; vec[idx["fear"]] += e * 0.3 | |
| c = intent_scores.get("concealment", 0) | |
| vec[idx["nervousness"]] += c * 0.5; vec[idx["fear"]] += c * 0.2; vec[idx["neutral"]] += c * 0.3 | |
| er = intent_scores.get("erratic", 0) | |
| vec[idx["confusion"]] += er * 0.5; vec[idx["surprise"]] += er * 0.3 | |
| ag = intent_scores.get("agitated", 0) | |
| vec[idx["anger"]] += ag * 0.3; vec[idx["nervousness"]] += ag * 0.4; vec[idx["annoyance"]] += ag * 0.3 | |
| lo = intent_scores.get("loitering", 0) | |
| vec[idx["neutral"]] += lo * 0.7; vec[idx["nervousness"]] += lo * 0.3 | |
| n = intent_scores.get("normal", 0) | |
| vec[idx["neutral"]] += n * 0.8; vec[idx["approval"]] += n * 0.1; vec[idx["optimism"]] += n * 0.1 | |
| total = vec.sum() | |
| if total > 0: vec = vec / total | |
| return vec | |
| # ============================================================================ | |
| # INTENT MAPPER | |
| # ============================================================================ | |
| def map_movement_to_intents(features: MovementFeatures) -> dict[str, float]: | |
| feat_dict = features.as_dict() | |
| scores = {intent: 0.0 for intent in INTENT_LABELS} | |
| scores["normal"] = 0.5 | |
| rules = [ | |
| {"intent": "attack_intent", "conditions": {"proximity_approach": (">=", 0.6), "body_tension": (">=", 0.5), "acceleration": (">=", 0.4)}, "weight": 1.0}, | |
| {"intent": "aggressive", "conditions": {"hand_activity": (">=", 0.5), "body_tension": (">=", 0.4), "velocity": (">=", 0.3)}, "weight": 0.9}, | |
| {"intent": "concealment", "conditions": {"hand_activity": (">=", 0.4), "body_tension": (">=", 0.3), "velocity": ("<=", 0.3)}, "weight": 0.8}, | |
| {"intent": "evasion", "conditions": {"direction_changes": (">=", 0.5), "proximity_avoid": (">=", 0.4), "velocity": (">=", 0.3)}, "weight": 0.85}, | |
| {"intent": "loitering", "conditions": {"loiter_score": (">=", 0.3), "velocity": ("<=", 0.2)}, "weight": 0.7}, | |
| {"intent": "panic", "conditions": {"velocity": (">=", 0.7), "acceleration": (">=", 0.5), "gait_regularity": (">=", 0.4)}, "weight": 0.95}, | |
| {"intent": "erratic", "conditions": {"gait_regularity": (">=", 0.5), "direction_changes": (">=", 0.4)}, "weight": 0.75}, | |
| {"intent": "agitated", "conditions": {"hand_activity": (">=", 0.3), "body_tension": (">=", 0.3)}, "weight": 0.6}, | |
| ] | |
| for rule in rules: | |
| activations = [] | |
| match = True | |
| for feature, (op, threshold) in rule["conditions"].items(): | |
| value = feat_dict.get(feature, 0.0) | |
| if op == ">=" and value >= threshold: | |
| activations.append(min((value - threshold) / (1.0 - threshold + 1e-6) + 0.5, 1.0)) | |
| elif op == "<=" and value <= threshold: | |
| activations.append(min((threshold - value) / (threshold + 1e-6) + 0.5, 1.0)) | |
| else: | |
| match = False; break | |
| if match and activations: | |
| activation = min(activations) | |
| weighted = activation * rule["weight"] | |
| scores[rule["intent"]] = max(scores[rule["intent"]], weighted) | |
| scores["normal"] *= (1 - weighted * 0.5) | |
| total = sum(scores.values()) | |
| if total > 0: | |
| scores = {k: v / total for k, v in scores.items()} | |
| return scores | |
| # ============================================================================ | |
| # CONFLICT DETECTION | |
| # ============================================================================ | |
| _LEVEL_ORDER = {"absent": 0, "low": 1, "moderate": 2, "high": 3, "very_high": 4} | |
| _CONFLICT_INTERPRETATIONS = { | |
| ("face", "voice", "joy", "sadness"): "Calm face masking stress -- possible deception", | |
| ("face", "voice", "joy", "anger"): "Feigned friendliness masking hostility", | |
| ("face", "voice", "neutral", "anger"): "Suppressed anger -- covert hostility", | |
| ("face", "voice", "neutral", "fear"): "Suppressed fear -- voice reveals anxiety", | |
| ("face", "text", "joy", "fear"): "Feigned composure -- smiling but expressing fear in words", | |
| ("face", "text", "neutral", "nervousness"): "Controlled exterior, anxious interior -- concealment", | |
| ("voice", "text", "neutral", "anger"): "Controlled voice, angry words -- measured hostility", | |
| } | |
| def detect_conflicts(modality_fuzzy, modality_names, threshold=0.3): | |
| conflicts = [] | |
| opposing = [("joy", "sadness"), ("joy", "anger"), ("joy", "fear"), ("anger", "fear"), | |
| ("neutral", "anger"), ("neutral", "fear"), ("neutral", "nervousness")] | |
| for i in range(len(modality_names)): | |
| for j in range(i + 1, len(modality_names)): | |
| mod_a, mod_b = modality_names[i], modality_names[j] | |
| states_a = modality_fuzzy.get(mod_a, {}) | |
| states_b = modality_fuzzy.get(mod_b, {}) | |
| for emo_a, emo_b in opposing: | |
| memb_a = states_a.get(emo_a, {"absent": 1.0}) | |
| memb_b = states_b.get(emo_b, {"absent": 1.0}) | |
| level_a = max(memb_a, key=memb_a.get) | |
| level_b = max(memb_b, key=memb_b.get) | |
| if _LEVEL_ORDER.get(level_a, 0) >= 2 and _LEVEL_ORDER.get(level_b, 0) >= 2: | |
| severity = min(1.0, (memb_a.get(level_a, 0) + memb_b.get(level_b, 0)) / 2.0) | |
| if severity >= threshold: | |
| key = (mod_a, mod_b, emo_a, emo_b) | |
| interp = _CONFLICT_INTERPRETATIONS.get(key, | |
| f"Cross-modal disagreement: {mod_a} shows {emo_a}({level_a}) while {mod_b} shows {emo_b}({level_b})") | |
| conflicts.append(CrossModalConflict( | |
| emotion=f"{emo_a}_vs_{emo_b}", modality_a=mod_a, modality_b=mod_b, | |
| level_a=level_a, level_b=level_b, severity=severity, interpretation=interp)) | |
| conflicts.sort(key=lambda c: c.severity, reverse=True) | |
| return conflicts | |
| # ============================================================================ | |
| # RULE EVALUATOR | |
| # ============================================================================ | |
| def evaluate_rules(rules, fuzzified_states, movement_features=None): | |
| fired = [] | |
| level_order = ["absent", "low", "moderate", "high", "very_high"] | |
| for rule in rules: | |
| activations = [] | |
| for cond in rule.conditions: | |
| modality, emotion, level = cond["modality"], cond["emotion"], cond["level"] | |
| op = cond.get("op", ">=") | |
| if modality == "movement" and movement_features: | |
| memberships = fuzzify(movement_features.get(emotion, 0.0)) | |
| elif modality in fuzzified_states and emotion in fuzzified_states[modality]: | |
| memberships = fuzzified_states[modality][emotion] | |
| else: | |
| activations.append(0.0); continue | |
| target_idx = level_order.index(level) if level in level_order else 0 | |
| if op == ">=": | |
| activation = sum(memberships.get(l, 0.0) for l in level_order[target_idx:]) | |
| elif op == "<=": | |
| activation = sum(memberships.get(l, 0.0) for l in level_order[:target_idx + 1]) | |
| else: | |
| activation = memberships.get(level, 0.0) | |
| activations.append(activation) | |
| rule_activation = min(activations) if activations else 0.0 | |
| if rule_activation > 0.01: | |
| fired.append((rule, rule_activation)) | |
| return fired | |
| # ============================================================================ | |
| # THREAT ASSESSOR | |
| # ============================================================================ | |
| def assess_threat(emotion_probs, movement_result, conflict_score, fired_rules, conflicts, zone_sensitivity=1.0): | |
| aggression = sum(emotion_probs.get(e, 0) for e in SECURITY_EMOTION_CLUSTERS["aggression"]) | |
| fear = sum(emotion_probs.get(e, 0) for e in SECURITY_EMOTION_CLUSTERS["fear_anxiety"]) | |
| agitation = sum(emotion_probs.get(e, 0) for e in SECURITY_EMOTION_CLUSTERS["agitation"]) | |
| emotion_threat = min(aggression * 0.5 + fear * 0.2 + agitation * 0.3, 1.0) | |
| movement_threat = 0.0; intent_scores = {}; primary_intent = "normal"; movement_summary = {} | |
| if movement_result: | |
| high_threat = {"attack_intent": 1.0, "aggressive": 0.8, "panic": 0.7, "evasion": 0.5, | |
| "concealment": 0.5, "erratic": 0.4, "loitering": 0.2, "agitated": 0.3, "deceptive": 0.4, "normal": 0.0} | |
| movement_threat = min(sum(movement_result.intent_scores.get(i, 0) * w for i, w in high_threat.items()), 1.0) | |
| intent_scores = movement_result.intent_scores | |
| primary_intent = movement_result.dominant_intent | |
| movement_summary = {k: v for k, v in movement_result.features.as_dict().items() if v > 0.1} | |
| conflict_threat = conflict_score * 0.3 | |
| rule_threat = max(min(sum(r.threat_boost * a for r, a in fired_rules), 1.0), 0.0) if fired_rules else 0.0 | |
| raw_score = emotion_threat * 0.25 + movement_threat * 0.35 + conflict_threat * 0.15 + rule_threat * 0.25 | |
| threat_score = min(raw_score * zone_sensitivity, 1.0) | |
| if threat_score < 0.15: threat_level = ThreatLevel.NONE | |
| elif threat_score < 0.35: threat_level = ThreatLevel.LOW | |
| elif threat_score < 0.55: threat_level = ThreatLevel.ELEVATED | |
| elif threat_score < 0.75: threat_level = ThreatLevel.HIGH | |
| else: threat_level = ThreatLevel.CRITICAL | |
| if fired_rules: | |
| rule_intents = {} | |
| for rule, activation in fired_rules: | |
| rule_intents[rule.intent_signal] = max(rule_intents.get(rule.intent_signal, 0), activation) | |
| if rule_intents: | |
| ri = max(rule_intents, key=rule_intents.get) | |
| if rule_intents[ri] > 0.3 and ri != "normal": primary_intent = ri | |
| factors = [] | |
| if emotion_threat > 0.3: | |
| top_emo = max(emotion_probs, key=emotion_probs.get) | |
| factors.append(f"Elevated emotion: {top_emo} ({emotion_probs[top_emo]:.2f})") | |
| if movement_threat > 0.3 and movement_result: | |
| factors.append(f"Movement intent: {movement_result.dominant_intent} ({movement_result.confidence:.2f})") | |
| if conflict_score > 0.3: | |
| factors.append(f"Cross-modal conflict detected (score: {conflict_score:.2f})") | |
| for conflict in conflicts[:3]: | |
| factors.append(f"Conflict: {conflict.interpretation}") | |
| for rule, activation in fired_rules: | |
| if activation > 0.3: | |
| factors.append(f"Rule {rule.rule_id}: {rule.description[:80]}") | |
| actions = { | |
| ThreatLevel.NONE: "No action required. Continue standard monitoring.", | |
| ThreatLevel.LOW: "Monitor subject. Log for review.", | |
| ThreatLevel.ELEVATED: "Increase surveillance. Alert nearby personnel.", | |
| ThreatLevel.HIGH: "Dispatch security team. Prepare for intervention.", | |
| ThreatLevel.CRITICAL: "IMMEDIATE RESPONSE. All security to location. Consider lockdown.", | |
| } | |
| action = actions.get(threat_level, "Monitor.") | |
| intent_actions = { | |
| "attack_intent": " Approach with caution. Subject may be armed.", | |
| "aggressive": " De-escalation team recommended.", | |
| "concealment": " Search may be warranted.", | |
| "evasion": " Track subject. Cover exit routes.", | |
| "panic": " Assess trigger. Crowd control may be needed.", | |
| "erratic": " Medical team on standby.", | |
| "deceptive": " Structured questioning. Second officer recommended.", | |
| } | |
| if primary_intent in intent_actions and threat_level.value in ("elevated", "high", "critical"): | |
| action += intent_actions[primary_intent] | |
| sorted_emo = sorted(emotion_probs.items(), key=lambda x: -x[1]) | |
| return ThreatAssessmentResult( | |
| threat_level=threat_level, threat_score=threat_score, primary_intent=primary_intent, | |
| intent_scores=intent_scores, emotion_summary=dict(sorted_emo[:5]), | |
| movement_summary=movement_summary, conflicts=conflicts, | |
| fired_rules=[r.rule_id for r, _ in fired_rules], | |
| contributing_factors=factors, recommended_action=action) | |
| # ============================================================================ | |
| # ALERT ENGINE | |
| # ============================================================================ | |
| def generate_alert(threat, zone_id, subject_id="SUBJ-001"): | |
| level_order = [ThreatLevel.NONE, ThreatLevel.LOW, ThreatLevel.ELEVATED, ThreatLevel.HIGH, ThreatLevel.CRITICAL] | |
| if level_order.index(threat.threat_level) < level_order.index(ThreatLevel.ELEVATED): | |
| return None | |
| priority_map = {ThreatLevel.ELEVATED: AlertPriority.WARNING, ThreatLevel.HIGH: AlertPriority.URGENT, ThreatLevel.CRITICAL: AlertPriority.CRITICAL} | |
| priority = priority_map.get(threat.threat_level, AlertPriority.INFO) | |
| intent_titles = { | |
| "attack_intent": "Potential Attack Behavior Detected", "aggressive": "Aggressive Behavior Detected", | |
| "concealment": "Suspicious Concealment Activity", "evasion": "Evasive Movement Detected", | |
| "loitering": "Suspicious Loitering", "panic": "Panic Response Detected", | |
| "erratic": "Erratic Behavior Observed", "deceptive": "Deceptive Behavior Indicators", | |
| "agitated": "Agitated Subject Detected", | |
| } | |
| title = intent_titles.get(threat.primary_intent, f"Security Alert: {threat.threat_level.value.upper()}") | |
| desc_lines = [f"Threat Score: {threat.threat_score:.2f} ({threat.threat_level.value})", | |
| f"Primary Intent: {threat.primary_intent}"] | |
| if threat.emotion_summary: | |
| top = list(threat.emotion_summary.items())[:3] | |
| desc_lines.append(f"Top Emotions: {', '.join(f'{e}: {s:.2f}' for e, s in top)}") | |
| if threat.contributing_factors: | |
| desc_lines.append("Contributing Factors:") | |
| for f in threat.contributing_factors[:5]: | |
| desc_lines.append(f" - {f}") | |
| return Alert(alert_id=str(uuid.uuid4())[:8], zone_id=zone_id, subject_id=subject_id, | |
| priority=priority, threat_level=threat.threat_level, title=title, | |
| description="\n".join(desc_lines), timestamp=time.time(), | |
| contributing_signals=threat.contributing_factors[:5], | |
| recommended_action=threat.recommended_action) | |
| # ============================================================================ | |
| # FULL FUSION PIPELINE | |
| # ============================================================================ | |
| def run_security_fusion(text, movement_features, zone_type, zone_sensitivity): | |
| text_emotions = simulate_text_emotions(text) | |
| face_emotions = simulate_face_emotions(text_emotions) | |
| voice_emotions = simulate_voice_emotions(text_emotions) | |
| face_28 = project_to_goemotions(face_emotions, FACE_TO_GOEMOTIONS_MAP) | |
| voice_28 = project_to_goemotions(voice_emotions, VOICE_TO_GOEMOTIONS_MAP) | |
| text_28 = np.array([text_emotions.get(l, 0.01) for l in GOEMOTIONS_LABELS]) | |
| t = text_28.sum() | |
| if t > 0: text_28 = text_28 / t | |
| intent_scores = map_movement_to_intents(movement_features) | |
| movement_28 = movement_to_emotion_space(intent_scores) | |
| dominant_intent = max(intent_scores, key=intent_scores.get) | |
| movement_result = MovementResult(features=movement_features, intent_scores=intent_scores, | |
| dominant_intent=dominant_intent, confidence=intent_scores[dominant_intent]) | |
| weights = {"face": 0.20, "voice": 0.25, "text": 0.20, "movement": 0.35} | |
| base_crisp = weights["face"] * face_28 + weights["voice"] * voice_28 + weights["text"] * text_28 + weights["movement"] * movement_28 | |
| modality_fuzzy = {} | |
| for mod_name, vec in [("face", face_28), ("voice", voice_28), ("text", text_28), ("movement", movement_28)]: | |
| fuzzy_states = {} | |
| for i, label in enumerate(GOEMOTIONS_LABELS): | |
| fuzzy_states[label] = fuzzify(float(vec[i])) | |
| modality_fuzzy[mod_name] = fuzzy_states | |
| rules = build_security_rules(zone_type) | |
| fired_rules = evaluate_rules(rules, modality_fuzzy, movement_features.as_dict()) | |
| conflicts = detect_conflicts(modality_fuzzy, ["face", "voice", "text", "movement"]) | |
| conflict_score = min(1.0, sum(c.severity ** 2 for c in conflicts) / max(len(conflicts), 1)) if conflicts else 0.0 | |
| # Defuzzification | |
| result = base_crisp.copy() | |
| label_idx = {label: i for i, label in enumerate(GOEMOTIONS_LABELS)} | |
| for rule, activation in fired_rules: | |
| for emotion_label, target_level in rule.outputs.items(): | |
| if emotion_label in label_idx: | |
| idx = label_idx[emotion_label] | |
| target_centroid = LEVEL_CENTROIDS.get(target_level, 0.35) | |
| effective = target_centroid * activation | |
| result[idx] = (1.0 - activation) * result[idx] + activation * effective | |
| base_fuzzy = {} | |
| for i, label in enumerate(GOEMOTIONS_LABELS): | |
| base_fuzzy[label] = fuzzify(float(base_crisp[i])) | |
| for label, memberships in base_fuzzy.items(): | |
| if label in label_idx: | |
| idx = label_idx[label] | |
| numerator = sum(mu * LEVEL_CENTROIDS.get(lev, 0) for lev, mu in memberships.items() if mu > 0) | |
| denominator = sum(mu for mu in memberships.values() if mu > 0) | |
| if denominator > 0: | |
| result[idx] = 0.7 * result[idx] + 0.3 * (numerator / denominator) | |
| result = np.maximum(result, 0) | |
| total = result.sum() | |
| if total > 0: result = result / total | |
| emotion_probs = dict(zip(GOEMOTIONS_LABELS, result.tolist())) | |
| threat = assess_threat(emotion_probs, movement_result, conflict_score, fired_rules, conflicts, zone_sensitivity) | |
| fuzzy_states = [] | |
| for i, label in enumerate(GOEMOTIONS_LABELS): | |
| memberships = base_fuzzy.get(label, {"absent": 1.0}) | |
| dominant = max(memberships, key=memberships.get) | |
| fuzzy_states.append(FuzzyEmotionState(emotion=label, memberships=memberships, | |
| dominant_level=dominant, crisp_value=float(result[i]))) | |
| alert = generate_alert(threat, f"zone_{zone_type}") | |
| return { | |
| "emotion_probs": emotion_probs, "face_emotions": face_emotions, | |
| "voice_emotions": voice_emotions, "text_emotions": text_emotions, | |
| "movement_result": movement_result, "threat": threat, | |
| "fuzzy_states": fuzzy_states, "conflicts": conflicts, | |
| "conflict_score": conflict_score, "fired_rules": fired_rules, | |
| "alert": alert, "weights": weights, | |
| } | |
| # ============================================================================ | |
| # SESSION STATE | |
| # ============================================================================ | |
| _DEFAULTS = { | |
| "auth_step": "email", | |
| "auth_email": "", | |
| "auth_code": "", | |
| "auth_code_ts": 0.0, | |
| "auth_email_pending": False, | |
| "session_start": 0.0, | |
| } | |
| for key, val in _DEFAULTS.items(): | |
| if key not in st.session_state: | |
| st.session_state[key] = val | |
| # ── Background email send (runs after rerun so UI stays responsive) ── | |
| if st.session_state.get("auth_email_pending") and st.session_state["auth_step"] == "verify": | |
| st.session_state["auth_email_pending"] = False | |
| print(f"[Auth] Attempting to send code to {st.session_state['auth_email']}...") | |
| try: | |
| sent = send_verification_email(st.session_state["auth_email"], st.session_state["auth_code"]) | |
| print(f"[Auth] send_verification_email returned: sent={sent}") | |
| if sent: | |
| st.session_state["auth_email_sent"] = True | |
| else: | |
| st.session_state["auth_email_error"] = "SMTP send failed" | |
| except Exception as _exc: | |
| print(f"[Auth] send_verification_email exception: {_exc}") | |
| st.session_state["auth_email_error"] = str(_exc) | |
| # ============================================================================ | |
| # AUTH GATE | |
| # ============================================================================ | |
| def _get_logo_b64(): | |
| import base64 | |
| from pathlib import Path | |
| logo_path = Path(__file__).parent / "logo.png" | |
| if logo_path.exists(): | |
| return base64.b64encode(logo_path.read_bytes()).decode() | |
| return None | |
| def _render_header(animated=True, size=300): | |
| _logo_b64 = _get_logo_b64() | |
| if _logo_b64: | |
| cls = 'floating-logo' if animated else '' | |
| st.markdown(f""" | |
| <div style="text-align:center; padding: 1rem 0;"> | |
| <img src="data:image/png;base64,{_logo_b64}" class="{cls}" | |
| style="width: {size}px; height: {size}px; object-fit: contain;"/> | |
| </div>""", unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="orionus-header" style="padding-top: 0;"> | |
| <p>Behavioral Intelligence for Security — Multimodal Threat Assessment & Intent Recognition System:<br/> | |
| Real-time threat detection through AI-powered analysis of facial expressions, voice patterns, text sentiment, and body movement</p> | |
| </div>""", unsafe_allow_html=True) | |
| def _valid_email(email): | |
| return bool(re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", email)) | |
| def _show_landing(): | |
| _render_header() | |
| st.markdown(""" | |
| <div class="auth-card"> | |
| <h3>Request Demo Access</h3> | |
| <p>Enter your email to receive a one-time verification code. | |
| Each email is valid for a single 60-second trial session.</p> | |
| </div>""", unsafe_allow_html=True) | |
| col_l, col_c, col_r = st.columns([1, 2, 1]) | |
| with col_c: | |
| with st.form("email_form", clear_on_submit=False): | |
| email = st.text_input("Email address", key="inp_email", placeholder="you@company.com") | |
| submitted = st.form_submit_button("Send Verification Code", use_container_width=True, type="primary") | |
| if submitted: | |
| email = email.strip().lower() | |
| if not _valid_email(email): | |
| st.error("Please enter a valid email address."); return | |
| if is_email_used(email): | |
| st.error("This email has already been used. Contact info@caitcore.com for full access."); return | |
| code = generate_code() | |
| st.session_state["auth_email"] = email | |
| st.session_state["auth_code"] = code | |
| st.session_state["auth_code_ts"] = time.time() | |
| st.session_state["auth_step"] = "verify" | |
| st.session_state["auth_email_pending"] = True | |
| st.rerun() | |
| def _show_verify(): | |
| _render_header() | |
| st.markdown(f""" | |
| <div class="auth-card"> | |
| <h3>Enter Verification Code</h3> | |
| <p>A 6-digit code was sent to <strong>{st.session_state["auth_email"]}</strong></p> | |
| </div>""", unsafe_allow_html=True) | |
| col_l, col_c, col_r = st.columns([1, 2, 1]) | |
| with col_c: | |
| with st.form("code_form", clear_on_submit=False): | |
| code_input = st.text_input("Verification code", key="inp_code", max_chars=6, placeholder="000000") | |
| verified = st.form_submit_button("Verify & Start Demo", use_container_width=True, type="primary") | |
| if verified: | |
| elapsed = time.time() - st.session_state["auth_code_ts"] | |
| if elapsed > CODE_EXPIRY_SEC: | |
| st.error("Code expired. Please request a new one.") | |
| st.session_state["auth_step"] = "email"; st.rerun(); return | |
| if code_input.strip() != st.session_state["auth_code"]: | |
| st.error("Invalid code."); return | |
| mark_email_used(st.session_state["auth_email"]) | |
| st.session_state["session_start"] = time.time() | |
| st.session_state["auth_step"] = "active" | |
| st.rerun() | |
| if st.button("Back", use_container_width=True): | |
| st.session_state["auth_step"] = "email"; st.rerun() | |
| def _show_trial_ended(): | |
| _render_header(animated=False, size=200) | |
| st.markdown(""" | |
| <div class="trial-ended"> | |
| <h2>Trial Complete</h2> | |
| <p>Your 60-second demo session has ended.</p> | |
| <p style="margin-top:1.5rem;"> | |
| Interested in the full Orionus platform?<br/> | |
| <a href="mailto:info@caitcore.com">Contact us at info@caitcore.com</a> for enterprise access. | |
| </p> | |
| <p style="margin-top:1rem; font-size:0.85rem; color:#64748b;"> | |
| Orionus provides real-time multimodal threat assessment for | |
| airports, transit hubs, stadiums, and critical infrastructure. | |
| </p> | |
| </div>""", unsafe_allow_html=True) | |
| def _render_timer(): | |
| secs = remaining_seconds(st.session_state["session_start"]) | |
| if secs <= 0: | |
| st.session_state["auth_step"] = "ended"; st.rerun(); return False | |
| css = "timer-green" if secs > 30 else ("timer-yellow" if secs > 10 else "timer-red") | |
| st.markdown(f'<div class="timer-bar {css}">Trial session: <strong>{secs // 60}:{secs % 60:02d}</strong> remaining</div>', unsafe_allow_html=True) | |
| return True | |
| # ============================================================================ | |
| # MAIN DEMO UI | |
| # ============================================================================ | |
| def _run_demo(): | |
| if session_expired(st.session_state["session_start"]): | |
| st.session_state["auth_step"] = "ended"; st.rerun(); return | |
| _render_header() | |
| if not _render_timer(): | |
| return | |
| # Sidebar | |
| with st.sidebar: | |
| st.markdown("### Zone Configuration") | |
| zone_type = st.selectbox("Monitoring Zone", ZONE_TYPES, index=0) | |
| zone_descriptions = { | |
| "checkpoint": "Security checkpoint -- mild anxiety normal, evasion critical", | |
| "boarding": "Boarding/gate area -- watch for concealment", | |
| "transit": "Corridors -- movement patterns are key", | |
| "queue": "Queuing -- agitation from waiting is common", | |
| "retail": "Commercial -- normal social behavior expected", | |
| "restricted": "Staff-only -- ANY presence suspicious", | |
| "entrance": "Entry/exit -- reversal behavior is key signal", | |
| "gathering": "Waiting/lounge -- crowd dynamics important", | |
| "perimeter": "External boundary -- loitering and evasion", | |
| "parking": "Parking -- concealment and evasion patterns", | |
| } | |
| st.caption(zone_descriptions.get(zone_type, "")) | |
| zone_sensitivity = st.slider("Zone Sensitivity", 0.5, 2.0, 1.0, 0.1) | |
| st.markdown("---") | |
| st.markdown("### Movement Simulation") | |
| velocity = st.slider("Velocity", 0.0, 1.0, 0.15, 0.05) | |
| acceleration = st.slider("Acceleration", 0.0, 1.0, 0.1, 0.05) | |
| direction_changes = st.slider("Direction Changes", 0.0, 1.0, 0.1, 0.05) | |
| proximity_approach = st.slider("Proximity Approach", 0.0, 1.0, 0.1, 0.05) | |
| proximity_avoid = st.slider("Proximity Avoid", 0.0, 1.0, 0.1, 0.05) | |
| hand_activity = st.slider("Hand Activity", 0.0, 1.0, 0.1, 0.05) | |
| body_tension = st.slider("Body Tension", 0.0, 1.0, 0.15, 0.05) | |
| gait_regularity = st.slider("Gait Irregularity", 0.0, 1.0, 0.05, 0.05) | |
| loiter_score = st.slider("Loiter Score", 0.0, 1.0, 0.0, 0.05) | |
| crowd_interaction = st.slider("Crowd Interaction", 0.0, 1.0, 0.1, 0.05) | |
| st.markdown("---") | |
| st.markdown("### Scenario Presets") | |
| preset = st.selectbox("Load Scenario", [ | |
| "-- Custom --", "Normal Passenger", "Agitated Traveler", | |
| "Aggressive Confrontation", "Suspicious Loitering", "Evasive Subject", | |
| "Concealment Behavior", "Panic Flight", "Deceptive Social Engineering", "Erratic Behavior"]) | |
| preset_texts = { | |
| "Normal Passenger": "Everything is fine. I'm just waiting for my flight.", | |
| "Agitated Traveler": "This is ridiculous! I've been waiting two hours. Frustrated and annoyed!", | |
| "Aggressive Confrontation": "I'm going to destroy you! Get out of my way! I hate this place!", | |
| "Suspicious Loitering": "Just looking around. Nothing special. Fine. Normal.", | |
| "Evasive Subject": "I don't know what you're talking about. I need to go somewhere else now.", | |
| "Concealment Behavior": "Everything is fine, nothing to worry about. Just standing here normally.", | |
| "Panic Flight": "Help! Run! There's danger! I'm scared! We need to get out!", | |
| "Deceptive Social Engineering": "Oh you're so kind! I'm so happy! Everything is wonderful! I just need past this area...", | |
| "Erratic Behavior": "What? Where am I? I don't understand anything. Why is everything spinning?", | |
| } | |
| preset_movements = { | |
| "Normal Passenger": MovementFeatures(velocity=0.15, acceleration=0.05, body_tension=0.1, gait_regularity=0.05), | |
| "Agitated Traveler": MovementFeatures(velocity=0.3, acceleration=0.2, hand_activity=0.4, body_tension=0.4, direction_changes=0.2), | |
| "Aggressive Confrontation": MovementFeatures(velocity=0.5, acceleration=0.6, proximity_approach=0.7, hand_activity=0.7, body_tension=0.8), | |
| "Suspicious Loitering": MovementFeatures(velocity=0.05, loiter_score=0.7, direction_changes=0.15), | |
| "Evasive Subject": MovementFeatures(velocity=0.5, acceleration=0.4, direction_changes=0.7, proximity_avoid=0.6, body_tension=0.3), | |
| "Concealment Behavior": MovementFeatures(velocity=0.1, hand_activity=0.7, body_tension=0.5, proximity_avoid=0.3), | |
| "Panic Flight": MovementFeatures(velocity=0.9, acceleration=0.8, gait_regularity=0.5, crowd_interaction=0.6), | |
| "Deceptive Social Engineering": MovementFeatures(velocity=0.2, hand_activity=0.3, body_tension=0.4, proximity_approach=0.3), | |
| "Erratic Behavior": MovementFeatures(velocity=0.4, acceleration=0.5, direction_changes=0.7, gait_regularity=0.8, hand_activity=0.3), | |
| } | |
| default_text = "Enter text to analyze for emotional content and security intent..." | |
| if preset != "-- Custom --": | |
| default_text = preset_texts.get(preset, default_text) | |
| st.markdown('<div class="section-header">Subject Communication Input</div>', unsafe_allow_html=True) | |
| text_input = st.text_area("Text input (simulates transcribed speech)", value=default_text, height=100, label_visibility="collapsed") | |
| if preset != "-- Custom --" and preset in preset_movements: | |
| mf = preset_movements[preset] | |
| else: | |
| mf = MovementFeatures(velocity=velocity, acceleration=acceleration, direction_changes=direction_changes, | |
| proximity_approach=proximity_approach, proximity_avoid=proximity_avoid, | |
| hand_activity=hand_activity, body_tension=body_tension, gait_regularity=gait_regularity, | |
| loiter_score=loiter_score, crowd_interaction=crowd_interaction) | |
| # Re-check timer | |
| if session_expired(st.session_state["session_start"]): | |
| st.session_state["auth_step"] = "ended"; st.rerun(); return | |
| if text_input and text_input != "Enter text to analyze for emotional content and security intent...": | |
| results = run_security_fusion(text_input, mf, zone_type, zone_sensitivity) | |
| threat = results["threat"] | |
| # === THREAT LEVEL BANNER === | |
| tc_map = {"none": "#2E7D32", "low": "#4CAF50", "elevated": "#FF9800", "high": "#f44336", "critical": "#d50000"} | |
| tc = tc_map.get(threat.threat_level.value, "#666") | |
| st.markdown(f""" | |
| <div class="threat-card threat-{threat.threat_level.value}"> | |
| <div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap;"> | |
| <div> | |
| <span style="color:{tc}; font-size:0.9rem; font-weight:600;">THREAT LEVEL</span> | |
| <h2 style="color:{tc}; margin:0; font-size:2rem;">{threat.threat_level.value.upper()}</h2> | |
| </div> | |
| <div style="text-align:right;"> | |
| <span style="color:#aaa; font-size:0.85rem;">Threat Score</span> | |
| <h2 style="color:{tc}; margin:0; font-size:2rem;">{threat.threat_score:.2f}</h2> | |
| </div> | |
| <div style="text-align:right;"> | |
| <span style="color:#aaa; font-size:0.85rem;">Primary Intent</span> | |
| <h2 style="color:{tc}; margin:0; font-size:1.5rem;">{threat.primary_intent.upper().replace('_', ' ')}</h2> | |
| </div> | |
| <div style="text-align:right;"> | |
| <span style="color:#aaa; font-size:0.85rem;">Zone</span> | |
| <h2 style="color:#81C784; margin:0; font-size:1.3rem;">{zone_type.upper()}</h2> | |
| </div> | |
| </div> | |
| </div>""", unsafe_allow_html=True) | |
| # Recommended action | |
| if threat.threat_level.value in ("elevated", "high", "critical"): | |
| alert_class = "critical" if threat.threat_level.value == "critical" else ("urgent" if threat.threat_level.value == "high" else "warning") | |
| ac = "#d50000" if threat.threat_level.value == "critical" else ("#f44336" if threat.threat_level.value == "high" else "#FF9800") | |
| st.markdown(f""" | |
| <div class="alert-box alert-{alert_class}"> | |
| <strong style="color:{ac};">RECOMMENDED ACTION:</strong><br/> | |
| <span style="color:#e0e0e0;">{threat.recommended_action}</span> | |
| </div>""", unsafe_allow_html=True) | |
| # Metrics | |
| c1, c2, c3, c4 = st.columns(4) | |
| with c1: st.metric("Conflict Score", f"{results['conflict_score']:.2f}") | |
| with c2: st.metric("Rules Fired", str(len(results['fired_rules']))) | |
| with c3: st.metric("Conflicts", str(len(results['conflicts']))) | |
| with c4: st.metric("Modalities", "4 (F+V+T+M)") | |
| # === TABS === | |
| tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([ | |
| "Intent Classification", "Emotion Analysis", "Fuzzy Inference", | |
| "Conflict Detection", "Movement Analysis", "Alert Log"]) | |
| with tab1: | |
| st.markdown('<div class="section-header">Security Intent Categories (10-Class)</div>', unsafe_allow_html=True) | |
| intent_scores = results["movement_result"].intent_scores | |
| intent_colors = {"normal": "#4CAF50", "agitated": "#FFC107", "aggressive": "#f44336", | |
| "attack_intent": "#d50000", "concealment": "#9C27B0", "evasion": "#FF5722", | |
| "loitering": "#FF9800", "panic": "#E91E63", "deceptive": "#673AB7", "erratic": "#795548"} | |
| sorted_intents = sorted(intent_scores.items(), key=lambda x: -x[1]) | |
| fig = go.Figure(go.Bar( | |
| x=[s for _, s in sorted_intents], y=[i.replace("_", " ").title() for i, _ in sorted_intents], | |
| orientation='h', marker_color=[intent_colors.get(i, "#666") for i, _ in sorted_intents], | |
| text=[f"{s:.3f}" for _, s in sorted_intents], textposition='auto', textfont=dict(color='white', size=12))) | |
| fig.update_layout(paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(15,20,30,0.8)', | |
| font=dict(color='#e0e0e0'), height=400, margin=dict(l=150, r=20, t=30, b=30), | |
| xaxis=dict(title="Score", gridcolor='#1e293b', range=[0, max(0.5, max(s for _, s in sorted_intents) * 1.2)]), | |
| yaxis=dict(gridcolor='#1e293b')) | |
| st.plotly_chart(fig, use_container_width=True) | |
| intent_desc = {"normal": "Baseline -- no threat", "agitated": "Elevated stress / aggression building", | |
| "aggressive": "Active aggression / confrontation", "attack_intent": "Pre-attack posture / movement", | |
| "concealment": "Hiding objects / face / identity", "evasion": "Avoiding detection / cameras / security", | |
| "loitering": "Unusual lingering / surveillance", "panic": "Fleeing / mass panic", | |
| "deceptive": "Emotional masking / social engineering", "erratic": "Unpredictable / drug-influenced"} | |
| for intent, desc in intent_desc.items(): | |
| score = intent_scores.get(intent, 0) | |
| color = intent_colors.get(intent, "#666") | |
| st.markdown(f'<div style="display:flex;align-items:center;margin:0.2rem 0;">' | |
| f'<span style="width:130px;font-weight:600;color:{color};font-size:0.85rem;">{intent.replace("_"," ").title()}</span>' | |
| f'<div style="flex:1;background:#111927;border-radius:4px;height:16px;margin:0 0.5rem;">' | |
| f'<div style="width:{int(score*100)}%;background:{color};height:100%;border-radius:4px;"></div></div>' | |
| f'<span style="width:50px;text-align:right;color:#aaa;font-size:0.8rem;">{score:.3f}</span>' | |
| f'<span style="width:250px;color:#64748b;font-size:0.75rem;padding-left:0.5rem;">{desc}</span></div>', unsafe_allow_html=True) | |
| with tab2: | |
| st.markdown('<div class="section-header">Fused Emotion Distribution (28 GoEmotions)</div>', unsafe_allow_html=True) | |
| emotion_probs = results["emotion_probs"] | |
| sorted_emo = sorted(emotion_probs.items(), key=lambda x: -x[1])[:15] | |
| fig = go.Figure(go.Bar( | |
| x=[e for e, _ in sorted_emo], y=[s for _, s in sorted_emo], | |
| marker_color=['#00E676' if s > 0.1 else '#2E7D32' if s > 0.05 else '#1a3a1a' for _, s in sorted_emo], | |
| text=[f"{s:.3f}" for _, s in sorted_emo], textposition='auto', textfont=dict(color='white', size=10))) | |
| fig.update_layout(paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(15,20,30,0.8)', | |
| font=dict(color='#e0e0e0'), height=350, margin=dict(l=40, r=20, t=30, b=80), | |
| xaxis=dict(tickangle=-45, gridcolor='#1e293b'), yaxis=dict(title="Probability", gridcolor='#1e293b')) | |
| st.plotly_chart(fig, use_container_width=True) | |
| st.markdown('<div class="section-header">Security Emotion Clusters</div>', unsafe_allow_html=True) | |
| cluster_scores = {} | |
| for cn, emotions in SECURITY_EMOTION_CLUSTERS.items(): | |
| cluster_scores[cn] = sum(emotion_probs.get(e, 0) for e in emotions) | |
| fig = go.Figure(go.Scatterpolar( | |
| r=list(cluster_scores.values()), | |
| theta=[c.replace("_", " ").title() for c in cluster_scores.keys()], | |
| fill='toself', fillcolor='rgba(0, 230, 118, 0.15)', | |
| line=dict(color='#00E676', width=2), marker=dict(color='#00E676', size=8))) | |
| fig.update_layout(polar=dict(bgcolor='rgba(15,20,30,0.8)', | |
| radialaxis=dict(visible=True, gridcolor='#1e293b', color='#64748b'), | |
| angularaxis=dict(gridcolor='#1e293b', color='#e0e0e0')), | |
| paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#e0e0e0'), | |
| height=400, margin=dict(l=60, r=60, t=40, b=40), showlegend=False) | |
| st.plotly_chart(fig, use_container_width=True) | |
| with tab3: | |
| st.markdown('<div class="section-header">Fuzzy Membership Visualization</div>', unsafe_allow_html=True) | |
| x_vals = np.linspace(0, 1, 200) | |
| mf_funcs = [("Absent", mf_absent, "#4CAF50"), ("Low", mf_low, "#8BC34A"), | |
| ("Moderate", mf_moderate, "#FFC107"), ("High", mf_high, "#FF9800"), ("Very High", mf_very_high, "#f44336")] | |
| fig = go.Figure() | |
| for name, func, color in mf_funcs: | |
| fig.add_trace(go.Scatter(x=x_vals, y=[func(x) for x in x_vals], mode='lines', name=name, line=dict(color=color, width=2))) | |
| fig.update_layout(title="Trapezoidal Membership Functions", paper_bgcolor='rgba(0,0,0,0)', | |
| plot_bgcolor='rgba(15,20,30,0.8)', font=dict(color='#e0e0e0'), height=300, | |
| margin=dict(l=40, r=20, t=40, b=30), | |
| xaxis=dict(title="Crisp Value", gridcolor='#1e293b'), yaxis=dict(title="Membership", gridcolor='#1e293b'), | |
| legend=dict(bgcolor='rgba(0,0,0,0.5)')) | |
| st.plotly_chart(fig, use_container_width=True) | |
| st.markdown('<div class="section-header">Top Emotion Fuzzy States</div>', unsafe_allow_html=True) | |
| top_fuzzy = sorted(results["fuzzy_states"], key=lambda x: x.crisp_value, reverse=True)[:10] | |
| fuzzy_data = [] | |
| for fs in top_fuzzy: | |
| for level, mu in fs.memberships.items(): | |
| if mu > 0.01: | |
| fuzzy_data.append({"Emotion": fs.emotion, "Level": level, "Membership": mu}) | |
| if fuzzy_data: | |
| lc = {"absent": "#4CAF50", "low": "#8BC34A", "moderate": "#FFC107", "high": "#FF9800", "very_high": "#f44336"} | |
| fig = go.Figure() | |
| for level in FUZZY_LEVELS: | |
| ld = [d for d in fuzzy_data if d["Level"] == level] | |
| if ld: | |
| fig.add_trace(go.Bar(x=[d["Emotion"] for d in ld], y=[d["Membership"] for d in ld], | |
| name=level.replace("_", " ").title(), marker_color=lc.get(level, "#666"))) | |
| fig.update_layout(barmode='stack', paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(15,20,30,0.8)', | |
| font=dict(color='#e0e0e0'), height=350, margin=dict(l=40, r=20, t=30, b=80), | |
| xaxis=dict(tickangle=-45, gridcolor='#1e293b'), yaxis=dict(title="Membership", gridcolor='#1e293b'), | |
| legend=dict(bgcolor='rgba(0,0,0,0.5)')) | |
| st.plotly_chart(fig, use_container_width=True) | |
| st.markdown('<div class="section-header">Fired Fuzzy Rules</div>', unsafe_allow_html=True) | |
| if results["fired_rules"]: | |
| cat_colors = {"aggression": "#f44336", "deception": "#673AB7", "panic": "#E91E63", | |
| "surveillance": "#FF5722", "zone": "#2196F3", "deescalation": "#4CAF50"} | |
| for rule, activation in results["fired_rules"]: | |
| cc = cat_colors.get(rule.category, "#888") | |
| st.markdown(f""" | |
| <div style="background:#0d1117; border:1px solid {cc}; border-radius:8px; padding:0.7rem; margin:0.3rem 0;"> | |
| <div style="display:flex; justify-content:space-between;"> | |
| <span style="color:{cc}; font-weight:600;">{rule.rule_id}</span> | |
| <span style="color:#64748b;">[{rule.category.upper()}]</span> | |
| <span style="color:#00E676;">Activation: {activation:.3f}</span> | |
| <span style="color:{'#f44336' if rule.threat_boost > 0 else '#4CAF50'};"> | |
| Threat: {'+' if rule.threat_boost > 0 else ''}{rule.threat_boost:.2f}</span> | |
| </div> | |
| <div style="color:#94a3b8; font-size:0.85rem; margin-top:0.3rem;">{rule.description}</div> | |
| </div>""", unsafe_allow_html=True) | |
| else: | |
| st.info("No fuzzy rules fired for current input.") | |
| with tab4: | |
| st.markdown('<div class="section-header">Cross-Modal Conflict Analysis</div>', unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div style="background:#0d1117; border:1px solid #1e293b; border-radius:8px; padding:0.8rem; margin-bottom:1rem;"> | |
| <span style="color:#81C784;">Cross-modal conflicts indicate when different channels disagree about the subject's | |
| emotional state -- a key indicator of deception, concealment, or coerced behavior.</span> | |
| </div>""", unsafe_allow_html=True) | |
| if results["conflicts"]: | |
| for conflict in results["conflicts"]: | |
| sc = "#4CAF50" if conflict.severity < 0.4 else ("#FF9800" if conflict.severity < 0.7 else "#f44336") | |
| st.markdown(f""" | |
| <div style="background:#0d1117; border:1px solid {sc}; border-radius:8px; padding:0.8rem; margin:0.4rem 0;"> | |
| <div style="display:flex; justify-content:space-between;"> | |
| <span style="color:#e0e0e0; font-weight:600;">{conflict.emotion.replace('_', ' ').title()}</span> | |
| <span style="color:{sc};">Severity: {conflict.severity:.2f}</span> | |
| </div> | |
| <div style="color:#64748b; font-size:0.85rem; margin-top:0.3rem;"> | |
| {conflict.modality_a.upper()} ({conflict.level_a}) vs {conflict.modality_b.upper()} ({conflict.level_b})</div> | |
| <div style="color:#81C784; font-size:0.85rem; margin-top:0.3rem; font-style:italic;">{conflict.interpretation}</div> | |
| </div>""", unsafe_allow_html=True) | |
| else: | |
| st.success("No significant cross-modal conflicts. Modalities are consistent.") | |
| with tab5: | |
| st.markdown('<div class="section-header">Movement Feature Analysis</div>', unsafe_allow_html=True) | |
| feat_dict = results["movement_result"].features.as_dict() | |
| fig = go.Figure(go.Scatterpolar( | |
| r=list(feat_dict.values()), theta=[f.replace("_", " ").title() for f in feat_dict.keys()], | |
| fill='toself', fillcolor='rgba(0, 230, 118, 0.15)', | |
| line=dict(color='#00E676', width=2), marker=dict(color='#00E676', size=6))) | |
| fig.update_layout(polar=dict(bgcolor='rgba(15,20,30,0.8)', | |
| radialaxis=dict(visible=True, range=[0, 1], gridcolor='#1e293b', color='#64748b'), | |
| angularaxis=dict(gridcolor='#1e293b', color='#e0e0e0')), | |
| paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#e0e0e0'), | |
| height=450, margin=dict(l=80, r=80, t=40, b=40), showlegend=False) | |
| st.plotly_chart(fig, use_container_width=True) | |
| feat_desc = { | |
| "velocity": "Overall movement speed", "acceleration": "Sudden speed changes", | |
| "direction_changes": "Frequency of path alterations", "proximity_approach": "Approach toward targets", | |
| "proximity_avoid": "Avoidance of security", "hand_activity": "Hand movement intensity", | |
| "body_tension": "Muscular rigidity", "gait_regularity": "Walking irregularity", | |
| "loiter_score": "Purposeless lingering", "crowd_interaction": "Movement vs crowd flow"} | |
| for feat, val in feat_dict.items(): | |
| color = "#4CAF50" if val < 0.3 else ("#FF9800" if val < 0.6 else "#f44336") | |
| st.markdown(f'<div style="display:flex;align-items:center;margin:0.2rem 0;">' | |
| f'<span style="width:160px;color:#81C784;font-size:0.85rem;">{feat.replace("_"," ").title()}</span>' | |
| f'<div style="flex:1;background:#111927;border-radius:4px;height:16px;margin:0 0.5rem;">' | |
| f'<div style="width:{int(val*100)}%;background:{color};height:100%;border-radius:4px;"></div></div>' | |
| f'<span style="width:50px;text-align:right;color:{color};font-weight:600;">{val:.2f}</span>' | |
| f'<span style="width:250px;color:#64748b;font-size:0.75rem;padding-left:0.5rem;">{feat_desc.get(feat,"")}</span></div>', unsafe_allow_html=True) | |
| with tab6: | |
| st.markdown('<div class="section-header">Security Alert Generation</div>', unsafe_allow_html=True) | |
| alert = results["alert"] | |
| if alert: | |
| pc_map = {"info": "#2196F3", "warning": "#FF9800", "urgent": "#f44336", "critical": "#d50000"} | |
| pc = pc_map.get(alert.priority.value, "#666") | |
| st.markdown(f""" | |
| <div style="background:#0d1117; border:2px solid {pc}; border-radius:12px; padding:1.2rem; margin:0.5rem 0;"> | |
| <div style="display:flex; justify-content:space-between; align-items:center;"> | |
| <div> | |
| <span style="background:{pc}; color:#fff; padding:0.2rem 0.6rem; border-radius:4px; | |
| font-size:0.8rem; font-weight:700;">{alert.priority.value.upper()}</span> | |
| <span style="color:#e0e0e0; font-size:1.1rem; font-weight:600; margin-left:0.5rem;">{alert.title}</span> | |
| </div> | |
| <span style="color:#64748b; font-size:0.8rem;">ID: {alert.alert_id}</span> | |
| </div> | |
| <div style="color:#94a3b8; margin-top:0.8rem; font-size:0.9rem; white-space:pre-line;">{alert.description}</div> | |
| <div style="margin-top:0.8rem; padding-top:0.5rem; border-top:1px solid #1e293b;"> | |
| <span style="color:#81C784; font-weight:600;">Recommended Action:</span> | |
| <span style="color:#e0e0e0;">{alert.recommended_action}</span> | |
| </div> | |
| <div style="margin-top:0.5rem;"> | |
| <span style="color:#64748b; font-size:0.8rem;">Zone: {alert.zone_id} | Subject: {alert.subject_id}</span> | |
| </div> | |
| </div>""", unsafe_allow_html=True) | |
| if alert.contributing_signals: | |
| st.markdown('<div class="section-header">Contributing Signals</div>', unsafe_allow_html=True) | |
| for signal in alert.contributing_signals: | |
| st.markdown(f'<div class="factor-item">{signal}</div>', unsafe_allow_html=True) | |
| else: | |
| st.success("No alert generated. Threat level below ELEVATED threshold.") | |
| if threat.contributing_factors: | |
| st.markdown('<div class="section-header">Contributing Factors Summary</div>', unsafe_allow_html=True) | |
| for factor in threat.contributing_factors: | |
| st.markdown(f'<div class="factor-item">{factor}</div>', unsafe_allow_html=True) | |
| else: | |
| st.markdown(""" | |
| <div style="text-align:center; padding:2rem; color:#64748b;"> | |
| <h2 style="color:#2E7D32;">Enter text or select a preset to begin analysis</h2> | |
| <p>Orionus AI performs multimodal behavioral intelligence fusion using:</p> | |
| <div style="display:flex; justify-content:center; gap:1.5rem; margin-top:1.5rem; flex-wrap:wrap;"> | |
| <div style="background:#0d1117; border:1px solid #2E7D32; border-radius:10px; padding:1rem; width:150px;"> | |
| <div style="color:#00E676; font-size:1.5rem;">28</div> | |
| <div style="color:#81C784; font-size:0.8rem;">GoEmotions Labels</div> | |
| </div> | |
| <div style="background:#0d1117; border:1px solid #2E7D32; border-radius:10px; padding:1rem; width:150px;"> | |
| <div style="color:#00E676; font-size:1.5rem;">10</div> | |
| <div style="color:#81C784; font-size:0.8rem;">Intent Categories</div> | |
| </div> | |
| <div style="background:#0d1117; border:1px solid #2E7D32; border-radius:10px; padding:1rem; width:150px;"> | |
| <div style="color:#00E676; font-size:1.5rem;">5</div> | |
| <div style="color:#81C784; font-size:0.8rem;">Threat Levels</div> | |
| </div> | |
| <div style="background:#0d1117; border:1px solid #2E7D32; border-radius:10px; padding:1rem; width:150px;"> | |
| <div style="color:#00E676; font-size:1.5rem;">4</div> | |
| <div style="color:#81C784; font-size:0.8rem;">Modalities Fused</div> | |
| </div> | |
| <div style="background:#0d1117; border:1px solid #2E7D32; border-radius:10px; padding:1rem; width:150px;"> | |
| <div style="color:#00E676; font-size:1.5rem;">16+</div> | |
| <div style="color:#81C784; font-size:0.8rem;">Fuzzy Rules</div> | |
| </div> | |
| </div> | |
| </div>""", unsafe_allow_html=True) | |
| # Footer | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style="text-align:center; color:#334155; font-size:0.8rem; padding:0.5rem;"> | |
| <strong style="color:#2E7D32;">ORIONUS AI</strong> — Behavioral Intelligence for Security<br/> | |
| Mamdani Fuzzy Inference • Multimodal Fusion • Real-Time Threat Assessment<br/> | |
| <span style="color:#1e293b;">Demo Mode — Text simulates multimodal analysis. Production uses live camera, microphone, and pose tracking.</span> | |
| </div>""", unsafe_allow_html=True) | |
| # Auto-refresh timer | |
| time.sleep(0.1) | |
| st.rerun() | |
| # ============================================================================ | |
| # MAIN ROUTING | |
| # ============================================================================ | |
| def main(): | |
| step = st.session_state["auth_step"] | |
| if step == "active" and session_expired(st.session_state["session_start"]): | |
| st.session_state["auth_step"] = "ended" | |
| step = "ended" | |
| if step == "email": _show_landing() | |
| elif step == "verify": _show_verify() | |
| elif step == "active": _run_demo() | |
| elif step == "ended": _show_trial_ended() | |
| else: st.session_state["auth_step"] = "email"; st.rerun() | |
| if __name__ == "__main__": | |
| main() | |