Skip to content

Commit 3b8cf76

Browse files
authored
Merge pull request #335 from COS301-SE-2025/bugfixes/translator
2 parents e8c65e7 + 10ad353 commit 3b8cf76

File tree

5 files changed

+186
-108
lines changed

5 files changed

+186
-108
lines changed

backend/api/controllers/lettersControllerS.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,19 @@ def processSequence(frames):
151151
return {'letter': '', 'confidenceLetter': 0.0, 'number': '', 'confidenceNumber': 0.0}
152152

153153
elif numFrames >= 10 and isDynamic:
154+
label1, confidence1, _, _ = processSingleFrame(sequenceBytesList[0])
154155
label2, confidence2 = processSequence(sequenceBytesList[:10])
155156
if label2 is None:
156157
return {'letter': '', 'confidenceLetter': 0.0, 'number': '', 'confidenceNumber': 0.0}
157158
_, _, label3, confidence3 = processSingleFrame(sequenceBytesList[-1])
158159
if confidence2 >= 0.6:
159-
return {'letter': label2, 'confidenceLetter': confidence2,
160+
if label1 == 'I':
161+
if label2 == 'J':
162+
return {'letter': label2, 'confidenceLetter': confidence2,
163+
'number': label3, 'confidenceNumber': confidence3}
164+
else:
165+
return {'letter': label1, 'confidenceLetter': confidence1,
166+
'number': label3, 'confidenceNumber': confidence3}
167+
return {'letter': label1, 'confidenceLetter': confidence1,
160168
'number': label3, 'confidenceNumber': confidence3}
161-
return {'letter': '', 'confidenceLetter': 0.0, 'number': label3, 'confidenceNumber': confidence3}
162-
163-
else:
164-
return {'letter': '', 'confidenceLetter': 0.0, 'number': '', 'confidenceNumber': 0.0}
169+
return {'letter': '', 'confidenceLetter': 0.0, 'number': label3, 'confidenceNumber': confidence3}
Lines changed: 152 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,164 @@
1-
import React, { useEffect, useState, useCallback } from 'react';
2-
import { useTranslator } from '../../hooks/translateResults';
3-
import { processLetters } from '../../utils/apiCalls';
1+
import React, { useEffect, useRef, useState, useCallback } from 'react';
42
import PropTypes from 'prop-types';
3+
import { useTranslationSocket } from '../../hooks/translationSocket';
54

65
export function CameraInput({ progress = 0, show = true, onSkip, onLetterDetected }) {
7-
const { videoRef, canvasRef2 } = useTranslator();
8-
const [frameBlobs, setFrameBlobs] = useState([]);
9-
const [processing, setProcessing] = useState(false);
6+
const videoRef = useRef(null);
7+
const canvasRef = useRef(null);
8+
const lastDetectedLetterRef = useRef(null);
9+
10+
const {
11+
wsRef,
12+
startRecording,
13+
stopRecording,
14+
sendFrame,
15+
wsStatus,
16+
isProcessing,
17+
setResult
18+
} = useTranslationSocket('right');
19+
20+
const [detectedLetter, setDetectedLetter] = useState(null);
1021

1122
const captureFrame = useCallback(() => {
1223
const video = videoRef.current;
13-
const canvas = canvasRef2.current;
14-
if (!video || !canvas) return;
24+
const canvas = canvasRef.current;
25+
if (!video || !canvas || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN)
26+
return;
1527

1628
const ctx = canvas.getContext('2d');
1729
canvas.width = video.videoWidth;
1830
canvas.height = video.videoHeight;
31+
1932
ctx.save();
2033
ctx.translate(canvas.width, 0);
2134
ctx.scale(-1, 1);
2235
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2336
ctx.restore();
2437

25-
canvas.toBlob(blob => {
26-
if (blob) setFrameBlobs(prev => [...prev, blob]);
27-
}, 'image/jpeg', 0.8);
28-
}, [videoRef, canvasRef2]);
38+
sendFrame(video);
39+
}, [sendFrame, wsRef]);
2940

