Spaces:
Sleeping
Sleeping
Commit ·
fa0d1cd
0
Parent(s):
Initial commit with Hugging Face Docker and LFS configurations
Browse files- .dockerignore +21 -0
- .gitattributes +1 -0
- .gitignore +13 -0
- Dockerfile +48 -0
- backend/requirements.txt +6 -0
- backend/result.py +76 -0
- backend/sign.py +69 -0
- backend/tain.py +61 -0
- frontend/gesture_words/.gitignore +24 -0
- frontend/gesture_words/README.md +12 -0
- frontend/gesture_words/eslint.config.js +33 -0
- frontend/gesture_words/index.html +13 -0
- frontend/gesture_words/package-lock.json +0 -0
- frontend/gesture_words/package.json +31 -0
- frontend/gesture_words/public/vite.svg +1 -0
- frontend/gesture_words/src/App.jsx +149 -0
- frontend/gesture_words/src/api.js +4 -0
- frontend/gesture_words/src/assets/react.svg +1 -0
- frontend/gesture_words/src/index.css +1 -0
- frontend/gesture_words/src/main.jsx +10 -0
- frontend/gesture_words/vite.config.js +9 -0
- sign_language_model.h5 +3 -0
.dockerignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git and local configs
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
.env
|
| 5 |
+
README.md
|
| 6 |
+
|
| 7 |
+
# Node modules and build output
|
| 8 |
+
**/node_modules/
|
| 9 |
+
**/dist/
|
| 10 |
+
|
| 11 |
+
# Python environment and cache
|
| 12 |
+
venv/
|
| 13 |
+
.venv/
|
| 14 |
+
__pycache__/
|
| 15 |
+
*.pyc
|
| 16 |
+
*.pyo
|
| 17 |
+
*.pyd
|
| 18 |
+
|
| 19 |
+
# Raw training data and duplicate models
|
| 20 |
+
MP_Data/
|
| 21 |
+
best_model.h5
|
.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
.venv/
|
| 3 |
+
node_modules/
|
| 4 |
+
MP_Data/
|
| 5 |
+
best_model.h5
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.pyc
|
| 8 |
+
*.pyo
|
| 9 |
+
*.pyd
|
| 10 |
+
.DS_Store
|
| 11 |
+
.env
|
| 12 |
+
dist/
|
| 13 |
+
build/
|
Dockerfile
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 1: Build the React frontend
|
| 2 |
+
FROM node:20-alpine AS frontend-builder
|
| 3 |
+
WORKDIR /frontend
|
| 4 |
+
|
| 5 |
+
# Copy dependency configs
|
| 6 |
+
COPY frontend/gesture_words/package*.json ./
|
| 7 |
+
|
| 8 |
+
# Install packages
|
| 9 |
+
RUN npm install
|
| 10 |
+
|
| 11 |
+
# Copy source code and build
|
| 12 |
+
COPY frontend/gesture_words/ ./
|
| 13 |
+
RUN npm run build
|
| 14 |
+
|
| 15 |
+
# Stage 2: Build the Python backend
|
| 16 |
+
FROM python:3.10-slim
|
| 17 |
+
|
| 18 |
+
# Install system dependencies for MediaPipe / OpenCV
|
| 19 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 20 |
+
libgl1 \
|
| 21 |
+
libglib2.0-0 \
|
| 22 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 23 |
+
|
| 24 |
+
WORKDIR /app
|
| 25 |
+
|
| 26 |
+
# Install Python requirements
|
| 27 |
+
COPY backend/requirements.txt ./
|
| 28 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 29 |
+
|
| 30 |
+
# Copy backend files
|
| 31 |
+
COPY backend/ /app/backend/
|
| 32 |
+
COPY sign_language_model.h5 /app/backend/sign_language_model.h5
|
| 33 |
+
|
| 34 |
+
# Copy built frontend assets
|
| 35 |
+
COPY --from=frontend-builder /frontend/dist /app/dist
|
| 36 |
+
|
| 37 |
+
# Expose port (Hugging Face Space default)
|
| 38 |
+
ENV PORT=7860
|
| 39 |
+
EXPOSE 7860
|
| 40 |
+
|
| 41 |
+
# Set Hugging Face non-root user permissions
|
| 42 |
+
RUN useradd -m -u 1000 user
|
| 43 |
+
RUN chown -R user:user /app
|
| 44 |
+
USER user
|
| 45 |
+
|
| 46 |
+
# Run Flask application from backend directory
|
| 47 |
+
WORKDIR /app/backend
|
| 48 |
+
CMD ["python", "result.py"]
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tensorflow-cpu
|
| 2 |
+
mediapipe
|
| 3 |
+
flask
|
| 4 |
+
flask-cors
|
| 5 |
+
opencv-python-headless
|
| 6 |
+
numpy
|
backend/result.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
from flask import Flask, request, jsonify, send_from_directory
|
| 4 |
+
from flask_cors import CORS
|
| 5 |
+
from tensorflow.keras.models import load_model
|
| 6 |
+
import base64
|
| 7 |
+
import cv2
|
| 8 |
+
import mediapipe as mp
|
| 9 |
+
|
| 10 |
+
app = Flask(__name__, static_folder='../dist', static_url_path='/')
|
| 11 |
+
CORS(app) # Allow requests from frontend (e.g., React)
|
| 12 |
+
|
| 13 |
+
@app.route('/')
|
| 14 |
+
def serve():
|
| 15 |
+
return send_from_directory(app.static_folder, 'index.html')
|
| 16 |
+
|
| 17 |
+
@app.route('/<path:path>')
|
| 18 |
+
def serve_static(path):
|
| 19 |
+
if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
|
| 20 |
+
return send_from_directory(app.static_folder, path)
|
| 21 |
+
else:
|
| 22 |
+
return send_from_directory(app.static_folder, 'index.html')
|
| 23 |
+
|
| 24 |
+
# Load modp
|
| 25 |
+
model = load_model('sign_language_model.h5')
|
| 26 |
+
actions = np.array(['hi', 'me', 'thank you'])
|
| 27 |
+
|
| 28 |
+
mp_hands = mp.solutions.hands
|
| 29 |
+
hands = mp_hands.Hands(static_image_mode=False, max_num_hands=1, min_detection_confidence=0.7)
|
| 30 |
+
|
| 31 |
+
@app.route('/predict', methods=['POST'])
|
| 32 |
+
def predict():
|
| 33 |
+
data = request.get_json()
|
| 34 |
+
if not data or 'image' not in data:
|
| 35 |
+
return jsonify({'error': 'No image provided'}), 400
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
# Decode base64 image
|
| 39 |
+
image_data = base64.b64decode(data['image'].split(',')[1])
|
| 40 |
+
nparr = np.frombuffer(image_data, np.uint8)
|
| 41 |
+
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 42 |
+
|
| 43 |
+
# Hand detection
|
| 44 |
+
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 45 |
+
results = hands.process(rgb)
|
| 46 |
+
|
| 47 |
+
if results.multi_hand_landmarks:
|
| 48 |
+
hand_landmarks = results.multi_hand_landmarks[0]
|
| 49 |
+
keypoints = []
|
| 50 |
+
for lm in hand_landmarks.landmark:
|
| 51 |
+
keypoints.extend([lm.x, lm.y, lm.z])
|
| 52 |
+
|
| 53 |
+
# Ensure consistent input shape: (30, 63)
|
| 54 |
+
sequence = np.array([keypoints] * 30) # 30 identical frames
|
| 55 |
+
input_data = np.expand_dims(sequence, axis=0)
|
| 56 |
+
input_data = input_data / np.max(input_data + 1e-6) # Avoid divide-by-zero
|
| 57 |
+
|
| 58 |
+
prediction = model.predict(input_data)[0]
|
| 59 |
+
confidence = float(np.max(prediction))
|
| 60 |
+
label = actions[np.argmax(prediction)]
|
| 61 |
+
|
| 62 |
+
return jsonify({
|
| 63 |
+
'prediction': label,
|
| 64 |
+
'confidence': round(confidence, 2)
|
| 65 |
+
}), 200
|
| 66 |
+
else:
|
| 67 |
+
return jsonify({'prediction': 'No hand detected', 'confidence': 0.0}), 200
|
| 68 |
+
|
| 69 |
+
except Exception as e:
|
| 70 |
+
return jsonify({'error': str(e)}), 500
|
| 71 |
+
|
| 72 |
+
if __name__ == '__main__':
|
| 73 |
+
port = int(os.environ.get('PORT', 5000))
|
| 74 |
+
app.run(host='0.0.0.0', port=port, debug=True)
|
| 75 |
+
|
| 76 |
+
|
backend/sign.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mediapipe as mp
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
+
import time
|
| 6 |
+
|
| 7 |
+
# Setup MediaPipe
|
| 8 |
+
mp_hands = mp.solutions.hands
|
| 9 |
+
hands = mp_hands.Hands(static_image_mode=False, max_num_hands=1, min_detection_confidence=0.5)
|
| 10 |
+
mp_drawing = mp.solutions.drawing_utils
|
| 11 |
+
|
| 12 |
+
# Create dataset directory
|
| 13 |
+
DATA_PATH = os.path.join('MP_Data')
|
| 14 |
+
actions = np.array(['hi', 'me', 'thank you']) # Remove extra space
|
| 15 |
+
no_sequences = 30
|
| 16 |
+
sequence_length = 30
|
| 17 |
+
|
| 18 |
+
# Create folders
|
| 19 |
+
for action in actions:
|
| 20 |
+
for sequence in range(no_sequences):
|
| 21 |
+
os.makedirs(os.path.join(DATA_PATH, action, str(sequence)), exist_ok=True)
|
| 22 |
+
|
| 23 |
+
# Start video capture
|
| 24 |
+
cap = cv2.VideoCapture(0)
|
| 25 |
+
for action in actions:
|
| 26 |
+
for sequence in range(no_sequences):
|
| 27 |
+
print(f"\nCollecting data for '{action}', sequence {sequence}")
|
| 28 |
+
time.sleep(2) # Delay before collecting
|
| 29 |
+
|
| 30 |
+
for frame_num in range(sequence_length):
|
| 31 |
+
ret, frame = cap.read()
|
| 32 |
+
if not ret:
|
| 33 |
+
print("Failed to grab frame.")
|
| 34 |
+
continue
|
| 35 |
+
|
| 36 |
+
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 37 |
+
results = hands.process(image)
|
| 38 |
+
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
| 39 |
+
|
| 40 |
+
if results.multi_hand_landmarks:
|
| 41 |
+
for hand_landmarks in results.multi_hand_landmarks:
|
| 42 |
+
mp_drawing.draw_landmarks(image, hand_landmarks, mp_hands.HAND_CONNECTIONS)
|
| 43 |
+
|
| 44 |
+
# Extract keypoints
|
| 45 |
+
keypoints = []
|
| 46 |
+
for lm in hand_landmarks.landmark:
|
| 47 |
+
keypoints.extend([lm.x, lm.y, lm.z])
|
| 48 |
+
|
| 49 |
+
# Save keypoints
|
| 50 |
+
npy_path = os.path.join(DATA_PATH, action, str(sequence), f"{frame_num}.npy")
|
| 51 |
+
np.save(npy_path, keypoints)
|
| 52 |
+
print(f"Saved: {npy_path}")
|
| 53 |
+
|
| 54 |
+
else:
|
| 55 |
+
print(f"No hand detected for frame {frame_num} in sequence {sequence}.")
|
| 56 |
+
|
| 57 |
+
# Add feedback to frame
|
| 58 |
+
cv2.putText(image, f'{action} - Seq {sequence}, Frame {frame_num}', (10, 30),
|
| 59 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
|
| 60 |
+
|
| 61 |
+
cv2.imshow('Hand Tracking', image)
|
| 62 |
+
if cv2.waitKey(10) & 0xFF == ord('q'):
|
| 63 |
+
cap.release()
|
| 64 |
+
cv2.destroyAllWindows()
|
| 65 |
+
exit()
|
| 66 |
+
|
| 67 |
+
cap.release()
|
| 68 |
+
cv2.destroyAllWindows()
|
| 69 |
+
|
backend/tain.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
from sklearn.model_selection import train_test_split
|
| 4 |
+
from tensorflow.keras.models import Sequential
|
| 5 |
+
from tensorflow.keras.layers import LSTM, Dense
|
| 6 |
+
from tensorflow.keras.utils import to_categorical
|
| 7 |
+
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
|
| 8 |
+
|
| 9 |
+
# ----- CONFIGURATION -----
|
| 10 |
+
DATA_PATH = os.path.join('MP_Data')
|
| 11 |
+
actions = np.array(['hi', 'me', 'thank you'])
|
| 12 |
+
no_sequences = 30
|
| 13 |
+
sequence_length = 30
|
| 14 |
+
label_map = {label: num for num, label in enumerate(actions)}
|
| 15 |
+
|
| 16 |
+
# ----- LOAD DATA -----
|
| 17 |
+
sequences, labels = [], []
|
| 18 |
+
|
| 19 |
+
for action in actions:
|
| 20 |
+
for sequence in range(no_sequences):
|
| 21 |
+
window = []
|
| 22 |
+
for frame_num in range(sequence_length):
|
| 23 |
+
try:
|
| 24 |
+
path = os.path.join(DATA_PATH, action, str(sequence), f"{frame_num}.npy")
|
| 25 |
+
res = np.load(path)
|
| 26 |
+
if res.shape == (63,):
|
| 27 |
+
window.append(res)
|
| 28 |
+
except Exception as e:
|
| 29 |
+
print(f"Error loading {path}: {e}")
|
| 30 |
+
if len(window) == sequence_length:
|
| 31 |
+
sequences.append(window)
|
| 32 |
+
labels.append(label_map[action])
|
| 33 |
+
|
| 34 |
+
X = np.array(sequences)
|
| 35 |
+
y = to_categorical(labels).astype(int)
|
| 36 |
+
|
| 37 |
+
# ----- NORMALIZE INPUT -----
|
| 38 |
+
X = X / np.max(X)
|
| 39 |
+
|
| 40 |
+
# ----- TRAIN TEST SPLIT -----
|
| 41 |
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
| 42 |
+
|
| 43 |
+
# ----- BUILD MODEL -----
|
| 44 |
+
model = Sequential()
|
| 45 |
+
model.add(LSTM(64, return_sequences=True, activation='relu', input_shape=(30, 63)))
|
| 46 |
+
model.add(LSTM(128, return_sequences=False, activation='relu'))
|
| 47 |
+
model.add(Dense(64, activation='relu'))
|
| 48 |
+
model.add(Dense(len(actions), activation='softmax'))
|
| 49 |
+
|
| 50 |
+
model.compile(optimizer='Adam', loss='categorical_crossentropy', metrics=['accuracy'])
|
| 51 |
+
|
| 52 |
+
# ----- CALLBACKS -----
|
| 53 |
+
checkpoint = ModelCheckpoint('best_model.h5', monitor='val_loss', save_best_only=True)
|
| 54 |
+
earlystop = EarlyStopping(monitor='val_loss', patience=5)
|
| 55 |
+
|
| 56 |
+
# ----- TRAIN MODEL -----
|
| 57 |
+
model.fit(X_train, y_train, epochs=50, validation_data=(X_test, y_test),
|
| 58 |
+
callbacks=[checkpoint, earlystop])
|
| 59 |
+
|
| 60 |
+
# ----- SAVE FINAL MODEL -----
|
| 61 |
+
model.save('sign_language_model.h5')
|
frontend/gesture_words/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
frontend/gesture_words/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
| 9 |
+
|
| 10 |
+
## Expanding the ESLint configuration
|
| 11 |
+
|
| 12 |
+
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
frontend/gesture_words/eslint.config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
|
| 6 |
+
export default [
|
| 7 |
+
{ ignores: ['dist'] },
|
| 8 |
+
{
|
| 9 |
+
files: ['**/*.{js,jsx}'],
|
| 10 |
+
languageOptions: {
|
| 11 |
+
ecmaVersion: 2020,
|
| 12 |
+
globals: globals.browser,
|
| 13 |
+
parserOptions: {
|
| 14 |
+
ecmaVersion: 'latest',
|
| 15 |
+
ecmaFeatures: { jsx: true },
|
| 16 |
+
sourceType: 'module',
|
| 17 |
+
},
|
| 18 |
+
},
|
| 19 |
+
plugins: {
|
| 20 |
+
'react-hooks': reactHooks,
|
| 21 |
+
'react-refresh': reactRefresh,
|
| 22 |
+
},
|
| 23 |
+
rules: {
|
| 24 |
+
...js.configs.recommended.rules,
|
| 25 |
+
...reactHooks.configs.recommended.rules,
|
| 26 |
+
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
| 27 |
+
'react-refresh/only-export-components': [
|
| 28 |
+
'warn',
|
| 29 |
+
{ allowConstantExport: true },
|
| 30 |
+
],
|
| 31 |
+
},
|
| 32 |
+
},
|
| 33 |
+
]
|
frontend/gesture_words/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Vite + React</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/gesture_words/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/gesture_words/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "gesture_words",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@tailwindcss/vite": "^4.1.5",
|
| 14 |
+
"axios": "^1.9.0",
|
| 15 |
+
"react": "^19.0.0",
|
| 16 |
+
"react-dom": "^19.0.0",
|
| 17 |
+
"react-webcam": "^7.2.0",
|
| 18 |
+
"tailwindcss": "^4.1.5"
|
| 19 |
+
},
|
| 20 |
+
"devDependencies": {
|
| 21 |
+
"@eslint/js": "^9.22.0",
|
| 22 |
+
"@types/react": "^19.0.10",
|
| 23 |
+
"@types/react-dom": "^19.0.4",
|
| 24 |
+
"@vitejs/plugin-react": "^4.3.4",
|
| 25 |
+
"eslint": "^9.22.0",
|
| 26 |
+
"eslint-plugin-react-hooks": "^5.2.0",
|
| 27 |
+
"eslint-plugin-react-refresh": "^0.4.19",
|
| 28 |
+
"globals": "^16.0.0",
|
| 29 |
+
"vite": "^6.3.4"
|
| 30 |
+
}
|
| 31 |
+
}
|
frontend/gesture_words/public/vite.svg
ADDED
|
|
frontend/gesture_words/src/App.jsx
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef, useState, useEffect } from 'react';
|
| 2 |
+
import Webcam from 'react-webcam';
|
| 3 |
+
import axios from 'axios';
|
| 4 |
+
|
| 5 |
+
const SignDetection = () => {
|
| 6 |
+
const webcamRef = useRef(null);
|
| 7 |
+
const [prediction, setPrediction] = useState('');
|
| 8 |
+
const [loading, setLoading] = useState(false);
|
| 9 |
+
const [history, setHistory] = useState([]);
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
const interval = setInterval(async () => {
|
| 13 |
+
if (webcamRef.current && webcamRef.current.getScreenshot && !loading) {
|
| 14 |
+
const imageSrc = webcamRef.current.getScreenshot();
|
| 15 |
+
if (imageSrc) {
|
| 16 |
+
setLoading(true);
|
| 17 |
+
try {
|
| 18 |
+
const apiBase = import.meta.env.DEV ? 'http://localhost:5000' : '';
|
| 19 |
+
const response = await axios.post(`${apiBase}/predict`, {
|
| 20 |
+
image: imageSrc
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
const result = response.data.prediction;
|
| 24 |
+
setPrediction(result);
|
| 25 |
+
setHistory((prev) => [result, ...prev.slice(0, 4)]);
|
| 26 |
+
|
| 27 |
+
const utterance = new SpeechSynthesisUtterance(result);
|
| 28 |
+
speechSynthesis.speak(utterance);
|
| 29 |
+
} catch (error) {
|
| 30 |
+
console.error("Prediction failed", error);
|
| 31 |
+
setPrediction("Error predicting sign.");
|
| 32 |
+
}
|
| 33 |
+
setLoading(false);
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}, 2000);
|
| 37 |
+
|
| 38 |
+
return () => clearInterval(interval);
|
| 39 |
+
}, [loading]);
|
| 40 |
+
|
| 41 |
+
return (
|
| 42 |
+
<div className="flex h-screen bg-gradient-to-br from-gray-100 to-gray-300">
|
| 43 |
+
|
| 44 |
+
{/* Sidebar */}
|
| 45 |
+
<aside className="w-72 bg-white/80 backdrop-blur-lg shadow-2xl
|
| 46 |
+
px-8 py-10 hidden sm:flex flex-col rounded-r-3xl">
|
| 47 |
+
<h2 className="text-3xl font-extrabold text-gray-800 mb-12 tracking-wide">
|
| 48 |
+
Dashboard
|
| 49 |
+
</h2>
|
| 50 |
+
|
| 51 |
+
<nav className="flex flex-col gap-5 text-gray-700 font-medium text-lg">
|
| 52 |
+
{["Home", "Sign Detection", "Training Module", "History", "Settings"].map((item) => (
|
| 53 |
+
<a
|
| 54 |
+
key={item}
|
| 55 |
+
href="#"
|
| 56 |
+
className="hover:text-blue-600 hover:bg-blue-100
|
| 57 |
+
rounded-lg px-4 py-2 transition-all"
|
| 58 |
+
>
|
| 59 |
+
{item}
|
| 60 |
+
</a>
|
| 61 |
+
))}
|
| 62 |
+
</nav>
|
| 63 |
+
</aside>
|
| 64 |
+
|
| 65 |
+
{/* Main Content */}
|
| 66 |
+
<div className="flex-1 flex flex-col">
|
| 67 |
+
|
| 68 |
+
{/* Navbar */}
|
| 69 |
+
<header className="bg-white/80 backdrop-blur-md shadow-lg
|
| 70 |
+
px-10 py-6 flex items-center justify-between border-b">
|
| 71 |
+
<h1 className="text-3xl font-bold text-gray-800 tracking-wide">
|
| 72 |
+
GESTURE WORDS
|
| 73 |
+
</h1>
|
| 74 |
+
|
| 75 |
+
<button
|
| 76 |
+
className="bg-blue-600 text-white px-6 py-3 rounded-xl font-semibold
|
| 77 |
+
shadow hover:bg-blue-700 transition-all text-lg"
|
| 78 |
+
>
|
| 79 |
+
Login
|
| 80 |
+
</button>
|
| 81 |
+
</header>
|
| 82 |
+
|
| 83 |
+
{/* Main Area */}
|
| 84 |
+
<main className="flex flex-col items-center justify-start flex-1 p-12 overflow-y-auto">
|
| 85 |
+
|
| 86 |
+
<div className="bg-white/70 backdrop-blur-xl rounded-3xl
|
| 87 |
+
shadow-2xl p-10 w-full max-w-4xl text-center
|
| 88 |
+
ring-1 ring-gray-300">
|
| 89 |
+
|
| 90 |
+
<h2 className="text-3xl font-semibold text-gray-700 mb-10 tracking-wide">
|
| 91 |
+
Live Sign Detection
|
| 92 |
+
</h2>
|
| 93 |
+
|
| 94 |
+
{/* Webcam Container */}
|
| 95 |
+
<div className="border-4 border-gray-300 rounded-2xl overflow-hidden shadow-xl
|
| 96 |
+
w-[480px] h-[360px] mx-auto mb-8">
|
| 97 |
+
<Webcam
|
| 98 |
+
audio={false}
|
| 99 |
+
ref={webcamRef}
|
| 100 |
+
screenshotFormat="image/jpeg"
|
| 101 |
+
width={480}
|
| 102 |
+
height={360}
|
| 103 |
+
className="rounded-2xl"
|
| 104 |
+
/>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
{/* Status */}
|
| 108 |
+
<p className="text-lg text-gray-600 mb-6 animate-pulse">
|
| 109 |
+
{loading ? "Analyzing..." : "Waiting for next frame..."}
|
| 110 |
+
</p>
|
| 111 |
+
|
| 112 |
+
{/* Prediction */}
|
| 113 |
+
{prediction && (
|
| 114 |
+
<div className="mt-4">
|
| 115 |
+
<h3 className="text-xl text-gray-600">Prediction:</h3>
|
| 116 |
+
<p className="text-blue-600 text-5xl font-extrabold drop-shadow-sm mt-2">
|
| 117 |
+
{prediction}
|
| 118 |
+
</p>
|
| 119 |
+
</div>
|
| 120 |
+
)}
|
| 121 |
+
|
| 122 |
+
{/* History */}
|
| 123 |
+
{history.length > 0 && (
|
| 124 |
+
<div className="mt-10 text-left">
|
| 125 |
+
<h4 className="text-lg font-semibold text-gray-600 mb-3">
|
| 126 |
+
Recent Predictions:
|
| 127 |
+
</h4>
|
| 128 |
+
|
| 129 |
+
<ul className="text-gray-700 text-lg space-y-3">
|
| 130 |
+
{history.map((item, index) => (
|
| 131 |
+
<li
|
| 132 |
+
key={index}
|
| 133 |
+
className="border py-3 px-4 rounded-xl bg-gray-50 shadow-sm"
|
| 134 |
+
>
|
| 135 |
+
{item}
|
| 136 |
+
</li>
|
| 137 |
+
))}
|
| 138 |
+
</ul>
|
| 139 |
+
</div>
|
| 140 |
+
)}
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
</main>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
);
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
export default SignDetection;
|
frontend/gesture_words/src/api.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
await axios.post('http://localhost:5000/predict', {
|
| 2 |
+
image: imageSrc // base64 string
|
| 3 |
+
});
|
| 4 |
+
|
frontend/gesture_words/src/assets/react.svg
ADDED
|
|
frontend/gesture_words/src/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
frontend/gesture_words/src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.jsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
frontend/gesture_words/vite.config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import tailwindcss from '@tailwindcss/vite'
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
// https://vite.dev/config/
|
| 7 |
+
export default defineConfig({
|
| 8 |
+
plugins: [react(), tailwindcss()],
|
| 9 |
+
})
|
sign_language_model.h5
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c52bd12dc1182f0d52535edfa998f78d73f0d375df31e81a50820590ab264e06
|
| 3 |
+
size 1724272
|