jssaluja commited on
Commit
2ee32cf
·
verified ·
1 Parent(s): dc2e00e

Deploy SignLingo Gradio app

Browse files
.gitattributes CHANGED
@@ -33,3 +33,14 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ dist/videos/back_lp.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ dist/videos/blood_lp.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ dist/videos/fever_lp.mp4 filter=lfs diff=lfs merge=lfs -text
39
+ dist/videos/hello_lp.mp4 filter=lfs diff=lfs merge=lfs -text
40
+ dist/videos/medicine_lp.mp4 filter=lfs diff=lfs merge=lfs -text
41
+ dist/videos/no_lp.mp4 filter=lfs diff=lfs merge=lfs -text
42
+ dist/videos/pain_lp.mp4 filter=lfs diff=lfs merge=lfs -text
43
+ dist/videos/sick_lp.mp4 filter=lfs diff=lfs merge=lfs -text
44
+ dist/videos/thank_you_lp.mp4 filter=lfs diff=lfs merge=lfs -text
45
+ dist/videos/water_lp.mp4 filter=lfs diff=lfs merge=lfs -text
46
+ dist/videos/yes_lp.mp4 filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy requirements first for caching
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy app files
10
+ COPY . .
11
+
12
+ # Expose Gradio port
13
+ EXPOSE 7860
14
+
15
+ # Run the app
16
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,13 +1,68 @@
1
  ---
2
- title: SignLingo
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: SignLingo ASL Coach
3
+ emoji: 🤟
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
 
 
7
  app_file: app.py
8
+ pinned: true
9
+ license: mit
10
+ hardware: zero-gpu
11
+ tags:
12
+ - asl
13
+ - sign-language
14
+ - mediapipe
15
+ - minicpm
16
+ - education
17
+ - backyard-ai
18
+ - build-small
19
+ - off-brand
20
+ - tiny-titan
21
  ---
22
 