3041
useEffect(() => {
31-
if (!show || processing) return;
42+
if (!show) return;
43+
const enableCamera = async () => {
44+
try {
45+
const stream = await navigator.mediaDevices.getUserMedia({
46+
video: { width: { ideal: 1280 }, height: { ideal: 720 } },
47+
});
48+
if (videoRef.current) videoRef.current.srcObject = stream;
49+
} catch (err) {
50+
console.error('Camera access denied:', err);
51+
}
52+
};
53+
enableCamera();
3254

33-
const interval = setInterval(() => {
34-
captureFrame();
35-
}, 100); // 10 fps
55+
const currentVideo = videoRef.current
3656

37-
return () => clearInterval(interval);
38-
}, [show, processing, captureFrame]);
57+
return () => {
58+
if (currentVideo?.srcObject) {
59+
currentVideo.srcObject.getTracks().forEach(t => t.stop());
60+
}
61+
};
62+
}, [show]);
3963

4064
useEffect(() => {
41-
const sendFrames = async () => {
42-
const requiredFrames = 20;
43-
if (frameBlobs.length < requiredFrames || processing) return;
65+
if (!show) {
66+
stopRecording();
67+
return;
68+
}
4469

45-
setProcessing(true);
46-
try {
47-
const formData = new FormData();
48-
frameBlobs.forEach((blob, idx) => {
49-
formData.append('frames', blob, `frame_${idx}.jpg`);
50-
});
70+
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
71+
startRecording('alpha', 10);
72+
}
5173

52-
const result = await processLetters(formData);
53-
console.log("API response:", result);
74+
const interval = setInterval(() => {
75+
captureFrame();
76+
}, 120);
77+
78+
return () => {
79+
clearInterval(interval);
80+
stopRecording();
81+
};
82+
}, [show, startRecording, stopRecording, captureFrame, wsRef]);
5483

55-
if (result?.letter) {
56-
onLetterDetected?.(result.letter.toUpperCase());
84+
useEffect(() => {
85+
const ws = wsRef.current;
86+
if (!ws) return;
87+
88+
ws.onmessage = async (event) => {
89+
const data = JSON.parse(event.data);
90+
const letter = data?.letter?.toUpperCase?.() ?? '';
91+
92+
if (/^[A-Z]$/.test(letter)) {
93+
if (letter !== detectedLetter && letter !== lastDetectedLetterRef.current) {
94+
// console.log('New letter detected:', letter);
95+
96+
setDetectedLetter(letter);
97+
lastDetectedLetterRef.current = letter;
98+
onLetterDetected?.(letter);
99+
stopRecording();
100+
101+
setTimeout(() => {
102+
setResult(null);
103+
setDetectedLetter(null);
104+
startRecording('alpha', 10);
105+
}, 1500);
106+
} else {
107+
// console.log(`Ignored duplicate letter: ${letter}`);
57108
}
58-
}
59-
catch (err) {
60-
console.error("Error sending frames:", err);
61-
}
62-
finally {
63-
setFrameBlobs([]);
64-
setProcessing(false);
65109
}
66110
};
67111

68-
sendFrames();
69-
}, [frameBlobs, processing, onLetterDetected]);
112+
ws.onclose = () => {
113+
// console.log('WebSocket closed from CameraInput');
114+
};
70115

116+
return () => {
117+
ws.onmessage = null;
118+
ws.onclose = null;
119+
};
120+
}, [
121+
wsRef,
122+
onLetterDetected,
123+
detectedLetter,
124+
stopRecording,
125+
startRecording,
126+
setResult,
127+
]);
71128
if (!show) return null;
72129

