Update templates/index.html
Browse files- templates/index.html +80 -16
templates/index.html
CHANGED
|
@@ -365,8 +365,7 @@
|
|
| 365 |
|
| 366 |
<video id="local-video" autoplay playsinline class="hidden"></video>
|
| 367 |
|
| 368 |
-
<
|
| 369 |
-
class="w-full h-auto max-h-[400px] md:max-h-[600px] object-contain hidden z-10">
|
| 370 |
|
| 371 |
<div
|
| 372 |
class="absolute top-2 left-2 md:top-4 md:left-4 w-6 h-6 md:w-8 md:h-8 border-t-4 border-l-4 border-blue-500 opacity-70 z-20 pointer-events-none">
|
|
@@ -436,19 +435,60 @@
|
|
| 436 |
sidebarOverlay.addEventListener('click', toggleSidebar);
|
| 437 |
|
| 438 |
|
| 439 |
-
//MAIN CAMERA
|
| 440 |
const toggleBtn = document.getElementById('toggle-camera');
|
| 441 |
const localVideo = document.getElementById('local-video');
|
| 442 |
-
const
|
|
|
|
| 443 |
const placeholder = document.getElementById('camera-placeholder');
|
| 444 |
|
| 445 |
let isCameraOn = false;
|
| 446 |
let localStream = null;
|
| 447 |
-
let
|
| 448 |
|
| 449 |
const canvas = document.createElement('canvas');
|
| 450 |
const context = canvas.getContext('2d');
|
| 451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
// TTS (SpeechSynthesis) Helper
|
| 453 |
let spokenNames = new Map();
|
| 454 |
|
|
@@ -464,13 +504,18 @@
|
|
| 464 |
|
| 465 |
async function startCamera() {
|
| 466 |
try {
|
| 467 |
-
localStream = await navigator.mediaDevices.getUserMedia({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
localVideo.srcObject = localStream;
|
| 469 |
placeholder.classList.add('hidden');
|
| 470 |
-
|
| 471 |
localVideo.onloadedmetadata = () => {
|
| 472 |
-
|
| 473 |
-
canvas.height = localVideo.videoHeight;
|
| 474 |
sendFrameToServer();
|
| 475 |
};
|
| 476 |
} catch (err) {
|
|
@@ -482,16 +527,36 @@
|
|
| 482 |
|
| 483 |
function stopCamera() {
|
| 484 |
if (localStream) localStream.getTracks().forEach(t => t.stop());
|
| 485 |
-
|
| 486 |
-
processedFeed.src = "";
|
| 487 |
-
processedFeed.classList.add('hidden');
|
| 488 |
placeholder.classList.remove('hidden');
|
|
|
|
| 489 |
}
|
| 490 |
|
| 491 |
async function sendFrameToServer() {
|
| 492 |
if (!isCameraOn) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
context.drawImage(localVideo, 0, 0, canvas.width, canvas.height);
|
| 494 |
-
const dataURL = canvas.toDataURL('image/jpeg', 0.
|
| 495 |
try {
|
| 496 |
const response = await fetch('/process_frame', {
|
| 497 |
method: 'POST',
|
|
@@ -499,10 +564,9 @@
|
|
| 499 |
body: JSON.stringify({ image: dataURL })
|
| 500 |
});
|
| 501 |
const data = await response.json();
|
| 502 |
-
if (data.image) processedFeed.src = data.image;
|
| 503 |
|
| 504 |
-
// Speak if attendance is successfully marked
|
| 505 |
if (data.faces) {
|
|
|
|
| 506 |
data.faces.forEach(face => {
|
| 507 |
if (face.name && face.name !== "Unknown") {
|
| 508 |
const now = Date.now();
|
|
@@ -521,7 +585,7 @@
|
|
| 521 |
} catch (err) {
|
| 522 |
console.error("Error communicating with backend", err);
|
| 523 |
} finally {
|
| 524 |
-
if (isCameraOn) setTimeout(sendFrameToServer,
|
| 525 |
}
|
| 526 |
}
|
| 527 |
|
|
|
|
| 365 |
|
| 366 |
<video id="local-video" autoplay playsinline class="hidden"></video>
|
| 367 |
|
| 368 |
+
<canvas id="output-canvas" class="w-full h-auto max-h-[400px] md:max-h-[600px] object-contain hidden z-10"></canvas>
|
|
|
|
| 369 |
|
| 370 |
<div
|
| 371 |
class="absolute top-2 left-2 md:top-4 md:left-4 w-6 h-6 md:w-8 md:h-8 border-t-4 border-l-4 border-blue-500 opacity-70 z-20 pointer-events-none">
|
|
|
|
| 435 |
sidebarOverlay.addEventListener('click', toggleSidebar);
|
| 436 |
|
| 437 |
|
|
|
|
| 438 |
const toggleBtn = document.getElementById('toggle-camera');
|
| 439 |
const localVideo = document.getElementById('local-video');
|
| 440 |
+
const outputCanvas = document.getElementById('output-canvas');
|
| 441 |
+
const outputCtx = outputCanvas.getContext('2d');
|
| 442 |
const placeholder = document.getElementById('camera-placeholder');
|
| 443 |
|
| 444 |
let isCameraOn = false;
|
| 445 |
let localStream = null;
|
| 446 |
+
let latestFaces = [];
|
| 447 |
|
| 448 |
const canvas = document.createElement('canvas');
|
| 449 |
const context = canvas.getContext('2d');
|
| 450 |
|
| 451 |
+
function renderLoop() {
|
| 452 |
+
if (!isCameraOn) return;
|
| 453 |
+
if (localVideo.readyState === localVideo.HAVE_ENOUGH_DATA) {
|
| 454 |
+
// Keep canvas resolution synced with original video resolution
|
| 455 |
+
if (outputCanvas.width !== localVideo.videoWidth) {
|
| 456 |
+
outputCanvas.width = localVideo.videoWidth;
|
| 457 |
+
outputCanvas.height = localVideo.videoHeight;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
// Draw local webcam feed smoothly
|
| 461 |
+
outputCtx.drawImage(localVideo, 0, 0, outputCanvas.width, outputCanvas.height);
|
| 462 |
+
|
| 463 |
+
// Draw bounding boxes based on backend coordinates
|
| 464 |
+
const scaleX = outputCanvas.width / (canvas.width || 640);
|
| 465 |
+
const scaleY = outputCanvas.height / (canvas.height || 480);
|
| 466 |
+
|
| 467 |
+
latestFaces.forEach(face => {
|
| 468 |
+
const top = face.box.top * scaleY;
|
| 469 |
+
const right = face.box.right * scaleX;
|
| 470 |
+
const bottom = face.box.bottom * scaleY;
|
| 471 |
+
const left = face.box.left * scaleX;
|
| 472 |
+
const name = face.name;
|
| 473 |
+
|
| 474 |
+
outputCtx.lineWidth = 4;
|
| 475 |
+
outputCtx.strokeStyle = name !== "Unknown" ? "#22c55e" : "#3b82f6";
|
| 476 |
+
outputCtx.fillStyle = name !== "Unknown" ? "#22c55e" : "#3b82f6";
|
| 477 |
+
|
| 478 |
+
outputCtx.strokeRect(left, top, right - left, bottom - top);
|
| 479 |
+
|
| 480 |
+
let labelText = name;
|
| 481 |
+
outputCtx.font = "bold 20px Arial";
|
| 482 |
+
const textWidth = outputCtx.measureText(labelText).width;
|
| 483 |
+
outputCtx.fillRect(left, top - 30, textWidth + 20, 30);
|
| 484 |
+
|
| 485 |
+
outputCtx.fillStyle = "#FFFFFF";
|
| 486 |
+
outputCtx.fillText(labelText, left + 10, top - 8);
|
| 487 |
+
});
|
| 488 |
+
}
|
| 489 |
+
requestAnimationFrame(renderLoop);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
// TTS (SpeechSynthesis) Helper
|
| 493 |
let spokenNames = new Map();
|
| 494 |
|
|
|
|
| 504 |
|
| 505 |
async function startCamera() {
|
| 506 |
try {
|
| 507 |
+
localStream = await navigator.mediaDevices.getUserMedia({
|
| 508 |
+
video: {
|
| 509 |
+
facingMode: 'user',
|
| 510 |
+
width: { ideal: 640 },
|
| 511 |
+
height: { ideal: 480 }
|
| 512 |
+
}
|
| 513 |
+
});
|
| 514 |
localVideo.srcObject = localStream;
|
| 515 |
placeholder.classList.add('hidden');
|
| 516 |
+
outputCanvas.classList.remove('hidden');
|
| 517 |
localVideo.onloadedmetadata = () => {
|
| 518 |
+
requestAnimationFrame(renderLoop);
|
|
|
|
| 519 |
sendFrameToServer();
|
| 520 |
};
|
| 521 |
} catch (err) {
|
|
|
|
| 527 |
|
| 528 |
function stopCamera() {
|
| 529 |
if (localStream) localStream.getTracks().forEach(t => t.stop());
|
| 530 |
+
outputCanvas.classList.add('hidden');
|
|
|
|
|
|
|
| 531 |
placeholder.classList.remove('hidden');
|
| 532 |
+
latestFaces = [];
|
| 533 |
}
|
| 534 |
|
| 535 |
async function sendFrameToServer() {
|
| 536 |
if (!isCameraOn) return;
|
| 537 |
+
|
| 538 |
+
// Resize image to max 640x480 to reduce CPU load and latency
|
| 539 |
+
const MAX_WIDTH = 640;
|
| 540 |
+
const MAX_HEIGHT = 480;
|
| 541 |
+
let width = localVideo.videoWidth;
|
| 542 |
+
let height = localVideo.videoHeight;
|
| 543 |
+
|
| 544 |
+
if (width > height) {
|
| 545 |
+
if (width > MAX_WIDTH) {
|
| 546 |
+
height *= MAX_WIDTH / width;
|
| 547 |
+
width = MAX_WIDTH;
|
| 548 |
+
}
|
| 549 |
+
} else {
|
| 550 |
+
if (height > MAX_HEIGHT) {
|
| 551 |
+
width *= MAX_HEIGHT / height;
|
| 552 |
+
height = MAX_HEIGHT;
|
| 553 |
+
}
|
| 554 |
+
}
|
| 555 |
+
canvas.width = width || 640;
|
| 556 |
+
canvas.height = height || 480;
|
| 557 |
+
|
| 558 |
context.drawImage(localVideo, 0, 0, canvas.width, canvas.height);
|
| 559 |
+
const dataURL = canvas.toDataURL('image/jpeg', 0.6);
|
| 560 |
try {
|
| 561 |
const response = await fetch('/process_frame', {
|
| 562 |
method: 'POST',
|
|
|
|
| 564 |
body: JSON.stringify({ image: dataURL })
|
| 565 |
});
|
| 566 |
const data = await response.json();
|
|
|
|
| 567 |
|
|
|
|
| 568 |
if (data.faces) {
|
| 569 |
+
latestFaces = data.faces;
|
| 570 |
data.faces.forEach(face => {
|
| 571 |
if (face.name && face.name !== "Unknown") {
|
| 572 |
const now = Date.now();
|
|
|
|
| 585 |
} catch (err) {
|
| 586 |
console.error("Error communicating with backend", err);
|
| 587 |
} finally {
|
| 588 |
+
if (isCameraOn) setTimeout(sendFrameToServer, 500); // reduced frequency to 2 FPS
|
| 589 |
}
|
| 590 |
}
|
| 591 |
|