Skip to content

Commit 2fb72dc

Browse files
author
Paul Richards
committed
Add GitHub.io links, sample video improvements, and UX enhancements
- Add 'Try it now' links to all tools in README pointing to GitHub Pages - Default to sample video mode (live webcam now optional) - Add CORS-enabled server.py for local development - Zone Monitor: capture snapshot on detection alert - Framing Assistant: increase PTZ move duration for smoother tracking - PTZOptics Tracker: add webcam selection dropdown - Smart Counter: default to 'Eggs' with new sample video - Scoreboard Extractor: add dedicated sample video - Swap sample videos between Scene Describer and Detection Boxes - Add OBS Visual Reasoning plugin - Improve tainted canvas error messaging
1 parent 068ad6e commit 2fb72dc

File tree

23 files changed

+3308
-85
lines changed

23 files changed

+3308
-85
lines changed

01-scene-describer/app.js

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,23 @@ document.addEventListener('DOMContentLoaded', async function() {
1717

1818
let client = null;
1919
let autoDescribeInterval = null;
20+
let autoDetectInterval = null;
2021
let currentStream = null;
2122

23+
const DEFAULT_COLORS = ['#93CCEA', '#2A9D8F', '#E9C46A', '#E76F51', '#9B5DE5'];
24+
const MAX_DETECTIONS = 5;
25+
let detections = [];
26+
let detectionResults = {};
27+
28+
const detectionCanvas = document.getElementById('detectionCanvas');
29+
const detectionCtx = detectionCanvas.getContext('2d');
30+
const detectionList = document.getElementById('detectionList');
31+
const addDetectionBtn = document.getElementById('addDetectionBtn');
32+
const detectNowBtn = document.getElementById('detectNowBtn');
33+
const autoDetectCheckbox = document.getElementById('autoDetect');
34+
const detectionToggle = document.getElementById('detectionToggle');
35+
const detectionSection = document.querySelector('.detection-section');
36+
2237
window.reasoningConsole = new ReasoningConsole({
2338
startCollapsed: false
2439
});
@@ -331,6 +346,257 @@ document.addEventListener('DOMContentLoaded', async function() {
331346

332347
loadSavedPreferences();
333348

349+
detectionToggle.addEventListener('click', () => {
350+
detectionSection.classList.toggle('collapsed');
351+
savePreference('detectionCollapsed', detectionSection.classList.contains('collapsed'));
352+
});
353+
354+
function getNextColor() {
355+
return DEFAULT_COLORS[detections.length % DEFAULT_COLORS.length];
356+
}
357+
358+
function createDetectionRow(target = '', color = null, enabled = true) {
359+
const id = Date.now() + Math.random();
360+
const rowColor = color || getNextColor();
361+
362+
const row = document.createElement('div');
363+
row.className = 'detection-row';
364+
row.dataset.id = id;
365+
366+
row.innerHTML = `
367+
<input type="text" placeholder="e.g., person, red car, coffee mug" value="${target}">
368+
<input type="color" value="${rowColor}">
369+
<label class="toggle-switch">
370+
<input type="checkbox" ${enabled ? 'checked' : ''}>
371+
<span class="toggle-slider"></span>
372+
</label>
373+
<button class="btn-delete-detection">✕</button>
374+
`;
375+
376+
const textInput = row.querySelector('input[type="text"]');
377+
const colorInput = row.querySelector('input[type="color"]');
378+
const toggleInput = row.querySelector('input[type="checkbox"]');
379+
const deleteBtn = row.querySelector('.btn-delete-detection');
380+
381+
textInput.addEventListener('input', () => {
382+
updateDetectionData();
383+
clearDetectionResults(id);
384+
});
385+
colorInput.addEventListener('input', () => {
386+
updateDetectionData();
387+
drawDetectionBoxes();
388+
});
389+
toggleInput.addEventListener('change', () => {
390+
updateDetectionData();
391+
drawDetectionBoxes();
392+
});
393+
deleteBtn.addEventListener('click', () => {
394+
row.remove();
395+
delete detectionResults[id];
396+
updateDetectionData();
397+
drawDetectionBoxes();
398+
updateAddButton();
399+
});
400+
401+
detectionList.appendChild(row);
402+
updateDetectionData();
403+
updateAddButton();
404+
405+
return row;
406+
}
407+
408+
function updateDetectionData() {
409+
detections = [];
410+
const rows = detectionList.querySelectorAll('.detection-row');
411+
rows.forEach(row => {
412+
const id = row.dataset.id;
413+
const target = row.querySelector('input[type="text"]').value.trim();
414+
const color = row.querySelector('input[type="color"]').value;
415+
const enabled = row.querySelector('input[type="checkbox"]').checked;
416+
detections.push({ id, target, color, enabled });
417+
});
418+
savePreference('detections', detections);
419+
}
420+
421+
function clearDetectionResults(id) {
422+
delete detectionResults[id];
423+
drawDetectionBoxes();
424+
}
425+
426+
function updateAddButton() {
427+
addDetectionBtn.disabled = detections.length >= MAX_DETECTIONS;
428+
}
429+
430+
function resizeCanvas() {
431+
const rect = video.getBoundingClientRect();
432+
detectionCanvas.width = rect.width;
433+
detectionCanvas.height = rect.height;
434+
drawDetectionBoxes();
435+
}
436+
437+
function drawDetectionBoxes() {
438+
detectionCtx.clearRect(0, 0, detectionCanvas.width, detectionCanvas.height);
439+
440+
const canvasWidth = detectionCanvas.width;
441+
const canvasHeight = detectionCanvas.height;
442+
443+
detections.forEach(detection => {
444+
if (!detection.enabled || !detection.target) return;
445+
446+
const results = detectionResults[detection.id];
447+
if (!results || !results.objects || results.objects.length === 0) return;
448+
449+
const count = results.objects.length;
450+
451+
results.objects.forEach((obj, idx) => {
452+
const x = obj.x_min * canvasWidth;
453+
const y = obj.y_min * canvasHeight;
454+
const width = (obj.x_max - obj.x_min) * canvasWidth;
455+
const height = (obj.y_max - obj.y_min) * canvasHeight;
456+
457+
detectionCtx.strokeStyle = detection.color;
458+
detectionCtx.lineWidth = 4;
459+
detectionCtx.strokeRect(x, y, width, height);
460+
461+
const labelText = count > 1 ? `${detection.target} (${idx + 1})` : detection.target;
462+
detectionCtx.font = 'bold 14px system-ui, -apple-system, sans-serif';
463+
const textMetrics = detectionCtx.measureText(labelText);
464+
const labelPadX = 10;
465+
const labelPadY = 6;
466+
const labelHeight = 14 + labelPadY * 2;
467+
const labelWidth = textMetrics.width + labelPadX * 2;
468+
469+
const labelX = x;
470+
const labelY = y - labelHeight;
471+
472+
detectionCtx.fillStyle = detection.color;
473+
detectionCtx.beginPath();
474+
detectionCtx.roundRect(labelX, labelY, labelWidth, labelHeight, 4);
475+
detectionCtx.fill();
476+
477+
detectionCtx.fillStyle = '#FFFFFF';
478+
detectionCtx.fillText(labelText, labelX + labelPadX, labelY + labelPadY + 12);
479+
});
480+
});
481+
}
482+
483+
async function runDetection() {
484+
if (!client) {
485+
window.reasoningConsole.logError('No API key configured');
486+
updateStatus('Please configure your Moondream API key', 'error');
487+
window.apiKeyManager.showModal();
488+
return;
489+
}
490+
491+
const activeDetections = detections.filter(d => d.enabled && d.target);
492+
if (activeDetections.length === 0) {
493+
window.reasoningConsole.logInfo('No active detections configured');
494+
return;
495+
}
496+
497+
detectNowBtn.disabled = true;
498+
updateStatus('Running detection...');
499+
500+
const startTime = Date.now();
501+
502+
try {
503+
const promises = activeDetections.map(async (detection) => {
504+
try {
505+
const result = await client.detectInVideo(video, detection.target);
506+
detectionResults[detection.id] = result;
507+
window.reasoningConsole.logInfo(`Detected ${result.objects.length} "${detection.target}"`);
508+
return { id: detection.id, success: true, count: result.objects.length };
509+
} catch (error) {
510+
window.reasoningConsole.logError(`Detection failed for "${detection.target}": ${error.message}`);
511+
return { id: detection.id, success: false, error: error.message };
512+
}
513+
});
514+
515+
const results = await Promise.all(promises);
516+
const elapsed = Date.now() - startTime;
517+
518+
drawDetectionBoxes();
519+
520+
const successCount = results.filter(r => r.success).length;
521+
const totalObjects = results.filter(r => r.success).reduce((sum, r) => sum + r.count, 0);
522+
523+
updateStatus(`Detected ${totalObjects} object(s) across ${successCount} target(s) in ${elapsed}ms`, 'success');
524+
window.reasoningConsole.logDecision('Detection complete', `${totalObjects} objects found in ${elapsed}ms`);
525+
526+
updateJsonOutput({
527+
type: 'object_detection',
528+
timestamp: new Date().toISOString(),
529+
latency_ms: elapsed,
530+
detections: activeDetections.map(d => ({
531+
target: d.target,
532+
color: d.color,
533+
results: detectionResults[d.id]
534+
}))
535+
});
536+
537+
} catch (error) {
538+
window.reasoningConsole.logError(error.message);
539+
updateStatus('Detection error: ' + error.message, 'error');
540+
} finally {
541+
detectNowBtn.disabled = false;
542+
}
543+
}
544+
545+
function toggleAutoDetect() {
546+
if (autoDetectCheckbox.checked) {
547+
const interval = parseInt(autoIntervalInput.value) * 1000;
548+
autoDetectInterval = setInterval(runDetection, interval);
549+
window.reasoningConsole.logInfo(`Auto-detect enabled: every ${autoIntervalInput.value}s`);
550+
} else {
551+
clearInterval(autoDetectInterval);
552+
autoDetectInterval = null;
553+
window.reasoningConsole.logInfo('Auto-detect disabled');
554+
}
555+
}
556+
557+
addDetectionBtn.addEventListener('click', () => {
558+
if (detections.length < MAX_DETECTIONS) {
559+
createDetectionRow();
560+
}
561+
});
562+
563+
detectNowBtn.addEventListener('click', runDetection);
564+
565+
autoDetectCheckbox.addEventListener('change', () => {
566+
savePreference('autoDetect', autoDetectCheckbox.checked);
567+
toggleAutoDetect();
568+
});
569+
570+
video.addEventListener('loadedmetadata', resizeCanvas);
571+
video.addEventListener('resize', resizeCanvas);
572+
window.addEventListener('resize', resizeCanvas);
573+
574+
function loadDetectionPreferences() {
575+
if (window.VRPPrefs) {
576+
const collapsed = VRPPrefs.getToolPref(TOOL_ID, 'detectionCollapsed', false);
577+
const savedDetections = VRPPrefs.getToolPref(TOOL_ID, 'detections', []);
578+
const savedAutoDetect = VRPPrefs.getToolPref(TOOL_ID, 'autoDetect', false);
579+
580+
if (collapsed) {
581+
detectionSection.classList.add('collapsed');
582+
}
583+
584+
if (savedDetections.length > 0) {
585+
savedDetections.forEach(d => {
586+
createDetectionRow(d.target, d.color, d.enabled);
587+
});
588+
}
589+
590+
autoDetectCheckbox.checked = savedAutoDetect;
591+
if (savedAutoDetect) {
592+
setTimeout(toggleAutoDetect, 2500);
593+
}
594+
}
595+
}
596+
597+
loadDetectionPreferences();
598+
setTimeout(resizeCanvas, 500);
599+
334600
if (window.VideoSourceAdapter) {
335601
VideoSourceAdapter.init({
336602
videoElement: video,

0 commit comments

Comments
 (0)