23
+ # SignLingo ASL Coach
24
+
25
+ Learn American Sign Language with real-time AI coaching. Built for the **Hugging Face × Gradio Build Small** hackathon.
26
+
27
+ ## What it does
28
+
29
+ - Watch the reference ASL sign on the left
30
+ - Sign in front of your webcam on the right
31
+ - **MediaPipe** detects your hands automatically — recording starts when you raise your hands, stops when you drop them
32
+ - **DTW (Dynamic Time Warping)** scores your attempt against the reference in real time
33
+ - On a failed attempt, frames from your recording are sent to **MiniCPM-V** which returns one specific coaching tip
34
+
35
+ ## How it works
36
+
37
+ - Hand + pose landmarks extracted in-browser via MediaPipe WASM (no server round-trip for detection)
38
+ - Normalized landmark sequences compared with DTW for a 0–100 similarity score
39
+ - Failed attempts: 1-2fps frames → MiniCPM-V 4.6 → natural language coaching tip
40
+ - No cloud APIs — scoring runs entirely in the browser; coaching runs on HF Zero GPU
41
+
42
+ ## Tech Stack
43
+
44
+ | Component | Choice |
45
+ |-----------|--------|
46
+ | Frontend | React + Vite served via `gr.Server` |
47
+ | Hand detection | MediaPipe Tasks Vision (WASM, in-browser) |
48
+ | Scoring | DTW on normalized landmark sequences |
49
+ | Coaching VLM | MiniCPM-V 4.6 on HF Zero GPU |
50
+ | Reference Signs | Lifeprint ASL Dictionary |
51
+
52
+ ## Vocabulary (12 signs)
53
+
54
+ Hello, Yes, No, Thank You, Pain, Head, Back, Sick, Fever, Medicine, Water, Blood
55
+
56
+ ## Demo Video
57
+
58
+ [Link to demo video]
59
+
60
+ ## Social Post
61
+
62
+ [Link to social post]
63
+
64
+ ## Built for
65
+
66
+ **Hugging Face × Gradio "Build Small" Hackathon** — Track: Backyard AI (Practical)
67
+
68
+ Made with care for the deaf and hard-of-hearing community.
app.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SignLingo ASL — Gradio Space
3
+ gr.Server serves the React/Vite build at /.
4
+ POST /api/feedback runs MiniCPM-V 4.6 locally for coaching feedback.
5
+
6
+ Run ./build.sh first to compile the frontend into gradio_app/dist/.
7
+ """
8
+
9
+ from gradio import Server
10
+ from fastapi import Request
11
+ from fastapi.responses import FileResponse, JSONResponse
12
+ from fastapi.staticfiles import StaticFiles
13
+ from pathlib import Path
14
+ import base64
15
+ import io
16
+ import json
17
+ import os
18
+ import torch
19
+ from PIL import Image
20
+
21
+ try:
22
+ import spaces
23
+ HAS_ZERO_GPU = True
24
+ except ImportError:
25
+ HAS_ZERO_GPU = False
26
+
27
+ HERE = Path(__file__).parent
28
+ DIST = HERE / "dist"
29
+
30
+ SIGN_DESCRIPTIONS = {}
31
+ desc_path = HERE / "sign_descriptions.json"
32
+ if desc_path.exists():
33
+ with open(desc_path) as f:
34
+ SIGN_DESCRIPTIONS = json.load(f)
35
+
36
+ # ── MiniCPM-V 4.6 (lazy load) ────────────────────────────────────────────────
37
+ _model = None
38
+ _processor = None
39
+ MODEL_ID = os.environ.get("MODEL_ID", "openbmb/MiniCPM-V-4.6")
40
+ DOWNSAMPLE_MODE = "16x"
41
+
42
+
43
+ def _get_device():
44
+ if torch.cuda.is_available():
45
+ return "cuda", torch.bfloat16
46
+ if torch.backends.mps.is_available():
47
+ return "mps", torch.float16
48
+ return "cpu", torch.float32
49
+
50
+
51
+ def load_model():
52
+ global _model, _processor
53
+ if _model is None:
54
+ from transformers import AutoModelForImageTextToText, AutoProcessor
55
+ hf_token = os.environ.get("HF_TOKEN")
56
+ device, dtype = _get_device()
57
+ print(f"Loading {MODEL_ID} on {device} ({dtype})…")
58
+ _model = AutoModelForImageTextToText.from_pretrained(
59
+ MODEL_ID,
60
+ torch_dtype=dtype,
61
+ device_map=device if device in ("cuda", "mps") else None,
62
+ token=hf_token,
63
+ )
64
+ _processor = AutoProcessor.from_pretrained(MODEL_ID, token=hf_token)
65
+ _model.eval()
66
+ print("Model ready.")
67
+ return _model, _processor
68
+
69
+
70
+ def _run_vlm_inner(images: list, prompt: str, max_new_tokens: int = 120) -> str:
71
+ model, processor = load_model()
72
+ device = next(model.parameters()).device
73
+ content = [{"type": "image", "image": img} for img in images]
74
+ content.append({"type": "text", "text": prompt})
75
+ messages = [{"role": "user", "content": content}]
76
+
77
+ inputs = processor.apply_chat_template(
78
+ messages,
79
+ tokenize=True,
80
+ add_generation_prompt=True,
81
+ return_dict=True,
82
+ return_tensors="pt",
83
+ downsample_mode=DOWNSAMPLE_MODE,
84
+ max_slice_nums=1,
85
+ ).to(device)
86
+
87
+ with torch.no_grad():
88
+ generated_ids = model.generate(
89
+ **inputs,
90
+ downsample_mode=DOWNSAMPLE_MODE,
91
+ max_new_tokens=max_new_tokens,
92
+ do_sample=True,
93
+ temperature=0.7,
94
+ )
95
+
96
+ trimmed = [out[len(inp):] for inp, out in zip(inputs.input_ids, generated_ids)]
97
+ return processor.batch_decode(trimmed, skip_special_tokens=True)[0].strip()
98
+
99
+
100
+ if HAS_ZERO_GPU:
101
+ run_vlm = spaces.GPU(_run_vlm_inner)
102
+ else:
103
+ run_vlm = _run_vlm_inner
104
+ # Eager load on startup so first request has no cold start
105
+ try:
106
+ load_model()
107
+ except Exception as e:
108
+ print(f"Warning: model preload failed: {e}")
109
+
110
+
111
+ # ── gr.Server ─────────────────────────────────────────────────────────────────
112
+ server = Server()
113
+
114
+
115
+ # ── VLM feedback endpoint ─────────────────────────────────────────────────────
116
+ @server.post("/api/feedback")
117
+ async def api_feedback(request: Request):
118
+ try:
119
+ data = await request.json()
120
+ except Exception:
121
+ return JSONResponse({"error": "invalid json"}, status_code=400)
122
+
123
+ user_frames_b64: list = data.get("userFrames", [])
124
+ ref_frames_b64: list = data.get("refFrames", [])
125
+ word: str = data.get("word", "")
126
+ description: str = data.get("description", "")
127
+ score: int = data.get("score", 0)
128
+
129
+ if not user_frames_b64:
130
+ return JSONResponse({"error": "no frames"}, status_code=400)
131
+
132
+ def decode_frames(raw_list):
133
+ imgs = []
134
+ for raw in raw_list:
135
+ try:
136
+ img = Image.open(io.BytesIO(base64.b64decode(raw.split(",", 1)[-1]))).convert("RGB")
137
+ imgs.append(img)
138
+ except Exception:
139
+ continue
140
+ return imgs
141
+
142
+ user_images = decode_frames(user_frames_b64)
143
+ ref_images = decode_frames(ref_frames_b64)
144
+
145
+ if not user_images:
146
+ return JSONResponse({"error": "could not decode frames"}, status_code=400)
147
+
148
+ try:
149
+ load_model()
150
+ except Exception as e:
151
+ return JSONResponse({"error": f"model load failed: {e}"}, status_code=500)
152
+
153
+ if ref_images:
154
+ prompt = (
155
+ f'You are an ASL coach. The student is learning to sign "{word}".\n'
156
+ f"Correct technique: {description}\n\n"
157
+ f"The first {len(ref_images)} images are the REFERENCE (correct sign at 1fps). "
158
+ f"The next {len(user_images)} images are the STUDENT's attempt (score: {score}% at 1fps).\n\n"
159
+ "Compare them and give ONE specific correction. Name exactly what's different — "
160
+ "hand shape, wrist position, movement path, or location. Maximum 2 sentences."
161
+ )
162
+ all_images = ref_images + user_images
163
+ else:
164
+ prompt = (
165
+ f'You are an ASL coach. The student is learning to sign "{word}".\n'
166
+ f"Correct technique: {description}\n\n"
167
+ f"These {len(user_images)} images show the student's attempt at 1fps (score: {score}%).\n"
168
+ "Give ONE specific correction — hand shape, position, or movement. Maximum 2 sentences."
169
+ )
170
+ all_images = user_images
171
+
172
+ try:
173
+ feedback = run_vlm(all_images, prompt, max_new_tokens=120)
174
+ return JSONResponse({"feedback": feedback})
175
+ except Exception as e:
176
+ return JSONResponse({"error": str(e)}, status_code=500)
177
+
178
+
179
+ # ── Sign description endpoint ─────────────────────────────────────────────────
180
+ @server.post("/api/describe")
181
+ async def api_describe(request: Request):
182
+ try:
183
+ data = await request.json()
184
+ except Exception:
185
+ return JSONResponse({"error": "invalid json"}, status_code=400)
186
+
187
+ frames_b64: list = data.get("frames", [])
188
+ word: str = data.get("word", "")
189
+
190
+ # Backward compat: single frame
191
+ if not frames_b64 and data.get("frame"):
192
+ frames_b64 = [data.get("frame")]
193
+
194
+ if not frames_b64:
195
+ return JSONResponse({"error": "no frames"}, status_code=400)
196
+
197
+ images = []
198
+ for fb64 in frames_b64:
199
+ try:
200
+ img_bytes = base64.b64decode(fb64.split(",", 1)[-1])
201
+ images.append(Image.open(io.BytesIO(img_bytes)).convert("RGB"))
202
+ except Exception:
203
+ continue
204
+
205
+ if not images:
206
+ return JSONResponse({"error": "bad frames"}, status_code=400)
207
+
208
+ try:
209
+ load_model()
210
+ except Exception as e:
211
+ return JSONResponse({"error": f"model load failed: {e}"}, status_code=500)
212
+
213
+ n = len(images)
214
+ prompt = (
215
+ f'These {n} images show the sequence of the ASL sign for "{word}" at 1 frame per second. '
216
+ "Describe step-by-step how to perform this sign: starting hand shape and position, "
217
+ "movement, and ending position. Be specific and practical for a learner. 2-3 sentences max."
218
+ )
219
+
220
+ try:
221
+ description = run_vlm(images, prompt, max_new_tokens=150)
222
+ return JSONResponse({"description": description})
223
+ except Exception as e:
224
+ return JSONResponse({"error": str(e)}, status_code=500)
225
+
226
+
227
+ # ── Static React build ────────────────────────────────────────────────────────
228
+ if (DIST / "assets").exists():
229
+ server.mount("/assets", StaticFiles(directory=str(DIST / "assets")), name="vite-assets")
230
+
231
+ if (DIST / "videos").exists():
232
+ server.mount("/videos", StaticFiles(directory=str(DIST / "videos")), name="videos")
233
+
234
+ if (DIST / "landmarks").exists():
235
+ server.mount("/landmarks", StaticFiles(directory=str(DIST / "landmarks")), name="landmarks")
236
+
237
+
238
+ @server.get("/favicon.svg")
239
+ async def favicon():
240
+ f = DIST / "favicon.svg"
241
+ return FileResponse(str(f)) if f.exists() else JSONResponse({})
242
+
243
+
244
+ @server.get("/icons.svg")
245
+ async def icons():
246
+ f = DIST / "icons.svg"
247
+ return FileResponse(str(f)) if f.exists() else JSONResponse({})
248
+
249
+
250
+ @server.get("/")
251
+ async def root():
252
+ return FileResponse(str(DIST / "index.html"))
253
+
254
+
255
+ @server.get("/{full_path:path}")
256
+ async def spa_fallback(full_path: str):
257
+ return FileResponse(str(DIST / "index.html"))
258
+
259
+
260
+ if __name__ == "__main__":
261
+ server.launch(server_name="0.0.0.0", server_port=7860)
build.sh ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
5
+ GRADIO_DIR="$REPO_ROOT/gradio_app"
6
+ APP_DIR="$REPO_ROOT/app"
7
+
8
+ echo "→ Building React app..."
9
+ cd "$APP_DIR"
10
+ npm run build
11
+
12
+ echo "→ Copying dist to gradio_app/dist..."
13
+ rm -rf "$GRADIO_DIR/dist"
14
+ cp -r "$APP_DIR/dist" "$GRADIO_DIR/dist"
15
+
16
+ echo "→ Copying videos..."
17
+ mkdir -p "$GRADIO_DIR/dist/videos"
18
+ cp "$APP_DIR/public/videos/"*.mp4 "$GRADIO_DIR/dist/videos/"
19
+
20
+ echo "→ Copying landmarks..."
21
+ mkdir -p "$GRADIO_DIR/dist/landmarks"
22
+ cp "$APP_DIR/public/landmarks/"*.json "$GRADIO_DIR/dist/landmarks/"
23
+
24
+ echo "→ Copying favicon/icons..."
25
+ cp "$APP_DIR/public/favicon.svg" "$GRADIO_DIR/dist/" 2>/dev/null || true
26
+ cp "$APP_DIR/public/icons.svg" "$GRADIO_DIR/dist/" 2>/dev/null || true
27
+
28
+ echo "✓ Done. Run: cd gradio_app && python3 app.py"
dist/assets/index-DB-pYQrV.css ADDED
@@ -0,0 +1 @@
 
 
1
+ body{margin:0;padding:0}#root{width:100%;min-height:100vh}:root{--bg-dark:#1a1a2e;--bg-panel:#16213e;--accent:#0f3460;--highlight:#e94560;--success:#4caf50;--text:#eee;--text-dim:#888}*{box-sizing:border-box;margin:0;padding:0}body{background:var(--bg-dark);color:var(--text);min-height:100vh;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.app{flex-direction:column;min-height:100vh;display:flex}header{background:var(--bg-panel);border-bottom:1px solid var(--accent);justify-content:space-between;align-items:center;padding:1rem 2rem;display:flex;position:relative}header h1{font-size:1.5rem;font-weight:600}.progress{flex-wrap:wrap;justify-content:center;gap:8px;display:flex}.progress .dot{background:var(--accent);cursor:pointer;border-radius:50%;width:12px;height:12px;transition:all .2s}.progress .dot:hover{transform:scale(1.2)}.progress .dot.current{background:var(--highlight);box-shadow:0 0 8px var(--highlight)}.progress .dot.completed{background:var(--success)}.auth-signin{border:1px solid var(--accent);color:var(--text);cursor:pointer;white-space:nowrap;background:0 0;border-radius:6px;padding:.4rem .9rem;font-size:.85rem;transition:all .2s}.auth-signin:hover{background:var(--accent)}.auth-user{align-items:center;gap:.5rem;display:flex}.auth-avatar{object-fit:cover;border-radius:50%;width:28px;height:28px}.auth-name{color:var(--text-dim);font-size:.85rem}.auth-signout{border:1px solid var(--accent);color:var(--text-dim);cursor:pointer;background:0 0;border-radius:6px;padding:.3rem .7rem;font-size:.8rem;transition:all .2s}.auth-signout:hover{border-color:var(--highlight);color:var(--highlight)}.sign-nav{background:var(--bg-panel);border-bottom:1px solid var(--accent);justify-content:center;align-items:center;gap:1rem;padding:.6rem 2rem;display:flex}.sign-nav-arrow{border:1px solid var(--accent);color:var(--text);cursor:pointer;background:0 0;border-radius:6px;padding:.25rem .7rem;font-size:1rem;line-height:1;transition:all .15s}.sign-nav-arrow:hover{background:var(--accent);border-color:var(--highlight)}.tabs{background:var(--bg-dark);border-radius:8px;gap:2px;padding:3px;display:flex;position:absolute;left:50%;transform:translate(-50%)}.tab{color:var(--text-dim);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:.4rem 1.2rem;font-size:.9rem;transition:all .2s}.tab:hover{color:var(--text)}.tab.active{background:var(--accent);color:var(--text)}.library-view{flex-direction:column;flex:1;gap:2rem;padding:1.5rem 2rem;display:flex;overflow-y:auto}.library-toolbar{justify-content:flex-end;margin-bottom:.5rem;display:flex}.library-section{flex-direction:column;gap:.75rem;display:flex}.library-section-title{text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);font-size:.75rem}.library-grid{grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:1rem;display:grid}.sign-card{background:var(--bg-panel);border:1px solid var(--accent);border-radius:10px;transition:border-color .2s,transform .15s,box-shadow .2s;overflow:hidden}.sign-card:hover{border-color:var(--text-dim);transform:translateY(-2px);box-shadow:0 4px 16px #0006}.sign-card.active{border-color:var(--highlight);box-shadow:0 0 0 1px var(--highlight)}.sign-card.completed{border-color:var(--success)}.sign-card-video{aspect-ratio:4/3;background:#000;position:relative;overflow:hidden}.sign-card-video video{object-fit:cover;width:100%;height:100%}.sign-card-badge{background:var(--success);color:#fff;border-radius:50%;justify-content:center;align-items:center;width:22px;height:22px;font-size:.75rem;font-weight:700;display:flex;position:absolute;top:6px;right:6px}.sign-card-delete{width:22px;height:22px;color:var(--text-dim);cursor:pointer;opacity:0;background:#0009;border:none;border-radius:50%;justify-content:center;align-items:center;font-size:.7rem;transition:all .15s;display:flex;position:absolute;top:6px;left:6px}.sign-card:hover .sign-card-delete{opacity:1}.sign-card-delete:hover{background:var(--highlight);color:#fff}.sign-card.selectable{cursor:pointer}.sign-card.selected{outline:2px solid var(--highlight);outline-offset:-2px}.sign-card-checkbox{color:#fff;background:#00000080;border:2px solid #ffffff80;border-radius:4px;justify-content:center;align-items:center;width:22px;height:22px;font-size:.75rem;font-weight:700;display:flex;position:absolute;top:6px;left:6px}.sign-card-checkbox.checked{background:var(--highlight);border-color:var(--highlight)}.sign-card-custom-badge{background:var(--accent);color:#fff;text-transform:uppercase;border-radius:4px;padding:2px 6px;font-size:.65rem;font-weight:600;position:absolute;top:6px;left:6px}.select-btn,.cancel-btn{background:var(--surface);border:1px solid var(--border);color:var(--text);cursor:pointer;border-radius:6px;padding:.45rem .9rem;font-size:.85rem;transition:all .2s}.select-btn:hover,.cancel-btn:hover{background:var(--border)}.delete-btn{color:#fff;cursor:pointer;background:#dc3545;border:none;border-radius:6px;padding:.45rem .9rem;font-size:.85rem;transition:all .2s}.delete-btn:hover:not(:disabled){background:#c82333}.delete-btn:disabled{opacity:.5;cursor:not-allowed}.sign-card-footer{flex-direction:column;align-items:flex-start;gap:.35rem;padding:.6rem .75rem;display:flex}.sign-card-footer-row{justify-content:space-between;align-items:center;gap:.5rem;width:100%;display:flex}.sign-card-description{color:var(--text-dim);margin:0;font-size:.72rem;font-style:italic;line-height:1.4}.sign-card-label{font-size:.95rem;font-weight:500}.sign-card-practice{background:var(--accent);color:var(--text);cursor:pointer;white-space:nowrap;border:none;border-radius:5px;padding:.3rem .7rem;font-size:.78rem;transition:background .2s}.sign-card-practice:hover{background:var(--highlight)}.library-empty{color:var(--text-dim);font-size:.9rem}.add-sign-btn{border:1px solid var(--accent);color:var(--text);cursor:pointer;white-space:nowrap;background:0 0;border-radius:6px;padding:.4rem .9rem;font-size:.9rem;transition:all .2s}.add-sign-btn:hover{background:var(--accent);border-color:var(--highlight)}.inventory-modal{width:560px;max-height:80vh;overflow-y:auto}.inventory-header{justify-content:space-between;align-items:center;display:flex}.inventory-close{color:var(--text-dim);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:.2rem .5rem;font-size:1.1rem;transition:color .2s}.inventory-close:hover{color:var(--highlight)}.inventory-section-label{color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.6rem;font-size:.8rem}.inventory-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:.5rem;margin-bottom:1.2rem;display:grid}.inventory-card{background:var(--bg-dark);border:1px solid var(--accent);cursor:pointer;color:var(--text);text-align:center;border-radius:8px;justify-content:center;align-items:center;padding:.7rem .5rem;font-size:.9rem;transition:all .15s;display:flex;position:relative}.inventory-card:hover{border-color:var(--text-dim)}.inventory-card.active{border-color:var(--highlight);box-shadow:0 0 0 1px var(--highlight)}.inventory-card.completed{border-color:var(--success)}.inventory-card.custom{padding:0;overflow:hidden}.inventory-card-main{color:var(--text);cursor:pointer;background:0 0;border:none;flex:1;justify-content:center;align-items:center;gap:.3rem;padding:.7rem .4rem;font-size:.9rem;display:flex}.inventory-delete{border:none;border-left:1px solid var(--accent);color:var(--text-dim);cursor:pointer;background:0 0;align-self:stretch;align-items:center;padding:.4rem .5rem;font-size:.75rem;transition:all .15s;display:flex}.inventory-delete:hover{background:var(--highlight);color:#fff}.inventory-check{color:var(--success);font-size:.8rem}.modal-overlay{z-index:100;background:#000000b3;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal{background:var(--bg-panel);border:1px solid var(--accent);border-radius:12px;flex-direction:column;gap:1.2rem;width:420px;max-width:90vw;padding:2rem;display:flex}.modal h2{font-size:1.3rem;font-weight:600}.modal-field{flex-direction:column;gap:.4rem;display:flex}.modal-field label{color:var(--text-dim);font-size:.85rem}.modal-field input{background:var(--bg-dark);border:1px solid var(--accent);color:var(--text);border-radius:6px;outline:none;padding:.6rem .8rem;font-size:1rem;transition:border-color .2s}.modal-field input:focus{border-color:var(--highlight)}.drop-zone{border:2px dashed var(--accent);cursor:pointer;color:var(--text-dim);border-radius:8px;flex-direction:column;align-items:center;gap:.5rem;padding:1.5rem;font-size:.95rem;transition:all .2s;display:flex}.drop-zone:hover,.drop-zone.dragging{border-color:var(--highlight);background:#e945600d}.drop-zone.has-file{border-style:solid;border-color:var(--success);color:var(--text)}.drop-icon{font-size:2rem}.drop-filename{word-break:break-all;text-align:center;font-size:.9rem}.extraction-progress{flex-direction:column;gap:.4rem;display:flex}.progress-msg{color:var(--text-dim);font-size:.85rem}.progress-track{background:var(--accent);border-radius:3px;height:6px;overflow:hidden}.progress-fill{background:var(--highlight);border-radius:3px;height:100%;transition:width .1s linear}.modal-error{color:var(--highlight);font-size:.9rem}.modal-actions{justify-content:flex-end;gap:.75rem;margin-top:.25rem;display:flex}.modal-cancel{border:1px solid var(--accent);color:var(--text);cursor:pointer;background:0 0;border-radius:6px;padding:.6rem 1.2rem;font-size:.95rem;transition:all .2s}.modal-cancel:hover:not(:disabled){background:var(--accent)}.modal-cancel:disabled{opacity:.4;cursor:not-allowed}.modal-submit{background:var(--highlight);color:#fff;cursor:pointer;border:none;border-radius:6px;padding:.6rem 1.2rem;font-size:.95rem;font-weight:600;transition:all .2s}.modal-submit:hover:not(:disabled){background:#ff6b81}.modal-submit:disabled{opacity:.4;cursor:not-allowed}main{flex:1;gap:1rem;padding:1rem;display:flex}.panel{background:var(--bg-panel);border-radius:12px;flex-direction:column;flex:1;padding:1rem;display:flex;overflow:hidden}.panel h2{text-align:center;margin-bottom:1rem;font-size:1.25rem;font-weight:500}.video-container{aspect-ratio:16/9;background:#000;border-radius:8px;width:100%;position:relative;overflow:hidden}.video-container video{object-fit:contain;width:100%;height:100%}.video-container canvas.overlay{pointer-events:none;width:100%;height:100%;position:absolute;top:0;left:0}.video-panel .controls{align-items:center;gap:1rem;margin-top:.5rem;padding:.5rem;display:flex}.play-btn{background:var(--accent);width:40px;height:40px;color:var(--text);cursor:pointer;border:none;border-radius:50%;font-size:1.2rem;transition:background .2s}.play-btn:hover{background:var(--highlight)}.scrubber{appearance:none;background:var(--accent);cursor:pointer;border-radius:3px;flex:1;height:6px}.scrubber::-webkit-slider-thumb{appearance:none;background:var(--highlight);cursor:pointer;border-radius:50%;width:16px;height:16px;transition:transform .1s}.scrubber::-webkit-slider-thumb:hover{transform:scale(1.2)}.scrubber::-moz-range-thumb{background:var(--highlight);cursor:pointer;border:none;border-radius:50%;width:16px;height:16px}.frame-counter{color:var(--text-dim);text-align:right;min-width:70px;font-size:.9rem}.time-display{color:var(--text-dim);font-variant-numeric:tabular-nums;min-width:40px;font-size:.85rem}.timeline-container{background:var(--accent);border-radius:3px;flex:1;height:6px;position:relative;overflow:hidden}.timeline-container .scrubber{z-index:2;background:0 0;width:100%;height:100%;margin:0;position:absolute;top:0;left:0}.timeline-progress{background:var(--highlight);pointer-events:none;border-radius:3px;height:100%;transition:width 50ms linear;position:absolute;top:0;left:0}.speed-controls{justify-content:center;gap:.5rem;margin-top:.5rem;display:flex}.speed-controls button{border:1px solid var(--accent);color:var(--text);cursor:pointer;background:0 0;border-radius:6px;padding:.4rem .8rem;transition:all .2s}.speed-controls button:hover,.speed-controls button.active{background:var(--accent);border-color:var(--highlight)}.webcam-panel{width:100%}.error-container{background:linear-gradient(135deg,#2a1a1a,#1a1a2a);justify-content:center;align-items:center;display:flex}.error-content{text-align:center;flex-direction:column;align-items:center;padding:2rem;display:flex}.error-icon{opacity:.8;margin-bottom:1rem;font-size:3rem}.error-content h3{color:var(--highlight);margin-bottom:.5rem}.error-content p{color:var(--text-dim);max-width:280px;margin-bottom:1rem}.retry-btn{background:var(--accent);color:var(--text);cursor:pointer;border:none;border-radius:6px;padding:.6rem 1.2rem;transition:background .2s}.retry-btn:hover{background:var(--highlight)}.webcam-panel.error{aspect-ratio:16/9;text-align:center;color:#e94560;background:#2a1a1a;border-radius:8px;flex-direction:column;justify-content:center;align-items:center;display:flex}.loading-overlay{background:#000c;flex-direction:column;justify-content:center;align-items:center;width:100%;height:100%;display:flex;position:absolute;top:0;left:0}.spinner{border:3px solid var(--accent);border-top-color:var(--highlight);border-radius:50%;width:40px;height:40px;animation:1s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.status-bar{align-items:center;gap:.5rem;margin-top:.5rem;padding:.5rem;font-size:.9rem;display:flex}.status-dot{background:var(--text-dim);border-radius:50%;width:10px;height:10px;transition:background .3s}.status-dot.detected{background:var(--success);box-shadow:0 0 6px var(--success)}.recording-badge{color:var(--highlight);margin-left:auto;font-weight:700;animation:1s ease-in-out infinite pulse}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.score-display{background:var(--accent);border-radius:6px;flex-direction:row;justify-content:center;align-items:center;gap:.75rem;min-height:2.4rem;margin-top:.4rem;padding:.4rem .75rem;display:flex}.score-display .word{font-size:1rem;font-weight:700}.score-display .instruction{color:var(--text-dim);font-size:1rem}.score-display .score{font-size:1rem;font-weight:700}.score-display .result{font-size:1rem}.score-display.passed{background:linear-gradient(135deg,#1b5e20,#2e7d32)}.score-display.passed .result{color:#a5d6a7}.score-display.failed{background:linear-gradient(135deg,#b71c1c,#c62828)}.score-display.failed .result{color:#ef9a9a}.sign-description{color:var(--text-dim);text-align:center;margin:.25rem 0 0;font-size:.8rem;font-style:italic;line-height:1.4}.feedback{color:var(--text-dim);border-left:2px solid var(--accent);margin:.4rem 0 0;padding:.4rem .6rem;font-size:.85rem;font-style:italic;line-height:1.5}.feedback-loading{opacity:.5;animation:1.5s ease-in-out infinite pulse}.action-buttons{justify-content:center;gap:1rem;margin-top:1rem;display:flex}.action-buttons button{cursor:pointer;border:none;border-radius:8px;padding:.8rem 1.5rem;font-size:1rem;font-weight:600;transition:all .2s}.record-btn{background:var(--highlight);color:#fff}.record-btn:hover{background:#ff6b81;transform:scale(1.02)}.stop-btn{color:#fff;background:#ff9800;animation:1s ease-in-out infinite pulse}.stop-btn:hover{background:#ffb74d}.next-btn{background:var(--success);color:#fff}.next-btn:hover{background:#66bb6a;transform:scale(1.02)}footer{text-align:center;background:var(--bg-panel);border-top:1px solid var(--accent);color:var(--text-dim);padding:1rem}footer kbd{background:var(--accent);border-radius:4px;padding:.2rem .5rem;font-family:monospace;display:inline-block}@media (width<=900px){main{flex-direction:column}.panel{min-height:auto}}.debug-panel{color:#0f0;z-index:1000;background:#000000e6;border-top:1px solid #333;max-height:150px;padding:8px;font-family:monospace;font-size:12px;position:fixed;bottom:0;left:0;right:0;overflow-y:auto}.debug-panel strong{color:#ff0}.recording-indicator{color:#e94560;font-weight:700;animation:1s infinite pulse}.ready-indicator{color:var(--text-dim)}.recording-overlay{color:#fff;z-index:10;background:#e94560e6;border-radius:4px;padding:6px 12px;font-size:14px;font-weight:700;animation:1s infinite pulse;position:absolute;top:10px;left:10px}.recording-overlay .rec-dot{color:#fff;margin-right:4px}
dist/assets/index-LAPx0NUf.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/favicon.svg ADDED
dist/icons.svg ADDED
dist/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>SignLingo</title>
8
+ <script type="module" crossorigin src="/assets/index-LAPx0NUf.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DB-pYQrV.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
dist/landmarks/back.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/blood.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/fever.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/head.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/hello.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/medicine.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/no.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/pain.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/sick.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/thank_you.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/water.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/landmarks/yes.json ADDED
The diff for this file is too large to render. See raw diff
 
dist/videos/back_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be849cf5224c6561f09a25ffafe01e9041ae2ae347aadf9f0a929094b542c6d0
3
+ size 490983
dist/videos/blood_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8eae83b176521a75b835743df4e0acc09eba0df8da62b4c5bb69448744474288
3
+ size 657095
dist/videos/fever_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2ac0750d4f1767ee199e07563f6aa5729c3fda14250f23dbcc371009bfc52565
3
+ size 416462
dist/videos/head_lp.mp4 ADDED
Binary file (96.7 kB). View file
 
dist/videos/hello_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6d2ecae05172aef2a3b9451a6c42af9447ce8e2496c2e55cb059345178c42592
3
+ size 136388
dist/videos/medicine_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:124e9f1c79072eb562419501d327ce821e6af1966c5feeeba0eea726aa6d2170
3
+ size 1115287
dist/videos/no_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6129ea33f3a7d5f2a946a2c7ba3baee4c89d8644136a6037a9e532cd29f22583
3
+ size 119262
dist/videos/pain_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:abb35960dfc803919241ce402d566316be409b87b912fb7976438ed36b621ea5
3
+ size 275116
dist/videos/sick_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4553fe6a75f7b63aec10629f65fea143e88c31839df243c02fb189f6f5b18391
3
+ size 159226
dist/videos/thank_you_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:823e77099cc68165970680af9863afead9a805b2d105e5a4746dce05c91d0678
3
+ size 576153
dist/videos/water_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c7415b4ffd5e0231c87921b7189910cf492f57f583ef52a5e1d520ca52cba79f
3
+ size 110101
dist/videos/yes_lp.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b268480a0dfd1406d3a13b9a7b0caded85d3b4dfbba6688a2e861d2d7a4b7ac3
3
+ size 128015
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>=5.0.0
2
+ spaces
3
+ torch>=2.0.0
4
+ transformers>=4.51.0
5
+ accelerate>=0.27.0
6
+ sentencepiece
7
+ Pillow>=10.0.0
8
+ numpy>=1.24.0
sign_descriptions.json ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "hello": {
3
+ "word": "hello",
4
+ "display": "Hello",
5
+ "description": "Wave motion near the side of the head. Dominant hand starts with open palm facing outward, positioned near the forehead/temple area. Hand moves outward in a small wave motion, like a casual wave goodbye. Single smooth outward movement. One hand used (dominant). Hand stays at head level throughout."
6
+ },
7
+ "yes": {
8
+ "word": "yes",
9
+ "display": "Yes",
10
+ "description": "Fist nodding motion. Dominant hand forms a fist (like the letter S). Fist moves up and down repeatedly at the wrist, mimicking a head nodding yes. Located in neutral signing space in front of the body. 2-3 small nodding movements. One hand used (dominant)."
11
+ },
12
+ "no": {
13
+ "word": "no",
14
+ "display": "No",
15
+ "description": "Index and middle finger snap to thumb. Dominant hand starts with index and middle fingers extended (like the letter U or N). Fingers snap down to meet the thumb in a pinching motion. Quick snapping movement repeated 1-2 times. Located in neutral space. One hand used (dominant)."
16
+ },
17
+ "thank_you": {
18
+ "word": "thank_you",
19
+ "display": "Thank You",
20
+ "description": "Flat hand moves from chin outward. Dominant hand is flat with fingers together (like the letter B). Hand starts touching the chin or lips area with fingertips. Hand moves outward and slightly downward, away from the face. Single smooth movement. One hand used (dominant). Similar to blowing a kiss motion."
21
+ },
22
+ "pain": {
23
+ "word": "pain",
24
+ "display": "Pain",
25
+ "description": "Two index fingers twist toward each other. Both hands form pointing index fingers (like the letter D or number 1). Index fingers point toward each other and twist or rotate in opposite directions. Twisting motion repeated 2-3 times. Hands positioned at location of pain or in neutral space. Both hands used, moving symmetrically."
26
+ },
27
+ "head": {
28
+ "word": "head",
29
+ "display": "Head",
30
+ "description": "Fingertips tap the side of the head. Dominant hand is flat with fingers together, bent at knuckles (bent-B handshape). Fingertips tap the temple or side of forehead area. Two quick taps. One hand used (dominant). Hand approaches head from the side."
31
+ },
32
+ "back": {
33
+ "word": "back",
34
+ "display": "Back",
35
+ "description": "Hand reaches behind to touch the back. Dominant hand reaches over the shoulder or around the side to touch or pat the upper back area. Palm faces the body. Single touch or pat motion. One hand used (dominant). Shows the location of the back on your own body."
36
+ },
37
+ "sick": {
38
+ "word": "sick",
39
+ "display": "Sick",
40
+ "description": "Middle fingers touch forehead and stomach. Both hands form open-5 handshape with middle finger bent forward. One hand touches forehead with bent middle finger, other hand touches stomach simultaneously. Can also alternate touching forehead then stomach. Both hands used. Shows sickness affecting head and stomach."
41
+ },
42
+ "fever": {
43
+ "word": "fever",
44
+ "display": "Fever",
45
+ "description": "Back of hand touches forehead, checking temperature. Dominant hand is flat or slightly bent. Back of the hand (not palm) touches the forehead, like checking for fever. May include slight upward movement afterward. One hand used (dominant). Mimics the gesture of feeling forehead for temperature."
46
+ },
47
+ "medicine": {
48
+ "word": "medicine",
49
+ "display": "Medicine",
50
+ "description": "Middle finger circles in palm. Non-dominant hand is flat with palm facing up. Dominant hand has middle finger extended and bent, touching the center of the flat palm. Middle finger makes small circular grinding motions in the palm. Multiple circular motions. Both hands used. Mimics grinding medicine with mortar and pestle."
51
+ },
52
+ "water": {
53
+ "word": "water",
54
+ "display": "Water",
55
+ "description": "W handshape taps chin. Dominant hand forms the letter W (three fingers extended and spread). The index finger side of the W hand taps the chin twice. Located at the chin/mouth area. Two quick taps. One hand used (dominant). The W stands for Water."
56
+ },
57
+ "blood": {
58
+ "word": "blood",
59
+ "display": "Blood",
60
+ "description": "Wiggling fingers flow down from the back of the hand. Non-dominant hand is flat, palm facing down. Dominant hand starts at the back of the non-dominant hand with fingers spread (5 handshape). Dominant hand moves downward while wiggling fingers, representing blood flowing. Both hands used. Shows liquid dripping down."
61
+ }
62
+ }