dev-Rachitgarg commited on
Commit
2b66289
·
verified ·
1 Parent(s): 367039f

<!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

Files changed (4) hide show
  1. README.md +8 -5
  2. index.html +120 -19
  3. script.js +282 -0
  4. style.css +54 -18
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Handspeak Ai Translator
3
- emoji: 🚀
4
- colorFrom: gray
5
- colorTo: pink
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
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).
index.html CHANGED
@@ -1,19 +1,120 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>
script.js ADDED
@@ -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
style.css CHANGED
@@ -1,28 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
 
 
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
 
 
 
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
 
 
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
+ }