<!doctype html>
Browse files<html>
<head>
<meta charset="utf-8" />
<title>Sign Language Recognizer — AI Vision</title>
<style>
:root {
--bg: #2c3e50;
--panel: #34495e;
--accent: #1abc9c;
--danger: #e74c3c;
--text: #ecf0f1;
}
body {
margin:0; font-family: "Segoe UI", system-ui;
background: var(--bg); color: var(--text);
display:flex; flex-wrap:wrap; gap:20px; padding:20px; justify-content:center;
}
#left {
position:relative;
width:640px;
}
video, canvas {
border-radius:12px;
width:640px; height:480px;
background:#000;
box-shadow:0 0 20px rgba(0,0,0,0.4);
}
#status {
margin-top:12px;
font-weight:600;
text-align:center;
font-size:18px;
color:var(--accent);
}
#controls {
width:360px;
background: var(--panel);
border-radius:12px;
padding:20px;
box-shadow:0 0 25px rgba(0,0,0,0.3);
}
h3 { margin-top:0; text-align:center; color:var(--accent); }
input, button, textarea {
font-family:inherit;
border:none;
outline:none;
border-radius:8px;
padding:8px 12px;
margin-top:6px;
}
input {
width: calc(100% - 24px);
background:#22313f;
color:var(--text);
}
button {
cursor:pointer;
background:var(--accent);
color:var(--text);
transition:0.2s;
}
button:hover { background:#16a085; }
.label-btn {
background:#3b5770;
color:#ecf0f1;
}
.recording { background:var(--danger) !important; }
#prediction {
font-weight:bold;
color:#f1c40f;
font-size:20px;
}
textarea {
width:100%; height:100px;
background:#22313f;
color:var(--text);
margin-top:8px;
resize:none;
}
/* Subscription Modal */
#subModal {
position: fixed; inset:0;
background: rgba(0,0,0,0.75);
display:flex; align-items:center; justify-content:center;
z-index:999; opacity:0; visibility:hidden;
transition: opacity 0.3s;
}
#subModal.active { opacity:1; visibility:visible; }
.sub-box {
background: var(--panel);
padding:30px; border-radius:16px; width:360px;
text-align:center; box-shadow:0 0 25px rgba(0,0,0,0.5);
}
.sub-box h2 { color:var(--accent); margin-bottom:10px; }
.sub-btn {
background: var(--accent);
color:white;
border:none;
border-radius:10px;
padding:10px 18px;
font-size:16px;
margin:10px 0;
cursor:pointer;
width:80%;
}
.trial-btn { background:#f39c12; }
</style>
</head>
<body>
<div id="left">
<div style="position:relative; width:640px; height:480px;">
<video id="video" autoplay muted playsinline></video>
<canvas id="overlay" width="640" height="480"></canvas>
</div>
<div id="status">Status: Idle</div>
</div>
<div id="controls">
<h3>✋ Sign Language Recognizer</h3>
<div>
<input id="labelInput" placeholder="Label name (e.g. Hello, A)" />
<button id="addLabel">Add Label</button>
</div>
<div id="labelsArea" style="margin-top:8px;"></div>
<div style="margin-top:12px;">
<button id="toggleRec">Start Recording Examples</button>
<button id="predictToggle">Start Predicting</button>
</div>
<div style="margin-top:12px;">
<button id="saveBtn">Save Dataset</button>
<button id="loadBtn">Load Dataset</button>
<input type="file" id="fileInput" style="display:none" />
</div>
<div style="margin-top:12px;">
<strong>Prediction:</strong> <span id="prediction">—</span>
</div>
<div style="margin-top:12px;">
<strong>Dataset JSON</strong>
<textarea id="datasetJSON" readonly></textarea>
</div>
</div>
<!-- Subscription Modal -->
<div id="subModal">
<div class="sub-box">
<h2>🔒 Subscribe to Unlock</h2>
<p>Access the AI Sign Recognizer for just <strong>₹199/month</strong>.</p>
<p>Or enjoy a <strong>3-Day Free Trial</strong>.</p>
<button class="sub-btn" id="subscribeBtn">Subscribe ₹199</button><br>
<button class="sub-btn trial-btn" id="trialBtn">Start Free Trial</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
<script>
(async ()=>{
// === CORE VARIABLES ===
const videoElem = document.getElementById('video');
const overlay = document.getElementById('overlay');
const ctx = overlay.getContext('2d');
let labels = [];
let recording=false, recordingLabel=null, predicting=false;
let knnK=5;
const status = document.getElementById('status');
const predictionSpan = document.getElementById('prediction');
const datasetJSON = document.getElementById('datasetJSON');
function setStatus(s){ status.textContent='Status: '+s; }
// === LANDMARK PROCESSING ===
function landmarksToVector(landmarks){
if(!landmarks || landmarks.length===0) return null;
const wrist = landmarks[0];
const ref = landmarks[9] || landmarks[8];
const dx = ref.x - wrist.x;
const dy = ref.y - wrist.y;
const scale = Math.hypot(dx, dy) || 1;
const vec=[];
for(let p of landmarks){
vec.push((p.x - wrist.x)/scale,(p.y - wrist.y)/scale,(p.z - wrist.z)/scale);
}
return vec;
}
function euclid(a,b){
let s=0;
for(let i=0;i<a.length;i++){ const d=a[i]-b[i]; s+=d*d; }
return Math.sqrt(s);
}
function knnPredict(vec){
const all=[];
for(const lab of labels) for(const ex of lab.examples)
all.push({label:lab.name, dist:euclid(vec,ex)});
if(!all.length) return {label:null,confidence:0};
all.sort((a,b)=>a.dist-b.dist);
const k=Math.min(knnK,all.length);
const top=all.slice(0,k);
const counts={};
for(const t of top) counts[t.label]=(counts[t.label]||0)+1;
let best=null, bestCount=0;
for(const l in counts) if(counts[l]>bestCount){best=l;bestCount=counts[l];}
return {label:best, confidence:bestCount/k};
}
// === UI: LABELS ===
const labelsArea=document.getElementById('labelsArea');
document.getElementById('addLabel').onclick=()=>{
const name=document.getElementById('labelInput').value.trim();
if(!name) return alert('Enter a label');
if(labels.find(l=>l.name===name)) return alert('Already exists');
labels.push({name,examples:[]});
renderLabels();
document.getElementById('labelInput').value='';
};
function renderLabels(){
labelsArea.innerHTML='';
labels.forEach((lab,idx)=>{
const div=document.createElement('div');
div.style.display='flex'; div.style.alignItems='center'; div.style.gap='6px';
const btn=document.createElement('button');
btn.textContent=lab.name+' ('+lab.examples.length+')';
btn.className='label-btn';
btn.onclick=()=>{
recordingLabel=lab.name;
document.querySelectorAll('.label-btn').forEach(b=>b.classList.remove('recording'));
btn.classList.add('recording');
};
const del=document.createElement('button');
del.textContent='✖'; del.style.background=varDanger='#c0392b';
del.onclick=()=>{if(confirm('Delete label?')){labels.splice(idx,1);renderLabels();}};
div.appendChild(btn); div.appendChild(del);
labelsArea.appendChild(div);
});
datasetJSON.value=JSON.stringify(labels,null,2);
}
// === SAVE / LOAD ===
document.getElementById('saveBtn').onclick=()=>{
const blob=new Blob([JSON.stringify(labels)],{type:'application/json'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob); a.download='sign_dataset.json'; a.click();
};
document.getElementById('loadBtn').onclick=()=>document.getElementById('fileInput').click();
document.getElementById('fileInput').onchange=e=>{
const f=e.target.files[0]; if(!f) return;
const r=new FileReader();
r.onload=()=>{
try{
const d=JSON.parse(r.result);
if(!Array.isArray(d)) throw 'Invalid';
labels=d.map(l=>({name:l.name,examples:l.examples||[]}));
renderLabels(); setStatus('Dataset loaded');
}catch{alert('Invalid file');}
};
r.readAsText(f);
};
// === RECORD & PREDICT ===
document.getElementById('toggleRec').onclick=()=>{
recording=!recording;
setStatus(recording?'Recording...':'Idle');
document.getElementById('toggleRec').textContent=recording?'Stop Recording':'Start Recording Examples';
};
document.getElementById('predictToggle').onclick=()=>{
predicting=!predicting;
setStatus(predicting?'Predicting...':'Idle');
document.getElementById('predictToggle').textContent=predicting?'Stop Predicting':'Start Predicting';
};
// === MEDIAPIPE HANDS ===
const hands=new Hands({locateFile:(file)=>`https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
hands.setOptions({maxNumHands:1,modelComplexity:1,minDetectionConfidence:0.7,minTrackingConfidence:0.6});
hands.onResults(onResults);
const camera=new Camera(videoElem,{onFrame:async()=>await hands.send({image:videoElem}),width:640,height:480});
camera.start();
let lastSpoken=null;
function speak(text){
if('speechSynthesis' in window){
if(lastSpoken===text) return; // avoid repetition
lastSpoken=text;
const msg=new SpeechSynthesisUtterance(text);
msg.rate=1; msg.pitch=1;
speechSynthesis.cancel();
speechSynthesis.speak(msg);
}
}
function onResults(results){
ctx.save();
ctx.clearRect(0,0,overlay.width,overlay.height);
ctx.drawImage(results.image,0,0,overlay.width,overlay.height);
if(results.multiHandLandmarks){
for(const lm of results.multiHandLandmarks){
drawConnectors(ctx,lm,HAND_CONNECTIONS,{color:'#1abc9c',lineWidth:2});
drawLandmarks(ctx,lm,{color:'#f1c40f',lineWidth:1});
}
}
ctx.restore();
if(results.multiHandLandmarks && results.multiHandLandmarks.length>0){
const vec=landmarksToVector(results.multiHandLandmarks[0]);
if(!vec) return;
if(recording && recordingLabel){
const lab=labels.find(l=>l.name===recordingLabel);
if(lab){lab.examples.push(vec);renderLabels();}
}
if(predicting){
const out=knnPredict(vec);
if(out.label){
predictionSpan.textCo
|
@@ -1,10 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: HandSpeak AI Translator ✋
|
| 3 |
+
colorFrom: blue
|
| 4 |
+
colorTo: yellow
|
| 5 |
+
emoji: 🐳
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite-v3
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Welcome to your new DeepSite project!
|
| 13 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
|
@@ -1,19 +1,120 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>HandSpeak AI Translator ✋</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 10 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 11 |
+
<script src="components/navbar.js"></script>
|
| 12 |
+
<script src="components/footer.js"></script>
|
| 13 |
+
</head>
|
| 14 |
+
<body class="bg-gray-900 text-gray-100 min-h-screen flex flex-col">
|
| 15 |
+
<custom-navbar></custom-navbar>
|
| 16 |
+
|
| 17 |
+
<main class="flex-grow container mx-auto px-4 py-8">
|
| 18 |
+
<div class="max-w-6xl mx-auto">
|
| 19 |
+
<div class="text-center mb-12">
|
| 20 |
+
<h1 class="text-4xl md:text-5xl font-bold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-teal-400 to-blue-500">
|
| 21 |
+
HandSpeak AI Translator
|
| 22 |
+
</h1>
|
| 23 |
+
<p class="text-xl text-gray-300 max-w-2xl mx-auto">
|
| 24 |
+
Real-time sign language recognition using AI and computer vision
|
| 25 |
+
</p>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="flex flex-col lg:flex-row gap-8">
|
| 29 |
+
<!-- Video Feed Section -->
|
| 30 |
+
<div class="lg:w-2/3">
|
| 31 |
+
<div class="relative rounded-xl overflow-hidden shadow-2xl bg-black">
|
| 32 |
+
<video id="video" class="w-full" autoplay muted playsinline></video>
|
| 33 |
+
<canvas id="overlay" class="absolute top-0 left-0 w-full h-full" width="640" height="480"></canvas>
|
| 34 |
+
</div>
|
| 35 |
+
<div id="status" class="mt-4 text-center text-lg font-medium text-teal-400">
|
| 36 |
+
Status: Initializing...
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<!-- Controls Section -->
|
| 41 |
+
<div class="lg:w-1/3 bg-gray-800 rounded-xl p-6 shadow-xl">
|
| 42 |
+
<h3 class="text-2xl font-bold mb-6 text-center flex items-center justify-center gap-2">
|
| 43 |
+
<i data-feather="settings"></i> Controls
|
| 44 |
+
</h3>
|
| 45 |
+
|
| 46 |
+
<div class="space-y-6">
|
| 47 |
+
<!-- Label Management -->
|
| 48 |
+
<div>
|
| 49 |
+
<label class="block text-sm font-medium mb-1">Add New Sign</label>
|
| 50 |
+
<div class="flex gap-2">
|
| 51 |
+
<input id="labelInput" type="text" placeholder="e.g. Hello, A"
|
| 52 |
+
class="flex-grow bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 focus:ring-2 focus:ring-teal-500 focus:border-transparent">
|
| 53 |
+
<button id="addLabel" class="bg-teal-600 hover:bg-teal-500 text-white px-4 py-2 rounded-lg transition">
|
| 54 |
+
Add
|
| 55 |
+
</button>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<!-- Labels Display -->
|
| 60 |
+
<div id="labelsArea" class="space-y-2 max-h-40 overflow-y-auto pr-2"></div>
|
| 61 |
+
|
| 62 |
+
<!-- Recording Controls -->
|
| 63 |
+
<div class="space-y-3">
|
| 64 |
+
<button id="toggleRec" class="w-full bg-blue-600 hover:bg-blue-500 text-white px-4 py-3 rounded-lg font-medium transition">
|
| 65 |
+
Start Recording Examples
|
| 66 |
+
</button>
|
| 67 |
+
<button id="predictToggle" class="w-full bg-purple-600 hover:bg-purple-500 text-white px-4 py-3 rounded-lg font-medium transition">
|
| 68 |
+
Start Predicting
|
| 69 |
+
</button>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<!-- Data Management -->
|
| 73 |
+
<div class="grid grid-cols-2 gap-3">
|
| 74 |
+
<button id="saveBtn" class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition">
|
| 75 |
+
Save Dataset
|
| 76 |
+
</button>
|
| 77 |
+
<button id="loadBtn" class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition">
|
| 78 |
+
Load Dataset
|
| 79 |
+
</button>
|
| 80 |
+
<input type="file" id="fileInput" class="hidden">
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<!-- Prediction Display -->
|
| 84 |
+
<div class="bg-gray-700 rounded-lg p-4">
|
| 85 |
+
<div class="flex justify-between items-center mb-2">
|
| 86 |
+
<span class="font-medium">Current Prediction:</span>
|
| 87 |
+
<span id="prediction" class="text-xl font-bold text-yellow-400">—</span>
|
| 88 |
+
</div>
|
| 89 |
+
<div class="h-2 bg-gray-600 rounded-full overflow-hidden">
|
| 90 |
+
<div id="confidenceBar" class="h-full bg-teal-500" style="width: 0%"></div>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<!-- Dataset JSON -->
|
| 98 |
+
<div class="mt-8 bg-gray-800 rounded-xl p-6 shadow-xl">
|
| 99 |
+
<h3 class="text-xl font-bold mb-4 flex items-center gap-2">
|
| 100 |
+
<i data-feather="database"></i> Dataset JSON
|
| 101 |
+
</h3>
|
| 102 |
+
<textarea id="datasetJSON" readonly
|
| 103 |
+
class="w-full h-40 bg-gray-700 border border-gray-600 rounded-lg p-3 font-mono text-sm"></textarea>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
</main>
|
| 107 |
+
|
| 108 |
+
<custom-footer></custom-footer>
|
| 109 |
+
|
| 110 |
+
<!-- Scripts -->
|
| 111 |
+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
|
| 112 |
+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
|
| 113 |
+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
|
| 114 |
+
<script src="script.js"></script>
|
| 115 |
+
<script>
|
| 116 |
+
feather.replace();
|
| 117 |
+
</script>
|
| 118 |
+
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
|
| 119 |
+
</body>
|
| 120 |
+
</html>
|
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 2 |
+
// Initialize elements
|
| 3 |
+
const videoElem = document.getElementById('video');
|
| 4 |
+
const overlay = document.getElementById('overlay');
|
| 5 |
+
const ctx = overlay.getContext('2d');
|
| 6 |
+
const status = document.getElementById('status');
|
| 7 |
+
const predictionSpan = document.getElementById('prediction');
|
| 8 |
+
const confidenceBar = document.getElementById('confidenceBar');
|
| 9 |
+
const datasetJSON = document.getElementById('datasetJSON');
|
| 10 |
+
|
| 11 |
+
let labels = [];
|
| 12 |
+
let recording = false;
|
| 13 |
+
let recordingLabel = null;
|
| 14 |
+
let predicting = false;
|
| 15 |
+
let knnK = 5;
|
| 16 |
+
let lastSpoken = null;
|
| 17 |
+
|
| 18 |
+
// Set initial status
|
| 19 |
+
setStatus('Initializing camera...');
|
| 20 |
+
|
| 21 |
+
// === Utility Functions ===
|
| 22 |
+
function setStatus(s) {
|
| 23 |
+
status.textContent = `Status: ${s}`;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function speak(text) {
|
| 27 |
+
if ('speechSynthesis' in window && text !== lastSpoken) {
|
| 28 |
+
lastSpoken = text;
|
| 29 |
+
const msg = new SpeechSynthesisUtterance(text);
|
| 30 |
+
msg.rate = 1;
|
| 31 |
+
msg.pitch = 1;
|
| 32 |
+
speechSynthesis.cancel();
|
| 33 |
+
speechSynthesis.speak(msg);
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// === Landmark Processing ===
|
| 38 |
+
function landmarksToVector(landmarks) {
|
| 39 |
+
if (!landmarks || landmarks.length === 0) return null;
|
| 40 |
+
|
| 41 |
+
const wrist = landmarks[0];
|
| 42 |
+
const ref = landmarks[9] || landmarks[8];
|
| 43 |
+
const dx = ref.x - wrist.x;
|
| 44 |
+
const dy = ref.y - wrist.y;
|
| 45 |
+
const scale = Math.hypot(dx, dy) || 1;
|
| 46 |
+
|
| 47 |
+
const vec = [];
|
| 48 |
+
for (let p of landmarks) {
|
| 49 |
+
vec.push(
|
| 50 |
+
(p.x - wrist.x) / scale,
|
| 51 |
+
(p.y - wrist.y) / scale,
|
| 52 |
+
(p.z - wrist.z) / scale
|
| 53 |
+
);
|
| 54 |
+
}
|
| 55 |
+
return vec;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
function euclid(a, b) {
|
| 59 |
+
let s = 0;
|
| 60 |
+
for (let i = 0; i < a.length; i++) {
|
| 61 |
+
const d = a[i] - b[i];
|
| 62 |
+
s += d * d;
|
| 63 |
+
}
|
| 64 |
+
return Math.sqrt(s);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function knnPredict(vec) {
|
| 68 |
+
const all = [];
|
| 69 |
+
for (const lab of labels) {
|
| 70 |
+
for (const ex of lab.examples) {
|
| 71 |
+
all.push({ label: lab.name, dist: euclid(vec, ex) });
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if (!all.length) return { label: null, confidence: 0 };
|
| 76 |
+
|
| 77 |
+
all.sort((a, b) => a.dist - b.dist);
|
| 78 |
+
const k = Math.min(knnK, all.length);
|
| 79 |
+
const top = all.slice(0, k);
|
| 80 |
+
|
| 81 |
+
const counts = {};
|
| 82 |
+
for (const t of top) counts[t.label] = (counts[t.label] || 0) + 1;
|
| 83 |
+
|
| 84 |
+
let best = null, bestCount = 0;
|
| 85 |
+
for (const l in counts) {
|
| 86 |
+
if (counts[l] > bestCount) {
|
| 87 |
+
best = l;
|
| 88 |
+
bestCount = counts[l];
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
return {
|
| 93 |
+
label: best,
|
| 94 |
+
confidence: bestCount / k
|
| 95 |
+
};
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// === UI: Labels Management ===
|
| 99 |
+
document.getElementById('addLabel').addEventListener('click', () => {
|
| 100 |
+
const name = document.getElementById('labelInput').value.trim();
|
| 101 |
+
if (!name) return alert('Please enter a label name');
|
| 102 |
+
if (labels.find(l => l.name === name)) return alert('Label already exists');
|
| 103 |
+
|
| 104 |
+
labels.push({ name, examples: [] });
|
| 105 |
+
renderLabels();
|
| 106 |
+
document.getElementById('labelInput').value = '';
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
function renderLabels() {
|
| 110 |
+
const labelsArea = document.getElementById('labelsArea');
|
| 111 |
+
labelsArea.innerHTML = '';
|
| 112 |
+
|
| 113 |
+
labels.forEach((lab, idx) => {
|
| 114 |
+
const div = document.createElement('div');
|
| 115 |
+
div.className = 'flex items-center justify-between bg-gray-700 p-2 rounded-lg';
|
| 116 |
+
|
| 117 |
+
const labelBtn = document.createElement('button');
|
| 118 |
+
labelBtn.className = `flex items-center gap-2 px-3 py-1 rounded-md transition ${
|
| 119 |
+
recordingLabel === lab.name ? 'bg-teal-600 text-white' : 'bg-gray-600 hover:bg-gray-500'
|
| 120 |
+
}`;
|
| 121 |
+
labelBtn.innerHTML = `
|
| 122 |
+
<i data-feather="${lab.name.length > 1 ? 'type' : 'hash'}" width="16"></i>
|
| 123 |
+
${lab.name} <span class="text-xs opacity-75">(${lab.examples.length})</span>
|
| 124 |
+
`;
|
| 125 |
+
labelBtn.addEventListener('click', () => {
|
| 126 |
+
recordingLabel = lab.name;
|
| 127 |
+
renderLabels();
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
const deleteBtn = document.createElement('button');
|
| 131 |
+
deleteBtn.className = 'p-1 text-red-400 hover:text-red-300 rounded-full';
|
| 132 |
+
deleteBtn.innerHTML = '<i data-feather="trash-2" width="16"></i>';
|
| 133 |
+
deleteBtn.addEventListener('click', () => {
|
| 134 |
+
if (confirm(`Delete label "${lab.name}"?`)) {
|
| 135 |
+
labels.splice(idx, 1);
|
| 136 |
+
if (recordingLabel === lab.name) recordingLabel = null;
|
| 137 |
+
renderLabels();
|
| 138 |
+
}
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
div.appendChild(labelBtn);
|
| 142 |
+
div.appendChild(deleteBtn);
|
| 143 |
+
labelsArea.appendChild(div);
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
datasetJSON.value = JSON.stringify(labels, null, 2);
|
| 147 |
+
feather.replace();
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
// === Data Management ===
|
| 151 |
+
document.getElementById('saveBtn').addEventListener('click', () => {
|
| 152 |
+
if (labels.length === 0) return alert('No data to save');
|
| 153 |
+
|
| 154 |
+
const blob = new Blob([JSON.stringify(labels)], { type: 'application/json' });
|
| 155 |
+
const url = URL.createObjectURL(blob);
|
| 156 |
+
|
| 157 |
+
const a = document.createElement('a');
|
| 158 |
+
a.href = url;
|
| 159 |
+
a.download = 'handspeak_dataset.json';
|
| 160 |
+
document.body.appendChild(a);
|
| 161 |
+
a.click();
|
| 162 |
+
document.body.removeChild(a);
|
| 163 |
+
URL.revokeObjectURL(url);
|
| 164 |
+
|
| 165 |
+
setStatus('Dataset saved');
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
document.getElementById('loadBtn').addEventListener('click', () => {
|
| 169 |
+
document.getElementById('fileInput').click();
|
| 170 |
+
});
|
| 171 |
+
|
| 172 |
+
document.getElementById('fileInput').addEventListener('change', (e) => {
|
| 173 |
+
const file = e.target.files[0];
|
| 174 |
+
if (!file) return;
|
| 175 |
+
|
| 176 |
+
const reader = new FileReader();
|
| 177 |
+
reader.onload = () => {
|
| 178 |
+
try {
|
| 179 |
+
const data = JSON.parse(reader.result);
|
| 180 |
+
if (!Array.isArray(data)) throw new Error('Invalid format');
|
| 181 |
+
|
| 182 |
+
labels = data.map(l => ({
|
| 183 |
+
name: l.name,
|
| 184 |
+
examples: l.examples || []
|
| 185 |
+
}));
|
| 186 |
+
|
| 187 |
+
renderLabels();
|
| 188 |
+
setStatus('Dataset loaded successfully');
|
| 189 |
+
} catch (err) {
|
| 190 |
+
alert('Error loading file: ' + err.message);
|
| 191 |
+
}
|
| 192 |
+
};
|
| 193 |
+
reader.readAsText(file);
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
// === Recording & Prediction ===
|
| 197 |
+
document.getElementById('toggleRec').addEventListener('click', () => {
|
| 198 |
+
recording = !recording;
|
| 199 |
+
|
| 200 |
+
if (recording && !recordingLabel) {
|
| 201 |
+
alert('Please select a label first');
|
| 202 |
+
recording = false;
|
| 203 |
+
return;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
setStatus(recording ? `Recording examples for "${recordingLabel}"` : 'Ready');
|
| 207 |
+
document.getElementById('toggleRec').textContent = recording ?
|
| 208 |
+
'Stop Recording' : 'Start Recording Examples';
|
| 209 |
+
|
| 210 |
+
document.getElementById('toggleRec').className = recording ?
|
| 211 |
+
'w-full bg-red-600 hover:bg-red-500 text-white px-4 py-3 rounded-lg font-medium transition' :
|
| 212 |
+
'w-full bg-blue-600 hover:bg-blue-500 text-white px-4 py-3 rounded-lg font-medium transition';
|
| 213 |
+
});
|
| 214 |
+
|
| 215 |
+
document.getElementById('predictToggle').addEventListener('click', () => {
|
| 216 |
+
predicting = !predicting;
|
| 217 |
+
|
| 218 |
+
if (predicting && labels.length === 0) {
|
| 219 |
+
alert('Please add some labels and examples first');
|
| 220 |
+
predicting = false;
|
| 221 |
+
return;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
setStatus(predicting ? 'Predicting...' : 'Ready');
|
| 225 |
+
document.getElementById('predictToggle').textContent = predicting ?
|
| 226 |
+
'Stop Predicting' : 'Start Predicting';
|
| 227 |
+
|
| 228 |
+
document.getElementById('predictToggle').className = predicting ?
|
| 229 |
+
'w-full bg-purple-700 hover:bg-purple-600 text-white px-4 py-3 rounded-lg font-medium transition' :
|
| 230 |
+
'w-full bg-purple-600 hover:bg-purple-500 text-white px-4 py-3 rounded-lg font-medium transition';
|
| 231 |
+
});
|
| 232 |
+
|
| 233 |
+
// === MediaPipe Hands Setup ===
|
| 234 |
+
const hands = new Hands({
|
| 235 |
+
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
hands.setOptions({
|
| 239 |
+
maxNumHands: 1,
|
| 240 |
+
modelComplexity: 1,
|
| 241 |
+
minDetectionConfidence: 0.7,
|
| 242 |
+
minTrackingConfidence: 0.6
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
hands.onResults(onResults);
|
| 246 |
+
|
| 247 |
+
const camera = new Camera(videoElem, {
|
| 248 |
+
onFrame: async () => {
|
| 249 |
+
await hands.send({ image: videoElem });
|
| 250 |
+
},
|
| 251 |
+
width: 640,
|
| 252 |
+
height: 480
|
| 253 |
+
});
|
| 254 |
+
|
| 255 |
+
camera.start();
|
| 256 |
+
|
| 257 |
+
// Results Handler
|
| 258 |
+
function onResults(results) {
|
| 259 |
+
ctx.save();
|
| 260 |
+
ctx.clearRect(0, 0, overlay.width, overlay.height);
|
| 261 |
+
ctx.drawImage(results.image, 0, 0, overlay.width, overlay.height);
|
| 262 |
+
|
| 263 |
+
if (results.multiHandLandmarks) {
|
| 264 |
+
for (const landmarks of results.multiHandLandmarks) {
|
| 265 |
+
drawConnectors(ctx, landmarks, HAND_CONNECTIONS, {
|
| 266 |
+
color: '#10B981',
|
| 267 |
+
lineWidth: 4
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
+
drawLandmarks(ctx, landmarks, {
|
| 271 |
+
color: '#F59E0B',
|
| 272 |
+
lineWidth: 2,
|
| 273 |
+
radius: (data) => {
|
| 274 |
+
return lerp(data.from.z, -0.15, 0.1, 5, 1);
|
| 275 |
+
}
|
| 276 |
+
});
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
ctx.restore();
|
| 281 |
+
|
| 282 |
+
// Process landmarks for recording/prediction
|
|
@@ -1,28 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
body {
|
| 2 |
-
|
| 3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--primary: #0ea5e9;
|
| 5 |
+
--secondary: #7c3aed;
|
| 6 |
+
--accent: #10b981;
|
| 7 |
+
--danger: #ef4444;
|
| 8 |
+
--warning: #f59e0b;
|
| 9 |
+
--info: #3b82f6;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
body {
|
| 13 |
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
+
/* Custom scrollbar */
|
| 17 |
+
::-webkit-scrollbar {
|
| 18 |
+
width: 8px;
|
| 19 |
+
}
|
| 20 |
+
::-webkit-scrollbar-track {
|
| 21 |
+
background: #1e293b;
|
| 22 |
+
}
|
| 23 |
+
::-webkit-scrollbar-thumb {
|
| 24 |
+
background: #334155;
|
| 25 |
+
border-radius: 4px;
|
| 26 |
+
}
|
| 27 |
+
::-webkit-scrollbar-thumb:hover {
|
| 28 |
+
background: #475569;
|
| 29 |
}
|
| 30 |
|
| 31 |
+
/* Animation classes */
|
| 32 |
+
.animate-pulse {
|
| 33 |
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 34 |
+
}
|
| 35 |
+
@keyframes pulse {
|
| 36 |
+
0%, 100% { opacity: 1; }
|
| 37 |
+
50% { opacity: 0.5; }
|
| 38 |
}
|
| 39 |
|
| 40 |
+
/* Custom button glow */
|
| 41 |
+
.btn-glow {
|
| 42 |
+
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
|
| 43 |
+
animation: pulse-glow 2s infinite;
|
| 44 |
+
}
|
| 45 |
+
@keyframes pulse-glow {
|
| 46 |
+
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); }
|
| 47 |
+
70% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); }
|
| 48 |
+
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
|
| 49 |
}
|
| 50 |
|
| 51 |
+
/* Responsive video container */
|
| 52 |
+
.video-container {
|
| 53 |
+
position: relative;
|
| 54 |
+
padding-bottom: 56.25%; /* 16:9 */
|
| 55 |
+
height: 0;
|
| 56 |
+
overflow: hidden;
|
| 57 |
}
|
| 58 |
+
.video-container video {
|
| 59 |
+
position: absolute;
|
| 60 |
+
top: 0;
|
| 61 |
+
left: 0;
|
| 62 |
+
width: 100%;
|
| 63 |
+
height: 100%;
|
| 64 |
+
}
|