Frontend & User Interface
Single-page application for ASL sign recognition and translation — branded as SignStream.
The Interface
The single-page app shows everything in one view: live camera feed (center), service health dots and user session (top right), recognized signs panel with per-sign feedback chips (right), the final English translation, sliced clips from the current session, and the full 100-sign model vocabulary (bottom).
Architecture
Framework: Vanilla HTML/JS (no build step) MediaPipe: Holistic pose detection via JS API HTTP Client: Fetch API with JWT token handling Styling: Plain CSS (responsive design) Server: nginx serving static files + reverse proxy to services
User Flow
1. Page Load
- User opens
http://localhost:8080/ - Browser loads
index.html; JS modules load:mediapipe.js,api.js,keypointPreprocessing.js,ui.js - Three health checks fire in parallel:
GET /health→ authServiceGET /health→ inferenceServiceGET /health→ agentService
- Status indicators rendered:
● auth · ● infer · ● agent
2. Start Signing
- User clicks Start signing → browser captures MediaPipe Holistic frames at ~30 fps
- For each frame:
- Run MediaPipe Holistic → full pose + hands + face
- Run
keypointPreprocessing.js:- Subset to 27 OpenHands keypoints
- Drop z (depth) coordinates
- Normalize by shoulder distance
- Collect in array
// frontend/js/mediapipe.js (abridged)
const vision = await mediapipe.FilesetResolver.forVisionTasks(
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm'
);
const poseDetector = await mediapipe.PoseLandmarker.createFromOptions(vision, {
baseOptions: { modelAssetPath: '...' },
runningMode: 'VIDEO',
numPoses: 1
});
function processFrame(videoElement) {
const result = poseDetector.detectForVideo(videoElement, performance.now());
return result.landmarks[0]; // 33 landmarks (full pose)
}
3. Keypoint Preprocessing
Full 33 MediaPipe landmarks → 27 OpenHands subset → drop z → shoulder-normalize → collect.
// frontend/js/keypointPreprocessing.js (abridged)
function preprocessLandmarks(fullLandmarks) {
const subset = OPENHANDS_INDICES.map(i => fullLandmarks[i]);
const leftShoulder = subset[0];
const rightShoulder = subset[1];
const shoulderDist = distance(leftShoulder, rightShoulder);
const center = [
(leftShoulder.x + rightShoulder.x) / 2,
(leftShoulder.y + rightShoulder.y) / 2
];
return subset.map(kp => [
(kp.x - center[0]) / shoulderDist,
(kp.y - center[1]) / shoulderDist
]);
}
4. Send for Inference
On clip boundary (motion pause), POST to /predict:
async function predict(landmarks, modelName = 'stgcn') {
const token = localStorage.getItem('accessToken');
const response = await fetch('/api/predict', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ landmarks, modelName })
});
if (response.status === 401) {
await refreshToken();
return predict(landmarks, modelName);
}
return response.json();
}
Response: { sign, confidence, latencyMs, eventId, modelVersion, alternatives: [...] }
5. Display Recognized Signs
Each recognized sign appears in the Recognized Signs panel (right) with:
- Sign label + corrected/raw badge
- Confidence + model name
- Thumbs-up / thumbs-down chips
- Swap-alternative dropdown
In the screenshot above:
THEY1(corrected from a lower-confidence alternative)GO(98.5% confidence · ST-GCN)HOSPITAL1(100.0% confidence · ST-GCN)PARTY1(corrected from 4% alternative)
6. Translate
User clicks Translate → batched signs POST to /translate:
{
"signs": [
{"label": "THEY1", "confidence": 0.17},
{"label": "GO", "confidence": 0.985},
{"label": "HOSPITAL1", "confidence": 1.0},
{"label": "PARTY1", "confidence": 0.04}
]
}
agentService runs the Gloss Assembler then the Translator (both gpt-4o-mini).
7. Final English
The English Translation panel renders the agent output:
They are going to the hospital for a party.
Plus a collapsible ▶ agent trace showing the corrections the assembler made and the grammar transformations the translator applied.
8. Feedback Loop
Every sign card has thumbs-up / thumbs-down and a swap dropdown. User actions write to asl.userFeedback:
{ "sign": "GO", "rating": 1, "chosenAlternative": null }
UI Components
| Component | Purpose |
|---|---|
| Header (SignStream logo) | Brand · service health dots (auth · infer · agent) · user id · sign out |
| Live Feed canvas | MediaPipe-overlaid camera frame · current frame count |
| Stop / Start / Model / Clear controls | Camera + capture controls; model selector toggles ST-GCN vs RF |
| Sliced Clips strip | Each captured clip shown as a chip with sign label, frame count, confidence |
| Recognized Signs panel | Per-sign cards with feedback chips and alternative-swap dropdown |
| English Translation panel | Final translator output + collapsible agent trace |
| Model Vocabulary | All 100 active signs (bottom) — useful during demos to know what the system can recognize |
Session Management
Login flow:
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
}
async function refreshToken() {
const refreshToken = localStorage.getItem('refreshToken');
const response = await fetch('/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
const data = await response.json();
localStorage.setItem('accessToken', data.accessToken);
}
Performance Considerations
| Factor | Impact | Mitigation |
|---|---|---|
| MediaPipe model size | 7 MB (first load) | Lazy load on “Start signing” click |
| Landmark JSON payload | ~13 KB per sign | Acceptable; gzip compresses to ~3 KB |
| Inference latency | p95 165 ms | User sees near-real-time feedback |
| LLM latency | 1–2 seconds (per translate batch) | Acceptable; only fires on user-initiated translate |
| Browser memory | ~200 MB (MediaPipe + collected frames) | Clear button drops accumulated state |