Spaces:
Runtime error
Runtime error
Upload 9 files
Browse files- Dockerfile +89 -0
- Modelfile +30 -0
- README_DOCKER.md +127 -0
- audio/calm_beep.wav +0 -0
- audio/urgent_beep.wav +0 -0
- main.py +966 -0
- requirements.txt +10 -0
- static/js/app.js +288 -0
- 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 |
+
'&': '&',
|
| 282 |
+
'<': '<',
|
| 283 |
+
'>': '>',
|
| 284 |
+
"'": ''',
|
| 285 |
+
'"': '"'
|
| 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>
|