themehmi commited on
Commit
c3383a4
·
verified ·
1 Parent(s): 238b1b1

Upload 9 files

Browse files
Files changed (9) hide show
  1. Dockerfile +89 -0
  2. Modelfile +30 -0
  3. README_DOCKER.md +127 -0
  4. audio/calm_beep.wav +0 -0
  5. audio/urgent_beep.wav +0 -0
  6. main.py +966 -0
  7. requirements.txt +10 -0
  8. static/js/app.js +288 -0
  9. templates/index.html +665 -0
Dockerfile ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile for Safe Driving Assistant
2
+
3
+ # This container sets up a fully functional, headless-capable runtime environment
4
+ # for the AI Safe Driving Assistant. It includes all necessary C/C++ build
5
+ # dependencies for compiling dlib, PortAudio bindings (PyAudio), and sound/video
6
+ # codecs, along with Xvfb (Virtual Framebuffer) to prevent OpenCV GUI crashes.
7
+
8
+ # Use official lightweight Python base image
9
+ FROM python:3.10-slim
10
+
11
+ # Set environment variables to optimize Python performance and docker execution
12
+ ENV PYTHONUNBUFFERED=1 \
13
+ PYTHONDONTWRITEBYTECODE=1 \
14
+ DEBIAN_FRONTEND=noninteractive \
15
+ PULSE_SERVER=unix:/tmp/pulseaudio.socket \
16
+ FLASK_HOST=0.0.0.0
17
+
18
+ # Set the working directory inside the container
19
+ WORKDIR /app
20
+
21
+ # Install system dependencies (build tools, audio, video, espeak, git, Xvfb)
22
+ RUN apt-get update && apt-get install -y --no-install-recommends \
23
+ # Standard compilation and build utilities (required for compiling dlib and pyaudio) \
24
+ build-essential \
25
+ cmake \
26
+ g++ \
27
+ git \
28
+ gfortran \
29
+ pkg-config \
30
+ # Math libraries for optimization \
31
+ libopenblas-dev \
32
+ liblapack-dev \
33
+ # OpenCV / UI system dependencies \
34
+ libgl1-mesa-glx \
35
+ libglib2.0-0 \
36
+ libsm6 \
37
+ libxext6 \
38
+ libxrender-dev \
39
+ libx11-dev \
40
+ # PortAudio & Audio libraries (required for PyAudio & SpeechRecognition) \
41
+ portaudio19-dev \
42
+ libasound2-dev \
43
+ alsa-utils \
44
+ pulseaudio \
45
+ # Pygame graphics/audio backends \
46
+ libsdl2-dev \
47
+ libsdl2-image-dev \
48
+ libsdl2-mixer-dev \
49
+ libsdl2-ttf-dev \
50
+ # Soundfile dependency \
51
+ libsndfile1 \
52
+ # Text-to-Speech (espeak) backend for pyttsx3 Linux fallback \
53
+ espeak \
54
+ libespeak-dev \
55
+ # Virtual Framebuffer (Xvfb) for headless OpenCV rendering (cv2.imshow) \
56
+ xvfb \
57
+ x11-apps \
58
+ # Cleanup to minimize image size \
59
+ && rm -rf /var/lib/apt/lists/*
60
+
61
+ # Upgrade pip, setuptools, and wheel
62
+ RUN pip install --no-cache-dir --upgrade pip setuptools wheel
63
+
64
+ # Copy requirements.txt first to leverage Docker's build cache
65
+ COPY requirements.txt .
66
+
67
+ # Install dependencies (dlib, face_recognition, pyaudio, and scipy compilation can take a while)
68
+ RUN pip install --no-cache-dir -r requirements.txt
69
+
70
+ # Copy the rest of the application files
71
+ COPY . .
72
+
73
+ # Expose Flask web HUD port (5000 by default in main.py)
74
+ EXPOSE 5000
75
+
76
+ # Create an entrypoint script to automatically spin up Xvfb and start the application
77
+ RUN echo '#!/bin/bash\n\
78
+ if [ -z "$DISPLAY" ]; then\n\
79
+ echo "[Docker-Entrypoint] No DISPLAY detected. Starting Virtual Framebuffer (Xvfb)..."\n\
80
+ Xvfb :99 -screen 0 640x480x24 &\n\
81
+ export DISPLAY=:99\n\
82
+ sleep 1\n\
83
+ fi\n\
84
+ echo "[Docker-Entrypoint] Starting Safe Driving Assistant..."\n\
85
+ exec python main.py\n\
86
+ ' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh
87
+
88
+ # Use the virtual display entrypoint script
89
+ ENTRYPOINT ["/app/entrypoint.sh"]
Modelfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM qwen2.5:3b
2
+
3
+ # Set parameters for rich conversational abilities
4
+ PARAMETER temperature 0.6
5
+ PARAMETER top_p 0.9
6
+
7
+ # Set system instructions
8
+ SYSTEM """You are 'DriveSafe', a local safe driving AI chatbot and co-pilot. Your primary goal is to engage with the driver, answer their questions, and keep them awake, alert, and safe.
9
+ Because the driver is currently driving, please follow these guidelines:
10
+ 1. Speak in a warm, helpful, natural, and conversational tone, like a real companion.
11
+ 2. Fully and intelligently answer the driver's questions. Do not truncate your answers artificially or enforce rigid word limits.
12
+ 3. Keep your answers clear, spoken-friendly, and conclude each response with a brief safety reminder or warm driving encouragement (e.g. 'Keep your eyes on the road!', 'Drive safely!', 'Stay focused!').
13
+
14
+ SPECIAL ACTION TAGS FOR BACKEND ROUTING:
15
+ 1. MUSIC PLAYBACK: If the driver asks you to play any song, music, genre, or artist (e.g., 'play paint it black' or 'play some rock beats' or 'play taylor swift'), your response MUST be EXACTLY '[PLAY] ' followed by the name of the song, music, genre, or artist they requested. Do NOT add any other words, markdown, or chat text.
16
+ Examples:
17
+ User: play paint it black -> Response: [PLAY] paint it black
18
+ User: play some rock music -> Response: [PLAY] rock music
19
+ User: play coldplay -> Response: [PLAY] coldplay
20
+
21
+ 2. MUSIC STOP: If the driver asks you to stop, pause, mute, or turn off the music/song (e.g., 'stop the music' or 'pause the song' or 'turn off sound'), your response MUST be EXACTLY '[STOP] ' followed by a brief confirmation.
22
+ Examples:
23
+ User: stop the music -> Response: [STOP] Stopping the music. Focus on driving!
24
+ User: pause the song -> Response: [STOP] Paused. Keep your eyes on the road.
25
+
26
+ 3. WARNING RESET: If the driver tells you they are awake, alert, or asks you to reset/clear the warnings, your response MUST start with '[RESET] ' followed by a brief encouraging confirmation.
27
+ Example:
28
+ User: reset warnings -> Response: [RESET] Warnings cleared. Keep your focus on the road.
29
+
30
+ For general questions, stories, jokes, or safe-driving talk, reply as a highly conversational chatbot companion."""
README_DOCKER.md ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚗 Safe Driving Assistant — Docker Integration Guide
2
+
3
+ Welcome to the premium Docker integration guide for your **Safe Driving Assistant**. This guide provides step-by-step instructions on how to package, configure, build, and run the assistant inside a fully isolated, headless-capable Docker container.
4
+
5
+ The containerized environment includes all C/C++ libraries, graphics backends, sound drivers, and speech synthesizers, pre-configured with a virtual framebuffer (**Xvfb**) to prevent OpenCV graphical crashes.
6
+
7
+ ---
8
+
9
+ ## 🌟 Highlights of the Docker Configuration
10
+
11
+ * **Zero-Dependency Setup:** No need to install `dlib`, `cmake`, `opencv`, or `pyaudio` manually on your host machine.
12
+ * **Virtual Framebuffer (Xvfb):** Headless-safe. If no display is detected, it automatically spawns a virtual X11 server so that `cv2.imshow` calls do not crash.
13
+ * **Dynamic Network Routing:** Pre-configured to easily bridge to your local **Ollama** SLM instance.
14
+ * **Unified Sound Backend:** Installs PulseAudio, ALSA, and `espeak` dependencies required for speech synthesis and playback.
15
+ * **Environment-Driven Configuration:** Highly customizable via environment variables (`FLASK_HOST`, `CAMERA_ID`, `OLLAMA_API_URL`, etc.).
16
+
17
+ ---
18
+
19
+ ## 🛠️ Step 1: Build the Docker Image
20
+
21
+ Open your terminal in the project directory (`Safe Driving Assistant`) and run the following command to build your custom co-pilot image:
22
+
23
+ ```bash
24
+ docker build -t safe-driving-assistant:latest .
25
+ ```
26
+
27
+ *This compilation will take several minutes during the first run because it builds `dlib` (facial recognition) and compiles native C extensions.*
28
+
29
+ ---
30
+
31
+ ## 🚀 Step 2: Run the Docker Container
32
+
33
+ Depending on your host Operating System and hardware setup, select one of the premium run configurations below:
34
+
35
+ ### Option A: Standard Headless / Web-HUD Only (Recommended)
36
+ This runs the assistant, serves the live futuristic Web-HUD on port `5000`, and bridges network queries to your host's local Ollama service.
37
+
38
+ ```bash
39
+ docker run -d \
40
+ --name driving_assistant \
41
+ --add-host=host.docker.internal:host-gateway \
42
+ -e OLLAMA_API_URL="http://host.docker.internal:11434/api/generate" \
43
+ -p 5000:5000 \
44
+ safe-driving-assistant:latest
45
+ ```
46
+
47
+ * **Web Dashboard:** Once started, open your browser and navigate to **`http://localhost:5000`** to view the live dashboard!
48
+ * **How it works:** The container runs headlessly. Face recognition and eye-aspect ratio tracking are active, and the live stream is pushed directly to the dashboard.
49
+
50
+ ---
51
+
52
+ ### Option B: Linux Run with Full Hardware Passthrough (Camera & Audio)
53
+ If you are running native Linux and want the container to access your physical USB webcam and audio hardware directly, run:
54
+
55
+ ```bash
56
+ docker run -it \
57
+ --name driving_assistant \
58
+ --device /dev/video0:/dev/video0 \
59
+ --device /dev/snd:/dev/snd \
60
+ --add-host=host.docker.internal:host-gateway \
61
+ -e CAMERA_ID=0 \
62
+ -e OLLAMA_API_URL="http://host.docker.internal:11434/api/generate" \
63
+ -p 5000:5000 \
64
+ safe-driving-assistant:latest
65
+ ```
66
+
67
+ * **`--device /dev/video0`:** Mounts your primary webcam inside the container.
68
+ * **`--device /dev/snd`:** Exposes speaker and mic controls.
69
+
70
+ ---
71
+
72
+ ### Option C: Windows Host (WSL2) with Web USB Webcam Passthrough
73
+ If you are using WSL2 on Windows and want to feed your physical webcam into the Docker container:
74
+
75
+ 1. Bind your USB camera to WSL2 using [usbipd-win](https://github.com/dorssel/usbipd-win):
76
+ ```powershell
77
+ usbipd list
78
+ usbipd bind --busid <BUSID>
79
+ usbipd attach --wsl --busid <BUSID>
80
+ ```
81
+ 2. Start the container with device mapping:
82
+ ```bash
83
+ docker run -it \
84
+ --name driving_assistant \
85
+ --device /dev/video0:/dev/video0 \
86
+ --add-host=host.docker.internal:host-gateway \
87
+ -e CAMERA_ID=0 \
88
+ -e OLLAMA_API_URL="http://host.docker.internal:11434/api/generate" \
89
+ -p 5000:5000 \
90
+ safe-driving-assistant:latest
91
+ ```
92
+
93
+ ---
94
+
95
+ ## ⚙️ Environment Variables Customization
96
+
97
+ You can dynamically tune your assistant container at runtime by passing `-e KEY=VALUE` parameters to `docker run`:
98
+
99
+ | Variable Name | Default Value | Description |
100
+ | :--- | :--- | :--- |
101
+ | `FLASK_HOST` | `0.0.0.0` | Binding IP address for Flask web dashboard. |
102
+ | `FLASK_PORT` | `5000` | Port on which the web HUD dashboard will be served. |
103
+ | `CAMERA_ID` | `0` | Camera index. |
104
+ | `OLLAMA_API_URL` | `http://localhost:11434/api/generate` | The API endpoint for the Ollama voice co-pilot. Use `http://host.docker.internal:11434/api/generate` for bridging to host. |
105
+ | `OLLAMA_MODEL` | `drivesafe` | Name of the custom SLM model configured inside Ollama. |
106
+ | `FRAME_WIDTH` | `640` | Video frame capture width. |
107
+ | `FRAME_HEIGHT` | `480` | Video frame capture height. |
108
+
109
+ ---
110
+
111
+ ## 🧼 Housekeeping & Diagnostics
112
+
113
+ ### View Real-Time Logs
114
+ To see the system warnings, detected EAR, and conversational chatbot interactions:
115
+ ```bash
116
+ docker logs -f driving_assistant
117
+ ```
118
+
119
+ ### Stop & Remove Container
120
+ To stop and clean up the assistant container instance:
121
+ ```bash
122
+ docker stop driving_assistant
123
+ docker rm driving_assistant
124
+ ```
125
+
126
+ ---
127
+ **Safe Driving Assistant** — *Keep your eyes on the road, your hands on the wheel, and drive safely!* 🚗💨
audio/calm_beep.wav ADDED
Binary file (35.3 kB). View file
 
audio/urgent_beep.wav ADDED
Binary file (34.4 kB). View file
 
main.py ADDED
@@ -0,0 +1,966 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ main.py — Self-Contained, Fully Integrated Safe Driving Assistant
3
+ Consolidates all system configuration, custom non-blocking sound synthesizer, dlib Eye Landmark processor,
4
+ Ollama SLM action voice-assistant parser, Flask SSE Telemetry Dashboard, and main drowsiness timer logic
5
+ into one unified, ultra-premium script.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import time
11
+ import json
12
+ import queue
13
+ import collections
14
+ import threading
15
+ import logging
16
+ import urllib.request
17
+ import urllib.parse
18
+ import webbrowser
19
+ import re
20
+
21
+ import numpy as np
22
+ import scipy.io.wavfile as wavfile
23
+ import pygame
24
+ import pyttsx3
25
+ import cv2
26
+ import face_recognition
27
+ import speech_recognition as sr
28
+ from flask import Flask, render_template, Response, jsonify, request
29
+
30
+
31
+ # 1. DriveSafe Assistant — Configuration Settings
32
+ CAMERA_ID = int(os.environ.get("CAMERA_ID", 0)) # Index of the webcam (usually 0)
33
+ FRAME_WIDTH = int(os.environ.get("FRAME_WIDTH", 640)) # Video capture width
34
+ FRAME_HEIGHT = int(os.environ.get("FRAME_HEIGHT", 480)) # Video capture height
35
+
36
+ # Drowsiness Detection Thresholds
37
+ EAR_THRESHOLD = 0.23 # Eye Aspect Ratio below this indicates closed eyes
38
+ EAR_CONSEC_FRAMES = 3 # Consecutive frames below threshold to trigger eye-closed timer
39
+
40
+ # Alert Severity Levels (Durations in Seconds)
41
+ ALERT_LEVEL1_MIN = 3.0 # Min duration of closed eyes for Level 1 ("stay focused")
42
+ ALERT_LEVEL1_MAX = 5.0 # Max duration of closed eyes for Level 1
43
+ ALERT_LEVEL2_MIN = 5.0 # Closed eyes duration for Level 2 ("wake up stay focus on road" louder)
44
+
45
+ # Frequent Drowsiness Pattern Tracking
46
+ FREQUENT_DROWSY_WINDOW = 60.0 # Sliding window (seconds) to monitor drowsiness event frequency
47
+ FREQUENT_DROWSY_LIMIT = 2 # Max drowsiness warnings allowed in window before advising a break (Level 3)
48
+
49
+ # Voice Assistant & SLM Settings
50
+ OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "drivesafe") # Our custom local Ollama model
51
+ OLLAMA_API_URL = os.environ.get("OLLAMA_API_URL", "http://localhost:11434/api/generate") # Ollama generation endpoint
52
+ SPEECH_RECOGNITION_TIMEOUT = 10 # Timeout for speech recognizer
53
+ WAKE_WORD = "assistant" # Wake word for general conversations
54
+
55
+ # Web HUD Dashboard Server
56
+ FLASK_HOST = os.environ.get("FLASK_HOST", "127.0.0.1")
57
+ FLASK_PORT = int(os.environ.get("FLASK_PORT", 5000))
58
+
59
+ # High Energy Music Links
60
+ ENERGETIC_MUSIC_URL = "https://music.youtube.com/playlist?list=PLYBSqm--lNVt1H63PlRvigxvPU_unQe8m"
61
+
62
+ # 2. Flask Web HUD Server & Shared DashboardState
63
+
64
+ # Initialize Flask app
65
+ app = Flask(__name__, template_folder='templates', static_folder='static')
66
+
67
+ # Thread-safe global state for Flask-main loop communication
68
+ class DashboardState:
69
+ def __init__(self):
70
+ self.lock = threading.Lock()
71
+ self.latest_frame = None
72
+ self.ear = 0.0
73
+ self.state = "NORMAL"
74
+ self.drowsiness_count = 0
75
+ self.fps = 0
76
+ self.alert_message = ""
77
+ self.chat_history = []
78
+ self.detection_active = True
79
+
80
+ dashboard_state = DashboardState()
81
+
82
+ @app.route('/')
83
+ def index():
84
+ """Renders the futuristic cyberpunk HUD dashboard."""
85
+ return render_template('index.html')
86
+
87
+ def gen_video_feed():
88
+ """Generator function that yields JPEG frames for the live camera stream."""
89
+ while True:
90
+ with dashboard_state.lock:
91
+ if dashboard_state.latest_frame is None:
92
+ frame_to_send = None
93
+ else:
94
+ frame_to_send = dashboard_state.latest_frame.copy()
95
+
96
+ if frame_to_send is not None:
97
+ # Encode BGR OpenCV frame to standard JPEG
98
+ ret, jpeg = cv2.imencode('.jpg', frame_to_send)
99
+ if ret:
100
+ yield (b'--frame\r\n'
101
+ b'Content-Type: image/jpeg\r\n\r\n' + jpeg.tobytes() + b'\r\n\r\n')
102
+
103
+ # Frame-rate limiter (30 FPS max for the web stream to keep networking lightweight)
104
+ time.sleep(1.0 / 30.0)
105
+
106
+ @app.route('/video_feed')
107
+ def video_feed():
108
+ """Serves the real-time annotated video stream inside standard HTML img tags."""
109
+ return Response(gen_video_feed(),
110
+ mimetype='multipart/x-mixed-replace; boundary=frame')
111
+
112
+ def gen_telemetry_stream():
113
+ """Streams real-time system diagnostics to the browser via HTML5 Server-Sent Events (SSE)."""
114
+ last_sent_time = 0
115
+ while True:
116
+ # Throttle telemetry updates slightly (e.g. 15 updates/second) to keep browser rendering butter-smooth
117
+ current_time = time.time()
118
+ if current_time - last_sent_time >= 0.06:
119
+ with dashboard_state.lock:
120
+ data = {
121
+ "ear": round(dashboard_state.ear, 3),
122
+ "state": dashboard_state.state,
123
+ "drowsiness_count": dashboard_state.drowsiness_count,
124
+ "fps": dashboard_state.fps,
125
+ "alert_message": dashboard_state.alert_message,
126
+ "chat_history": dashboard_state.chat_history,
127
+ "detection_active": dashboard_state.detection_active
128
+ }
129
+
130
+ # SSE data format: "data: <json>\n\n"
131
+ yield f"data: {json.dumps(data)}\n\n"
132
+ last_sent_time = current_time
133
+
134
+ time.sleep(0.01)
135
+
136
+ @app.route('/telemetry')
137
+ def telemetry():
138
+ """SSE endpoint for high-speed diagnostic telemetry streaming."""
139
+ return Response(gen_telemetry_stream(), mimetype='text/event-stream')
140
+
141
+ # Interactive Control APIs
142
+
143
+ @app.route('/api/toggle_detection', methods=['POST'])
144
+ def toggle_detection():
145
+ """Enables or disables active face and eye tracking."""
146
+ with dashboard_state.lock:
147
+ dashboard_state.detection_active = not dashboard_state.detection_active
148
+ status = dashboard_state.detection_active
149
+ return jsonify({"status": "success", "detection_active": status})
150
+
151
+ @app.route('/api/reset', methods=['POST'])
152
+ def api_reset():
153
+ """Triggers a complete system reset from the dashboard panel."""
154
+ if hasattr(app, 'reset_callback') and app.reset_callback:
155
+ app.reset_callback()
156
+ return jsonify({"status": "success", "message": "System alerts and warning log reset."})
157
+ return jsonify({"status": "error", "message": "Reset callback not configured."})
158
+
159
+ @app.route('/api/trigger_music', methods=['POST'])
160
+ def api_trigger_music():
161
+ """Manually triggers the energetic song from the dashboard panel."""
162
+ if hasattr(app, 'play_music_callback') and app.play_music_callback:
163
+ app.play_music_callback()
164
+ return jsonify({"status": "success", "message": "Playing energetic synthwave music!"})
165
+ return jsonify({"status": "error", "message": "Music callback not configured."})
166
+
167
+ def start_server_async():
168
+ """Runs the Flask development server on a dedicated background thread."""
169
+ # Suppress Flask development server startup messages to keep terminal clean
170
+ log = logging.getLogger('werkzeug')
171
+ log.setLevel(logging.ERROR)
172
+
173
+ server_thread = threading.Thread(
174
+ target=lambda: app.run(host=FLASK_HOST, port=FLASK_PORT, debug=False, use_reloader=False),
175
+ daemon=True
176
+ )
177
+ server_thread.start()
178
+ print(f"[Flask Server] Running in background at http://{FLASK_HOST}:{FLASK_PORT}")
179
+
180
+ # 3. AlertManager — Programmatic Sound Synthesis & Multi-Threaded Audio
181
+ class AlertManager:
182
+ def __init__(self):
183
+ # Ensure directories exist
184
+ os.makedirs("audio", exist_ok=True)
185
+
186
+ # Programmatically synthesize our warning and chime audio files
187
+ self._synthesize_audio_assets()
188
+
189
+ # Initialize Pygame Mixer for non-blocking SFX playback
190
+ pygame.mixer.init()
191
+
192
+ # Audio file paths
193
+ self.calm_beep_path = os.path.join("audio", "calm_beep.wav")
194
+ self.urgent_beep_path = os.path.join("audio", "urgent_beep.wav")
195
+
196
+ # Thread-safe speech queue & worker setup
197
+ self.speech_queue = queue.Queue()
198
+ self.is_speaking = False
199
+ self.speech_thread = threading.Thread(target=self._speech_worker, daemon=True)
200
+ self.speech_thread.start()
201
+
202
+ def _synthesize_audio_assets(self):
203
+ """Synthesizes custom chime and alert WAV files using numpy and scipy."""
204
+ sample_rate = 44100
205
+
206
+ # 1. Calm chime (gentle 550Hz sine wave decaying)
207
+ duration = 0.4
208
+ t = np.linspace(0, duration, int(sample_rate * duration), False)
209
+ envelope = np.exp(-5 * t) # decay envelope
210
+ tone = np.sin(2 * np.pi * 550 * t) * envelope
211
+ calm_data = (tone * 20000).astype(np.int16)
212
+ wavfile.write(os.path.join("audio", "calm_beep.wav"), sample_rate, calm_data)
213
+
214
+ # 2. Urgent pulsing beeps (three rapid 1200Hz pulse bursts)
215
+ urgent_data = []
216
+ burst_duration = 0.08
217
+ gap_duration = 0.05
218
+ t_burst = np.linspace(0, burst_duration, int(sample_rate * burst_duration), False)
219
+ burst = np.sin(2 * np.pi * 1200 * t_burst) * 32000
220
+ gap = np.zeros(int(sample_rate * gap_duration))
221
+
222
+ # Combine three bursts
223
+ for _ in range(3):
224
+ urgent_data.extend(burst)
225
+ urgent_data.extend(gap)
226
+
227
+ urgent_np = np.array(urgent_data, dtype=np.int16)
228
+ wavfile.write(os.path.join("audio", "urgent_beep.wav"), sample_rate, urgent_np)
229
+
230
+ def _speech_worker(self):
231
+ """Background worker thread that serializes all speech requests using native PowerShell synthesis to prevent COM/threading locks."""
232
+ print("[AlertManager] Speech worker thread active.")
233
+ import subprocess
234
+ while True:
235
+ try:
236
+ # Blocks until an item is available
237
+ text, volume, rate = self.speech_queue.get()
238
+
239
+ self.is_speaking = True
240
+
241
+ # Escape single quotes and backslashes for PowerShell safety
242
+ escaped_text = text.replace("\\", "\\\\").replace("'", "''")
243
+
244
+ # Map rate (150-190) to PowerShell Rate (-10 to 10)
245
+ ps_rate = 0
246
+ if rate > 180:
247
+ ps_rate = 2
248
+ elif rate < 150:
249
+ ps_rate = -2
250
+
251
+ # Map volume (0.0 to 1.0) to PowerShell Volume (0 to 100)
252
+ ps_volume = int(volume * 100)
253
+
254
+ ps_command = (
255
+ f"Add-Type -AssemblyName System.Speech; "
256
+ f"$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; "
257
+ f"$speak.Rate = {ps_rate}; "
258
+ f"$speak.Volume = {ps_volume}; "
259
+ f"$speak.Speak('{escaped_text}')"
260
+ )
261
+
262
+ # Run synchronously inside the worker thread to maintain sequential speech
263
+ subprocess.run(
264
+ ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_command],
265
+ stdout=subprocess.DEVNULL,
266
+ stderr=subprocess.DEVNULL
267
+ )
268
+
269
+ self.is_speaking = False
270
+ self.speech_queue.task_done()
271
+ except Exception as e:
272
+ print(f"[AlertManager] Speech worker exception: {e}")
273
+ self.is_speaking = False
274
+ time.sleep(0.5)
275
+
276
+ def speak(self, text, volume=0.8, rate=170):
277
+ """Enqueues a text string to be spoken in the background thread."""
278
+ self.speech_queue.put((text, volume, rate))
279
+
280
+ def trigger_level1(self):
281
+ """Level 1 Alert (3-5s closed): Soft chime, then calm voice."""
282
+ print("[AlertManager] Triggering Level 1 Alert: Calm Stay Focused")
283
+ pygame.mixer.Sound(self.calm_beep_path).play()
284
+ self.speak("Stay focused on the road", volume=0.7, rate=160)
285
+
286
+ def trigger_level2(self):
287
+ """Level 2 Alert (>5s closed): Loud siren beep, then loud voice."""
288
+ print("[AlertManager] Triggering Level 2 Alert: Loud WAKE UP!")
289
+ pygame.mixer.Sound(self.urgent_beep_path).play()
290
+ self.speak("Wake up! Stay focused on the road!", volume=1.0, rate=190)
291
+
292
+ def trigger_level3_advisory(self):
293
+ """Level 3 Alert (Frequent drowsiness): Ask to take a rest break on the side."""
294
+ print("[AlertManager] Triggering Level 3 Alert: Rest break advisory")
295
+ pygame.mixer.Sound(self.urgent_beep_path).play()
296
+ self.speak("You are getting drowsy frequently. Please pull over on the side and take a rest.", volume=0.9, rate=170)
297
+
298
+ def ask_energetic_song(self):
299
+ """Ask the driver if they want to listen to an energetic song."""
300
+ print("[AlertManager] Querying driver for energetic song")
301
+ self.speak("Alright. Would you like to listen to an energetic song to help you stay awake?", volume=0.85, rate=170)
302
+
303
+ def play_energetic_music(self):
304
+ """Announce and play energetic music."""
305
+ print("[AlertManager] Playing energetic music")
306
+ self.speak("Playing some high energy synthwave beats. Turn it up and stay alert!", volume=0.9, rate=170)
307
+ webbrowser.open(ENERGETIC_MUSIC_URL)
308
+
309
+
310
+ # 4. EyeDetector — 2x Downsampling dlib Eye Landmark Processor with Fallback
311
+
312
+ class EyeDetector:
313
+ def __init__(self):
314
+ self.scale_factor = 2 # Resizes to 50% width/height (4x speedup)
315
+ self.last_warning_time = 0
316
+
317
+ def _calculate_ear(self, eye_points):
318
+ """Calculates the Eye Aspect Ratio (EAR) for a single eye list of 6 points."""
319
+ p1 = np.array(eye_points[0])
320
+ p2 = np.array(eye_points[1])
321
+ p3 = np.array(eye_points[2])
322
+ p4 = np.array(eye_points[3])
323
+ p5 = np.array(eye_points[4])
324
+ p6 = np.array(eye_points[5])
325
+
326
+ vertical1 = np.linalg.norm(p2 - p6)
327
+ vertical2 = np.linalg.norm(p3 - p5)
328
+ horizontal = np.linalg.norm(p1 - p4)
329
+
330
+ if horizontal == 0:
331
+ return 0.0
332
+
333
+ return (vertical1 + vertical2) / (2.0 * horizontal)
334
+
335
+ def process_frame(self, frame):
336
+ """Processes a single BGR camera frame with a robust full-res fallback."""
337
+ height, width, _ = frame.shape
338
+ debug_frame = frame.copy()
339
+
340
+ # 1. Downsample the frame for high-speed face detection
341
+ small_frame = cv2.resize(frame, (0, 0), fx=1.0/self.scale_factor, fy=1.0/self.scale_factor)
342
+ rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB)
343
+
344
+ # 2. Try fast downscaled detection first
345
+ face_landmarks_list = face_recognition.face_landmarks(rgb_small_frame)
346
+ current_scale = self.scale_factor
347
+
348
+ # 3. Fallback: If no face found in small frame, try the full-resolution frame!
349
+ if not face_landmarks_list:
350
+ rgb_full_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
351
+ face_landmarks_list = face_recognition.face_landmarks(rgb_full_frame)
352
+ current_scale = 1
353
+
354
+ avg_ear = None
355
+ landmarks_found = None
356
+
357
+ if face_landmarks_list:
358
+ face_landmarks = face_landmarks_list[0]
359
+ landmarks_found = face_landmarks
360
+
361
+ left_eye_raw = face_landmarks.get('left_eye', [])
362
+ right_eye_raw = face_landmarks.get('right_eye', [])
363
+
364
+ if len(left_eye_raw) == 6 and len(right_eye_raw) == 6:
365
+ # Scale coordinates back up to original frame dimensions
366
+ left_eye = [(int(x * current_scale), int(y * current_scale)) for (x, y) in left_eye_raw]
367
+ right_eye = [(int(x * current_scale), int(y * current_scale)) for (x, y) in right_eye_raw]
368
+
369
+ left_ear = self._calculate_ear(left_eye)
370
+ right_ear = self._calculate_ear(right_eye)
371
+ avg_ear = (left_ear + right_ear) / 2.0
372
+
373
+ # Draw the glowing tech HUD outlines
374
+ self._draw_eye_hud(debug_frame, left_eye, right_eye, avg_ear)
375
+ else:
376
+ # No face detected! Print throttled console warning and show overlay text
377
+ current_time = time.time()
378
+ if current_time - self.last_warning_time > 2.5:
379
+ print("[EyeDetector] WARNING: No face detected in camera stream! Adjust position or lighting.")
380
+ self.last_warning_time = current_time
381
+
382
+ # Draw warning overlay on dashboard feed
383
+ cv2.putText(debug_frame, "NO FACE DETECTED", (width // 2 - 120, height // 2),
384
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
385
+ cv2.putText(debug_frame, "Adjust Camera / Lighting", (width // 2 - 140, height // 2 + 30),
386
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1)
387
+
388
+ return avg_ear, landmarks_found, debug_frame
389
+
390
+ def _draw_eye_hud(self, frame, left_eye, right_eye, ear):
391
+ """Draws glowing HUD tech contours on eyes and shows EAR readout."""
392
+ if ear is not None and ear < EAR_THRESHOLD:
393
+ color = (0, 0, 255) # Red: Closed/Drowsy
394
+ thickness = 2
395
+ else:
396
+ color = (0, 255, 0) # Green: Open/Safe
397
+ thickness = 1
398
+
399
+ left_pts = np.array(left_eye, np.int32)
400
+ cv2.polylines(frame, [left_pts], True, color, thickness)
401
+
402
+ right_pts = np.array(right_eye, np.int32)
403
+ cv2.polylines(frame, [right_pts], True, color, thickness)
404
+
405
+ for (x, y) in left_eye + right_eye:
406
+ cv2.circle(frame, (x, y), 2, (255, 255, 0), -1)
407
+
408
+ if ear is not None:
409
+ text = f"EAR: {ear:.2f}"
410
+ cv2.putText(frame, text, (30, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
411
+
412
+
413
+ # 5. VoiceAssistant — Dynamic Action Router & Background Speech Recognition
414
+
415
+ class VoiceAssistant:
416
+ def __init__(self, alert_manager, state_callbacks):
417
+ self.alert_manager = alert_manager
418
+ self.callbacks = state_callbacks
419
+
420
+ self.recognizer = sr.Recognizer()
421
+ self.recognizer.energy_threshold = 4000
422
+ self.recognizer.dynamic_energy_threshold = True
423
+
424
+ self.running = True
425
+ self.thread = threading.Thread(target=self._assistant_loop, daemon=True)
426
+ self.thread.start()
427
+
428
+ def query_ollama_slm(self, prompt):
429
+ """Sends user transcription to the local custom drivesafe SLM on Ollama."""
430
+ payload = {
431
+ "model": OLLAMA_MODEL,
432
+ "prompt": prompt,
433
+ "stream": False
434
+ }
435
+ headers = {"Content-Type": "application/json"}
436
+
437
+ try:
438
+ req = urllib.request.Request(
439
+ OLLAMA_API_URL,
440
+ data=json.dumps(payload).encode("utf-8"),
441
+ headers=headers,
442
+ method="POST"
443
+ )
444
+ # Use a 10-second timeout to accommodate initial Ollama cold start weight loading
445
+ with urllib.request.urlopen(req, timeout=10) as response:
446
+ res_data = json.loads(response.read().decode("utf-8"))
447
+ reply = res_data.get("response", "").strip()
448
+ # Clean up any quotes or markdown from the SLM
449
+ reply = reply.replace('"', '').replace('*', '').strip()
450
+ return reply
451
+ except Exception as e:
452
+ print(f"[Ollama SLM] Error or timeout querying local model: {e}")
453
+ lower_prompt = prompt.lower()
454
+ if "hello" in lower_prompt or "hi" in lower_prompt:
455
+ return "Hello! I am here. Eyes on the road, friend."
456
+ elif "joke" in lower_prompt:
457
+ return "Why did the scarecrow win an award? Because he was outstanding in his field. Stay alert!"
458
+ else:
459
+ return "Understood. Keep driving safely, stay focused on the road."
460
+
461
+ def _assistant_loop(self):
462
+ """Background continuous microphone listening loop."""
463
+ print("[VoiceAssistant] Speech recognizer thread started.")
464
+
465
+ try:
466
+ mic = sr.Microphone()
467
+ except Exception as e:
468
+ print(f"[VoiceAssistant] Error accessing microphone: {e}. Voice controls disabled.")
469
+ return
470
+
471
+ with mic as source:
472
+ print("[VoiceAssistant] Calibrating microphone for driving background noise...")
473
+ self.recognizer.adjust_for_ambient_noise(source, duration=2)
474
+ print("[VoiceAssistant] Calibration complete. Ready for voice interaction.")
475
+
476
+ while self.running:
477
+ if self.alert_manager.is_speaking:
478
+ time.sleep(0.3)
479
+ continue
480
+
481
+ try:
482
+ audio = self.recognizer.listen(source, timeout=1.5, phrase_time_limit=4.0)
483
+ except sr.WaitTimeoutError:
484
+ continue
485
+ except Exception as e:
486
+ print(f"[VoiceAssistant] Microphone capture error: {e}")
487
+ time.sleep(0.5)
488
+ continue
489
+
490
+ if self.alert_manager.is_speaking:
491
+ continue
492
+
493
+ # Run speech recognition in a separate thread to keep mic pipeline responsive
494
+ threading.Thread(target=self._process_audio, args=(audio,), daemon=True).start()
495
+
496
+ def _process_audio(self, audio):
497
+ """Recognizes speech and routes commands dynamically."""
498
+ try:
499
+ text = self.recognizer.recognize_google(audio)
500
+ print(f"[Driver Heard] {text}")
501
+ except sr.UnknownValueError:
502
+ return
503
+ except sr.RequestError:
504
+ try:
505
+ print("[VoiceAssistant] Cloud Speech API unavailable. Attempting local Whisper...")
506
+ text = self.recognizer.recognize_whisper(audio, model="base.en")
507
+ print(f"[Driver Heard (Whisper)] {text}")
508
+ except Exception as e:
509
+ print(f"[VoiceAssistant] Offline recognition failed: {e}")
510
+ return
511
+
512
+ cleaned_text = text.strip().lower()
513
+ if not cleaned_text:
514
+ return
515
+
516
+ # STATE-SPECIFIC ROUTING (Emergency Rest / Song Prompts)
517
+ current_state = self.callbacks['get_system_state']()
518
+
519
+ # 1. State: Driver has been warned of frequent drowsiness (Level 3 Advisory)
520
+ if current_state == "WAITING_REST_RESPONSE":
521
+ refusal_words = ["no", "never", "can't", "wont", "won't", "refuse", "impossible", "fine", "good", "no thanks", "no rest", "keep driving"]
522
+ accepted_words = ["yes", "yeah", "ok", "okay", "fine I will", "sure", "pulling over"]
523
+
524
+ if any(word in cleaned_text for word in refusal_words):
525
+ print("[VoiceAssistant] Driver refused rest. Prompting for energetic song.")
526
+ self.callbacks['set_system_state']("WAITING_SONG_RESPONSE")
527
+ self.callbacks['add_chat_log'](text, "No, I'm fine. I won't stop.")
528
+
529
+ time.sleep(0.5)
530
+ self.alert_manager.ask_energetic_song()
531
+ self.callbacks['add_chat_log']("System", "Alright. Would you like to listen to an energetic song to help you stay awake?")
532
+ return
533
+
534
+ elif any(word in cleaned_text for word in accepted_words) or "pull" in cleaned_text:
535
+ print("[VoiceAssistant] Driver accepted rest.")
536
+ self.callbacks['reset_warnings']()
537
+ self.callbacks['add_chat_log'](text, "Okay, pulling over.")
538
+ self.alert_manager.speak("Good decision. Pull over safely and take some rest.")
539
+ self.callbacks['add_chat_log']("System", "Good decision. Pull over safely and take some rest.")
540
+ return
541
+
542
+ # 2. State: Driver refused rest, now confirming if they want a song
543
+ elif current_state == "WAITING_SONG_RESPONSE":
544
+ accepted_words = ["yes", "yeah", "sure", "ok", "okay", "play", "song", "music", "please"]
545
+
546
+ if any(word in cleaned_text for word in accepted_words):
547
+ print("[VoiceAssistant] Driver accepted song.")
548
+ self.callbacks['add_chat_log'](text, "Yes, play some music.")
549
+ self.alert_manager.play_energetic_music()
550
+ self.callbacks['add_chat_log']("System", "Playing energetic synthwave beats! Stay awake!")
551
+ self.callbacks['set_system_state']("PLAYING_MUSIC")
552
+ return
553
+ else:
554
+ print("[VoiceAssistant] Driver declined song.")
555
+ self.callbacks['add_chat_log'](text, "No, I'm okay.")
556
+ self.alert_manager.speak("Understood. Keep your eyes on the road. Stay focused.")
557
+ self.callbacks['add_chat_log']("System", "Understood. Keep your eyes on the road. Stay focused.")
558
+ self.callbacks['reset_warnings']()
559
+ return
560
+
561
+ # DIRECT SYSTEM BACKUP COMMANDS (Local Regex Override)
562
+ if "reset" in cleaned_text or "clear" in cleaned_text or "awake" in cleaned_text or "focused" in cleaned_text:
563
+ print("[VoiceAssistant] Safe state reset command received.")
564
+ self.callbacks['reset_warnings']()
565
+ self.callbacks['add_chat_log'](text, "Reset assistant")
566
+ self.alert_manager.speak("System reset. Let's keep driving safely.")
567
+ self.callbacks['add_chat_log']("System", "System reset. Let's keep driving safely.")
568
+ return
569
+
570
+ has_play = any(p in cleaned_text for p in ["play", "start", "turn on", "listen", "put on", "launch"])
571
+ has_music_kw = any(kw in cleaned_text for kw in ["music", "song", "beat", "tune", "track", "musc", "melody", "audio", "lofi"])
572
+
573
+ if has_play:
574
+ # Extract query after the play keyword
575
+ play_keyword = next((p for p in ["play", "start", "turn on", "listen to", "put on", "launch"] if p in cleaned_text), "play")
576
+ idx = cleaned_text.find(play_keyword)
577
+ music_query = cleaned_text[idx + len(play_keyword):].strip()
578
+
579
+ # Clean common filler words
580
+ for filler in ["some", "a", "the", "music", "song", "track", "musc"]:
581
+ if music_query.startswith(filler):
582
+ music_query = music_query[len(filler):].strip()
583
+ if music_query.endswith(filler):
584
+ music_query = music_query[:-len(filler)].strip()
585
+
586
+ # If the remaining query is empty or generic, play the custom playlist
587
+ if not music_query or music_query in ["music", "song", "beat", "tune", "track", "musc", "melody"]:
588
+ print("[VoiceAssistant] General music command recognized locally. Playing playlist.")
589
+ self.callbacks['add_chat_log'](text, "Requested general music playback")
590
+ self.alert_manager.play_energetic_music()
591
+ self.callbacks['set_system_state']("PLAYING_MUSIC")
592
+ return
593
+ else:
594
+ # Play specific song directly!
595
+ print(f"[VoiceAssistant] Specific song command recognized locally: {music_query}")
596
+ confirm_msg = f"Sure thing! Autoplay in progress for {music_query}."
597
+ self.callbacks['add_chat_log'](text, confirm_msg)
598
+ self.alert_manager.speak(confirm_msg)
599
+
600
+ # Fetch first search result and autoplay!
601
+ search_url = f"https://www.youtube.com/results?search_query={urllib.parse.quote(music_query)}"
602
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
603
+ try:
604
+ req = urllib.request.Request(search_url, headers=headers)
605
+ with urllib.request.urlopen(req, timeout=5) as res:
606
+ html = res.read().decode('utf-8')
607
+ video_ids = re.findall(r'/watch\?v=([a-zA-Z0-9_-]{11})', html)
608
+ if video_ids:
609
+ first_video_id = video_ids[0]
610
+ direct_url = f"https://www.youtube.com/watch?v={first_video_id}&autoplay=1"
611
+ print(f"[VoiceAssistant] Auto-playing first matching YouTube video: {direct_url}")
612
+ webbrowser.open(direct_url)
613
+ else:
614
+ webbrowser.open(search_url)
615
+ except Exception as e:
616
+ print(f"[VoiceAssistant] Autoplay scraper failed: {e}. Falling back to search page.")
617
+ webbrowser.open(search_url)
618
+
619
+ self.callbacks['set_system_state']("PLAYING_MUSIC")
620
+ return
621
+
622
+ has_stop = any(s in cleaned_text for s in ["stop", "pause", "turn off", "mute", "quiet", "halt", "shut up"])
623
+ if has_stop and (has_music_kw or "music" in cleaned_text or "song" in cleaned_text or "sound" in cleaned_text or "radio" in cleaned_text):
624
+ print("[VoiceAssistant] Flexible stop music command recognized.")
625
+ # Simulate media play/pause key to halt browser/audio stream
626
+ import ctypes
627
+ VK_MEDIA_PLAY_PAUSE = 0xB3
628
+ try:
629
+ ctypes.windll.user32.keybd_event(VK_MEDIA_PLAY_PAUSE, 0, 0, 0)
630
+ ctypes.windll.user32.keybd_event(VK_MEDIA_PLAY_PAUSE, 0, 2, 0)
631
+ except Exception as e:
632
+ print(f"[VoiceAssistant] Failed simulating media key: {e}")
633
+
634
+ self.callbacks['set_system_state']("NORMAL")
635
+ self.callbacks['add_chat_log'](text, "Stop the music")
636
+ self.alert_manager.speak("Stopping the music. Keep your eyes on the road.")
637
+ self.callbacks['add_chat_log']("System", "Music stopped.")
638
+ return
639
+
640
+ # CONVERSATIONAL LOCAL SLM (Always Active - No Wake Word/Filters Required!)
641
+ # Route ANY general speech dynamically straight to our local Ollama custom model!
642
+ print(f"[Ollama Query] {text}")
643
+ reply = self.query_ollama_slm(text)
644
+ print(f"[SLM Reply] {reply}")
645
+
646
+ # Check if Ollama returned a dynamic PLAY action tag (e.g. "[PLAY] paint it black")
647
+ if "[play]" in reply.lower():
648
+ match = re.search(r'\[play\]\s*(.*)', reply, re.IGNORECASE)
649
+ if match:
650
+ music_query = match.group(1).strip()
651
+ music_query = music_query.replace('"', '').replace('[', '').replace(']', '').strip()
652
+
653
+ confirm_msg = f"Sure thing! Autoplay in progress for {music_query}."
654
+ self.callbacks['add_chat_log'](text, confirm_msg)
655
+ self.alert_manager.speak(confirm_msg)
656
+
657
+ # Fetch the first search result from YouTube dynamically and play it directly!
658
+ search_url = f"https://www.youtube.com/results?search_query={urllib.parse.quote(music_query)}"
659
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
660
+ try:
661
+ req = urllib.request.Request(search_url, headers=headers)
662
+ with urllib.request.urlopen(req, timeout=5) as res:
663
+ html = res.read().decode('utf-8')
664
+ # Search for video watch paths
665
+ video_ids = re.findall(r'/watch\?v=([a-zA-Z0-9_-]{11})', html)
666
+ if video_ids:
667
+ first_video_id = video_ids[0]
668
+ direct_url = f"https://www.youtube.com/watch?v={first_video_id}&autoplay=1"
669
+ print(f"[VoiceAssistant] Auto-playing first matching YouTube video: {direct_url}")
670
+ webbrowser.open(direct_url)
671
+ else:
672
+ webbrowser.open(search_url)
673
+ except Exception as e:
674
+ print(f"[VoiceAssistant] Autoplay scraper failed: {e}. Falling back to search page.")
675
+ webbrowser.open(search_url)
676
+
677
+ self.callbacks['set_system_state']("PLAYING_MUSIC")
678
+ return
679
+
680
+ # Check if Ollama returned a dynamic STOP action tag (e.g. "[STOP]")
681
+ if "[stop]" in reply.lower():
682
+ match = re.search(r'\[stop\]\s*(.*)', reply, re.IGNORECASE)
683
+ clean_reply = match.group(1).strip() if match else "Stopping the music. Keep your eyes on the road!"
684
+ clean_reply = clean_reply.replace('[', '').replace(']', '').strip()
685
+
686
+ print("[VoiceAssistant] Action STOP triggered dynamically by Ollama.")
687
+ # Simulate media play/pause key to stop the browser audio stream
688
+ import ctypes
689
+ VK_MEDIA_PLAY_PAUSE = 0xB3
690
+ try:
691
+ ctypes.windll.user32.keybd_event(VK_MEDIA_PLAY_PAUSE, 0, 0, 0)
692
+ ctypes.windll.user32.keybd_event(VK_MEDIA_PLAY_PAUSE, 0, 2, 0)
693
+ except Exception as e:
694
+ print(f"[VoiceAssistant] Failed simulating media key: {e}")
695
+
696
+ self.callbacks['set_system_state']("NORMAL")
697
+ self.callbacks['add_chat_log'](text, clean_reply)
698
+ self.alert_manager.speak(clean_reply)
699
+ return
700
+
701
+ # Check if Ollama returned a dynamic RESET action tag (e.g. "[RESET]")
702
+ if "[reset]" in reply.lower():
703
+ match = re.search(r'\[reset\]\s*(.*)', reply, re.IGNORECASE)
704
+ clean_reply = match.group(1).strip() if match else "System warnings cleared. Drive safely!"
705
+ clean_reply = clean_reply.replace('[', '').replace(']', '').strip()
706
+
707
+ print("[VoiceAssistant] Action RESET triggered dynamically by Ollama.")
708
+ self.callbacks['reset_warnings']()
709
+ self.callbacks['add_chat_log'](text, clean_reply)
710
+ self.alert_manager.speak(clean_reply)
711
+ return
712
+
713
+ # General conversational response
714
+ self.callbacks['add_chat_log'](text, reply)
715
+ self.alert_manager.speak(reply)
716
+
717
+ def stop(self):
718
+ """Stops the assistant background thread."""
719
+ self.running = False
720
+
721
+
722
+ # 6. SafeDrivingAssistant Core Engine & Orchestrator Coordinator
723
+
724
+ class SafeDrivingAssistant:
725
+ def __init__(self):
726
+ print("[CoreEngine] Initializing Safe Driving Assistant...")
727
+
728
+ # Initialize Audio Alert & Sound Synthesizer
729
+ self.alert_manager = AlertManager()
730
+
731
+ # Initialize face_recognition Eye Landmark Processor
732
+ self.detector = EyeDetector()
733
+
734
+ # Tracking states and timelines
735
+ self.consec_closed_frames = 0
736
+ self.eyes_closed_start_time = None
737
+ self.active_alert_level = 0 # 0: None, 1: Stay Focused, 2: Wake Up Loud
738
+
739
+ # Rolling log of drowsiness timestamps to monitor frequency
740
+ self.drowsiness_events = collections.deque()
741
+
742
+ # Keyboard reset helper
743
+ self.last_key_press = None
744
+
745
+ # Setup conversational callbacks for our Voice Assistant & SLM
746
+ self.callbacks = {
747
+ 'get_system_state': self.get_system_state,
748
+ 'set_system_state': self.set_system_state,
749
+ 'reset_warnings': self.reset_warnings,
750
+ 'add_chat_log': self.add_chat_log
751
+ }
752
+
753
+ # Bind callbacks back to Flask REST API endpoints
754
+ app.reset_callback = self.reset_warnings
755
+ app.play_music_callback = self.play_energetic_music
756
+
757
+ # Initialize speech listener thread
758
+ self.assistant = VoiceAssistant(self.alert_manager, self.callbacks)
759
+
760
+ # Boot Flask HUD Web Dashboard in the background
761
+ start_server_async()
762
+
763
+ # Coordinator Callback Handlers
764
+
765
+ def get_system_state(self):
766
+ """Thread-safe state getter for the Voice Assistant."""
767
+ with dashboard_state.lock:
768
+ return dashboard_state.state
769
+
770
+ def set_system_state(self, new_state):
771
+ """Thread-safe state setter for the Voice Assistant."""
772
+ with dashboard_state.lock:
773
+ dashboard_state.state = new_state
774
+ if new_state == "NORMAL":
775
+ dashboard_state.alert_message = ""
776
+ elif new_state == "WAITING_REST_RESPONSE":
777
+ dashboard_state.alert_message = "ADVISING REST BREAK"
778
+ elif new_state == "WAITING_SONG_RESPONSE":
779
+ dashboard_state.alert_message = "OFFERING ENERGETIC MUSIC"
780
+ elif new_state == "PLAYING_MUSIC":
781
+ dashboard_state.alert_message = "PLAYING HIGH ENERGY BEATS"
782
+
783
+ def reset_warnings(self):
784
+ """Complete reset of all active alarms, timers, and warning metrics."""
785
+ print("[CoreEngine] Performing comprehensive system alert reset.")
786
+ with dashboard_state.lock:
787
+ dashboard_state.state = "NORMAL"
788
+ dashboard_state.alert_message = ""
789
+ dashboard_state.drowsiness_count = 0
790
+ self.consec_closed_frames = 0
791
+ self.eyes_closed_start_time = None
792
+ self.active_alert_level = 0
793
+ self.drowsiness_events.clear()
794
+
795
+ # Enqueue a log message
796
+ self.add_chat_log("System", "System alerts and warnings reset to NORMAL.")
797
+
798
+ def add_chat_log(self, user_query, slm_reply=""):
799
+ """Pushes voice transcripts to the dashboard log log history."""
800
+ with dashboard_state.lock:
801
+ if user_query == "System":
802
+ dashboard_state.chat_history.append({
803
+ "speaker": "System",
804
+ "message": slm_reply
805
+ })
806
+ else:
807
+ dashboard_state.chat_history.append({
808
+ "speaker": "Driver",
809
+ "query": user_query,
810
+ "message": slm_reply
811
+ })
812
+
813
+ def play_energetic_music(self):
814
+ """Orchestrator hook to trigger the energetic music sequence."""
815
+ self.set_system_state("PLAYING_MUSIC")
816
+ self.alert_manager.play_energetic_music()
817
+ self.add_chat_log("System", "Energetic synthwave music started. Stay alert!")
818
+
819
+ # Core Drowsiness Evaluation & Loop
820
+
821
+ def run(self):
822
+ """Main camera acquisition loop that drives the safe assistant."""
823
+ print("[CoreEngine] Accessing camera stream...")
824
+ cap = cv2.VideoCapture(CAMERA_ID)
825
+
826
+ # Configure video dimension overrides from settings
827
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH)
828
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
829
+
830
+ if not cap.isOpened():
831
+ print("[CoreEngine] FATAL: Could not access web camera.")
832
+ return
833
+
834
+ print("[CoreEngine] Camera stream operational. System fully active.")
835
+ print("[CoreEngine] Press 'q' in CV window or click Dashboard Reset to quit.")
836
+
837
+ prev_time = time.time()
838
+
839
+ try:
840
+ while True:
841
+ ret, frame = cap.read()
842
+ if not ret:
843
+ time.sleep(0.01)
844
+ continue
845
+
846
+ # Mirror frame for intuitive pilot HUD overlay
847
+ frame = cv2.flip(frame, 1)
848
+
849
+ # Check if tracking is active (controlled from Dashboard)
850
+ with dashboard_state.lock:
851
+ active = dashboard_state.detection_active
852
+
853
+ if active:
854
+ # Calculate EAR and overlay glow contours on frame
855
+ ear, landmarks, processed_frame = self.detector.process_frame(frame)
856
+ else:
857
+ processed_frame = frame.copy()
858
+ cv2.putText(processed_frame, "TRACKING PAUSED", (50, 50),
859
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 165, 255), 2)
860
+ ear = None
861
+
862
+ current_time = time.time()
863
+
864
+ # Calculate processing frame rate (FPS)
865
+ fps = int(1.0 / (current_time - prev_time))
866
+ prev_time = current_time
867
+
868
+ # Drowsiness Logic Decision Engine
869
+
870
+ if ear is not None and active:
871
+ if ear < EAR_THRESHOLD:
872
+ self.consec_closed_frames += 1
873
+
874
+ # Once consecutive frames pass noise filter, start duration timer
875
+ if self.consec_closed_frames >= EAR_CONSEC_FRAMES:
876
+ if self.eyes_closed_start_time is None:
877
+ self.eyes_closed_start_time = current_time
878
+ else:
879
+ closed_duration = current_time - self.eyes_closed_start_time
880
+
881
+ # LEVEL 1: Eyes Closed 3-5 seconds
882
+ if ALERT_LEVEL1_MIN <= closed_duration < ALERT_LEVEL1_MAX:
883
+ if self.active_alert_level < 1:
884
+ self.active_alert_level = 1
885
+ self.set_system_state("CLOSED_3S")
886
+ self.alert_manager.trigger_level1()
887
+
888
+ # Record timestamp to sliding frequency tracker
889
+ self.drowsiness_events.append(current_time)
890
+ with dashboard_state.lock:
891
+ dashboard_state.drowsiness_count += 1
892
+
893
+ self.add_chat_log("System", "WARNING: Eyes closed for 3 seconds! Stay focused!")
894
+
895
+ # LEVEL 2: Eyes Closed > 5 seconds (Louder Warning!)
896
+ elif closed_duration >= ALERT_LEVEL2_MIN:
897
+ if self.active_alert_level < 2:
898
+ self.active_alert_level = 2
899
+ self.set_system_state("CLOSED_5S")
900
+ self.alert_manager.trigger_level2()
901
+
902
+ # Record second timestamp
903
+ self.drowsiness_events.append(current_time)
904
+ with dashboard_state.lock:
905
+ dashboard_state.drowsiness_count += 1
906
+
907
+ self.add_chat_log("System", "CRITICAL ALARM: Eyes closed for 5+ seconds! WAKE UP!")
908
+ else:
909
+ # Eyes are open! Reset filters and check for Level 3 Advisory escalation
910
+ self.consec_closed_frames = 0
911
+
912
+ if self.eyes_closed_start_time is not None:
913
+ self.eyes_closed_start_time = None
914
+
915
+ while self.drowsiness_events and (current_time - self.drowsiness_events[0] > FREQUENT_DROWSY_WINDOW):
916
+ self.drowsiness_events.popleft()
917
+
918
+ # LEVEL 3: Frequent Drowsiness check (if closed events occur >= limit in last 60s)
919
+ if len(self.drowsiness_events) >= FREQUENT_DROWSY_LIMIT:
920
+ print(f"[CoreEngine] Frequent drowsiness detected ({len(self.drowsiness_events)} events in 60s). Escalating to Level 3.")
921
+ self.set_system_state("WAITING_REST_RESPONSE")
922
+ self.alert_manager.trigger_level3_advisory()
923
+ self.add_chat_log("System", "FREQUENT DROWSINESS DETECTED. Prompting driver to pull over.")
924
+ else:
925
+ # Normal recovery (driver had a brief blink/close and is now normal)
926
+ current_state = self.get_system_state()
927
+ if current_state not in ["WAITING_REST_RESPONSE", "WAITING_SONG_RESPONSE"]:
928
+ self.set_system_state("NORMAL")
929
+ self.active_alert_level = 0
930
+ else:
931
+ self.consec_closed_frames = 0
932
+ self.eyes_closed_start_time = None
933
+
934
+ # Update Global Telemetry Buffer for Flask Server
935
+
936
+ with dashboard_state.lock:
937
+ dashboard_state.latest_frame = processed_frame.copy()
938
+ if ear is not None:
939
+ dashboard_state.ear = ear
940
+ else:
941
+ dashboard_state.ear = 0.30 # Default baseline when no face present
942
+ dashboard_state.fps = fps
943
+
944
+ # OpenCV display output fallback (allows running headless or with double screens)
945
+ cv2.imshow("DriveSafe HUD AI Console", processed_frame)
946
+
947
+ # Check for keyboard inputs on CV Window ('q' to quit, 'r' to reset)
948
+ key = cv2.waitKey(1) & 0xFF
949
+ if key == ord('q') or key == 27: # ESC or Q
950
+ print("[CoreEngine] Exit key received. Terminating system.")
951
+ break
952
+ elif key == ord('r'):
953
+ self.reset_warnings()
954
+
955
+ except KeyboardInterrupt:
956
+ print("[CoreEngine] Keyboard interrupt. Shutting down.")
957
+ finally:
958
+ print("[CoreEngine] Releasing resources...")
959
+ cap.release()
960
+ cv2.destroyAllWindows()
961
+ self.assistant.stop()
962
+ sys.exit(0)
963
+
964
+ if __name__ == "__main__":
965
+ assistant_app = SafeDrivingAssistant()
966
+ assistant_app.run()
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ opencv-python>=4.8.0
2
+ pyttsx3>=2.90
3
+ SpeechRecognition>=3.10.0
4
+ pyaudio>=0.2.14
5
+ pygame>=2.5.0
6
+ numpy>=1.24.0
7
+ scipy>=1.11.0
8
+ soundfile>=0.12.0
9
+ flask>=3.0.0
10
+ face_recognition @ git+https://github.com/lovnishverma/face_recognition.git
static/js/app.js ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ==============================================================================
2
+ // DriveSafe HUD Dashboard JavaScript
3
+ // Establishes real-time EventSource connection, draws Chart.js EKG, and manages HUD state.
4
+ // ==============================================================================
5
+
6
+ let earChart;
7
+ const maxChartPoints = 40;
8
+ let chartData = Array(maxChartPoints).fill(0.3);
9
+ let chartLabels = Array(maxChartPoints).fill("");
10
+ let renderedChatLogsCount = 0;
11
+
12
+ // --- Initialize Chart.js on DOM Load ---
13
+ document.addEventListener("DOMContentLoaded", () => {
14
+ const ctx = document.getElementById('earChart').getContext('2d');
15
+
16
+ // Create futuristic neon line chart
17
+ earChart = new Chart(ctx, {
18
+ type: 'line',
19
+ data: {
20
+ labels: chartLabels,
21
+ datasets: [{
22
+ label: 'Eye Aspect Ratio (EAR)',
23
+ data: chartData,
24
+ borderColor: '#00bfff',
25
+ borderWidth: 2,
26
+ pointRadius: 0,
27
+ fill: true,
28
+ backgroundColor: 'rgba(0, 191, 255, 0.05)',
29
+ tension: 0.4
30
+ }]
31
+ },
32
+ options: {
33
+ responsive: true,
34
+ maintainAspectRatio: false,
35
+ plugins: {
36
+ legend: { display: false }
37
+ },
38
+ scales: {
39
+ x: { display: false },
40
+ y: {
41
+ min: 0.0,
42
+ max: 0.45,
43
+ grid: { color: 'rgba(255, 255, 255, 0.03)' },
44
+ ticks: {
45
+ color: '#64748b',
46
+ font: { family: 'Share Tech Mono', size: 10 }
47
+ }
48
+ }
49
+ },
50
+ animation: { duration: 0 } // Disable default anims for absolute high-speed rendering
51
+ }
52
+ });
53
+
54
+ // Initialize Telemetry EventSource listener
55
+ initTelemetry();
56
+ });
57
+
58
+ // --- Server-Sent Events (SSE) Telemetry Listener ---
59
+ function initTelemetry() {
60
+ const sse = new EventSource('/telemetry');
61
+
62
+ sse.onmessage = (event) => {
63
+ const data = JSON.parse(event.data);
64
+
65
+ // 1. Update Core Readouts
66
+ updateMetrics(data);
67
+
68
+ // 2. Shift EAR Graph Data
69
+ updateChart(data.ear);
70
+
71
+ // 3. Update Conversation Chat Logs
72
+ updateChat(data.chat_history);
73
+
74
+ // 4. Update HUD panel glowing states
75
+ updateStateOverlays(data.state, data.alert_message);
76
+ };
77
+
78
+ sse.onerror = (err) => {
79
+ console.error("SSE connection dropped:", err);
80
+ };
81
+ }
82
+
83
+ // --- Update Circular Gauge & Numeric Metrics ---
84
+ function updateMetrics(data) {
85
+ // Current numeric readout
86
+ document.getElementById("ear-val").innerText = data.ear.toFixed(2);
87
+
88
+ // Circular radial progress math
89
+ const gauge = document.getElementById("ear-gauge");
90
+ const radius = gauge.r.baseVal.value;
91
+ const circumference = 2 * Math.PI * radius;
92
+
93
+ // EAR bounds are normally between 0.10 (fully closed) and 0.35 (wide open)
94
+ // Normalize EAR to 0% - 100% range
95
+ let percentage = (data.ear - 0.10) / (0.35 - 0.10);
96
+ percentage = Math.max(0, Math.min(1, percentage)); // Clamp between 0 and 1
97
+
98
+ const offset = circumference - (percentage * circumference);
99
+ gauge.style.strokeDashoffset = offset;
100
+
101
+ // Change gauge color relative to EAR thresholds
102
+ const warnThreshold = 0.23;
103
+ const closedThreshold = 0.20;
104
+
105
+ if (data.ear <= closedThreshold) {
106
+ gauge.style.stroke = "var(--danger-color)";
107
+ } else if (data.ear <= warnThreshold) {
108
+ gauge.style.stroke = "var(--warn-color)";
109
+ } else {
110
+ gauge.style.stroke = "var(--safe-color)";
111
+ }
112
+
113
+ // Update Drowsiness Count
114
+ const countElement = document.getElementById("drowsiness-count");
115
+ countElement.innerText = data.drowsiness_count;
116
+ if (data.drowsiness_count > 0) {
117
+ countElement.style.color = "var(--danger-color)";
118
+ countElement.style.textShadow = "0 0 10px rgba(255, 40, 80, 0.4)";
119
+ } else {
120
+ countElement.style.color = "var(--safe-color)";
121
+ countElement.style.textShadow = "none";
122
+ }
123
+
124
+ // Update Engine FPS
125
+ document.getElementById("engine-fps").innerText = `${data.fps} FPS`;
126
+
127
+ // Update Tracking active button toggle
128
+ const trackingBtn = document.getElementById("toggle-tracking-btn");
129
+ const label = trackingBtn.querySelector("span");
130
+ if (data.detection_active) {
131
+ trackingBtn.classList.add("active");
132
+ label.innerText = "ACTIVE TRACKING";
133
+ } else {
134
+ trackingBtn.classList.remove("active");
135
+ label.innerText = "TRACKING PAUSED";
136
+ }
137
+ }
138
+
139
+ // --- EKG EAR Waveform Chart Shifter ---
140
+ function updateChart(newEar) {
141
+ chartData.push(newEar);
142
+ chartData.shift();
143
+
144
+ // Swap graph colors based on EAR values
145
+ if (newEar < 0.20) {
146
+ earChart.data.datasets[0].borderColor = "#ff2850";
147
+ earChart.data.datasets[0].backgroundColor = "rgba(255, 40, 80, 0.05)";
148
+ } else if (newEar < 0.23) {
149
+ earChart.data.datasets[0].borderColor = "#ffaa00";
150
+ earChart.data.datasets[0].backgroundColor = "rgba(255, 170, 0, 0.05)";
151
+ } else {
152
+ earChart.data.datasets[0].borderColor = "#00ff80";
153
+ earChart.data.datasets[0].backgroundColor = "rgba(0, 255, 128, 0.05)";
154
+ }
155
+
156
+ earChart.update();
157
+ }
158
+
159
+ // --- Dynamic Conversational Chat Logs renderer ---
160
+ function updateChat(history) {
161
+ if (history.length === renderedChatLogsCount) return;
162
+
163
+ const chatBox = document.getElementById("chat-box");
164
+
165
+ // Render only new logs added since last pass
166
+ for (let i = renderedChatLogsCount; i < history.length; i++) {
167
+ const log = history[i];
168
+
169
+ // System logs represent state announcements
170
+ if (log.speaker === "System") {
171
+ const systemDiv = document.createElement("div");
172
+ systemDiv.className = "chat-bubble system-bubble";
173
+ systemDiv.innerText = log.message;
174
+ chatBox.appendChild(systemDiv);
175
+ } else {
176
+ // User query
177
+ if (log.query) {
178
+ const userDiv = document.createElement("div");
179
+ userDiv.className = "chat-bubble driver-bubble";
180
+ userDiv.innerHTML = `<span class="bubble-label">Driver</span>${escapeHTML(log.query)}`;
181
+ chatBox.appendChild(userDiv);
182
+ }
183
+
184
+ // AI response
185
+ if (log.message) {
186
+ const aiDiv = document.createElement("div");
187
+ aiDiv.className = "chat-bubble slm-bubble";
188
+ aiDiv.innerHTML = `<span class="bubble-label">DriveSafe SLM</span>${escapeHTML(log.message)}`;
189
+ chatBox.appendChild(aiDiv);
190
+ }
191
+ }
192
+ }
193
+
194
+ renderedChatLogsCount = history.length;
195
+
196
+ // Auto-scroll chat box down with a short delay for fluid rendering
197
+ setTimeout(() => {
198
+ chatBox.scrollTop = chatBox.scrollHeight;
199
+ }, 50);
200
+ }
201
+
202
+ // --- Toggle HUD Glowing Cockpit States ---
203
+ function updateStateOverlays(state, alertMsg) {
204
+ const mainPanel = document.getElementById("main-panel");
205
+ const badge = document.getElementById("hud-state-badge");
206
+ const slmIndicator = document.getElementById("slm-indicator");
207
+
208
+ // Clean current state classes
209
+ mainPanel.className = "glass-panel video-panel";
210
+
211
+ if (state === "NORMAL") {
212
+ mainPanel.classList.add("state-normal");
213
+ badge.innerText = alertMsg ? alertMsg : "NORMAL";
214
+ badge.style.borderColor = "var(--safe-color)";
215
+ badge.style.color = "var(--safe-color)";
216
+ badge.style.boxShadow = "0 0 10px rgba(0, 255, 128, 0.2)";
217
+ slmIndicator.innerText = "SLM STANDBY";
218
+ slmIndicator.style.color = "var(--safe-color)";
219
+ }
220
+ else if (state === "CLOSED_3S") {
221
+ mainPanel.classList.add("state-warn");
222
+ badge.innerText = alertMsg ? alertMsg : "EYES CLOSED (3-5s)";
223
+ badge.style.borderColor = "var(--warn-color)";
224
+ badge.style.color = "var(--warn-color)";
225
+ badge.style.boxShadow = "0 0 15px rgba(255, 170, 0, 0.3)";
226
+ }
227
+ else if (state === "CLOSED_5S" || state === "WAITING_REST_RESPONSE" || state === "WAITING_SONG_RESPONSE") {
228
+ mainPanel.classList.add("state-danger");
229
+ badge.innerText = alertMsg ? alertMsg : "CRITICAL WARNING";
230
+ badge.style.borderColor = "var(--danger-color)";
231
+ badge.style.color = "var(--danger-color)";
232
+ badge.style.boxShadow = "0 0 25px rgba(255, 40, 80, 0.5)";
233
+
234
+ if (state.startsWith("WAITING")) {
235
+ slmIndicator.innerText = "SLM ACTIVE DIALOGUE";
236
+ slmIndicator.style.color = "var(--accent-color)";
237
+ }
238
+ }
239
+ else if (state === "PLAYING_MUSIC") {
240
+ mainPanel.classList.add("state-normal");
241
+ badge.innerText = "BEATS PLAYING";
242
+ badge.style.borderColor = "var(--accent-color)";
243
+ badge.style.color = "var(--accent-color)";
244
+ badge.style.boxShadow = "0 0 10px rgba(0, 191, 255, 0.2)";
245
+ }
246
+ }
247
+
248
+ // --- Control Desk AJAX REST Bridge ---
249
+
250
+ function toggleTracking() {
251
+ fetch('/api/toggle_detection', { method: 'POST' })
252
+ .then(response => response.json())
253
+ .then(data => {
254
+ console.log("Tracking toggled, active state:", data.detection_active);
255
+ })
256
+ .catch(err => console.error("Error toggling tracking:", err));
257
+ }
258
+
259
+ function triggerReset() {
260
+ fetch('/api/reset', { method: 'POST' })
261
+ .then(response => response.json())
262
+ .then(data => {
263
+ console.log("System reset:", data.message);
264
+ })
265
+ .catch(err => console.error("Error triggering reset:", err));
266
+ }
267
+
268
+ function triggerMusic() {
269
+ fetch('/api/trigger_music', { method: 'POST' })
270
+ .then(response => response.json())
271
+ .then(data => {
272
+ console.log("Music play request sent:", data.message);
273
+ })
274
+ .catch(err => console.error("Error playing energetic beats:", err));
275
+ }
276
+
277
+ // --- Helper Functions ---
278
+ function escapeHTML(str) {
279
+ return str.replace(/[&<>'"]/g,
280
+ tag => ({
281
+ '&': '&amp;',
282
+ '<': '&lt;',
283
+ '>': '&gt;',
284
+ "'": '&#39;',
285
+ '"': '&quot;'
286
+ }[tag] || tag)
287
+ );
288
+ }
templates/index.html ADDED
@@ -0,0 +1,665 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DriveSafe — AI Safety HUD & Conversational SLM</title>
7
+ <!-- Google Fonts for High-Tech Dashboard Aesthetic -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
11
+ <!-- Chart.js for High-Performance EKG Graph -->
12
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
13
+ <style>
14
+ :root {
15
+ --bg-color: #08090e;
16
+ --panel-bg: rgba(15, 18, 28, 0.7);
17
+ --border-glow: rgba(0, 255, 128, 0.15);
18
+ --safe-color: #00ff80;
19
+ --warn-color: #ffaa00;
20
+ --danger-color: #ff2850;
21
+ --accent-color: #00bfff;
22
+ --text-color: #e2e8f0;
23
+ --text-muted: #64748b;
24
+ }
25
+
26
+ * {
27
+ box-sizing: border-box;
28
+ margin: 0;
29
+ padding: 0;
30
+ font-family: 'Outfit', sans-serif;
31
+ -webkit-font-smoothing: antialiased;
32
+ }
33
+
34
+ body {
35
+ background-color: var(--bg-color);
36
+ background-image:
37
+ radial-gradient(at 0% 0%, rgba(0, 191, 255, 0.05) 0px, transparent 50%),
38
+ radial-gradient(at 100% 0%, rgba(0, 255, 128, 0.05) 0px, transparent 50%),
39
+ radial-gradient(at 50% 100%, rgba(255, 40, 80, 0.03) 0px, transparent 50%);
40
+ color: var(--text-color);
41
+ min-height: 100vh;
42
+ overflow-x: hidden;
43
+ display: flex;
44
+ flex-direction: column;
45
+ }
46
+
47
+ /* --- Header --- */
48
+ header {
49
+ display: flex;
50
+ justify-content: space-between;
51
+ align-items: center;
52
+ padding: 20px 40px;
53
+ background: rgba(8, 9, 14, 0.85);
54
+ backdrop-filter: blur(12px);
55
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
56
+ position: sticky;
57
+ top: 0;
58
+ z-index: 100;
59
+ }
60
+
61
+ .logo-section {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 15px;
65
+ }
66
+
67
+ .logo-section h1 {
68
+ font-size: 24px;
69
+ font-weight: 800;
70
+ letter-spacing: 2px;
71
+ background: linear-gradient(to right, #00ff80, #00bfff);
72
+ -webkit-background-clip: text;
73
+ -webkit-text-fill-color: transparent;
74
+ }
75
+
76
+ .logo-section span {
77
+ font-family: 'Share Tech Mono', monospace;
78
+ font-size: 11px;
79
+ background: rgba(0, 191, 255, 0.1);
80
+ color: var(--accent-color);
81
+ padding: 2px 8px;
82
+ border: 1px solid rgba(0, 191, 255, 0.2);
83
+ border-radius: 4px;
84
+ }
85
+
86
+ .connection-status {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 8px;
90
+ font-size: 13px;
91
+ font-weight: 600;
92
+ color: var(--safe-color);
93
+ }
94
+
95
+ .pulse-dot {
96
+ width: 8px;
97
+ height: 8px;
98
+ background-color: var(--safe-color);
99
+ border-radius: 50%;
100
+ box-shadow: 0 0 10px var(--safe-color);
101
+ animation: pulse 1.5s infinite;
102
+ }
103
+
104
+ /* --- Main Dashboard Container --- */
105
+ .dashboard-container {
106
+ display: grid;
107
+ grid-template-columns: 1.1fr 0.9fr;
108
+ gap: 30px;
109
+ padding: 30px 40px;
110
+ flex-grow: 1;
111
+ max-width: 1800px;
112
+ margin: 0 auto;
113
+ width: 100%;
114
+ }
115
+
116
+ .glass-panel {
117
+ background: var(--panel-bg);
118
+ backdrop-filter: blur(16px);
119
+ border: 1px solid rgba(255, 255, 255, 0.05);
120
+ border-radius: 20px;
121
+ padding: 24px;
122
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
123
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
124
+ position: relative;
125
+ overflow: hidden;
126
+ }
127
+
128
+ .glass-panel::before {
129
+ content: '';
130
+ position: absolute;
131
+ top: 0;
132
+ left: 0;
133
+ width: 100%;
134
+ height: 4px;
135
+ background: linear-gradient(90deg, transparent, var(--border-glow), transparent);
136
+ transition: all 0.4s ease;
137
+ }
138
+
139
+ /* --- LEFT COLUMN: CAMERA & VISUAL HUD --- */
140
+ .left-column {
141
+ display: flex;
142
+ flex-direction: column;
143
+ gap: 30px;
144
+ }
145
+
146
+ .video-panel {
147
+ display: flex;
148
+ flex-direction: column;
149
+ align-items: center;
150
+ justify-content: center;
151
+ }
152
+
153
+ .video-container {
154
+ width: 100%;
155
+ aspect-ratio: 4/3;
156
+ background-color: #050608;
157
+ border-radius: 12px;
158
+ overflow: hidden;
159
+ border: 1px solid rgba(255, 255, 255, 0.08);
160
+ position: relative;
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ }
165
+
166
+ .video-stream {
167
+ width: 100%;
168
+ height: 100%;
169
+ object-fit: cover;
170
+ }
171
+
172
+ .video-placeholder {
173
+ display: flex;
174
+ flex-direction: column;
175
+ align-items: center;
176
+ gap: 15px;
177
+ color: var(--text-muted);
178
+ text-align: center;
179
+ }
180
+
181
+ .video-placeholder svg {
182
+ width: 60px;
183
+ height: 60px;
184
+ stroke: var(--text-muted);
185
+ animation: rotate-sync 4s linear infinite;
186
+ }
187
+
188
+ .hud-overlay-badge {
189
+ position: absolute;
190
+ top: 15px;
191
+ right: 15px;
192
+ background: rgba(0, 0, 0, 0.7);
193
+ border: 1px solid var(--safe-color);
194
+ padding: 4px 12px;
195
+ border-radius: 6px;
196
+ font-size: 12px;
197
+ font-weight: 600;
198
+ color: var(--safe-color);
199
+ text-transform: uppercase;
200
+ letter-spacing: 1px;
201
+ box-shadow: 0 0 10px rgba(0, 255, 128, 0.2);
202
+ transition: all 0.3s ease;
203
+ }
204
+
205
+ /* --- Indicators Panel --- */
206
+ .indicators-grid {
207
+ display: grid;
208
+ grid-template-columns: repeat(3, 1fr);
209
+ gap: 20px;
210
+ }
211
+
212
+ .metric-card {
213
+ background: rgba(255, 255, 255, 0.02);
214
+ border: 1px solid rgba(255, 255, 255, 0.04);
215
+ border-radius: 14px;
216
+ padding: 18px;
217
+ display: flex;
218
+ flex-direction: column;
219
+ align-items: center;
220
+ justify-content: center;
221
+ text-align: center;
222
+ position: relative;
223
+ }
224
+
225
+ .metric-title {
226
+ font-size: 12px;
227
+ color: var(--text-muted);
228
+ text-transform: uppercase;
229
+ letter-spacing: 1px;
230
+ margin-bottom: 8px;
231
+ }
232
+
233
+ .metric-value {
234
+ font-size: 32px;
235
+ font-weight: 800;
236
+ font-family: 'Share Tech Mono', monospace;
237
+ }
238
+
239
+ /* Gauge Meter styling */
240
+ .gauge-container {
241
+ width: 100px;
242
+ height: 100px;
243
+ position: relative;
244
+ margin-bottom: 5px;
245
+ }
246
+
247
+ .radial-gauge {
248
+ width: 100%;
249
+ height: 100%;
250
+ transform: rotate(-90deg);
251
+ }
252
+
253
+ .gauge-circle {
254
+ fill: none;
255
+ stroke-width: 4;
256
+ stroke: rgba(255, 255, 255, 0.05);
257
+ }
258
+
259
+ .gauge-bar {
260
+ fill: none;
261
+ stroke-width: 6;
262
+ stroke-dasharray: 251.2;
263
+ stroke-dashoffset: 251.2;
264
+ stroke: var(--safe-color);
265
+ stroke-linecap: round;
266
+ transition: stroke-dashoffset 0.1s ease, stroke 0.3s ease;
267
+ }
268
+
269
+ .gauge-text {
270
+ position: absolute;
271
+ top: 50%;
272
+ left: 50%;
273
+ transform: translate(-50%, -50%);
274
+ font-size: 20px;
275
+ font-weight: 800;
276
+ font-family: 'Share Tech Mono', monospace;
277
+ color: var(--text-color);
278
+ }
279
+
280
+ /* --- RIGHT COLUMN: SLM TERMINAL & DIAGNOSTICS --- */
281
+ .right-column {
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 30px;
285
+ }
286
+
287
+ /* Chart Panel */
288
+ .chart-container {
289
+ height: 180px;
290
+ width: 100%;
291
+ }
292
+
293
+ /* SLM Conversation terminal */
294
+ .terminal-panel {
295
+ display: flex;
296
+ flex-direction: column;
297
+ height: 380px;
298
+ }
299
+
300
+ .terminal-header {
301
+ display: flex;
302
+ justify-content: space-between;
303
+ align-items: center;
304
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
305
+ padding-bottom: 12px;
306
+ margin-bottom: 15px;
307
+ }
308
+
309
+ .terminal-title {
310
+ font-size: 14px;
311
+ font-weight: 700;
312
+ letter-spacing: 1px;
313
+ text-transform: uppercase;
314
+ color: var(--accent-color);
315
+ display: flex;
316
+ align-items: center;
317
+ gap: 8px;
318
+ }
319
+
320
+ .terminal-body {
321
+ flex-grow: 1;
322
+ overflow-y: auto;
323
+ display: flex;
324
+ flex-direction: column;
325
+ gap: 15px;
326
+ padding-right: 5px;
327
+ font-family: 'Outfit', sans-serif;
328
+ scroll-behavior: smooth;
329
+ }
330
+
331
+ /* Scrollbar custom styling */
332
+ .terminal-body::-webkit-scrollbar {
333
+ width: 4px;
334
+ }
335
+ .terminal-body::-webkit-scrollbar-thumb {
336
+ background: rgba(255, 255, 255, 0.1);
337
+ border-radius: 2px;
338
+ }
339
+
340
+ /* Chat bubbles */
341
+ .chat-bubble {
342
+ max-width: 80%;
343
+ padding: 12px 16px;
344
+ border-radius: 12px;
345
+ font-size: 14px;
346
+ line-height: 1.5;
347
+ animation: slide-up 0.3s ease;
348
+ }
349
+
350
+ .driver-bubble {
351
+ align-self: flex-end;
352
+ background: rgba(0, 191, 255, 0.08);
353
+ border: 1px solid rgba(0, 191, 255, 0.15);
354
+ color: #dbeafe;
355
+ border-bottom-right-radius: 2px;
356
+ }
357
+
358
+ .slm-bubble {
359
+ align-self: flex-start;
360
+ background: rgba(0, 255, 128, 0.05);
361
+ border: 1px solid rgba(0, 255, 128, 0.12);
362
+ color: #dcfce7;
363
+ border-bottom-left-radius: 2px;
364
+ }
365
+
366
+ .system-bubble {
367
+ align-self: center;
368
+ background: rgba(255, 40, 80, 0.08);
369
+ border: 1px solid rgba(255, 40, 80, 0.15);
370
+ color: #ffe4e6;
371
+ max-width: 90%;
372
+ font-family: 'Share Tech Mono', monospace;
373
+ font-size: 12px;
374
+ text-transform: uppercase;
375
+ text-align: center;
376
+ border-radius: 6px;
377
+ padding: 6px 12px;
378
+ }
379
+
380
+ .bubble-label {
381
+ font-size: 10px;
382
+ font-weight: 800;
383
+ letter-spacing: 1px;
384
+ text-transform: uppercase;
385
+ margin-bottom: 4px;
386
+ display: block;
387
+ }
388
+
389
+ .driver-bubble .bubble-label {
390
+ color: var(--accent-color);
391
+ }
392
+
393
+ .slm-bubble .bubble-label {
394
+ color: var(--safe-color);
395
+ }
396
+
397
+ /* --- Control Desk Buttons --- */
398
+ .control-desk {
399
+ display: grid;
400
+ grid-template-columns: repeat(3, 1fr);
401
+ gap: 15px;
402
+ }
403
+
404
+ .cyber-btn {
405
+ background: rgba(255, 255, 255, 0.02);
406
+ border: 1px solid rgba(255, 255, 255, 0.06);
407
+ border-radius: 10px;
408
+ padding: 12px;
409
+ font-size: 13px;
410
+ font-weight: 600;
411
+ color: var(--text-color);
412
+ cursor: pointer;
413
+ display: flex;
414
+ flex-direction: column;
415
+ align-items: center;
416
+ justify-content: center;
417
+ gap: 6px;
418
+ transition: all 0.2s ease;
419
+ }
420
+
421
+ .cyber-btn:hover {
422
+ background: rgba(255, 255, 255, 0.06);
423
+ border-color: rgba(255, 255, 255, 0.15);
424
+ transform: translateY(-2px);
425
+ }
426
+
427
+ .cyber-btn:active {
428
+ transform: translateY(0);
429
+ }
430
+
431
+ .cyber-btn svg {
432
+ width: 20px;
433
+ height: 20px;
434
+ stroke: var(--text-color);
435
+ transition: all 0.2s ease;
436
+ }
437
+
438
+ .cyber-btn.active {
439
+ background: rgba(0, 255, 128, 0.08);
440
+ border-color: var(--safe-color);
441
+ color: var(--safe-color);
442
+ }
443
+
444
+ .cyber-btn.active svg {
445
+ stroke: var(--safe-color);
446
+ }
447
+
448
+ .cyber-btn.danger {
449
+ background: rgba(255, 40, 80, 0.05);
450
+ border-color: rgba(255, 40, 80, 0.2);
451
+ }
452
+
453
+ .cyber-btn.danger:hover {
454
+ background: rgba(255, 40, 80, 0.12);
455
+ border-color: var(--danger-color);
456
+ color: var(--danger-color);
457
+ }
458
+
459
+ .cyber-btn.danger:hover svg {
460
+ stroke: var(--danger-color);
461
+ }
462
+
463
+ /* --- DYNAMIC STATE OVERLAYS (WARNING SHADOWS) --- */
464
+ .glass-panel.state-normal::before {
465
+ background: linear-gradient(90deg, transparent, var(--safe-color), transparent);
466
+ }
467
+
468
+ .glass-panel.state-warn::before {
469
+ background: linear-gradient(90deg, transparent, var(--warn-color), transparent);
470
+ }
471
+ .glass-panel.state-warn {
472
+ border-color: rgba(255, 170, 0, 0.15);
473
+ box-shadow: 0 0 20px rgba(255, 170, 0, 0.1);
474
+ }
475
+
476
+ .glass-panel.state-danger::before {
477
+ background: linear-gradient(90deg, transparent, var(--danger-color), transparent);
478
+ }
479
+ .glass-panel.state-danger {
480
+ border-color: rgba(255, 40, 80, 0.3);
481
+ box-shadow: 0 0 30px rgba(255, 40, 80, 0.2);
482
+ animation: pulse-border 1.5s infinite;
483
+ }
484
+
485
+ /* --- Animations --- */
486
+ @keyframes pulse {
487
+ 0% { transform: scale(1); opacity: 1; box-shadow: 0 0 10px var(--safe-color); }
488
+ 50% { transform: scale(1.1); opacity: 0.7; box-shadow: 0 0 20px var(--safe-color); }
489
+ 100% { transform: scale(1); opacity: 1; box-shadow: 0 0 10px var(--safe-color); }
490
+ }
491
+
492
+ @keyframes pulse-border {
493
+ 0% { border-color: rgba(255, 40, 80, 0.2); }
494
+ 50% { border-color: rgba(255, 40, 80, 0.6); }
495
+ 100% { border-color: rgba(255, 40, 80, 0.2); }
496
+ }
497
+
498
+ @keyframes rotate-sync {
499
+ 0% { transform: rotate(0deg); }
500
+ 100% { transform: rotate(360deg); }
501
+ }
502
+
503
+ @keyframes slide-up {
504
+ 0% { transform: translateY(10px); opacity: 0; }
505
+ 100% { transform: translateY(0); opacity: 1; }
506
+ }
507
+
508
+ /* Responsive */
509
+ @media (max-width: 1200px) {
510
+ .dashboard-container {
511
+ grid-template-columns: 1fr;
512
+ padding: 20px;
513
+ }
514
+ }
515
+ </style>
516
+ </head>
517
+ <body>
518
+
519
+ <header>
520
+ <div class="logo-section">
521
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="url(#logo-grad)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
522
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
523
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
524
+ </svg>
525
+ <svg style="width:0;height:0;position:absolute">
526
+ <linearGradient id="logo-grad" x1="0%" y1="0%" x2="100%" y2="0%">
527
+ <stop offset="0%" stop-color="#00ff80"/>
528
+ <stop offset="100%" stop-color="#00bfff"/>
529
+ </linearGradient>
530
+ </svg>
531
+ <h1>DRIVESAFE</h1>
532
+ <span>LOCAL SLM V1.0</span>
533
+ </div>
534
+ <div class="connection-status">
535
+ <div class="pulse-dot"></div>
536
+ HUD SYSTEM RUNNING
537
+ </div>
538
+ </header>
539
+
540
+ <div class="dashboard-container">
541
+
542
+ <!-- LEFT COLUMN: VIDEO FEED & METRIC READOUTS -->
543
+ <div class="left-column">
544
+
545
+ <!-- Video feed Panel -->
546
+ <div class="glass-panel video-panel state-normal" id="main-panel">
547
+ <div class="video-container">
548
+ <img src="/video_feed" class="video-stream" id="video-feed" onerror="showPlaceholder()">
549
+ <div class="hud-overlay-badge" id="hud-state-badge">NORMAL</div>
550
+
551
+ <div class="video-placeholder" id="video-placeholder" style="display:none;">
552
+ <svg viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
553
+ <line x1="1" y1="1" x2="23" y2="23"></line>
554
+ <path d="M21 21H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3-3h6l2 3h4a2 2 0 0 1 2 2v9.34"></path>
555
+ <path d="M10.02 4.35L8.06 6H3"></path>
556
+ <circle cx="12" cy="13" r="4"></circle>
557
+ </svg>
558
+ <p>Awaiting Safe Driving Camera Stream...</p>
559
+ </div>
560
+ </div>
561
+ </div>
562
+
563
+ <!-- Core Indicators grid -->
564
+ <div class="indicators-grid">
565
+
566
+ <!-- Eye Aspect Ratio (EAR) Gauge -->
567
+ <div class="metric-card">
568
+ <div class="metric-title">Eye Aspect Ratio</div>
569
+ <div class="gauge-container">
570
+ <svg class="radial-gauge" viewBox="0 0 100 100">
571
+ <circle class="gauge-circle" cx="50" cy="50" r="40"></circle>
572
+ <circle class="gauge-bar" id="ear-gauge" cx="50" cy="50" r="40"></circle>
573
+ </svg>
574
+ <div class="gauge-text" id="ear-val">0.00</div>
575
+ </div>
576
+ </div>
577
+
578
+ <!-- Event counter -->
579
+ <div class="metric-card">
580
+ <div class="metric-title">Drowsy Detections</div>
581
+ <div class="metric-value" id="drowsiness-count" style="color: var(--safe-color);">0</div>
582
+ </div>
583
+
584
+ <!-- System FPS -->
585
+ <div class="metric-card">
586
+ <div class="metric-title">Core Engine Speed</div>
587
+ <div class="metric-value" id="engine-fps" style="color: var(--accent-color);">0 FPS</div>
588
+ </div>
589
+
590
+ </div>
591
+
592
+ </div>
593
+
594
+ <!-- RIGHT COLUMN: CHAT LOG, CHART & DESK CONTROLS -->
595
+ <div class="right-column">
596
+
597
+ <!-- Real-time Line Graph Panel -->
598
+ <div class="glass-panel">
599
+ <div class="metric-title" style="margin-bottom:12px;">Active EKG Eye Waveform</div>
600
+ <div class="chart-container">
601
+ <canvas id="earChart"></canvas>
602
+ </div>
603
+ </div>
604
+
605
+ <!-- DriveSafe Conversational Terminal -->
606
+ <div class="glass-panel terminal-panel">
607
+ <div class="terminal-header">
608
+ <div class="terminal-title">
609
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
610
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
611
+ </svg>
612
+ COGNITIVE VOICE ASSISTANT
613
+ </div>
614
+ <span id="slm-indicator" style="font-size:10px; font-family:'Share Tech Mono', monospace; color: var(--safe-color);">SLM ONLINE</span>
615
+ </div>
616
+
617
+ <div class="terminal-body" id="chat-box">
618
+ <div class="chat-bubble slm-bubble">
619
+ <span class="bubble-label">DriveSafe SLM</span>
620
+ Hello driver! I am active. Let's drive safely today. Ask me any question, or say "play music"!
621
+ </div>
622
+ </div>
623
+ </div>
624
+
625
+ <!-- Cyber Control Desk -->
626
+ <div class="control-desk">
627
+ <button class="cyber-btn active" id="toggle-tracking-btn" onclick="toggleTracking()">
628
+ <svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
629
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
630
+ <circle cx="12" cy="12" r="3"></circle>
631
+ </svg>
632
+ <span>ACTIVE TRACKING</span>
633
+ </button>
634
+
635
+ <button class="cyber-btn" onclick="triggerReset()">
636
+ <svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
637
+ <path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"></path>
638
+ </svg>
639
+ <span>SYSTEM RESET</span>
640
+ </button>
641
+
642
+ <button class="cyber-btn danger" onclick="triggerMusic()">
643
+ <svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
644
+ <path d="M9 18V5l12-2v13"></path>
645
+ <circle cx="6" cy="18" r="3"></circle>
646
+ <circle cx="18" cy="16" r="3"></circle>
647
+ </svg>
648
+ <span>PLAY BEATS</span>
649
+ </button>
650
+ </div>
651
+
652
+ </div>
653
+
654
+ </div>
655
+
656
+ <!-- Script file mapping -->
657
+ <script src="/static/js/app.js"></script>
658
+ <script>
659
+ function showPlaceholder() {
660
+ document.getElementById('video-feed').style.display = 'none';
661
+ document.getElementById('video-placeholder').style.display = 'flex';
662
+ }
663
+ </script>
664
+ </body>
665
+ </html>