73130
return (
74-
<div style={{
75-
position: 'absolute',
76-
top: '30%',
77-
left: '50%',
78-
transform: 'translateX(-50%)',
79-
display: 'flex',
80-
flexDirection: 'column',
81-
alignItems: 'center',
82-
zIndex: 50,
83-
gap: '1rem'
84-
}}>
85-
<div style={{
86-
width: '25vw',
87-
maxWidth: '300px',
88-
aspectRatio: '1 / 1',
89-
borderRadius: '50%',
90-
overflow: 'hidden',
91-
position: 'relative',
92-
background: 'white',
93-
}}>
94-
<video
95-
ref={videoRef}
96-
autoPlay
97-
playsInline
98-
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
99-
/>
100-
<canvas
101-
ref={canvasRef2}
102-
hidden
131+
<div
132+
style={{
133+
position: 'absolute',
134+
top: '30%',
135+
left: '50%',
136+
transform: 'translateX(-50%)',
137+
display: 'flex',
138+
flexDirection: 'column',
139+
alignItems: 'center',
140+
zIndex: 50,
141+
gap: '1rem',
142+
}}
143+
>
144+
<div
145+
style={{
146+
width: '25vw',
147+
maxWidth: '300px',
148+
aspectRatio: '1 / 1',
149+
borderRadius: '50%',
150+
overflow: 'hidden',
151+
position: 'relative',
152+
background: 'white',
153+
}}
154+
>
155+
<video
156+
ref={videoRef}
157+
autoPlay
158+
playsInline
159+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
103160
/>
161+
<canvas ref={canvasRef} hidden />
104162
<svg
105163
viewBox="0 0 100 100"
106164
style={{
@@ -109,7 +167,7 @@ export function CameraInput({ progress = 0, show = true, onSkip, onLetterDetecte
109167
left: 0,
110168
width: '100%',
111169
height: '100%',
112-
zIndex: 1
170+
zIndex: 1,
113171
}}
114172
>
115173
<circle
@@ -124,14 +182,17 @@ export function CameraInput({ progress = 0, show = true, onSkip, onLetterDetecte
124182
style={{
125183
transition: 'stroke-dashoffset 0.1s linear',
126184
transform: 'rotate(-90deg)',
127-
transformOrigin: '50% 50%'
185+
transformOrigin: '50% 50%',
128186
}}
129187
/>
130188
</svg>
131189
</div>
132190

133-
<button
134-
onClick={onSkip}
191+
<button
192+
onClick={() => {
193+
stopRecording();
194+
onSkip?.();
195+
}}
135196
style={{
136197
padding: '0.5rem 1rem',
137198
background: 'red',
@@ -140,23 +201,31 @@ export function CameraInput({ progress = 0, show = true, onSkip, onLetterDetecte
140201
borderRadius: '8px',
141202
fontWeight: 'bold',
142203
cursor: 'pointer',
143-
zIndex: 51
204+
zIndex: 51,
144205
}}
145206
>
146207
Skip
147208
</button>
148209

149-
{processing && (
150-
<div style={{
151-
color: 'red',
152-
fontSize: '1.2rem',
153-
fontWeight: 'bold',
154-
textAlign: 'center',
155-
minHeight: '1.5rem'
156-
}}>
210+
{isProcessing && (
211+
<div
212+
style={{
213+
color: 'red',
214+
fontSize: '1rem',
215+
fontWeight: 'bold',
216+
textAlign: 'center',
217+
minHeight: '1.5rem',
218+
}}
219+
>
157220
PROCESSING...
158221
</div>
159222
)}
223+
224+
{wsStatus === 'collecting' && !isProcessing && (
225+
<div style={{ color: 'green', fontWeight: 'bold', fontSize: '1rem' }}>
226+
COLLECTING FRAMES...
227+
</div>
228+
)}
160229
</div>
161230
);
162231
}
@@ -166,4 +235,4 @@ CameraInput.propTypes = {
166235
show: PropTypes.bool.isRequired,
167236
onSkip: PropTypes.func.isRequired,
168237
onLetterDetected: PropTypes.func.isRequired,
169-
};
238+
};

frontend/src/components/game/gameGuide.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export default function GameGuide() {
9797
setShowCamera(true);
9898
setProgress(0);
9999

100-
const duration = 20000;
100+
const duration = 30000;
101101
let elapsed = 0;
102102
intervalTimer = setInterval(() => {
103103
elapsed += 100;
@@ -208,10 +208,10 @@ export default function GameGuide() {
208208
setReceivedLetter(true);
209209

210210
if (letter === targetLetter) {
211-
setTimeout(() => {
211+
setLetterIndex(idx => {let nextIndex = idx + 1; return nextIndex;});
212212
setShowCamera(false);
213213
setStep(6);
214-
}, 1000);
214+
setTimeout(() => navigate('/game'), 2500);
215215
} else {
216216
setLifeLost(true);
217217
setTimeout(() => setLifeLost(false), 2500);
@@ -224,10 +224,10 @@ export default function GameGuide() {
224224
{receivedLetter && (
225225
<div style={{
226226
position: 'absolute',
227-
top: '25vh',
227+
top: '35vh',
228228
width: '100%',
229229
textAlign: 'center',
230-
fontSize: '2vw',
230+
fontSize: '1vw',
231231
color: 'red',
232232
zIndex: 100
233233
}}>

0 commit comments

Comments
 (0)