Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- README_DOCKER.md +127 -0
- app.py +60 -16
- templates/index.html +223 -2
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!* 🚗💨
|
app.py
CHANGED
|
@@ -27,6 +27,7 @@ import re
|
|
| 27 |
from dotenv import load_dotenv
|
| 28 |
import urllib.parse
|
| 29 |
import urllib.request
|
|
|
|
| 30 |
|
| 31 |
load_dotenv()
|
| 32 |
|
|
@@ -77,6 +78,25 @@ def index():
|
|
| 77 |
def api_status():
|
| 78 |
return jsonify(system_status)
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
@app.route('/api/frame', methods=['POST'])
|
| 81 |
def api_frame():
|
| 82 |
global start_closed_time, last_announced_state
|
|
@@ -195,13 +215,12 @@ def api_voice():
|
|
| 195 |
html = urllib.request.urlopen(f'https://www.youtube.com/results?search_query={query}').read().decode()
|
| 196 |
video_ids = re.findall(r'watch\?v=(\S{11})', html)
|
| 197 |
if video_ids:
|
| 198 |
-
|
| 199 |
-
return jsonify({"speak": f"Playing {stripped} on YouTube Music to keep you alert.", "action": "open_url", "url": url})
|
| 200 |
else:
|
| 201 |
return jsonify({"speak": "I couldn't find that song."})
|
| 202 |
except Exception as e:
|
| 203 |
print(f"[YouTube Search Error] {repr(e)}")
|
| 204 |
-
return jsonify({"speak": "I had trouble searching for the song on YouTube. It might be blocking automated requests."})
|
| 205 |
return jsonify({})
|
| 206 |
|
| 207 |
# Normal wake-word logic
|
|
@@ -212,9 +231,9 @@ def api_voice():
|
|
| 212 |
active_listening = False
|
| 213 |
return jsonify({"speak": "Goodbye."})
|
| 214 |
else:
|
| 215 |
-
if '
|
| 216 |
return jsonify({})
|
| 217 |
-
idx = text.find('
|
| 218 |
command = re.sub(r'[^\w\s]', '', text[idx:]).strip()
|
| 219 |
|
| 220 |
if len(command) < 3:
|
|
@@ -223,26 +242,43 @@ def api_voice():
|
|
| 223 |
|
| 224 |
print(f'[Voice API] Command: "{command}"')
|
| 225 |
|
| 226 |
-
# Media Commands (
|
| 227 |
-
|
| 228 |
-
if
|
| 229 |
-
return jsonify({"speak": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
if command.lower().startswith('play '):
|
| 232 |
song = command[5:].strip()
|
|
|
|
|
|
|
| 233 |
if song:
|
| 234 |
try:
|
| 235 |
query = urllib.parse.quote_plus(song)
|
| 236 |
html = urllib.request.urlopen(f'https://www.youtube.com/results?search_query={query}').read().decode()
|
| 237 |
video_ids = re.findall(r'watch\?v=(\S{11})', html)
|
| 238 |
if video_ids:
|
| 239 |
-
|
| 240 |
-
return jsonify({"speak": f"Playing {song} on YouTube Music.", "action": "open_url", "url": url})
|
| 241 |
else:
|
| 242 |
return jsonify({"speak": "I couldn't find that song."})
|
| 243 |
except Exception as e:
|
| 244 |
print(f"[YouTube Search Error] {repr(e)}")
|
| 245 |
-
return jsonify({"speak": "I had trouble searching for the song on YouTube. It might be blocking automated requests."})
|
| 246 |
|
| 247 |
# LLM Interaction
|
| 248 |
api_key = os.getenv('API_KEY', '')
|
|
@@ -252,9 +288,11 @@ def api_voice():
|
|
| 252 |
try:
|
| 253 |
llm = OpenAI(api_key=api_key, base_url='https://integrate.api.nvidia.com/v1')
|
| 254 |
SYS_PROMPT = (
|
| 255 |
-
'You are
|
| 256 |
-
'
|
| 257 |
-
'
|
|
|
|
|
|
|
| 258 |
)
|
| 259 |
resp = llm.chat.completions.create(
|
| 260 |
model='meta/llama-3.1-8b-instruct',
|
|
@@ -288,7 +326,13 @@ def api_chat():
|
|
| 288 |
|
| 289 |
try:
|
| 290 |
client = OpenAI(api_key=api_key, base_url="https://integrate.api.nvidia.com/v1")
|
| 291 |
-
sys_prompt =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
completion = client.chat.completions.create(
|
| 293 |
model='meta/llama-3.1-8b-instruct',
|
| 294 |
max_tokens=300,
|
|
|
|
| 27 |
from dotenv import load_dotenv
|
| 28 |
import urllib.parse
|
| 29 |
import urllib.request
|
| 30 |
+
import subprocess
|
| 31 |
|
| 32 |
load_dotenv()
|
| 33 |
|
|
|
|
| 78 |
def api_status():
|
| 79 |
return jsonify(system_status)
|
| 80 |
|
| 81 |
+
@app.route('/api/music_url', methods=['GET'])
|
| 82 |
+
def api_music_url():
|
| 83 |
+
video_id = request.args.get('video_id')
|
| 84 |
+
if not video_id:
|
| 85 |
+
return jsonify({"error": "No video_id provided"}), 400
|
| 86 |
+
try:
|
| 87 |
+
cmd = ["yt-dlp", "-J", "-f", "bestaudio", f"https://www.youtube.com/watch?v={video_id}"]
|
| 88 |
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
| 89 |
+
info = json.loads(result.stdout)
|
| 90 |
+
return jsonify({
|
| 91 |
+
"url": info.get("url"),
|
| 92 |
+
"title": info.get("title"),
|
| 93 |
+
"artist": info.get("uploader"),
|
| 94 |
+
"thumbnail": info.get("thumbnail")
|
| 95 |
+
})
|
| 96 |
+
except Exception as e:
|
| 97 |
+
print(f"[yt-dlp error] {repr(e)}")
|
| 98 |
+
return jsonify({"error": str(e)}), 500
|
| 99 |
+
|
| 100 |
@app.route('/api/frame', methods=['POST'])
|
| 101 |
def api_frame():
|
| 102 |
global start_closed_time, last_announced_state
|
|
|
|
| 215 |
html = urllib.request.urlopen(f'https://www.youtube.com/results?search_query={query}').read().decode()
|
| 216 |
video_ids = re.findall(r'watch\?v=(\S{11})', html)
|
| 217 |
if video_ids:
|
| 218 |
+
return jsonify({"speak": f"Playing {stripped} to keep you alert.", "action": "play_native", "video_ids": video_ids[:20], "title": stripped})
|
|
|
|
| 219 |
else:
|
| 220 |
return jsonify({"speak": "I couldn't find that song."})
|
| 221 |
except Exception as e:
|
| 222 |
print(f"[YouTube Search Error] {repr(e)}")
|
| 223 |
+
return jsonify({"speak": "I had trouble searching for the song on YouTube Music. It might be blocking automated requests."})
|
| 224 |
return jsonify({})
|
| 225 |
|
| 226 |
# Normal wake-word logic
|
|
|
|
| 231 |
active_listening = False
|
| 232 |
return jsonify({"speak": "Goodbye."})
|
| 233 |
else:
|
| 234 |
+
if 'lara' not in text:
|
| 235 |
return jsonify({})
|
| 236 |
+
idx = text.find('lara') + len('lara')
|
| 237 |
command = re.sub(r'[^\w\s]', '', text[idx:]).strip()
|
| 238 |
|
| 239 |
if len(command) < 3:
|
|
|
|
| 242 |
|
| 243 |
print(f'[Voice API] Command: "{command}"')
|
| 244 |
|
| 245 |
+
# Media Commands (Native Player Control)
|
| 246 |
+
cmd = command.lower()
|
| 247 |
+
if 'pause' in cmd:
|
| 248 |
+
return jsonify({"speak": "Pausing.", "action": "pause_native"})
|
| 249 |
+
if 'resume' in cmd or cmd == 'play':
|
| 250 |
+
return jsonify({"speak": "Resuming.", "action": "resume_native"})
|
| 251 |
+
if 'stop' in cmd:
|
| 252 |
+
return jsonify({"speak": "Stopping.", "action": "stop_native"})
|
| 253 |
+
if 'next' in cmd or 'skip' in cmd:
|
| 254 |
+
return jsonify({"speak": "Skipping.", "action": "next_native"})
|
| 255 |
+
if 'previous' in cmd or 'back' in cmd:
|
| 256 |
+
return jsonify({"speak": "Going back.", "action": "prev_native"})
|
| 257 |
+
if 'volume up' in cmd or 'louder' in cmd or 'increase volume' in cmd:
|
| 258 |
+
return jsonify({"speak": "Volume up.", "action": "vol_up_native"})
|
| 259 |
+
if 'volume down' in cmd or 'quieter' in cmd or 'decrease volume' in cmd:
|
| 260 |
+
return jsonify({"speak": "Volume down.", "action": "vol_down_native"})
|
| 261 |
+
if 'unmute' in cmd:
|
| 262 |
+
return jsonify({"speak": "Unmuting.", "action": "unmute_native"})
|
| 263 |
+
elif 'mute' in cmd:
|
| 264 |
+
return jsonify({"speak": "Muting.", "action": "mute_native"})
|
| 265 |
|
| 266 |
if command.lower().startswith('play '):
|
| 267 |
song = command[5:].strip()
|
| 268 |
+
if song.lower() in ['music', 'song', 'the music', 'the song', 'it']:
|
| 269 |
+
return jsonify({"speak": "Resuming.", "action": "resume_native"})
|
| 270 |
if song:
|
| 271 |
try:
|
| 272 |
query = urllib.parse.quote_plus(song)
|
| 273 |
html = urllib.request.urlopen(f'https://www.youtube.com/results?search_query={query}').read().decode()
|
| 274 |
video_ids = re.findall(r'watch\?v=(\S{11})', html)
|
| 275 |
if video_ids:
|
| 276 |
+
return jsonify({"speak": f"Playing {song}.", "action": "play_native", "video_ids": video_ids[:20], "title": song})
|
|
|
|
| 277 |
else:
|
| 278 |
return jsonify({"speak": "I couldn't find that song."})
|
| 279 |
except Exception as e:
|
| 280 |
print(f"[YouTube Search Error] {repr(e)}")
|
| 281 |
+
return jsonify({"speak": "I had trouble searching for the song on YouTube Music. It might be blocking automated requests."})
|
| 282 |
|
| 283 |
# LLM Interaction
|
| 284 |
api_key = os.getenv('API_KEY', '')
|
|
|
|
| 288 |
try:
|
| 289 |
llm = OpenAI(api_key=api_key, base_url='https://integrate.api.nvidia.com/v1')
|
| 290 |
SYS_PROMPT = (
|
| 291 |
+
'You are Lara, a voice assistant and copilot for a driver. '
|
| 292 |
+
'You MUST ONLY assist with driving, road-related questions, directions, routes, and playing music. '
|
| 293 |
+
'If the user asks about ANYTHING else, politely refuse to answer and state that you can only help with driving and music. '
|
| 294 |
+
'Provide helpful, concise, and clear answers so the driver can stay focused on the road. '
|
| 295 |
+
'Keep your responses brief (1-2 sentences max) and do not use conversational filler.'
|
| 296 |
)
|
| 297 |
resp = llm.chat.completions.create(
|
| 298 |
model='meta/llama-3.1-8b-instruct',
|
|
|
|
| 326 |
|
| 327 |
try:
|
| 328 |
client = OpenAI(api_key=api_key, base_url="https://integrate.api.nvidia.com/v1")
|
| 329 |
+
sys_prompt = (
|
| 330 |
+
'You are Lara, a voice assistant and copilot for a driver. '
|
| 331 |
+
'You MUST ONLY assist with driving, road-related questions, directions, routes, and playing music. '
|
| 332 |
+
'If the user asks about ANYTHING else, politely refuse to answer and state that you can only help with driving and music. '
|
| 333 |
+
'Provide helpful, concise, and clear answers so the driver can stay focused on the road. '
|
| 334 |
+
'Keep your responses brief (1-2 sentences max) and do not use conversational filler.'
|
| 335 |
+
)
|
| 336 |
completion = client.chat.completions.create(
|
| 337 |
model='meta/llama-3.1-8b-instruct',
|
| 338 |
max_tokens=300,
|
templates/index.html
CHANGED
|
@@ -466,6 +466,100 @@
|
|
| 466 |
justify-content: center;
|
| 467 |
}
|
| 468 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
</style>
|
| 470 |
</head>
|
| 471 |
|
|
@@ -499,6 +593,28 @@
|
|
| 499 |
<strong id="drowsyEvents" style="color: var(--text-main);">0</strong>
|
| 500 |
</div>
|
| 501 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
</aside>
|
| 503 |
|
| 504 |
<!-- Main Content -->
|
|
@@ -520,6 +636,11 @@
|
|
| 520 |
</div>
|
| 521 |
</div>
|
| 522 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
<!-- Chat Tab -->
|
| 524 |
<div class="tab-content" id="chat-tab">
|
| 525 |
<div class="chat-area" id="chatHistory">
|
|
@@ -540,6 +661,7 @@
|
|
| 540 |
</main>
|
| 541 |
</div>
|
| 542 |
|
|
|
|
| 543 |
<script>
|
| 544 |
// Tab Navigation
|
| 545 |
const tabBtns = document.querySelectorAll('.tab-btn');
|
|
@@ -584,6 +706,71 @@
|
|
| 584 |
let isHardwareActive = false;
|
| 585 |
let isSpeaking = false;
|
| 586 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
// TTS Engine
|
| 588 |
function speakOutLoud(text) {
|
| 589 |
if (!text) return;
|
|
@@ -637,11 +824,45 @@
|
|
| 637 |
if (data.speak) {
|
| 638 |
speakOutLoud(data.speak);
|
| 639 |
}
|
| 640 |
-
if (data.action === "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
let newWin = window.open(data.url, "_blank");
|
| 642 |
if (!newWin || newWin.closed || typeof newWin.closed == 'undefined') {
|
| 643 |
// Popup blocked
|
| 644 |
-
appendMessage('system', `⚠️ **Popup Blocked**<br>I tried to open the song, but your browser blocked it.<br><a href="${data.url}" target="_blank" style="color:var(--primary); font-weight:bold; font-size:1.1rem;">👉 Click Here to Open YouTube Music</a>
|
| 645 |
}
|
| 646 |
}
|
| 647 |
if (data.text) {
|
|
|
|
| 466 |
justify-content: center;
|
| 467 |
}
|
| 468 |
}
|
| 469 |
+
|
| 470 |
+
/* Modern Media Player */
|
| 471 |
+
.modern-media-player {
|
| 472 |
+
background: #2a2a2a;
|
| 473 |
+
border-radius: 24px;
|
| 474 |
+
padding: 10px 14px;
|
| 475 |
+
display: flex;
|
| 476 |
+
align-items: center;
|
| 477 |
+
gap: 14px;
|
| 478 |
+
color: white;
|
| 479 |
+
position: relative;
|
| 480 |
+
margin-top: 1.5rem;
|
| 481 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
| 482 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 483 |
+
}
|
| 484 |
+
.mmp-art {
|
| 485 |
+
width: 50px;
|
| 486 |
+
height: 50px;
|
| 487 |
+
border-radius: 12px;
|
| 488 |
+
overflow: hidden;
|
| 489 |
+
flex-shrink: 0;
|
| 490 |
+
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
| 491 |
+
}
|
| 492 |
+
.mmp-art img {
|
| 493 |
+
width: 100%;
|
| 494 |
+
height: 100%;
|
| 495 |
+
object-fit: cover;
|
| 496 |
+
}
|
| 497 |
+
.mmp-info {
|
| 498 |
+
flex-grow: 1;
|
| 499 |
+
display: flex;
|
| 500 |
+
flex-direction: column;
|
| 501 |
+
justify-content: center;
|
| 502 |
+
min-width: 0;
|
| 503 |
+
}
|
| 504 |
+
.mmp-source {
|
| 505 |
+
font-size: 0.7rem;
|
| 506 |
+
color: #b3b3b3;
|
| 507 |
+
display: flex;
|
| 508 |
+
align-items: center;
|
| 509 |
+
gap: 5px;
|
| 510 |
+
margin-bottom: 2px;
|
| 511 |
+
}
|
| 512 |
+
.mmp-title {
|
| 513 |
+
font-size: 0.95rem;
|
| 514 |
+
font-weight: 600;
|
| 515 |
+
white-space: nowrap;
|
| 516 |
+
overflow: hidden;
|
| 517 |
+
text-overflow: ellipsis;
|
| 518 |
+
margin-bottom: 1px;
|
| 519 |
+
color: #ffffff;
|
| 520 |
+
}
|
| 521 |
+
.mmp-artist {
|
| 522 |
+
font-size: 0.8rem;
|
| 523 |
+
color: #b3b3b3;
|
| 524 |
+
white-space: nowrap;
|
| 525 |
+
overflow: hidden;
|
| 526 |
+
text-overflow: ellipsis;
|
| 527 |
+
}
|
| 528 |
+
.mmp-controls {
|
| 529 |
+
display: flex;
|
| 530 |
+
align-items: center;
|
| 531 |
+
gap: 12px;
|
| 532 |
+
margin-right: 18px;
|
| 533 |
+
}
|
| 534 |
+
.mmp-btn {
|
| 535 |
+
background: none;
|
| 536 |
+
border: none;
|
| 537 |
+
color: white;
|
| 538 |
+
font-size: 1.1rem;
|
| 539 |
+
cursor: pointer;
|
| 540 |
+
display: flex;
|
| 541 |
+
align-items: center;
|
| 542 |
+
justify-content: center;
|
| 543 |
+
padding: 4px;
|
| 544 |
+
transition: transform 0.1s;
|
| 545 |
+
}
|
| 546 |
+
.mmp-btn:active {
|
| 547 |
+
transform: scale(0.9);
|
| 548 |
+
}
|
| 549 |
+
.mmp-btn.play-pause {
|
| 550 |
+
font-size: 1.4rem;
|
| 551 |
+
}
|
| 552 |
+
.mmp-btn:hover {
|
| 553 |
+
color: #e0e0e0;
|
| 554 |
+
}
|
| 555 |
+
.mmp-expand {
|
| 556 |
+
position: absolute;
|
| 557 |
+
top: 14px;
|
| 558 |
+
right: 14px;
|
| 559 |
+
color: #b3b3b3;
|
| 560 |
+
font-size: 0.85rem;
|
| 561 |
+
cursor: pointer;
|
| 562 |
+
}
|
| 563 |
</style>
|
| 564 |
</head>
|
| 565 |
|
|
|
|
| 593 |
<strong id="drowsyEvents" style="color: var(--text-main);">0</strong>
|
| 594 |
</div>
|
| 595 |
</div>
|
| 596 |
+
|
| 597 |
+
<!-- MODERN MEDIA PLAYER -->
|
| 598 |
+
<div class="modern-media-player" id="custom-media-player" style="display:none;">
|
| 599 |
+
<div class="mmp-art">
|
| 600 |
+
<img src="https://via.placeholder.com/60" alt="Album Art" id="mmp-thumbnail">
|
| 601 |
+
</div>
|
| 602 |
+
<div class="mmp-info">
|
| 603 |
+
<div class="mmp-source">
|
| 604 |
+
<i class="fa-regular fa-circle-play"></i> This copilot
|
| 605 |
+
</div>
|
| 606 |
+
<div class="mmp-title" id="mmp-title">Unknown Song</div>
|
| 607 |
+
<div class="mmp-artist" id="mmp-artist">Unknown Artist</div>
|
| 608 |
+
</div>
|
| 609 |
+
<div class="mmp-controls">
|
| 610 |
+
<button class="mmp-btn" id="mmp-prev" onclick="if(ytPlayer && typeof ytPlayer.previousVideo === 'function') ytPlayer.previousVideo()"><i class="fa-solid fa-backward-step"></i></button>
|
| 611 |
+
<button class="mmp-btn play-pause" id="mmp-play-pause" onclick="toggleMmpPlay()"><i class="fa-solid fa-pause"></i></button>
|
| 612 |
+
<button class="mmp-btn" id="mmp-next" onclick="if(ytPlayer && typeof ytPlayer.nextVideo === 'function') ytPlayer.nextVideo()"><i class="fa-solid fa-forward-step"></i></button>
|
| 613 |
+
</div>
|
| 614 |
+
<div class="mmp-expand">
|
| 615 |
+
<i class="fa-solid fa-chevron-down"></i>
|
| 616 |
+
</div>
|
| 617 |
+
</div>
|
| 618 |
</aside>
|
| 619 |
|
| 620 |
<!-- Main Content -->
|
|
|
|
| 636 |
</div>
|
| 637 |
</div>
|
| 638 |
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
<!-- NATIVE AUDIO PLAYER -->
|
| 642 |
+
<audio id="native-audio" style="display:none;"></audio>
|
| 643 |
+
|
| 644 |
<!-- Chat Tab -->
|
| 645 |
<div class="tab-content" id="chat-tab">
|
| 646 |
<div class="chat-area" id="chatHistory">
|
|
|
|
| 661 |
</main>
|
| 662 |
</div>
|
| 663 |
|
| 664 |
+
<!-- No external iframe scripts needed -->
|
| 665 |
<script>
|
| 666 |
// Tab Navigation
|
| 667 |
const tabBtns = document.querySelectorAll('.tab-btn');
|
|
|
|
| 706 |
let isHardwareActive = false;
|
| 707 |
let isSpeaking = false;
|
| 708 |
|
| 709 |
+
|
| 710 |
+
// --- NATIVE AUDIO API ---
|
| 711 |
+
const nativeAudio = document.getElementById('native-audio');
|
| 712 |
+
let isMmpPlaying = false;
|
| 713 |
+
|
| 714 |
+
nativeAudio.addEventListener('play', () => {
|
| 715 |
+
document.getElementById('mmp-play-pause').innerHTML = '<i class="fa-solid fa-pause"></i>';
|
| 716 |
+
isMmpPlaying = true;
|
| 717 |
+
});
|
| 718 |
+
|
| 719 |
+
nativeAudio.addEventListener('pause', () => {
|
| 720 |
+
document.getElementById('mmp-play-pause').innerHTML = '<i class="fa-solid fa-play"></i>';
|
| 721 |
+
isMmpPlaying = false;
|
| 722 |
+
});
|
| 723 |
+
|
| 724 |
+
nativeAudio.addEventListener('ended', () => {
|
| 725 |
+
document.getElementById('mmp-play-pause').innerHTML = '<i class="fa-solid fa-play"></i>';
|
| 726 |
+
isMmpPlaying = false;
|
| 727 |
+
if (currentQueue.length > currentQueueIndex + 1) {
|
| 728 |
+
currentQueueIndex++;
|
| 729 |
+
playNativeAudio(currentQueue[currentQueueIndex], "Up Next...");
|
| 730 |
+
}
|
| 731 |
+
});
|
| 732 |
+
|
| 733 |
+
let currentQueue = [];
|
| 734 |
+
let currentQueueIndex = 0;
|
| 735 |
+
|
| 736 |
+
function toggleMmpPlay() {
|
| 737 |
+
if (isMmpPlaying) {
|
| 738 |
+
nativeAudio.pause();
|
| 739 |
+
} else {
|
| 740 |
+
nativeAudio.play();
|
| 741 |
+
}
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
async function playNativeAudio(videoId, titleGuess) {
|
| 745 |
+
document.getElementById('custom-media-player').style.display = 'flex';
|
| 746 |
+
document.getElementById('mmp-thumbnail').src = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
|
| 747 |
+
document.getElementById('mmp-title').innerText = "Extracting Stream...";
|
| 748 |
+
document.getElementById('mmp-artist').innerText = titleGuess || "Loading";
|
| 749 |
+
document.getElementById('mmp-play-pause').innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
| 750 |
+
|
| 751 |
+
try {
|
| 752 |
+
const res = await fetch(`/api/music_url?video_id=${videoId}`);
|
| 753 |
+
const data = await res.json();
|
| 754 |
+
if (data.url) {
|
| 755 |
+
nativeAudio.src = data.url;
|
| 756 |
+
nativeAudio.volume = 1.0;
|
| 757 |
+
nativeAudio.play();
|
| 758 |
+
document.getElementById('mmp-title').innerText = data.title || titleGuess;
|
| 759 |
+
document.getElementById('mmp-artist').innerText = data.artist || "yt-dlp Engine";
|
| 760 |
+
if (data.thumbnail) {
|
| 761 |
+
document.getElementById('mmp-thumbnail').src = data.thumbnail;
|
| 762 |
+
}
|
| 763 |
+
} else {
|
| 764 |
+
document.getElementById('mmp-title').innerText = "Stream Failed";
|
| 765 |
+
document.getElementById('mmp-play-pause').innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
| 766 |
+
}
|
| 767 |
+
} catch (err) {
|
| 768 |
+
console.error("Audio fetch error:", err);
|
| 769 |
+
document.getElementById('mmp-title').innerText = "Network Error";
|
| 770 |
+
document.getElementById('mmp-play-pause').innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
| 771 |
+
}
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
// TTS Engine
|
| 775 |
function speakOutLoud(text) {
|
| 776 |
if (!text) return;
|
|
|
|
| 824 |
if (data.speak) {
|
| 825 |
speakOutLoud(data.speak);
|
| 826 |
}
|
| 827 |
+
if (data.action === "play_native" && data.video_ids && data.video_ids.length > 0) {
|
| 828 |
+
currentQueue = data.video_ids;
|
| 829 |
+
currentQueueIndex = 0;
|
| 830 |
+
playNativeAudio(currentQueue[currentQueueIndex], data.title);
|
| 831 |
+
} else if (data.action === "pause_native") {
|
| 832 |
+
nativeAudio.pause();
|
| 833 |
+
} else if (data.action === "resume_native") {
|
| 834 |
+
nativeAudio.play();
|
| 835 |
+
} else if (data.action === "stop_native") {
|
| 836 |
+
nativeAudio.pause();
|
| 837 |
+
nativeAudio.currentTime = 0;
|
| 838 |
+
document.getElementById('custom-media-player').style.display = 'none';
|
| 839 |
+
} else if (data.action === "next_native") {
|
| 840 |
+
if (currentQueue.length > currentQueueIndex + 1) {
|
| 841 |
+
currentQueueIndex++;
|
| 842 |
+
playNativeAudio(currentQueue[currentQueueIndex], "Up Next...");
|
| 843 |
+
} else {
|
| 844 |
+
appendMessage('assistant', "End of queue reached.");
|
| 845 |
+
}
|
| 846 |
+
} else if (data.action === "prev_native") {
|
| 847 |
+
if (currentQueueIndex > 0) {
|
| 848 |
+
currentQueueIndex--;
|
| 849 |
+
playNativeAudio(currentQueue[currentQueueIndex], "Going Back...");
|
| 850 |
+
} else {
|
| 851 |
+
appendMessage('assistant', "No previous song in queue.");
|
| 852 |
+
}
|
| 853 |
+
} else if (data.action === "vol_up_native") {
|
| 854 |
+
nativeAudio.volume = Math.min(1.0, nativeAudio.volume + 0.2);
|
| 855 |
+
} else if (data.action === "vol_down_native") {
|
| 856 |
+
nativeAudio.volume = Math.max(0.0, nativeAudio.volume - 0.2);
|
| 857 |
+
} else if (data.action === "mute_native") {
|
| 858 |
+
nativeAudio.muted = true;
|
| 859 |
+
} else if (data.action === "unmute_native") {
|
| 860 |
+
nativeAudio.muted = false;
|
| 861 |
+
} else if (data.action === "open_url" && data.url) {
|
| 862 |
let newWin = window.open(data.url, "_blank");
|
| 863 |
if (!newWin || newWin.closed || typeof newWin.closed == 'undefined') {
|
| 864 |
// Popup blocked
|
| 865 |
+
appendMessage('system', `⚠️ **Popup Blocked**<br>I tried to open the song, but your browser blocked it.<br><a href="${data.url}" target="_blank" style="color:var(--primary); font-weight:bold; font-size:1.1rem;">👉 Click Here to Open YouTube Music</a>`);
|
| 866 |
}
|
| 867 |
}
|
| 868 |
if (data.text) {
|