Spaces:
Running on Zero
Running on Zero
Deploy SignLingo Gradio app
Browse files- .gitattributes +11 -0
- Dockerfile +16 -0
- README.md +63 -8
- app.py +261 -0
- build.sh +28 -0
- dist/assets/index-DB-pYQrV.css +1 -0
- dist/assets/index-LAPx0NUf.js +0 -0
- dist/favicon.svg +1 -0
- dist/icons.svg +24 -0
- dist/index.html +14 -0
- dist/landmarks/back.json +0 -0
- dist/landmarks/blood.json +0 -0
- dist/landmarks/fever.json +0 -0
- dist/landmarks/head.json +0 -0
- dist/landmarks/hello.json +0 -0
- dist/landmarks/medicine.json +0 -0
- dist/landmarks/no.json +0 -0
- dist/landmarks/pain.json +0 -0
- dist/landmarks/sick.json +0 -0
- dist/landmarks/thank_you.json +0 -0
- dist/landmarks/water.json +0 -0
- dist/landmarks/yes.json +0 -0
- dist/videos/back_lp.mp4 +3 -0
- dist/videos/blood_lp.mp4 +3 -0
- dist/videos/fever_lp.mp4 +3 -0
- dist/videos/head_lp.mp4 +0 -0
- dist/videos/hello_lp.mp4 +3 -0
- dist/videos/medicine_lp.mp4 +3 -0
- dist/videos/no_lp.mp4 +3 -0
- dist/videos/pain_lp.mp4 +3 -0
- dist/videos/sick_lp.mp4 +3 -0
- dist/videos/thank_you_lp.mp4 +3 -0
- dist/videos/water_lp.mp4 +3 -0
- dist/videos/yes_lp.mp4 +3 -0
- requirements.txt +8 -0
- sign_descriptions.json +62 -0
.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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.18.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
pinned:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|