sentinel / sensor_bridge.py
kswffs's picture
Upload folder using huggingface_hub
b96103d verified
Raw
History Blame Contribute Delete
35.1 kB
# JavaScript Sensor Bridge for Sentinel Gradio Space
# Feeds real-time browser sensor data to hidden Gradio components
SENSOR_BRIDGE_HTML = """
<div id="sensor-status-indicator" style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px;">
<span id="status-dot" style="width: 10px; height: 10px; border-radius: 50%; background: #ef4444; display: inline-block;"></span>
<span id="status-text" style="color: #9ca3af; font-weight: 500;">Telemetry: Offline</span>
</div>
<!-- Hidden DOM elements used for camera stream capture -->
<video id="sentinel-video" autoplay playsinline style="display: none;"></video>
<canvas id="sentinel-canvas" style="display: none;"></canvas>
<script>
(function() {
let videoStream = null;
let audioStream = null;
let audioCtx = null;
let analyser = null;
let dopplerOsc = null;
let dopplerGain = null;
let cameraActive = false;
let watchId = null;
let lightSensor = null;
let isActivated = false;
// Audio volume monitoring variables
let loudAudioStartTime = null;
const LOUD_THRESHOLD = 0.08; // RMS volume threshold (0.0 to 1.0)
// Telemetry throttling (send updates every 200ms to avoid flooding Gradio)
let lastUpdateTimes = {
motion: 0,
orientation: 0,
audio: 0
};
console.log("Sentinel Sensor Bridge Initialized.");
// Helper to safely set Gradio hidden component values in the DOM and trigger updates
function setGradioValue(elemId, value) {
const container = document.getElementById(elemId);
if (!container) return;
const input = container.querySelector("input") || container.querySelector("textarea");
if (input) {
// Only update if value changed to prevent unnecessary Svelte re-renders
if (input.value !== String(value)) {
input.value = String(value);
// Dispatch event so Gradio's Svelte binding captures the change
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}
}
// --- 1. CAMERA STREAM (2 FPS) ---
async function startCamera() {
const video = document.getElementById("sentinel-video");
const canvas = document.getElementById("sentinel-canvas");
if (!video || !canvas) return;
try {
videoStream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment', // Request rear camera
width: { ideal: 640 },
height: { ideal: 480 }
},
audio: false
});
video.srcObject = videoStream;
cameraActive = true;
logger("Camera active.");
// Start the frame capture loop
requestAnimationFrame(captureFrame);
} catch (e) {
console.error("Camera access failed:", e);
logger("Camera error: " + e.message, true);
}
}
function captureFrame() {
if (!cameraActive) return;
const video = document.getElementById("sentinel-video");
const canvas = document.getElementById("sentinel-canvas");
if (video && video.readyState === video.HAVE_ENOUGH_DATA) {
const ctx = canvas.getContext("2d");
// Downscale to 320x240 for fast CPU differencing
canvas.width = 320;
canvas.height = 240;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// Convert to base64 JPEG at 0.6 quality (compromise between size and visual cues)
const base64Data = canvas.toDataURL("image/jpeg", 0.6);
// Extract raw base64 payload (strip header)
const rawBase64 = base64Data.split(",")[1];
// Update hidden Gradio textbox
setGradioValue("image-data", rawBase64);
setGradioValue("pipeline-stage", "yolo");
}
// Loop every 500ms (2 FPS)
setTimeout(() => {
if (cameraActive) {
requestAnimationFrame(captureFrame);
}
}, 500);
}
// --- 2. MOTION (ACCELERATION) ---
async function requestMotionPermission() {
if (typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function') {
try {
const permission = await DeviceMotionEvent.requestPermission();
if (permission === 'granted') {
window.addEventListener('devicemotion', handleMotion);
logger("Motion sensors active (iOS).");
} else {
logger("Motion permission denied.", true);
}
} catch (e) {
logger("Motion init error: " + e.message, true);
}
} else {
// Android / Desktop
window.addEventListener('devicemotion', handleMotion);
logger("Motion sensors active.");
}
}
function handleMotion(event) {
const now = Date.now();
if (now - lastUpdateTimes.motion < 200) return; // Throttle to 5Hz
lastUpdateTimes.motion = now;
const acc = event.accelerationIncludingGravity || event.acceleration;
if (acc) {
// Normalize values. Default to 0 if NaN.
const x = acc.x ? acc.x.toFixed(2) : "0.00";
const y = acc.y ? acc.y.toFixed(2) : "0.00";
const z = acc.z ? acc.z.toFixed(2) : "0.00";
setGradioValue("accel-x", x);
setGradioValue("accel-y", y);
setGradioValue("accel-z", z);
}
}
// --- 3. ORIENTATION (COMPASS / TILT) ---
function startOrientation() {
window.addEventListener('deviceorientation', handleOrientation);
}
function handleOrientation(event) {
const now = Date.now();
if (now - lastUpdateTimes.orientation < 250) return; // Throttle to 4Hz
lastUpdateTimes.orientation = now;
// alpha: rotation around z-axis (compass heading 0-360)
let heading = event.alpha;
// iOS specific heading
if (event.webkitCompassHeading) {
heading = event.webkitCompassHeading;
}
if (heading !== null && heading !== undefined) {
setGradioValue("heading-val", heading.toFixed(1));
}
if (event.beta !== null && event.beta !== undefined) {
setGradioValue("gyro-beta", event.beta.toFixed(2));
}
if (event.gamma !== null && event.gamma !== undefined) {
setGradioValue("gyro-gamma", event.gamma.toFixed(2));
}
}
// --- 4. GPS GEOLOCATION ---
function startGPS() {
if (navigator.geolocation) {
watchId = navigator.geolocation.watchPosition(
(pos) => {
const lat = pos.coords.latitude.toFixed(6);
const lon = pos.coords.longitude.toFixed(6);
setGradioValue("gps-lat", lat);
setGradioValue("gps-lon", lon);
},
(err) => {
console.error("GPS Watch failed:", err);
logger("GPS: Unavailable", true);
},
{ enableHighAccuracy: true, timeout: 10000 }
);
logger("GPS Active.");
} else {
logger("GPS unsupported.", true);
}
}
// --- 5. AMBIENT LIGHT SENSOR ---
function startLightSensor() {
try {
if ('AmbientLightSensor' in window) {
lightSensor = new AmbientLightSensor();
lightSensor.addEventListener('reading', () => {
setGradioValue("light-level", lightSensor.illuminance.toFixed(1));
});
lightSensor.addEventListener('error', (event) => {
console.error("AmbientLightSensor error:", event.error);
});
lightSensor.start();
logger("Light sensor active.");
} else {
logger("Light sensor unsupported.");
}
} catch (e) {
console.warn("AmbientLightSensor failed initialization:", e);
}
}
// --- 6. BATTERY STATUS ---
async function startBatteryMonitoring() {
if (navigator.getBattery) {
try {
const battery = await navigator.getBattery();
const updateBattery = () => {
const pct = Math.round(battery.level * 100);
setGradioValue("battery-pct", pct);
};
updateBattery();
battery.addEventListener('levelchange', updateBattery);
} catch (e) {
console.error("Battery API failed:", e);
}
}
}
// --- 7. AUDIO VOLUME MONITOR (RMS) ---
async function startAudioCapture() {
try {
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
// Initialize Web Audio Context
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const source = audioCtx.createMediaStreamSource(audioStream);
analyser = audioCtx.createAnalyser();
analyser.fftSize = 512;
source.connect(analyser);
logger("Microphone active.");
monitorAudioVolume();
} catch (e) {
console.error("Microphone access failed:", e);
logger("Mic error: " + e.message, true);
}
}
function monitorAudioVolume() {
if (!audioCtx || !analyser) return;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const checkVolume = () => {
if (!audioCtx || !analyser) return;
analyser.getByteTimeDomainData(dataArray);
// Compute Root Mean Square (RMS) volume
let sum = 0;
for (let i = 0; i < bufferLength; i++) {
const val = (dataArray[i] - 128) / 128.0;
sum += val * val;
}
const rms = Math.sqrt(sum / bufferLength);
const now = Date.now();
if (now - lastUpdateTimes.audio > 200) {
lastUpdateTimes.audio = now;
// Scale RMS to a percentage for the UI
const volPct = Math.min(Math.round(rms * 100 * 5), 100); // amplify for visual readability
setGradioValue("audio-level", volPct);
}
// Loud Audio detection logic: must be above threshold for > 1 second
if (rms > LOUD_THRESHOLD) {
if (loudAudioStartTime === null) {
loudAudioStartTime = Date.now();
} else if (Date.now() - loudAudioStartTime > 1000) {
// Trigger alert flag
setGradioValue("loud-audio-flag", "true");
}
} else {
loudAudioStartTime = null;
setGradioValue("loud-audio-flag", "false");
}
setTimeout(checkVolume, 100); // Check every 100ms
};
checkVolume();
}
// --- 8. ULTRASONIC DOPPLER MOTION SENSING (20kHz) ---
function startUltrasonicDoppler() {
if (!audioCtx) return;
try {
logger("Starting ultrasonic Doppler sensor...");
// 1. Create a high-frequency oscillator near ultrasound limit (20 kHz)
dopplerOsc = audioCtx.createOscillator();
dopplerOsc.type = "sine";
dopplerOsc.frequency.value = 20000; // 20 kHz
// 2. Control volume (low gain to keep it safe for pets/hearing range)
dopplerGain = audioCtx.createGain();
dopplerGain.gain.value = 0.02; // Very quiet
dopplerOsc.connect(dopplerGain);
dopplerGain.connect(audioCtx.destination);
dopplerOsc.start();
// 3. Monitor reflections in FFT bins around 20kHz
monitorDopplerReflections();
} catch (e) {
console.warn("Ultrasonic Doppler failed initialization:", e);
}
}
function monitorDopplerReflections() {
if (!audioCtx || !analyser || !dopplerOsc) return;
const sampleRate = audioCtx.sampleRate;
const fftSize = analyser.fftSize;
const binHz = sampleRate / fftSize;
// Find index of FFT bin closest to 20kHz
const targetBin = Math.round(20000 / binHz);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
const checkDoppler = () => {
if (!audioCtx || !analyser || !dopplerOsc) return;
analyser.getByteFrequencyData(dataArray);
// Check adjacent bins to see if energy is leaking (indicates Doppler frequency shift)
const leftBinVal = dataArray[targetBin - 1] || 0;
const rightBinVal = dataArray[targetBin + 1] || 0;
const centerBinVal = dataArray[targetBin] || 0;
const sideEnergy = leftBinVal + rightBinVal;
// If side energy is high relative to center energy, motion is shifting the frequency
if (centerBinVal > 30 && sideEnergy > centerBinVal * 0.4) {
setGradioValue("doppler-motion-flag", "true");
} else {
setGradioValue("doppler-motion-flag", "false");
}
setTimeout(checkDoppler, 200);
};
checkDoppler();
}
// --- STATUS INDICATORS & STATE ---
function logger(msg, isError = false) {
const dot = document.getElementById("status-dot");
const txt = document.getElementById("status-text");
if (dot && txt) {
if (isError) {
dot.style.background = "#ef4444"; // Red
txt.innerHTML = "Status: " + msg;
txt.style.color = "#f87171";
} else {
dot.style.background = "#22c55e"; // Green
txt.innerHTML = "Telemetry: " + msg;
txt.style.color = "#34d399";
}
}
}
// --- MAIN CONTROLS ---
async function startAllSensors() {
logger("Initializing telemetry...");
isActivated = true;
// Start non-permission variables
startOrientation();
startBatteryMonitoring();
// Request GPS
startGPS();
// Request Camera (triggers capture loops)
await startCamera();
// Request Microphone (triggers volume loops)
await startAudioCapture();
// Request Motion / Accel (requires iOS permission popup)
await requestMotionPermission();
// Start experimental Doppler if audio context loaded
startUltrasonicDoppler();
// Start Chrome-specific Light sensor
startLightSensor();
// Update Gradio active state
setGradioValue("sentinel-active-state", "true");
}
function stopAllSensors() {
logger("Offline");
isActivated = false;
cameraActive = false;
// Stop Camera Streams
if (videoStream) {
videoStream.getTracks().forEach(track => track.stop());
videoStream = null;
}
// Stop Mic Streams
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
audioStream = null;
}
// Stop Doppler Oscillator
if (dopplerOsc) {
try { dopplerOsc.stop(); } catch(e) {}
dopplerOsc = null;
}
// Stop Geolocation watch
if (watchId !== null) {
navigator.geolocation.clearWatch(watchId);
watchId = null;
}
// Stop Light Sensor
if (lightSensor) {
try { lightSensor.stop(); } catch(e) {}
lightSensor = null;
}
// Stop listeners
window.removeEventListener('devicemotion', handleMotion);
window.removeEventListener('deviceorientation', handleOrientation);
// Update Gradio active state
setGradioValue("sentinel-active-state", "false");
}
// --- VISUAL UPGRADES: SVG DETECTIONS, COST TICKER, HAPTICS ---
function drawDetections(dets) {
let svg = document.getElementById("sentinel-svg-overlay");
if (!svg) {
const parent = document.getElementById("camera-feed");
if (!parent) return;
parent.style.position = "relative";
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.id = "sentinel-svg-overlay";
svg.setAttribute("viewBox", "0 0 320 240");
svg.style.position = "absolute";
svg.style.top = "0";
svg.style.left = "0";
svg.style.width = "100%";
svg.style.height = "100%";
svg.style.pointerEvents = "none";
svg.style.zIndex = "10";
parent.appendChild(svg);
}
svg.innerHTML = "";
if (!dets || dets.length === 0) return;
dets.forEach(det => {
const [x1, y1, x2, y2] = det.bbox;
const width = x2 - x1;
const height = y2 - y1;
let color = "#10b981"; // green
if (det.class_name === "person") color = "#3b82f6"; // blue
else if (det.class_name === "vehicle" || det.class_name === "car" || det.class_name === "truck") color = "#ef4444"; // red
else if (det.class_name === "fire" || det.class_name === "smoke") color = "#f97316"; // orange
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
const dVal = `
M ${x1 + 10} ${y1} H ${x1} V ${y1 + 10}
M ${x2 - 10} ${y1} H ${x2} V ${y1 + 10}
M ${x1 + 10} ${y2} H ${x1} V ${y2 - 10}
M ${x2 - 10} ${y2} H ${x2} V ${y2 - 10}
`;
path.setAttribute("d", dVal);
path.setAttribute("stroke", color);
path.setAttribute("stroke-width", "2");
path.setAttribute("fill", "none");
g.appendChild(path);
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", x1);
rect.setAttribute("y", y1);
rect.setAttribute("width", width);
rect.setAttribute("height", height);
rect.setAttribute("fill", color);
rect.setAttribute("fill-opacity", "0.08");
g.appendChild(rect);
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", x1 + 4);
text.setAttribute("y", y1 - 4 > 12 ? y1 - 4 : y1 + 14);
text.setAttribute("fill", color);
text.setAttribute("font-size", "10px");
text.setAttribute("font-weight", "bold");
text.setAttribute("font-family", "monospace");
text.textContent = `${det.class_name} ${(det.confidence * 100).toFixed(0)}%`;
g.appendChild(text);
svg.appendChild(g);
});
}
function bindDetectionDataObserver() {
const container = document.getElementById("detection-data");
if (container) {
const textarea = container.querySelector("textarea") || container.querySelector("input");
if (textarea) {
const observer = new MutationObserver(() => {
try {
const val = textarea.value;
if (val) drawDetections(JSON.parse(val));
} catch (e) {}
});
observer.observe(textarea, { attributes: true, attributeFilter: ['value'] });
let lastVal = "";
setInterval(() => {
const val = textarea.value;
if (val !== lastVal) {
lastVal = val;
try {
if (val) drawDetections(JSON.parse(val));
} catch (e) {}
}
}, 250);
console.log("Sentinel detection-data observer bound.");
return;
}
}
setTimeout(bindDetectionDataObserver, 500);
}
function bindAlertBannerObserver() {
const container = document.getElementById("alert-banner-container");
if (container) {
const observer = new MutationObserver(() => {
const banner = container.querySelector(".alert-banner-critical, .alert-banner-warning");
if (banner) {
if (banner.classList.contains('alert-banner-critical')) {
if (navigator.vibrate) navigator.vibrate([500]);
} else if (banner.classList.contains('alert-banner-warning')) {
if (navigator.vibrate) navigator.vibrate([200, 100, 200]);
}
}
});
observer.observe(container, { childList: true, subtree: true });
console.log("Sentinel alert banner observer bound for haptics.");
return;
}
setTimeout(bindAlertBannerObserver, 500);
}
function updateCostTicker() {
const costInput = document.querySelector("#cost-total input");
const frameInput = document.querySelector("#frame-count input");
const ticker = document.getElementById("cost-ticker");
if (!ticker) return;
let sentinelCost = 0.0;
let frameCount = 0;
if (costInput && costInput.value) {
sentinelCost = parseFloat(costInput.value);
}
if (frameInput && frameInput.value) {
frameCount = parseInt(frameInput.value);
}
const gptCost = frameCount * 0.01;
const savings = gptCost > 0 ? ((gptCost - sentinelCost) / gptCost * 100).toFixed(1) : "0.0";
ticker.innerHTML = `
<div class="ticker-wrapper" style="display: flex; justify-content: space-between; align-items: center; background: rgba(30, 30, 50, 0.6); padding: 12px; border-radius: 8px; border: 1px solid rgba(99, 102, 241, 0.2); font-family: monospace; font-size: 14px; margin-top: 10px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);">
<div class="ticker-col" style="flex: 1; text-align: left;">
<div style="color: #9ca3af; font-size: 11px; text-transform: uppercase;">🛡️ Sentinel Cost</div>
<div style="font-size: 20px; font-weight: bold; color: #34d399; margin-top: 4px;">$${sentinelCost.toFixed(5)}</div>
</div>
<div class="ticker-arrow" style="padding: 0 16px; color: #6366f1; font-size: 16px; font-weight: bold; text-shadow: 0 0 8px rgba(99,102,241,0.5);">
&larr; vs &rarr;
</div>
<div class="ticker-col" style="flex: 1; text-align: right;">
<div style="color: #9ca3af; font-size: 11px; text-transform: uppercase;">🔴 Naive GPT-4o</div>
<div style="font-size: 20px; font-weight: bold; color: #f87171; margin-top: 4px;">$${gptCost.toFixed(4)}</div>
</div>
</div>
<div style="text-align: center; font-size: 12px; color: #818cf8; font-weight: 500; margin-top: 6px; text-shadow: 0 0 8px rgba(99, 102, 241, 0.4);">
⚡ Saving ${savings}% on API costs (Evaluating ${frameCount} frames)
</div>
`;
}
function bindVoiceQueryButton() {
const btn = document.getElementById("voice-btn");
if (!btn) {
setTimeout(bindVoiceQueryButton, 500);
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
btn.innerHTML = "🎤 VOICE UNSUPPORTED";
btn.disabled = true;
return;
}
const recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.interimResults = false;
recognition.lang = 'en-US';
let isRecording = false;
btn.addEventListener("click", () => {
if (!isRecording) {
try {
recognition.start();
btn.innerHTML = "🔴 LISTENING...";
btn.style.background = "linear-gradient(135deg, #ef4444 0%, #dc2626 100%)";
isRecording = true;
} catch (e) {
console.error("Speech recognition start failed:", e);
}
} else {
try {
recognition.stop();
} catch (e) {}
}
});
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
console.log("Speech recognized:", transcript);
setGradioValue("voice-query", transcript);
};
recognition.onend = () => {
btn.innerHTML = "🎤 VOICE QUERY";
btn.style.background = "";
isRecording = false;
};
recognition.onerror = (event) => {
console.error("Speech recognition error:", event.error);
recognition.stop();
};
}
function bindSOSButton() {
const btn = document.getElementById("sos-btn");
if (!btn) {
setTimeout(bindSOSButton, 500);
return;
}
btn.addEventListener("click", () => {
const latInput = document.querySelector("#gps-lat input");
const lonInput = document.querySelector("#gps-lon input");
let lat = "unknown";
let lon = "unknown";
if (latInput && latInput.value && latInput.value !== "0") lat = latInput.value;
if (lonInput && lonInput.value && lonInput.value !== "0") lon = lonInput.value;
const banner = document.getElementById("alert-banner-container");
let alertText = "No active alert.";
if (banner) {
alertText = banner.innerText.trim();
}
const message = `SENTINEL EMERGENCY: Visually Impaired user needs assistance. Last Alert: ${alertText}. GPS Location: https://maps.google.com/?q=${lat},${lon}`;
console.log("SOS triggered. Message payload:", message);
try {
navigator.clipboard.writeText(message);
console.log("SOS message copied to clipboard.");
} catch (err) {
console.error("Failed to copy SOS message to clipboard:", err);
}
const smsUrl = `sms:?body=${encodeURIComponent(message)}`;
window.open(smsUrl, '_blank');
});
}
function initPipelineIndicator() {
const container = document.getElementById("pipeline-indicator");
if (!container) {
setTimeout(initPipelineIndicator, 500);
return;
}
const style = document.createElement("style");
style.innerHTML = `
.pipeline-row {
display: flex;
justify-content: space-around;
align-items: center;
background: rgba(15, 23, 42, 0.4);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 10px;
margin: 10px 0;
}
.pipeline-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
color: #64748b;
font-family: monospace;
font-size: 10px;
transition: all 0.3s ease;
}
.pipeline-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #334155;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
transition: all 0.3s ease;
}
.pipeline-step.active {
color: #38bdf8;
text-shadow: 0 0 8px rgba(56, 189, 248, 0.5);
}
.pipeline-step.active .pipeline-dot {
background: #38bdf8;
box-shadow: 0 0 12px #38bdf8, inset 0 1px 2px rgba(255,255,255,0.8);
animation: pulse 1s infinite alternate;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 0.8; }
100% { transform: scale(1.2); opacity: 1; }
}
`;
document.head.appendChild(style);
container.innerHTML = `
<div class="pipeline-row">
<div class="pipeline-step" id="step-frame">
<div class="pipeline-dot"></div>
<span>Frame</span>
</div>
<div style="color: #334155; font-size: 12px;">&rarr;</div>
<div class="pipeline-step" id="step-yolo">
<div class="pipeline-dot"></div>
<span>YOLO</span>
</div>
<div style="color: #334155; font-size: 12px;">&rarr;</div>
<div class="pipeline-step" id="step-pose">
<div class="pipeline-dot"></div>
<span>Pose</span>
</div>
<div style="color: #334155; font-size: 12px;">&rarr;</div>
<div class="pipeline-step" id="step-vlm">
<div class="pipeline-dot"></div>
<span>VLM</span>
</div>
<div style="color: #334155; font-size: 12px;">&rarr;</div>
<div class="pipeline-step" id="step-tts">
<div class="pipeline-dot"></div>
<span>TTS</span>
</div>
</div>
`;
console.log("Sentinel pipeline indicator initialized.");
bindPipelineObserver();
}
function bindPipelineObserver() {
const container = document.getElementById("pipeline-stage");
if (!container) return;
const textarea = container.querySelector("textarea") || container.querySelector("input");
if (!textarea) return;
function updateStage(stage) {
document.querySelectorAll(".pipeline-step").forEach(step => step.classList.remove("active"));
if (stage === "frame") {
document.getElementById("step-frame")?.classList.add("active");
} else if (stage === "yolo") {
document.getElementById("step-frame")?.classList.add("active");
document.getElementById("step-yolo")?.classList.add("active");
} else if (stage === "pose") {
document.getElementById("step-frame")?.classList.add("active");
document.getElementById("step-yolo")?.classList.add("active");
document.getElementById("step-pose")?.classList.add("active");
} else if (stage === "vlm") {
document.getElementById("step-frame")?.classList.add("active");
document.getElementById("step-yolo")?.classList.add("active");
document.getElementById("step-pose")?.classList.add("active");
document.getElementById("step-vlm")?.classList.add("active");
} else if (stage === "tts") {
document.getElementById("step-frame")?.classList.add("active");
document.getElementById("step-yolo")?.classList.add("active");
document.getElementById("step-pose")?.classList.add("active");
document.getElementById("step-vlm")?.classList.add("active");
document.getElementById("step-tts")?.classList.add("active");
}
}
const observer = new MutationObserver(() => {
updateStage(textarea.value);
});
observer.observe(textarea, { attributes: true, attributeFilter: ['value'] });
let lastVal = "";
setInterval(() => {
const val = textarea.value;
if (val !== lastVal) {
lastVal = val;
updateStage(val);
}
}, 250);
}
// Attach to the Gradio Activate Button once it is loaded in the DOM
function bindActivationButton() {
// Look for the primary activation button
const button = document.getElementById("activate-btn");
if (button) {
button.addEventListener("click", () => {
if (!isActivated) {
button.innerHTML = "🟥 DEACTIVATE";
button.style.background = "linear-gradient(135deg, #ef4444 0%, #dc2626 100%)";
startAllSensors();
} else {
button.innerHTML = "🛡️ ACTIVATE SENTINEL";
button.style.background = "linear-gradient(135deg, #6366f1 0%, #4f46e5 100%)";
stopAllSensors();
}
});
console.log("Sentinel activation button binding successful.");
} else {
// If layout hasn't rendered yet, check again shortly
setTimeout(bindActivationButton, 500);
}
}
// Run binding hooks
if (document.readyState === "complete" || document.readyState === "interactive") {
bindActivationButton();
bindDetectionDataObserver();
bindAlertBannerObserver();
bindVoiceQueryButton();
bindSOSButton();
initPipelineIndicator();
setInterval(updateCostTicker, 250);
} else {
document.addEventListener("DOMContentLoaded", () => {
bindActivationButton();
bindDetectionDataObserver();
bindAlertBannerObserver();
bindVoiceQueryButton();
bindSOSButton();
initPipelineIndicator();
setInterval(updateCostTicker, 250);
});
}
})();
</script>
"""