Sync from GitHub via huggingface-sync-action
Browse files- demo.js +59 -8
- index.html +2 -1
- judgy_reachy_no_phone/detection.py +204 -15
- judgy_reachy_no_phone/main.py +47 -5
- judgy_reachy_no_phone/static/index.html +8 -2
- judgy_reachy_no_phone/static/main.js +67 -1
- judgy_reachy_no_phone/static/style.css +47 -0
- style.css +1 -545
demo.js
CHANGED
|
@@ -106,23 +106,53 @@ async function init() {
|
|
| 106 |
cameraBtn.disabled = true;
|
| 107 |
startBtn.disabled = true;
|
| 108 |
|
|
|
|
|
|
|
|
|
|
| 109 |
// Show loader
|
| 110 |
showLoader('Loading YOLO26m model...');
|
| 111 |
statusText.textContent = 'Loading AI model...';
|
| 112 |
statusIndicator.className = 'status-dot loading';
|
| 113 |
|
| 114 |
-
//
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
showLoader('Loading processor...');
|
| 121 |
processor = await AutoProcessor.from_pretrained('onnx-community/yolo26m-ONNX');
|
| 122 |
|
| 123 |
// Hide loader
|
| 124 |
hideLoader();
|
| 125 |
-
statusText.textContent =
|
|
|
|
|
|
|
| 126 |
statusIndicator.className = 'status-dot ready';
|
| 127 |
cameraBtn.disabled = false;
|
| 128 |
|
|
@@ -284,9 +314,8 @@ async function detectAndProcess() {
|
|
| 284 |
async function detectPhoneAndGetBoxes() {
|
| 285 |
try {
|
| 286 |
// Resize for faster inference (trade accuracy for speed)
|
| 287 |
-
const targetWidth =
|
| 288 |
const targetHeight = Math.round((targetWidth / offscreen.width) * offscreen.height);
|
| 289 |
-
console.log(`Detection resolution: ${targetWidth}x${targetHeight}`);
|
| 290 |
|
| 291 |
// Create smaller canvas for YOLO
|
| 292 |
const smallCanvas = document.createElement('canvas');
|
|
@@ -531,5 +560,27 @@ startBtn.addEventListener('click', async () => {
|
|
| 531 |
}
|
| 532 |
});
|
| 533 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
// Initialize on load
|
| 535 |
init();
|
|
|
|
| 106 |
cameraBtn.disabled = true;
|
| 107 |
startBtn.disabled = true;
|
| 108 |
|
| 109 |
+
// Detect mobile device
|
| 110 |
+
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
| 111 |
+
|
| 112 |
// Show loader
|
| 113 |
showLoader('Loading YOLO26m model...');
|
| 114 |
statusText.textContent = 'Loading AI model...';
|
| 115 |
statusIndicator.className = 'status-dot loading';
|
| 116 |
|
| 117 |
+
// Try WebGPU first on desktop, use WASM directly on mobile
|
| 118 |
+
let modelLoaded = false;
|
| 119 |
+
|
| 120 |
+
if (!isMobile) {
|
| 121 |
+
// Desktop: Try WebGPU first (faster)
|
| 122 |
+
try {
|
| 123 |
+
console.log('Attempting to load model with WebGPU...');
|
| 124 |
+
model = await AutoModel.from_pretrained('onnx-community/yolo26m-ONNX', {
|
| 125 |
+
device: 'webgpu',
|
| 126 |
+
dtype: 'fp16'
|
| 127 |
+
});
|
| 128 |
+
console.log('✅ Model loaded on WebGPU (fast)');
|
| 129 |
+
modelLoaded = true;
|
| 130 |
+
} catch (webgpuError) {
|
| 131 |
+
console.warn('WebGPU failed, falling back to WASM:', webgpuError);
|
| 132 |
+
}
|
| 133 |
+
} else {
|
| 134 |
+
console.log('Mobile device detected, using WASM backend');
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Fallback to WASM (works everywhere, including mobile)
|
| 138 |
+
if (!modelLoaded) {
|
| 139 |
+
showLoader(isMobile ? 'Loading model (mobile-optimized)...' : 'WebGPU not available, using WASM...');
|
| 140 |
+
console.log('Loading model with WASM backend...');
|
| 141 |
+
|
| 142 |
+
model = await AutoModel.from_pretrained('onnx-community/yolo26m-ONNX', {
|
| 143 |
+
device: 'wasm'
|
| 144 |
+
});
|
| 145 |
+
console.log('✅ Model loaded on WASM (compatible)');
|
| 146 |
+
}
|
| 147 |
|
| 148 |
showLoader('Loading processor...');
|
| 149 |
processor = await AutoProcessor.from_pretrained('onnx-community/yolo26m-ONNX');
|
| 150 |
|
| 151 |
// Hide loader
|
| 152 |
hideLoader();
|
| 153 |
+
statusText.textContent = isMobile
|
| 154 |
+
? 'Model ready (WASM)! Open camera to begin'
|
| 155 |
+
: 'Model ready! Open camera to begin';
|
| 156 |
statusIndicator.className = 'status-dot ready';
|
| 157 |
cameraBtn.disabled = false;
|
| 158 |
|
|
|
|
| 314 |
async function detectPhoneAndGetBoxes() {
|
| 315 |
try {
|
| 316 |
// Resize for faster inference (trade accuracy for speed)
|
| 317 |
+
const targetWidth = 256; // Smaller = faster (256 for +5 FPS boost)
|
| 318 |
const targetHeight = Math.round((targetWidth / offscreen.width) * offscreen.height);
|
|
|
|
| 319 |
|
| 320 |
// Create smaller canvas for YOLO
|
| 321 |
const smallCanvas = document.createElement('canvas');
|
|
|
|
| 560 |
}
|
| 561 |
});
|
| 562 |
|
| 563 |
+
// Page Visibility API - pause ONLY on mobile when tab hidden
|
| 564 |
+
// Desktop: Keep running in background (users want continuous monitoring while working)
|
| 565 |
+
// Mobile: Pause to prevent browser from killing tab
|
| 566 |
+
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
| 567 |
+
|
| 568 |
+
if (isMobile) {
|
| 569 |
+
document.addEventListener('visibilitychange', () => {
|
| 570 |
+
if (document.hidden && isMonitoring) {
|
| 571 |
+
console.log('Mobile: Tab hidden, pausing to prevent browser from killing tab');
|
| 572 |
+
isMonitoring = false;
|
| 573 |
+
btnIcon.textContent = '▶️';
|
| 574 |
+
btnText.textContent = 'Start Detection';
|
| 575 |
+
statusText.textContent = 'Paused (tab hidden)';
|
| 576 |
+
statusIndicator.className = 'status-dot ready';
|
| 577 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 578 |
+
}
|
| 579 |
+
});
|
| 580 |
+
console.log('Mobile detected: Will auto-pause when tab hidden');
|
| 581 |
+
} else {
|
| 582 |
+
console.log('Desktop: Will keep monitoring in background tabs');
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
// Initialize on load
|
| 586 |
init();
|
index.html
CHANGED
|
@@ -55,7 +55,8 @@
|
|
| 55 |
</div>
|
| 56 |
<div class="hero-tech" style="margin-top: 1.5rem;">
|
| 57 |
<div class="hero-tech-stack">
|
| 58 |
-
<span class="tech-pill">
|
|
|
|
| 59 |
<span class="tech-pill">OpenCV</span>
|
| 60 |
<span class="tech-pill">Groq (Llama 3.1)</span>
|
| 61 |
<span class="tech-pill">Edge TTS</span>
|
|
|
|
| 55 |
</div>
|
| 56 |
<div class="hero-tech" style="margin-top: 1.5rem;">
|
| 57 |
<div class="hero-tech-stack">
|
| 58 |
+
<span class="tech-pill">YOLO26m</span>
|
| 59 |
+
<span class="tech-pill">NVIDIA TensorRT</span>
|
| 60 |
<span class="tech-pill">OpenCV</span>
|
| 61 |
<span class="tech-pill">Groq (Llama 3.1)</span>
|
| 62 |
<span class="tech-pill">Edge TTS</span>
|
judgy_reachy_no_phone/detection.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
import time
|
| 4 |
import logging
|
| 5 |
from collections import deque
|
| 6 |
-
from typing import Optional
|
| 7 |
|
| 8 |
import cv2
|
| 9 |
import numpy as np
|
|
@@ -16,10 +16,16 @@ class PhoneDetector:
|
|
| 16 |
|
| 17 |
PHONE_CLASS_ID = 67 # "cell phone" in COCO dataset
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
self.yolo_model = None
|
| 22 |
self._initialized = False
|
|
|
|
| 23 |
|
| 24 |
# State tracking
|
| 25 |
self.phone_visible = False
|
|
@@ -31,53 +37,206 @@ class PhoneDetector:
|
|
| 31 |
# History for robust detection
|
| 32 |
self.history = deque(maxlen=30)
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# For visualization
|
| 35 |
self.last_detections = []
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
def initialize(self):
|
| 38 |
-
"""Load YOLO model."""
|
| 39 |
if self._initialized:
|
| 40 |
return True
|
| 41 |
|
| 42 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
import torch
|
| 44 |
from ultralytics import YOLO
|
|
|
|
| 45 |
|
| 46 |
# Auto-detect best device (supports CUDA, MPS, and CPU)
|
| 47 |
if torch.cuda.is_available():
|
| 48 |
device = 'cuda' # NVIDIA GPU
|
|
|
|
| 49 |
elif torch.backends.mps.is_available():
|
| 50 |
device = 'mps' # Apple Silicon GPU
|
|
|
|
| 51 |
else:
|
| 52 |
device = 'cpu' # Fallback to CPU
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
# Use pretrained YOLO26n model
|
| 55 |
-
self.yolo_model = YOLO("yolo26n.pt").to(device)
|
| 56 |
self._initialized = True
|
| 57 |
-
logger.info(f"
|
| 58 |
return True
|
|
|
|
| 59 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
logger.error(f"Failed to load YOLO: {e}")
|
| 61 |
return False
|
| 62 |
|
| 63 |
def detect_phone(self, frame: np.ndarray) -> bool:
|
| 64 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
if not self._initialized:
|
| 66 |
if not self.initialize():
|
| 67 |
-
return
|
| 68 |
|
| 69 |
try:
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
self.last_detections = results # Save for visualization
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
for result in results:
|
|
|
|
|
|
|
|
|
|
| 74 |
for box in result.boxes:
|
| 75 |
if int(box.cls) == self.PHONE_CLASS_ID:
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
except Exception as e:
|
| 79 |
-
logger.debug(f"YOLO
|
| 80 |
-
return
|
| 81 |
|
| 82 |
def draw_detections(self, frame: np.ndarray) -> np.ndarray:
|
| 83 |
"""Draw detection boxes on frame."""
|
|
@@ -126,7 +285,9 @@ class PhoneDetector:
|
|
| 126 |
"put_down" - Phone just put down (optional praise)
|
| 127 |
None - No state change
|
| 128 |
"""
|
| 129 |
-
|
|
|
|
|
|
|
| 130 |
|
| 131 |
# Add to history
|
| 132 |
self.history.append(phone_in_frame)
|
|
@@ -150,6 +311,14 @@ class PhoneDetector:
|
|
| 150 |
self.last_reaction_time = now
|
| 151 |
return "picked_up"
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
# Check for phone put down (slow to confirm - avoids flickering)
|
| 154 |
if self.consecutive_no_phone >= putdown_threshold and self.phone_visible:
|
| 155 |
self.phone_visible = False
|
|
@@ -172,3 +341,23 @@ class PhoneDetector:
|
|
| 172 |
def reset_count(self):
|
| 173 |
"""Reset daily count."""
|
| 174 |
self.phone_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import time
|
| 4 |
import logging
|
| 5 |
from collections import deque
|
| 6 |
+
from typing import Optional, Dict, Any
|
| 7 |
|
| 8 |
import cv2
|
| 9 |
import numpy as np
|
|
|
|
| 16 |
|
| 17 |
PHONE_CLASS_ID = 67 # "cell phone" in COCO dataset
|
| 18 |
|
| 19 |
+
# Adaptive confidence thresholds (like demo.js)
|
| 20 |
+
DETECTION_CONFIDENCE = 0.5 # Initial detection threshold
|
| 21 |
+
TRACKING_CONFIDENCE = 0.2 # Lower threshold when tracking existing phone
|
| 22 |
+
TRACKING_PERSIST_FRAMES = 3 # Keep tracking for N frames after losing detection
|
| 23 |
+
|
| 24 |
+
def __init__(self, confidence: float = 0.5, loading_callback=None):
|
| 25 |
+
self.confidence = confidence # Kept for backward compatibility
|
| 26 |
self.yolo_model = None
|
| 27 |
self._initialized = False
|
| 28 |
+
self.loading_callback = loading_callback # Callback to report loading progress
|
| 29 |
|
| 30 |
# State tracking
|
| 31 |
self.phone_visible = False
|
|
|
|
| 37 |
# History for robust detection
|
| 38 |
self.history = deque(maxlen=30)
|
| 39 |
|
| 40 |
+
# Tracking persistence (like demo.js)
|
| 41 |
+
self.last_phone_box: Optional[Dict[str, Any]] = None
|
| 42 |
+
self.frames_without_detection = 0
|
| 43 |
+
|
| 44 |
# For visualization
|
| 45 |
self.last_detections = []
|
| 46 |
|
| 47 |
+
# Loading state (like demo.js)
|
| 48 |
+
self.loading_status = "idle" # idle, loading, ready, error
|
| 49 |
+
self.loading_message = ""
|
| 50 |
+
|
| 51 |
def initialize(self):
|
| 52 |
+
"""Load YOLO model with progress reporting and TensorRT support."""
|
| 53 |
if self._initialized:
|
| 54 |
return True
|
| 55 |
|
| 56 |
try:
|
| 57 |
+
# Report loading start
|
| 58 |
+
self.loading_status = "loading"
|
| 59 |
+
self.loading_message = "Loading YOLO26m model..."
|
| 60 |
+
if self.loading_callback:
|
| 61 |
+
self.loading_callback("loading", "Loading YOLO26m model...")
|
| 62 |
+
logger.info("Starting YOLO model initialization...")
|
| 63 |
+
|
| 64 |
import torch
|
| 65 |
from ultralytics import YOLO
|
| 66 |
+
import os
|
| 67 |
|
| 68 |
# Auto-detect best device (supports CUDA, MPS, and CPU)
|
| 69 |
if torch.cuda.is_available():
|
| 70 |
device = 'cuda' # NVIDIA GPU
|
| 71 |
+
use_tensorrt = True
|
| 72 |
elif torch.backends.mps.is_available():
|
| 73 |
device = 'mps' # Apple Silicon GPU
|
| 74 |
+
use_tensorrt = False
|
| 75 |
else:
|
| 76 |
device = 'cpu' # Fallback to CPU
|
| 77 |
+
use_tensorrt = False
|
| 78 |
+
|
| 79 |
+
# TensorRT optimization for NVIDIA GPUs (2-3x faster!)
|
| 80 |
+
if use_tensorrt:
|
| 81 |
+
engine_path = "yolo26m.engine"
|
| 82 |
+
|
| 83 |
+
# Check if TensorRT engine already exists
|
| 84 |
+
if os.path.exists(engine_path):
|
| 85 |
+
try:
|
| 86 |
+
logger.info("Found existing TensorRT engine, loading...")
|
| 87 |
+
self.loading_message = "Loading TensorRT engine..."
|
| 88 |
+
if self.loading_callback:
|
| 89 |
+
self.loading_callback("loading", "Loading TensorRT engine...")
|
| 90 |
+
|
| 91 |
+
self.yolo_model = YOLO(engine_path)
|
| 92 |
+
logger.info("✅ Loaded TensorRT engine (2-3x faster!)")
|
| 93 |
+
|
| 94 |
+
except Exception as e:
|
| 95 |
+
logger.warning(f"TensorRT engine load failed: {e}, falling back to PyTorch")
|
| 96 |
+
use_tensorrt = False
|
| 97 |
+
else:
|
| 98 |
+
# Export to TensorRT engine (one-time, takes 1-2 minutes)
|
| 99 |
+
try:
|
| 100 |
+
logger.info("TensorRT engine not found, exporting (one-time setup, ~1-2 min)...")
|
| 101 |
+
self.loading_message = "Exporting to TensorRT (first time, ~1-2 min)..."
|
| 102 |
+
if self.loading_callback:
|
| 103 |
+
self.loading_callback("loading", "Exporting to TensorRT (first time, ~1-2 min)...")
|
| 104 |
+
|
| 105 |
+
# Load PyTorch model first
|
| 106 |
+
temp_model = YOLO("yolo26m.pt")
|
| 107 |
+
|
| 108 |
+
# Export to TensorRT
|
| 109 |
+
temp_model.export(format='engine', device=0, half=True, workspace=4)
|
| 110 |
+
logger.info("✅ TensorRT export complete!")
|
| 111 |
+
|
| 112 |
+
# Load the exported engine
|
| 113 |
+
self.yolo_model = YOLO(engine_path)
|
| 114 |
+
logger.info("✅ Loaded TensorRT engine (2-3x faster!)")
|
| 115 |
+
|
| 116 |
+
except Exception as e:
|
| 117 |
+
logger.warning(f"TensorRT export failed: {e}, using PyTorch instead")
|
| 118 |
+
use_tensorrt = False
|
| 119 |
+
|
| 120 |
+
# Fallback to PyTorch (if not NVIDIA GPU or TensorRT failed)
|
| 121 |
+
if not use_tensorrt:
|
| 122 |
+
self.loading_message = f"Loading YOLO26m on {device.upper()}..."
|
| 123 |
+
if self.loading_callback:
|
| 124 |
+
self.loading_callback("loading", f"Loading YOLO26m on {device.upper()}...")
|
| 125 |
+
|
| 126 |
+
self.yolo_model = YOLO("yolo26m.pt").to(device)
|
| 127 |
+
logger.info(f"Loaded YOLO26m on {device.upper()} (PyTorch)")
|
| 128 |
+
|
| 129 |
+
# Report success
|
| 130 |
+
backend = "TensorRT" if use_tensorrt else device.upper()
|
| 131 |
+
self.loading_status = "ready"
|
| 132 |
+
self.loading_message = f"Model ready on {backend}"
|
| 133 |
+
if self.loading_callback:
|
| 134 |
+
self.loading_callback("ready", f"Model ready on {backend}")
|
| 135 |
|
|
|
|
|
|
|
| 136 |
self._initialized = True
|
| 137 |
+
logger.info(f"YOLO26m model loaded on {backend}")
|
| 138 |
return True
|
| 139 |
+
|
| 140 |
except Exception as e:
|
| 141 |
+
# Report error
|
| 142 |
+
self.loading_status = "error"
|
| 143 |
+
self.loading_message = f"Failed to load model: {str(e)}"
|
| 144 |
+
if self.loading_callback:
|
| 145 |
+
self.loading_callback("error", f"Failed to load model: {str(e)}")
|
| 146 |
logger.error(f"Failed to load YOLO: {e}")
|
| 147 |
return False
|
| 148 |
|
| 149 |
def detect_phone(self, frame: np.ndarray) -> bool:
|
| 150 |
+
"""
|
| 151 |
+
Check if phone is in frame (backward compatible).
|
| 152 |
+
|
| 153 |
+
For new tracking features, use detect_phone_with_tracking() instead.
|
| 154 |
+
"""
|
| 155 |
+
detections = self.detect_phone_with_tracking(frame)
|
| 156 |
+
return len(detections) > 0
|
| 157 |
+
|
| 158 |
+
def detect_phone_with_tracking(self, frame: np.ndarray) -> list:
|
| 159 |
+
"""
|
| 160 |
+
Detect phone with YOLO's built-in ByteTrack tracking + adaptive confidence.
|
| 161 |
+
|
| 162 |
+
Returns:
|
| 163 |
+
List of detection dicts with keys: x1, y1, x2, y2, confidence, class_name, track_id
|
| 164 |
+
|
| 165 |
+
NOTE: To revert to custom tracking, see git history or the old implementation
|
| 166 |
+
that used manual tracking persistence (TRACKING_PERSIST_FRAMES approach).
|
| 167 |
+
"""
|
| 168 |
if not self._initialized:
|
| 169 |
if not self.initialize():
|
| 170 |
+
return []
|
| 171 |
|
| 172 |
try:
|
| 173 |
+
# Adaptive confidence: lower threshold when we have active tracks
|
| 174 |
+
confidence_threshold = (
|
| 175 |
+
self.TRACKING_CONFIDENCE if self.last_phone_box
|
| 176 |
+
else self.DETECTION_CONFIDENCE
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
# Use YOLO's built-in tracker (ByteTrack) instead of manual tracking
|
| 180 |
+
# persist=True keeps track IDs across frames, tracker="bytetrack.yaml"
|
| 181 |
+
results = self.yolo_model.track(
|
| 182 |
+
frame,
|
| 183 |
+
persist=True, # Maintain track IDs across frames
|
| 184 |
+
conf=confidence_threshold, # Adaptive confidence
|
| 185 |
+
tracker="bytetrack.yaml", # ByteTrack algorithm (robust, fast)
|
| 186 |
+
verbose=False,
|
| 187 |
+
classes=[self.PHONE_CLASS_ID] # Only track phones
|
| 188 |
+
)
|
| 189 |
self.last_detections = results # Save for visualization
|
| 190 |
|
| 191 |
+
# Collect tracked phones with their IDs
|
| 192 |
+
new_detections = []
|
| 193 |
+
best_phone = None
|
| 194 |
+
best_score = 0.0
|
| 195 |
+
|
| 196 |
for result in results:
|
| 197 |
+
if result.boxes is None or len(result.boxes) == 0:
|
| 198 |
+
continue
|
| 199 |
+
|
| 200 |
for box in result.boxes:
|
| 201 |
if int(box.cls) == self.PHONE_CLASS_ID:
|
| 202 |
+
conf = float(box.conf)
|
| 203 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
| 204 |
+
|
| 205 |
+
# Get track ID (ByteTrack assigns persistent IDs)
|
| 206 |
+
track_id = int(box.id[0]) if box.id is not None else None
|
| 207 |
+
|
| 208 |
+
detection = {
|
| 209 |
+
'x1': x1,
|
| 210 |
+
'y1': y1,
|
| 211 |
+
'x2': x2,
|
| 212 |
+
'y2': y2,
|
| 213 |
+
'confidence': conf,
|
| 214 |
+
'class_name': 'cell phone',
|
| 215 |
+
'track_id': track_id
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
new_detections.append(detection)
|
| 219 |
+
|
| 220 |
+
# Track the most confident phone for state tracking
|
| 221 |
+
if conf > best_score:
|
| 222 |
+
best_score = conf
|
| 223 |
+
best_phone = detection
|
| 224 |
+
|
| 225 |
+
# Update last_phone_box with the best detection (for adaptive confidence)
|
| 226 |
+
if best_phone:
|
| 227 |
+
self.last_phone_box = best_phone
|
| 228 |
+
self.frames_without_detection = 0
|
| 229 |
+
else:
|
| 230 |
+
# ByteTrack handles occlusion, but we still track when we lose all detections
|
| 231 |
+
self.frames_without_detection += 1
|
| 232 |
+
if self.frames_without_detection >= self.TRACKING_PERSIST_FRAMES:
|
| 233 |
+
self.last_phone_box = None
|
| 234 |
+
|
| 235 |
+
return new_detections
|
| 236 |
+
|
| 237 |
except Exception as e:
|
| 238 |
+
logger.debug(f"YOLO tracking error: {e}")
|
| 239 |
+
return []
|
| 240 |
|
| 241 |
def draw_detections(self, frame: np.ndarray) -> np.ndarray:
|
| 242 |
"""Draw detection boxes on frame."""
|
|
|
|
| 285 |
"put_down" - Phone just put down (optional praise)
|
| 286 |
None - No state change
|
| 287 |
"""
|
| 288 |
+
# Use new tracking-enabled detection
|
| 289 |
+
detections = self.detect_phone_with_tracking(frame)
|
| 290 |
+
phone_in_frame = len(detections) > 0
|
| 291 |
|
| 292 |
# Add to history
|
| 293 |
self.history.append(phone_in_frame)
|
|
|
|
| 311 |
self.last_reaction_time = now
|
| 312 |
return "picked_up"
|
| 313 |
|
| 314 |
+
# Periodic reactions while STILL holding phone (like demo.js)
|
| 315 |
+
if self.phone_visible and phone_in_frame:
|
| 316 |
+
now = time.time()
|
| 317 |
+
if now - self.last_reaction_time >= cooldown:
|
| 318 |
+
self.phone_count += 1
|
| 319 |
+
self.last_reaction_time = now
|
| 320 |
+
return "picked_up" # Shame again!
|
| 321 |
+
|
| 322 |
# Check for phone put down (slow to confirm - avoids flickering)
|
| 323 |
if self.consecutive_no_phone >= putdown_threshold and self.phone_visible:
|
| 324 |
self.phone_visible = False
|
|
|
|
| 341 |
def reset_count(self):
|
| 342 |
"""Reset daily count."""
|
| 343 |
self.phone_count = 0
|
| 344 |
+
|
| 345 |
+
def reset_tracking(self):
|
| 346 |
+
"""Reset tracking state (useful when stopping/starting monitoring)."""
|
| 347 |
+
self.phone_visible = False
|
| 348 |
+
self.consecutive_phone = 0
|
| 349 |
+
self.consecutive_no_phone = 0
|
| 350 |
+
self.last_phone_box = None
|
| 351 |
+
self.frames_without_detection = 0
|
| 352 |
+
self.last_reaction_time = 0
|
| 353 |
+
|
| 354 |
+
# Reset ByteTrack tracker (clear track IDs)
|
| 355 |
+
if self.yolo_model and hasattr(self.yolo_model, 'predictor'):
|
| 356 |
+
try:
|
| 357 |
+
# This resets the tracker's internal state
|
| 358 |
+
self.yolo_model.predictor.trackers = []
|
| 359 |
+
logger.debug("ByteTrack tracker reset")
|
| 360 |
+
except Exception as e:
|
| 361 |
+
logger.debug(f"Tracker reset error (non-critical): {e}")
|
| 362 |
+
|
| 363 |
+
logger.debug("Tracking state reset")
|
judgy_reachy_no_phone/main.py
CHANGED
|
@@ -42,8 +42,17 @@ class JudgyReachyNoPhone(ReachyMiniApp):
|
|
| 42 |
super().__init__()
|
| 43 |
self.config = Config()
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
self.llm = LLMResponder(api_key=self.config.GROQ_API_KEY, personality="pure_reachy")
|
| 48 |
# Don't pass config voice defaults - let personalities use their own defaults
|
| 49 |
self.tts = TextToSpeech(
|
|
@@ -80,6 +89,12 @@ class JudgyReachyNoPhone(ReachyMiniApp):
|
|
| 80 |
self.camera_fps = 0
|
| 81 |
self.detection_event_queue = []
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
# Register API endpoint for personalities (must be before server starts)
|
| 84 |
@self.settings_app.get("/api/personalities")
|
| 85 |
def get_personalities():
|
|
@@ -222,7 +237,8 @@ class JudgyReachyNoPhone(ReachyMiniApp):
|
|
| 222 |
)
|
| 223 |
ui_thread.start()
|
| 224 |
|
| 225 |
-
# Initialize detector
|
|
|
|
| 226 |
self.detector.initialize()
|
| 227 |
|
| 228 |
# Auto-detect: Use laptop webcam in simulation, robot camera otherwise
|
|
@@ -231,12 +247,19 @@ class JudgyReachyNoPhone(ReachyMiniApp):
|
|
| 231 |
|
| 232 |
if is_simulation:
|
| 233 |
logger.info("Simulation mode detected - using laptop webcam...")
|
|
|
|
|
|
|
|
|
|
| 234 |
webcam = cv2.VideoCapture(0)
|
| 235 |
if not webcam.isOpened():
|
| 236 |
logger.error("Failed to open laptop webcam!")
|
|
|
|
|
|
|
| 237 |
webcam = None
|
| 238 |
else:
|
| 239 |
logger.info("Laptop webcam opened successfully!")
|
|
|
|
|
|
|
| 240 |
self.camera_running = True
|
| 241 |
|
| 242 |
# Start fast camera thread
|
|
@@ -248,7 +271,12 @@ class JudgyReachyNoPhone(ReachyMiniApp):
|
|
| 248 |
camera_thread.start()
|
| 249 |
else:
|
| 250 |
logger.info("Real robot detected - using robot camera...")
|
|
|
|
|
|
|
|
|
|
| 251 |
self.camera_running = True
|
|
|
|
|
|
|
| 252 |
|
| 253 |
# Start camera thread with robot's media system
|
| 254 |
camera_thread = threading.Thread(
|
|
@@ -408,6 +436,20 @@ class JudgyReachyNoPhone(ReachyMiniApp):
|
|
| 408 |
reset: bool = False # If True, reset all stats (Start Fresh)
|
| 409 |
personality: str = "pure_reachy" # Robot personality
|
| 410 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
# API endpoint: Get video frame
|
| 412 |
@self.settings_app.get("/api/video-frame")
|
| 413 |
def get_video_frame():
|
|
@@ -440,7 +482,7 @@ class JudgyReachyNoPhone(ReachyMiniApp):
|
|
| 440 |
else:
|
| 441 |
status_text = "✅ Phone-free"
|
| 442 |
|
| 443 |
-
mode_text = f"
|
| 444 |
|
| 445 |
# Determine button text
|
| 446 |
if self.is_monitoring:
|
|
@@ -622,7 +664,7 @@ class JudgyReachyNoPhone(ReachyMiniApp):
|
|
| 622 |
# Build mode string
|
| 623 |
llm_text = "LLM + TTS" if result["groq_valid"] else "Pre-written lines"
|
| 624 |
tts_text = "ElevenLabs" if result["eleven_valid"] else "Edge TTS"
|
| 625 |
-
result["mode"] = f"
|
| 626 |
|
| 627 |
return result
|
| 628 |
|
|
|
|
| 42 |
super().__init__()
|
| 43 |
self.config = Config()
|
| 44 |
|
| 45 |
+
# Loading state tracking (like demo.js)
|
| 46 |
+
self.model_loading_status = "idle" # idle, loading, ready, error
|
| 47 |
+
self.model_loading_message = ""
|
| 48 |
+
self.camera_loading_status = "idle" # idle, connecting, ready, error
|
| 49 |
+
self.camera_loading_message = "Waiting for camera connection..."
|
| 50 |
+
|
| 51 |
+
# Components (pass loading callback to detector)
|
| 52 |
+
self.detector = PhoneDetector(
|
| 53 |
+
confidence=self.config.DETECTION_CONFIDENCE,
|
| 54 |
+
loading_callback=self._on_model_loading
|
| 55 |
+
)
|
| 56 |
self.llm = LLMResponder(api_key=self.config.GROQ_API_KEY, personality="pure_reachy")
|
| 57 |
# Don't pass config voice defaults - let personalities use their own defaults
|
| 58 |
self.tts = TextToSpeech(
|
|
|
|
| 89 |
self.camera_fps = 0
|
| 90 |
self.detection_event_queue = []
|
| 91 |
|
| 92 |
+
def _on_model_loading(self, status: str, message: str):
|
| 93 |
+
"""Callback for model loading progress (like demo.js)."""
|
| 94 |
+
self.model_loading_status = status
|
| 95 |
+
self.model_loading_message = message
|
| 96 |
+
logger.info(f"Model loading: {status} - {message}")
|
| 97 |
+
|
| 98 |
# Register API endpoint for personalities (must be before server starts)
|
| 99 |
@self.settings_app.get("/api/personalities")
|
| 100 |
def get_personalities():
|
|
|
|
| 237 |
)
|
| 238 |
ui_thread.start()
|
| 239 |
|
| 240 |
+
# Initialize detector (reports loading progress)
|
| 241 |
+
logger.info("Initializing YOLO model...")
|
| 242 |
self.detector.initialize()
|
| 243 |
|
| 244 |
# Auto-detect: Use laptop webcam in simulation, robot camera otherwise
|
|
|
|
| 247 |
|
| 248 |
if is_simulation:
|
| 249 |
logger.info("Simulation mode detected - using laptop webcam...")
|
| 250 |
+
self.camera_loading_status = "connecting"
|
| 251 |
+
self.camera_loading_message = "Opening laptop webcam..."
|
| 252 |
+
|
| 253 |
webcam = cv2.VideoCapture(0)
|
| 254 |
if not webcam.isOpened():
|
| 255 |
logger.error("Failed to open laptop webcam!")
|
| 256 |
+
self.camera_loading_status = "error"
|
| 257 |
+
self.camera_loading_message = "Failed to open webcam"
|
| 258 |
webcam = None
|
| 259 |
else:
|
| 260 |
logger.info("Laptop webcam opened successfully!")
|
| 261 |
+
self.camera_loading_status = "ready"
|
| 262 |
+
self.camera_loading_message = "Camera connected"
|
| 263 |
self.camera_running = True
|
| 264 |
|
| 265 |
# Start fast camera thread
|
|
|
|
| 271 |
camera_thread.start()
|
| 272 |
else:
|
| 273 |
logger.info("Real robot detected - using robot camera...")
|
| 274 |
+
self.camera_loading_status = "connecting"
|
| 275 |
+
self.camera_loading_message = "Connecting to robot camera..."
|
| 276 |
+
|
| 277 |
self.camera_running = True
|
| 278 |
+
self.camera_loading_status = "ready"
|
| 279 |
+
self.camera_loading_message = "Camera connected"
|
| 280 |
|
| 281 |
# Start camera thread with robot's media system
|
| 282 |
camera_thread = threading.Thread(
|
|
|
|
| 436 |
reset: bool = False # If True, reset all stats (Start Fresh)
|
| 437 |
personality: str = "pure_reachy" # Robot personality
|
| 438 |
|
| 439 |
+
# API endpoint: Get loading status (like demo.js)
|
| 440 |
+
@self.settings_app.get("/api/loading-status")
|
| 441 |
+
def get_loading_status():
|
| 442 |
+
return {
|
| 443 |
+
"model_status": self.model_loading_status,
|
| 444 |
+
"model_message": self.model_loading_message,
|
| 445 |
+
"camera_status": self.camera_loading_status,
|
| 446 |
+
"camera_message": self.camera_loading_message,
|
| 447 |
+
"overall_ready": (
|
| 448 |
+
self.model_loading_status == "ready" and
|
| 449 |
+
self.camera_loading_status == "ready"
|
| 450 |
+
)
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
# API endpoint: Get video frame
|
| 454 |
@self.settings_app.get("/api/video-frame")
|
| 455 |
def get_video_frame():
|
|
|
|
| 482 |
else:
|
| 483 |
status_text = "✅ Phone-free"
|
| 484 |
|
| 485 |
+
mode_text = f"YOLO26m | {'LLM + TTS' if self.llm.client else 'Pre-written lines'} → {'ElevenLabs' if self.tts.eleven_client else 'Edge TTS'}"
|
| 486 |
|
| 487 |
# Determine button text
|
| 488 |
if self.is_monitoring:
|
|
|
|
| 664 |
# Build mode string
|
| 665 |
llm_text = "LLM + TTS" if result["groq_valid"] else "Pre-written lines"
|
| 666 |
tts_text = "ElevenLabs" if result["eleven_valid"] else "Edge TTS"
|
| 667 |
+
result["mode"] = f"YOLO26m | {llm_text} → {tts_text}"
|
| 668 |
|
| 669 |
return result
|
| 670 |
|
judgy_reachy_no_phone/static/index.html
CHANGED
|
@@ -51,8 +51,14 @@
|
|
| 51 |
<path d="M16 16v1a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2m5.66 0H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10"></path>
|
| 52 |
<line x1="1" y1="1" x2="23" y2="23"></line>
|
| 53 |
</svg>
|
| 54 |
-
<div class="camera-status">Camera Inactive</div>
|
| 55 |
-
<div class="camera-message">Waiting for camera connection...</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</div>
|
| 57 |
</div>
|
| 58 |
</div>
|
|
|
|
| 51 |
<path d="M16 16v1a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2m5.66 0H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10"></path>
|
| 52 |
<line x1="1" y1="1" x2="23" y2="23"></line>
|
| 53 |
</svg>
|
| 54 |
+
<div class="camera-status" id="camera-status">Camera Inactive</div>
|
| 55 |
+
<div class="camera-message" id="camera-message">Waiting for camera connection...</div>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<!-- Loading Overlay (like demo.js) -->
|
| 59 |
+
<div id="loader-overlay" class="loader-overlay">
|
| 60 |
+
<div class="spinner"></div>
|
| 61 |
+
<p id="loader-text" class="loader-text">Initializing...</p>
|
| 62 |
</div>
|
| 63 |
</div>
|
| 64 |
</div>
|
judgy_reachy_no_phone/static/main.js
CHANGED
|
@@ -3,6 +3,9 @@ let selectedPersonality = 'pure_reachy';
|
|
| 3 |
let currentVoicePersonality = null;
|
| 4 |
let voiceOverrides = JSON.parse(localStorage.getItem('voiceOverrides') || '{}');
|
| 5 |
|
|
|
|
|
|
|
|
|
|
| 6 |
// Load personalities from config dynamically
|
| 7 |
async function loadPersonalities() {
|
| 8 |
try {
|
|
@@ -74,7 +77,7 @@ async function updateUIForAPIKeys() {
|
|
| 74 |
|
| 75 |
// If no API keys at all, show default message
|
| 76 |
if (!groqKey && !elevenKey) {
|
| 77 |
-
document.getElementById('mode-text').textContent = '
|
| 78 |
document.getElementById('api-notice').classList.remove('hidden');
|
| 79 |
// Keep personalities enabled - they still have different voices and pre-written lines
|
| 80 |
return;
|
|
@@ -421,6 +424,65 @@ async function updatePersonalityWhileRunning() {
|
|
| 421 |
}
|
| 422 |
}
|
| 423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
// Initial update - wait for personalities to load first
|
| 425 |
async function initialize() {
|
| 426 |
// Load personalities first
|
|
@@ -451,6 +513,10 @@ async function initialize() {
|
|
| 451 |
updateVideo();
|
| 452 |
updateUIForAPIKeys();
|
| 453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
// Auto-update every 100ms for smooth video
|
| 455 |
setInterval(() => {
|
| 456 |
updateVideo();
|
|
|
|
| 3 |
let currentVoicePersonality = null;
|
| 4 |
let voiceOverrides = JSON.parse(localStorage.getItem('voiceOverrides') || '{}');
|
| 5 |
|
| 6 |
+
// Loading state (like demo.js)
|
| 7 |
+
let loadingCheckInterval = null;
|
| 8 |
+
|
| 9 |
// Load personalities from config dynamically
|
| 10 |
async function loadPersonalities() {
|
| 11 |
try {
|
|
|
|
| 77 |
|
| 78 |
// If no API keys at all, show default message
|
| 79 |
if (!groqKey && !elevenKey) {
|
| 80 |
+
document.getElementById('mode-text').textContent = 'YOLO26m | Pre-written personality lines → Edge TTS';
|
| 81 |
document.getElementById('api-notice').classList.remove('hidden');
|
| 82 |
// Keep personalities enabled - they still have different voices and pre-written lines
|
| 83 |
return;
|
|
|
|
| 424 |
}
|
| 425 |
}
|
| 426 |
|
| 427 |
+
// Loading status functions (like demo.js)
|
| 428 |
+
const showLoader = (text) => {
|
| 429 |
+
const loader = document.getElementById('loader-overlay');
|
| 430 |
+
const loaderText = document.getElementById('loader-text');
|
| 431 |
+
loaderText.textContent = text;
|
| 432 |
+
loader.classList.add('visible');
|
| 433 |
+
};
|
| 434 |
+
|
| 435 |
+
const hideLoader = () => {
|
| 436 |
+
const loader = document.getElementById('loader-overlay');
|
| 437 |
+
loader.classList.remove('visible');
|
| 438 |
+
};
|
| 439 |
+
|
| 440 |
+
// Check loading status and update UI
|
| 441 |
+
async function checkLoadingStatus() {
|
| 442 |
+
try {
|
| 443 |
+
const response = await fetch('/api/loading-status');
|
| 444 |
+
const data = await response.json();
|
| 445 |
+
|
| 446 |
+
const cameraStatus = document.getElementById('camera-status');
|
| 447 |
+
const cameraMessage = document.getElementById('camera-message');
|
| 448 |
+
|
| 449 |
+
// Update loader overlay
|
| 450 |
+
if (data.model_status === 'loading') {
|
| 451 |
+
showLoader(data.model_message);
|
| 452 |
+
} else if (data.camera_status === 'connecting') {
|
| 453 |
+
showLoader(data.camera_message);
|
| 454 |
+
} else if (data.overall_ready) {
|
| 455 |
+
hideLoader();
|
| 456 |
+
// Stop polling once everything is ready
|
| 457 |
+
if (loadingCheckInterval) {
|
| 458 |
+
clearInterval(loadingCheckInterval);
|
| 459 |
+
loadingCheckInterval = null;
|
| 460 |
+
}
|
| 461 |
+
} else if (data.model_status === 'error' || data.camera_status === 'error') {
|
| 462 |
+
hideLoader();
|
| 463 |
+
if (loadingCheckInterval) {
|
| 464 |
+
clearInterval(loadingCheckInterval);
|
| 465 |
+
loadingCheckInterval = null;
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
// Update camera placeholder status
|
| 470 |
+
if (data.camera_status === 'connecting') {
|
| 471 |
+
cameraStatus.textContent = 'Connecting...';
|
| 472 |
+
cameraMessage.textContent = data.camera_message;
|
| 473 |
+
} else if (data.camera_status === 'ready') {
|
| 474 |
+
cameraStatus.textContent = 'Camera Ready';
|
| 475 |
+
cameraMessage.textContent = data.camera_message;
|
| 476 |
+
} else if (data.camera_status === 'error') {
|
| 477 |
+
cameraStatus.textContent = 'Camera Error';
|
| 478 |
+
cameraMessage.textContent = data.camera_message;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
} catch (e) {
|
| 482 |
+
console.error('Loading status check failed:', e);
|
| 483 |
+
}
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
// Initial update - wait for personalities to load first
|
| 487 |
async function initialize() {
|
| 488 |
// Load personalities first
|
|
|
|
| 513 |
updateVideo();
|
| 514 |
updateUIForAPIKeys();
|
| 515 |
|
| 516 |
+
// Start loading status check (poll every 500ms until ready)
|
| 517 |
+
checkLoadingStatus(); // Check immediately
|
| 518 |
+
loadingCheckInterval = setInterval(checkLoadingStatus, 500);
|
| 519 |
+
|
| 520 |
// Auto-update every 100ms for smooth video
|
| 521 |
setInterval(() => {
|
| 522 |
updateVideo();
|
judgy_reachy_no_phone/static/style.css
CHANGED
|
@@ -729,6 +729,53 @@ body {
|
|
| 729 |
50% { opacity: 0.5; }
|
| 730 |
}
|
| 731 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
/* Responsive */
|
| 733 |
@media (max-width: 1200px) {
|
| 734 |
.main-container {
|
|
|
|
| 729 |
50% { opacity: 0.5; }
|
| 730 |
}
|
| 731 |
|
| 732 |
+
/* Loading Overlay (like demo.js) */
|
| 733 |
+
.loader-overlay {
|
| 734 |
+
position: absolute;
|
| 735 |
+
top: 0;
|
| 736 |
+
left: 0;
|
| 737 |
+
width: 100%;
|
| 738 |
+
height: 100%;
|
| 739 |
+
background: rgba(0, 0, 0, 0.9);
|
| 740 |
+
backdrop-filter: blur(8px);
|
| 741 |
+
display: flex;
|
| 742 |
+
flex-direction: column;
|
| 743 |
+
align-items: center;
|
| 744 |
+
justify-content: center;
|
| 745 |
+
z-index: 10;
|
| 746 |
+
opacity: 0;
|
| 747 |
+
visibility: hidden;
|
| 748 |
+
transition: opacity 0.3s ease, visibility 0.3s ease;
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
.loader-overlay.visible {
|
| 752 |
+
opacity: 1;
|
| 753 |
+
visibility: visible;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
.spinner {
|
| 757 |
+
width: 50px;
|
| 758 |
+
height: 50px;
|
| 759 |
+
border: 4px solid rgba(255, 255, 255, 0.1);
|
| 760 |
+
border-top-color: #667eea;
|
| 761 |
+
border-radius: 50%;
|
| 762 |
+
animation: spin 1s linear infinite;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
@keyframes spin {
|
| 766 |
+
to {
|
| 767 |
+
transform: rotate(360deg);
|
| 768 |
+
}
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.loader-text {
|
| 772 |
+
color: #f1f5f9;
|
| 773 |
+
font-size: 1rem;
|
| 774 |
+
font-weight: 600;
|
| 775 |
+
margin-top: 1.5rem;
|
| 776 |
+
text-align: center;
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
/* Responsive */
|
| 780 |
@media (max-width: 1200px) {
|
| 781 |
.main-container {
|
style.css
CHANGED
|
@@ -335,46 +335,6 @@ section {
|
|
| 335 |
margin-right: auto;
|
| 336 |
}
|
| 337 |
|
| 338 |
-
/* Callout Box */
|
| 339 |
-
.callout-box {
|
| 340 |
-
background: linear-gradient(135deg, rgba(255, 107, 74, 0.1), rgba(155, 126, 232, 0.08));
|
| 341 |
-
border: 2px solid rgba(255, 107, 74, 0.3);
|
| 342 |
-
border-radius: var(--radius-lg);
|
| 343 |
-
padding: 2.5rem;
|
| 344 |
-
text-align: center;
|
| 345 |
-
box-shadow: var(--shadow-lg);
|
| 346 |
-
height: 100%;
|
| 347 |
-
display: flex;
|
| 348 |
-
flex-direction: column;
|
| 349 |
-
justify-content: center;
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
.callout-icon {
|
| 353 |
-
font-size: 3.5rem;
|
| 354 |
-
margin-bottom: 1.25rem;
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
.callout-title {
|
| 358 |
-
font-family: 'Inter', sans-serif;
|
| 359 |
-
font-size: 1.75rem;
|
| 360 |
-
font-weight: 700;
|
| 361 |
-
margin-bottom: 1.25rem;
|
| 362 |
-
color: var(--text-primary);
|
| 363 |
-
line-height: 1.3;
|
| 364 |
-
}
|
| 365 |
-
|
| 366 |
-
.callout-text {
|
| 367 |
-
font-size: 1rem;
|
| 368 |
-
line-height: 1.7;
|
| 369 |
-
color: var(--text-body);
|
| 370 |
-
margin: 0;
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
.callout-text strong {
|
| 374 |
-
color: var(--coral-light);
|
| 375 |
-
font-weight: 700;
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
/* Configuration Section */
|
| 379 |
.config-section {
|
| 380 |
text-align: center;
|
|
@@ -431,12 +391,6 @@ section {
|
|
| 431 |
opacity: 1;
|
| 432 |
}
|
| 433 |
|
| 434 |
-
.config-icon {
|
| 435 |
-
font-size: 3rem;
|
| 436 |
-
margin-bottom: 1.25rem;
|
| 437 |
-
display: inline-block;
|
| 438 |
-
}
|
| 439 |
-
|
| 440 |
.config-card h3 {
|
| 441 |
font-family: 'Inter', sans-serif;
|
| 442 |
font-size: 1.5rem;
|
|
@@ -751,47 +705,6 @@ section {
|
|
| 751 |
color: var(--coral);
|
| 752 |
}
|
| 753 |
|
| 754 |
-
text-align: center;
|
| 755 |
-
}
|
| 756 |
-
|
| 757 |
-
.tech-grid {
|
| 758 |
-
display: grid;
|
| 759 |
-
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
| 760 |
-
gap: 1rem;
|
| 761 |
-
max-width: 800px;
|
| 762 |
-
margin: 0 auto;
|
| 763 |
-
}
|
| 764 |
-
|
| 765 |
-
.tech-badge {
|
| 766 |
-
background: var(--bg-card);
|
| 767 |
-
border: 2px solid var(--border-soft);
|
| 768 |
-
border-radius: var(--radius-md);
|
| 769 |
-
padding: 1.5rem 1.25rem;
|
| 770 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 771 |
-
box-shadow: var(--shadow-sm);
|
| 772 |
-
}
|
| 773 |
-
|
| 774 |
-
.tech-badge:hover {
|
| 775 |
-
border-color: var(--coral);
|
| 776 |
-
transform: translateY(-3px) scale(1.02);
|
| 777 |
-
box-shadow: var(--shadow-md);
|
| 778 |
-
}
|
| 779 |
-
|
| 780 |
-
.tech-label {
|
| 781 |
-
font-size: 0.7rem;
|
| 782 |
-
text-transform: uppercase;
|
| 783 |
-
letter-spacing: 0.06em;
|
| 784 |
-
color: var(--coral);
|
| 785 |
-
margin-bottom: 0.5rem;
|
| 786 |
-
font-weight: 700;
|
| 787 |
-
}
|
| 788 |
-
|
| 789 |
-
.tech-value {
|
| 790 |
-
font-family: 'Inter', sans-serif;
|
| 791 |
-
font-size: 0.95rem;
|
| 792 |
-
font-weight: 600;
|
| 793 |
-
color: var(--text-primary);
|
| 794 |
-
}
|
| 795 |
|
| 796 |
/* Footer */
|
| 797 |
.footer {
|
|
@@ -837,20 +750,6 @@ section {
|
|
| 837 |
}
|
| 838 |
|
| 839 |
/* Responsive Design */
|
| 840 |
-
@media (max-width: 900px) {
|
| 841 |
-
.steps-flow {
|
| 842 |
-
grid-template-columns: repeat(2, 1fr);
|
| 843 |
-
gap: 1rem;
|
| 844 |
-
}
|
| 845 |
-
|
| 846 |
-
.steps-flow::before {
|
| 847 |
-
display: none;
|
| 848 |
-
}
|
| 849 |
-
|
| 850 |
-
.step-box {
|
| 851 |
-
margin: 0;
|
| 852 |
-
}
|
| 853 |
-
}
|
| 854 |
|
| 855 |
@media (max-width: 768px) {
|
| 856 |
.hero-grid {
|
|
@@ -909,14 +808,6 @@ section {
|
|
| 909 |
.api-link-inline {
|
| 910 |
display: none;
|
| 911 |
}
|
| 912 |
-
|
| 913 |
-
.steps-flow {
|
| 914 |
-
grid-template-columns: 1fr;
|
| 915 |
-
}
|
| 916 |
-
|
| 917 |
-
.tech-grid {
|
| 918 |
-
grid-template-columns: repeat(2, 1fr);
|
| 919 |
-
}
|
| 920 |
}
|
| 921 |
|
| 922 |
@media (max-width: 480px) {
|
|
@@ -928,10 +819,6 @@ section {
|
|
| 928 |
font-size: 1.75rem;
|
| 929 |
}
|
| 930 |
|
| 931 |
-
.tech-grid {
|
| 932 |
-
grid-template-columns: 1fr;
|
| 933 |
-
}
|
| 934 |
-
|
| 935 |
.logo-text {
|
| 936 |
font-size: 0.9rem;
|
| 937 |
}
|
|
@@ -1196,262 +1083,6 @@ section {
|
|
| 1196 |
font-style: italic;
|
| 1197 |
}
|
| 1198 |
|
| 1199 |
-
/* Sidebar */
|
| 1200 |
-
.demo-sidebar {
|
| 1201 |
-
display: flex;
|
| 1202 |
-
flex-direction: column;
|
| 1203 |
-
gap: 1.5rem;
|
| 1204 |
-
}
|
| 1205 |
-
|
| 1206 |
-
.demo-card {
|
| 1207 |
-
background: #1e293b;
|
| 1208 |
-
border: 2px solid #334155;
|
| 1209 |
-
border-radius: 16px;
|
| 1210 |
-
padding: 1.5rem;
|
| 1211 |
-
}
|
| 1212 |
-
|
| 1213 |
-
.demo-card-title {
|
| 1214 |
-
color: #f1f5f9;
|
| 1215 |
-
font-size: 1.1rem;
|
| 1216 |
-
font-weight: 700;
|
| 1217 |
-
margin-bottom: 1rem;
|
| 1218 |
-
}
|
| 1219 |
-
|
| 1220 |
-
/* Stats */
|
| 1221 |
-
.stat-grid {
|
| 1222 |
-
display: grid;
|
| 1223 |
-
gap: 0.75rem;
|
| 1224 |
-
}
|
| 1225 |
-
|
| 1226 |
-
.stat-item {
|
| 1227 |
-
display: flex;
|
| 1228 |
-
justify-content: space-between;
|
| 1229 |
-
align-items: center;
|
| 1230 |
-
padding: 0.75rem 0;
|
| 1231 |
-
border-bottom: 1px solid #334155;
|
| 1232 |
-
}
|
| 1233 |
-
|
| 1234 |
-
.stat-item:last-child {
|
| 1235 |
-
border-bottom: none;
|
| 1236 |
-
}
|
| 1237 |
-
|
| 1238 |
-
.stat-label {
|
| 1239 |
-
color: #94a3b8;
|
| 1240 |
-
font-size: 0.9rem;
|
| 1241 |
-
}
|
| 1242 |
-
|
| 1243 |
-
.stat-value {
|
| 1244 |
-
color: #f1f5f9;
|
| 1245 |
-
font-weight: 700;
|
| 1246 |
-
font-size: 1.1rem;
|
| 1247 |
-
}
|
| 1248 |
-
|
| 1249 |
-
/* Select */
|
| 1250 |
-
.demo-select {
|
| 1251 |
-
width: 100%;
|
| 1252 |
-
padding: 0.75rem 1rem;
|
| 1253 |
-
background: #0f172a;
|
| 1254 |
-
border: 2px solid #334155;
|
| 1255 |
-
border-radius: 8px;
|
| 1256 |
-
color: #f1f5f9;
|
| 1257 |
-
font-size: 1rem;
|
| 1258 |
-
cursor: pointer;
|
| 1259 |
-
transition: all 0.2s ease;
|
| 1260 |
-
}
|
| 1261 |
-
|
| 1262 |
-
.demo-select:hover, .demo-select:focus {
|
| 1263 |
-
border-color: #667eea;
|
| 1264 |
-
outline: none;
|
| 1265 |
-
}
|
| 1266 |
-
|
| 1267 |
-
/* Settings */
|
| 1268 |
-
.setting-item {
|
| 1269 |
-
margin-bottom: 1.5rem;
|
| 1270 |
-
}
|
| 1271 |
-
|
| 1272 |
-
.setting-item:last-child {
|
| 1273 |
-
margin-bottom: 0;
|
| 1274 |
-
}
|
| 1275 |
-
|
| 1276 |
-
.setting-label {
|
| 1277 |
-
display: block;
|
| 1278 |
-
color: #94a3b8;
|
| 1279 |
-
font-size: 0.9rem;
|
| 1280 |
-
margin-bottom: 0.5rem;
|
| 1281 |
-
}
|
| 1282 |
-
|
| 1283 |
-
.demo-slider {
|
| 1284 |
-
width: 100%;
|
| 1285 |
-
height: 6px;
|
| 1286 |
-
border-radius: 3px;
|
| 1287 |
-
background: #334155;
|
| 1288 |
-
outline: none;
|
| 1289 |
-
-webkit-appearance: none;
|
| 1290 |
-
}
|
| 1291 |
-
|
| 1292 |
-
.demo-slider::-webkit-slider-thumb {
|
| 1293 |
-
-webkit-appearance: none;
|
| 1294 |
-
appearance: none;
|
| 1295 |
-
width: 20px;
|
| 1296 |
-
height: 20px;
|
| 1297 |
-
border-radius: 50%;
|
| 1298 |
-
background: #667eea;
|
| 1299 |
-
cursor: pointer;
|
| 1300 |
-
transition: all 0.2s ease;
|
| 1301 |
-
}
|
| 1302 |
-
|
| 1303 |
-
.demo-slider::-webkit-slider-thumb:hover {
|
| 1304 |
-
background: #764ba2;
|
| 1305 |
-
transform: scale(1.1);
|
| 1306 |
-
}
|
| 1307 |
-
|
| 1308 |
-
.demo-slider::-moz-range-thumb {
|
| 1309 |
-
width: 20px;
|
| 1310 |
-
height: 20px;
|
| 1311 |
-
border-radius: 50%;
|
| 1312 |
-
background: #667eea;
|
| 1313 |
-
cursor: pointer;
|
| 1314 |
-
border: none;
|
| 1315 |
-
transition: all 0.2s ease;
|
| 1316 |
-
}
|
| 1317 |
-
|
| 1318 |
-
.demo-slider::-moz-range-thumb:hover {
|
| 1319 |
-
background: #764ba2;
|
| 1320 |
-
transform: scale(1.1);
|
| 1321 |
-
}
|
| 1322 |
-
|
| 1323 |
-
.setting-checkbox {
|
| 1324 |
-
display: flex;
|
| 1325 |
-
align-items: center;
|
| 1326 |
-
gap: 0.75rem;
|
| 1327 |
-
color: #f1f5f9;
|
| 1328 |
-
cursor: pointer;
|
| 1329 |
-
user-select: none;
|
| 1330 |
-
}
|
| 1331 |
-
|
| 1332 |
-
.setting-checkbox input[type="checkbox"] {
|
| 1333 |
-
width: 20px;
|
| 1334 |
-
height: 20px;
|
| 1335 |
-
cursor: pointer;
|
| 1336 |
-
accent-color: #667eea;
|
| 1337 |
-
}
|
| 1338 |
-
|
| 1339 |
-
/* Note */
|
| 1340 |
-
.demo-note {
|
| 1341 |
-
background: rgba(102, 126, 234, 0.1);
|
| 1342 |
-
border: 2px solid rgba(102, 126, 234, 0.3);
|
| 1343 |
-
border-radius: 12px;
|
| 1344 |
-
padding: 1rem;
|
| 1345 |
-
color: #cbd5e1;
|
| 1346 |
-
font-size: 0.85rem;
|
| 1347 |
-
line-height: 1.6;
|
| 1348 |
-
}
|
| 1349 |
-
|
| 1350 |
-
.demo-note strong {
|
| 1351 |
-
color: #f1f5f9;
|
| 1352 |
-
display: block;
|
| 1353 |
-
margin-bottom: 0.5rem;
|
| 1354 |
-
}
|
| 1355 |
-
|
| 1356 |
-
.demo-note p {
|
| 1357 |
-
margin: 0;
|
| 1358 |
-
}
|
| 1359 |
-
|
| 1360 |
-
/* API Keys Section */
|
| 1361 |
-
.api-note {
|
| 1362 |
-
color: #94a3b8;
|
| 1363 |
-
font-size: 0.85rem;
|
| 1364 |
-
margin-bottom: 1rem;
|
| 1365 |
-
line-height: 1.5;
|
| 1366 |
-
}
|
| 1367 |
-
|
| 1368 |
-
.demo-input {
|
| 1369 |
-
width: 100%;
|
| 1370 |
-
padding: 0.75rem 1rem;
|
| 1371 |
-
background: #0f172a;
|
| 1372 |
-
border: 2px solid #334155;
|
| 1373 |
-
border-radius: 8px;
|
| 1374 |
-
color: #f1f5f9;
|
| 1375 |
-
font-size: 0.9rem;
|
| 1376 |
-
transition: all 0.2s ease;
|
| 1377 |
-
}
|
| 1378 |
-
|
| 1379 |
-
.demo-input:hover,
|
| 1380 |
-
.demo-input:focus {
|
| 1381 |
-
border-color: #667eea;
|
| 1382 |
-
outline: none;
|
| 1383 |
-
}
|
| 1384 |
-
|
| 1385 |
-
.demo-input::placeholder {
|
| 1386 |
-
color: #475569;
|
| 1387 |
-
}
|
| 1388 |
-
|
| 1389 |
-
.api-link-inline {
|
| 1390 |
-
color: #667eea;
|
| 1391 |
-
text-decoration: none;
|
| 1392 |
-
font-size: 0.85rem;
|
| 1393 |
-
margin-left: 0.5rem;
|
| 1394 |
-
transition: color 0.2s ease;
|
| 1395 |
-
}
|
| 1396 |
-
|
| 1397 |
-
.api-link-inline:hover {
|
| 1398 |
-
color: #764ba2;
|
| 1399 |
-
text-decoration: underline;
|
| 1400 |
-
}
|
| 1401 |
-
|
| 1402 |
-
.mode-indicator {
|
| 1403 |
-
margin-top: 1rem;
|
| 1404 |
-
padding: 0.75rem;
|
| 1405 |
-
background: rgba(102, 126, 234, 0.1);
|
| 1406 |
-
border-radius: 8px;
|
| 1407 |
-
text-align: center;
|
| 1408 |
-
}
|
| 1409 |
-
|
| 1410 |
-
#mode-text {
|
| 1411 |
-
color: #cbd5e1;
|
| 1412 |
-
font-size: 0.85rem;
|
| 1413 |
-
font-weight: 600;
|
| 1414 |
-
}
|
| 1415 |
-
|
| 1416 |
-
/* Secondary Controls Row */
|
| 1417 |
-
.demo-controls-secondary {
|
| 1418 |
-
display: flex;
|
| 1419 |
-
gap: 1rem;
|
| 1420 |
-
flex-wrap: wrap;
|
| 1421 |
-
margin-top: 1rem;
|
| 1422 |
-
}
|
| 1423 |
-
|
| 1424 |
-
.demo-controls-secondary .btn-demo {
|
| 1425 |
-
flex: 1;
|
| 1426 |
-
min-width: 120px;
|
| 1427 |
-
}
|
| 1428 |
-
|
| 1429 |
-
/* Pure Reachy Mode Info */
|
| 1430 |
-
.reachy-description {
|
| 1431 |
-
color: #cbd5e1;
|
| 1432 |
-
font-size: 0.85rem;
|
| 1433 |
-
line-height: 1.6;
|
| 1434 |
-
margin: 0;
|
| 1435 |
-
}
|
| 1436 |
-
|
| 1437 |
-
/* Demo Download Button */
|
| 1438 |
-
.demo-download-btn {
|
| 1439 |
-
display: inline-block;
|
| 1440 |
-
margin-top: 1rem;
|
| 1441 |
-
padding: 0.75rem 1.5rem;
|
| 1442 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 1443 |
-
color: white;
|
| 1444 |
-
text-decoration: none;
|
| 1445 |
-
border-radius: 8px;
|
| 1446 |
-
font-weight: 600;
|
| 1447 |
-
transition: all 0.2s ease;
|
| 1448 |
-
}
|
| 1449 |
-
|
| 1450 |
-
.demo-download-btn:hover {
|
| 1451 |
-
transform: translateY(-2px);
|
| 1452 |
-
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
| 1453 |
-
}
|
| 1454 |
-
|
| 1455 |
.demo-section .section-subtitle {
|
| 1456 |
max-width: 1500px;
|
| 1457 |
line-height: 1.8;
|
|
@@ -1514,43 +1145,6 @@ section {
|
|
| 1514 |
transform: translateY(-3px);
|
| 1515 |
}
|
| 1516 |
|
| 1517 |
-
/* Scroll Indicator */
|
| 1518 |
-
.scroll-indicator {
|
| 1519 |
-
position: absolute;
|
| 1520 |
-
bottom: 2rem;
|
| 1521 |
-
left: 50%;
|
| 1522 |
-
transform: translateX(-50%);
|
| 1523 |
-
z-index: 10;
|
| 1524 |
-
}
|
| 1525 |
-
|
| 1526 |
-
.scroll-arrow {
|
| 1527 |
-
display: flex;
|
| 1528 |
-
align-items: center;
|
| 1529 |
-
justify-content: center;
|
| 1530 |
-
width: 48px;
|
| 1531 |
-
height: 48px;
|
| 1532 |
-
border-radius: 50%;
|
| 1533 |
-
background: rgba(255, 255, 255, 0.1);
|
| 1534 |
-
border: 2px solid rgba(255, 255, 255, 0.3);
|
| 1535 |
-
color: white;
|
| 1536 |
-
text-decoration: none;
|
| 1537 |
-
transition: all 0.3s ease;
|
| 1538 |
-
animation: bounce 2s infinite;
|
| 1539 |
-
}
|
| 1540 |
-
|
| 1541 |
-
.scroll-arrow:hover {
|
| 1542 |
-
background: rgba(255, 255, 255, 0.2);
|
| 1543 |
-
border-color: rgba(255, 255, 255, 0.5);
|
| 1544 |
-
}
|
| 1545 |
-
|
| 1546 |
-
@keyframes bounce {
|
| 1547 |
-
0%, 100% {
|
| 1548 |
-
transform: translateY(0);
|
| 1549 |
-
}
|
| 1550 |
-
50% {
|
| 1551 |
-
transform: translateY(-10px);
|
| 1552 |
-
}
|
| 1553 |
-
}
|
| 1554 |
|
| 1555 |
/* Make hero section position relative for scroll indicator */
|
| 1556 |
.hero-section {
|
|
@@ -1562,14 +1156,6 @@ section {
|
|
| 1562 |
max-width: 1450px; /* Wider for more spread out content */
|
| 1563 |
}
|
| 1564 |
|
| 1565 |
-
/* Robot Animation Container */
|
| 1566 |
-
.robot-container {
|
| 1567 |
-
display: flex;
|
| 1568 |
-
flex-direction: column;
|
| 1569 |
-
align-items: center;
|
| 1570 |
-
gap: 1rem;
|
| 1571 |
-
}
|
| 1572 |
-
|
| 1573 |
/* Green background container (stationary, vertical rectangle) */
|
| 1574 |
.robot-bg {
|
| 1575 |
background: #3DDE99; /* Reachy green */
|
|
@@ -1588,7 +1174,7 @@ section {
|
|
| 1588 |
height: 150%;
|
| 1589 |
display: block;
|
| 1590 |
object-fit: cover; /* Fill the container */
|
| 1591 |
-
transform-origin: center center;
|
| 1592 |
margin: -25%; /* Re-center after zoom */
|
| 1593 |
animation: robot-idle 3s ease-in-out infinite;
|
| 1594 |
}
|
|
@@ -1871,22 +1457,6 @@ section {
|
|
| 1871 |
}
|
| 1872 |
}
|
| 1873 |
|
| 1874 |
-
/* Mobile-specific subtitle */
|
| 1875 |
-
.mobile-subtitle {
|
| 1876 |
-
display: none; /* Hidden on desktop */
|
| 1877 |
-
}
|
| 1878 |
-
|
| 1879 |
-
@media (max-width: 640px) {
|
| 1880 |
-
.mobile-subtitle {
|
| 1881 |
-
display: block; /* Show on mobile */
|
| 1882 |
-
color: #cbd5e1;
|
| 1883 |
-
font-size: 0.85rem;
|
| 1884 |
-
line-height: 1.5;
|
| 1885 |
-
text-align: center;
|
| 1886 |
-
margin-bottom: 2rem;
|
| 1887 |
-
padding: 0 1rem;
|
| 1888 |
-
}
|
| 1889 |
-
}
|
| 1890 |
|
| 1891 |
/* Mobile: Tighter hero spacing and GIF closer to top */
|
| 1892 |
@media (max-width: 640px) {
|
|
@@ -1985,119 +1555,6 @@ h4, h5, h6, strong {
|
|
| 1985 |
font-weight: 500; /* Medium for buttons */
|
| 1986 |
}
|
| 1987 |
|
| 1988 |
-
/* Mobile: Smaller 3-column personalities */
|
| 1989 |
-
@media (max-width: 640px) {
|
| 1990 |
-
.personalities-grid {
|
| 1991 |
-
grid-template-columns: repeat(3, 1fr);
|
| 1992 |
-
gap: 0.25rem; /* Tighter spacing */
|
| 1993 |
-
}
|
| 1994 |
-
|
| 1995 |
-
.personality-card {
|
| 1996 |
-
padding: 0.5rem 0.25rem; /* Minimal padding */
|
| 1997 |
-
border: none;
|
| 1998 |
-
background: transparent;
|
| 1999 |
-
box-shadow: none;
|
| 2000 |
-
}
|
| 2001 |
-
|
| 2002 |
-
.personality-card:hover {
|
| 2003 |
-
transform: none;
|
| 2004 |
-
box-shadow: none;
|
| 2005 |
-
}
|
| 2006 |
-
|
| 2007 |
-
.personality-emoji {
|
| 2008 |
-
font-size: 1.5rem;
|
| 2009 |
-
margin-bottom: 0.25rem;
|
| 2010 |
-
}
|
| 2011 |
-
|
| 2012 |
-
.personality-card h3 {
|
| 2013 |
-
font-size: 0.75rem;
|
| 2014 |
-
margin-bottom: 0.25rem;
|
| 2015 |
-
}
|
| 2016 |
-
|
| 2017 |
-
.personality-card p {
|
| 2018 |
-
display: none; /* Hide description on mobile */
|
| 2019 |
-
}
|
| 2020 |
-
|
| 2021 |
-
.personality-tags {
|
| 2022 |
-
display: none; /* Hide tags to save space */
|
| 2023 |
-
}
|
| 2024 |
-
}
|
| 2025 |
-
|
| 2026 |
-
/* Mobile: Compact features (3 columns) */
|
| 2027 |
-
@media (max-width: 640px) {
|
| 2028 |
-
.features-grid {
|
| 2029 |
-
grid-template-columns: repeat(3, 1fr);
|
| 2030 |
-
gap: 0.25rem;
|
| 2031 |
-
}
|
| 2032 |
-
|
| 2033 |
-
.feature-item {
|
| 2034 |
-
padding: 0.75rem 0.5rem;
|
| 2035 |
-
border: none;
|
| 2036 |
-
background: transparent;
|
| 2037 |
-
box-shadow: none;
|
| 2038 |
-
}
|
| 2039 |
-
|
| 2040 |
-
.feature-item:hover {
|
| 2041 |
-
transform: none;
|
| 2042 |
-
}
|
| 2043 |
-
|
| 2044 |
-
.feature-icon-badge {
|
| 2045 |
-
width: 36px;
|
| 2046 |
-
height: 36px;
|
| 2047 |
-
margin-bottom: 0.5rem;
|
| 2048 |
-
border-radius: 8px;
|
| 2049 |
-
}
|
| 2050 |
-
|
| 2051 |
-
.feature-icon-badge svg {
|
| 2052 |
-
width: 16px;
|
| 2053 |
-
height: 16px;
|
| 2054 |
-
}
|
| 2055 |
-
|
| 2056 |
-
.feature-item h3 {
|
| 2057 |
-
font-size: 0.75rem;
|
| 2058 |
-
margin-bottom: 0.25rem;
|
| 2059 |
-
}
|
| 2060 |
-
|
| 2061 |
-
.feature-item p {
|
| 2062 |
-
display: none; /* Hide description */
|
| 2063 |
-
}
|
| 2064 |
-
}
|
| 2065 |
-
|
| 2066 |
-
/* Mobile: Compact steps (2x2 grid for 4 steps) */
|
| 2067 |
-
@media (max-width: 640px) {
|
| 2068 |
-
.steps-flow {
|
| 2069 |
-
grid-template-columns: repeat(2, 1fr);
|
| 2070 |
-
gap: 0.5rem;
|
| 2071 |
-
}
|
| 2072 |
-
|
| 2073 |
-
.step-box {
|
| 2074 |
-
padding: 1rem 0.75rem;
|
| 2075 |
-
margin: 0;
|
| 2076 |
-
border: none;
|
| 2077 |
-
background: transparent;
|
| 2078 |
-
box-shadow: none;
|
| 2079 |
-
}
|
| 2080 |
-
|
| 2081 |
-
.step-box:hover {
|
| 2082 |
-
transform: none;
|
| 2083 |
-
}
|
| 2084 |
-
|
| 2085 |
-
.step-num {
|
| 2086 |
-
width: 32px;
|
| 2087 |
-
height: 32px;
|
| 2088 |
-
font-size: 0.9rem;
|
| 2089 |
-
margin-bottom: 0.5rem;
|
| 2090 |
-
}
|
| 2091 |
-
|
| 2092 |
-
.step-box h3 {
|
| 2093 |
-
font-size: 0.75rem;
|
| 2094 |
-
margin-bottom: 0.25rem;
|
| 2095 |
-
}
|
| 2096 |
-
|
| 2097 |
-
.step-box p {
|
| 2098 |
-
font-size: 0.65rem;
|
| 2099 |
-
}
|
| 2100 |
-
}
|
| 2101 |
|
| 2102 |
/* Mobile: Prevent all overflow */
|
| 2103 |
@media (max-width: 640px) {
|
|
@@ -2126,7 +1583,6 @@ h4, h5, h6, strong {
|
|
| 2126 |
@media (max-width: 640px) {
|
| 2127 |
.config-section,
|
| 2128 |
.features-section,
|
| 2129 |
-
.how-section,
|
| 2130 |
.personalities-section {
|
| 2131 |
display: none;
|
| 2132 |
}
|
|
|
|
| 335 |
margin-right: auto;
|
| 336 |
}
|
| 337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
/* Configuration Section */
|
| 339 |
.config-section {
|
| 340 |
text-align: center;
|
|
|
|
| 391 |
opacity: 1;
|
| 392 |
}
|
| 393 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
.config-card h3 {
|
| 395 |
font-family: 'Inter', sans-serif;
|
| 396 |
font-size: 1.5rem;
|
|
|
|
| 705 |
color: var(--coral);
|
| 706 |
}
|
| 707 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
|
| 709 |
/* Footer */
|
| 710 |
.footer {
|
|
|
|
| 750 |
}
|
| 751 |
|
| 752 |
/* Responsive Design */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
|
| 754 |
@media (max-width: 768px) {
|
| 755 |
.hero-grid {
|
|
|
|
| 808 |
.api-link-inline {
|
| 809 |
display: none;
|
| 810 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
}
|
| 812 |
|
| 813 |
@media (max-width: 480px) {
|
|
|
|
| 819 |
font-size: 1.75rem;
|
| 820 |
}
|
| 821 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 822 |
.logo-text {
|
| 823 |
font-size: 0.9rem;
|
| 824 |
}
|
|
|
|
| 1083 |
font-style: italic;
|
| 1084 |
}
|
| 1085 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1086 |
.demo-section .section-subtitle {
|
| 1087 |
max-width: 1500px;
|
| 1088 |
line-height: 1.8;
|
|
|
|
| 1145 |
transform: translateY(-3px);
|
| 1146 |
}
|
| 1147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1148 |
|
| 1149 |
/* Make hero section position relative for scroll indicator */
|
| 1150 |
.hero-section {
|
|
|
|
| 1156 |
max-width: 1450px; /* Wider for more spread out content */
|
| 1157 |
}
|
| 1158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1159 |
/* Green background container (stationary, vertical rectangle) */
|
| 1160 |
.robot-bg {
|
| 1161 |
background: #3DDE99; /* Reachy green */
|
|
|
|
| 1174 |
height: 150%;
|
| 1175 |
display: block;
|
| 1176 |
object-fit: cover; /* Fill the container */
|
| 1177 |
+
transform-origin: center center;
|
| 1178 |
margin: -25%; /* Re-center after zoom */
|
| 1179 |
animation: robot-idle 3s ease-in-out infinite;
|
| 1180 |
}
|
|
|
|
| 1457 |
}
|
| 1458 |
}
|
| 1459 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1460 |
|
| 1461 |
/* Mobile: Tighter hero spacing and GIF closer to top */
|
| 1462 |
@media (max-width: 640px) {
|
|
|
|
| 1555 |
font-weight: 500; /* Medium for buttons */
|
| 1556 |
}
|
| 1557 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1558 |
|
| 1559 |
/* Mobile: Prevent all overflow */
|
| 1560 |
@media (max-width: 640px) {
|
|
|
|
| 1583 |
@media (max-width: 640px) {
|
| 1584 |
.config-section,
|
| 1585 |
.features-section,
|
|
|
|
| 1586 |
.personalities-section {
|
| 1587 |
display: none;
|
| 1588 |
}
|