drenayaz commited on
Commit
947c1da
·
1 Parent(s): 1b5177c

add loading on the page and animation at start

Browse files
hand_tracker_v2/main.py CHANGED
@@ -7,9 +7,10 @@ from reachy_mini import ReachyMini, ReachyMiniApp
7
  from hand_tracker_v2.hand_tracker import HandTracker
8
  from scipy.spatial.transform import Rotation as R
9
  from hand_tracker_v2.utils import finger_orientation_deg, allow_multiturn
10
- from hand_tracker_v2.recorded_moves import RecordedMoves
11
  from fastapi.responses import StreamingResponse
12
  from pydantic import BaseModel
 
13
 
14
 
15
  DEBUG = True
@@ -52,17 +53,20 @@ class HandTrackerV2(ReachyMiniApp):
52
  self.preferred_side = "Left"
53
  self.antenna_mode = "Same Movement"
54
 
55
- # Recorded moves / sounds
56
- self.recorded_moves = RecordedMoves(
57
- "pollen-robotics/reachy-mini-emotions-library"
58
- )
59
  self.last_frame = None
60
 
61
  @self.settings_app.get("/video_feed")
62
  def video_feed():
63
  return StreamingResponse(self.frame_generator(),
64
  media_type="multipart/x-mixed-replace; boundary=frame")
65
-
 
 
 
 
 
66
  class UIState(BaseModel):
67
  video: bool | None = None
68
  tracking: bool | None = None
@@ -208,6 +212,8 @@ class HandTrackerV2(ReachyMiniApp):
208
 
209
 
210
  while not stop_event.is_set():
 
 
211
  self.hand_count_history.append(self.number_hands)
212
  if len(self.hand_count_history) > self.hand_count_buffer_size:
213
  self.hand_count_history.pop(0)
@@ -222,7 +228,6 @@ class HandTrackerV2(ReachyMiniApp):
222
 
223
  self.previous_number_hands = stable_hand_count
224
 
225
- t0 = time.time()
226
  time_since_last_hand = time.time() - self.last_hand_seen
227
 
228
  if time_since_last_hand > self.idle_timeout or (not self.track_mode and not self.antenna_tracking):
@@ -310,15 +315,29 @@ class HandTrackerV2(ReachyMiniApp):
310
 
311
  time.sleep(max(0, (1.0 / FREQUENCY) - (time.time() - t0)))
312
 
 
 
 
 
 
313
 
314
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
 
 
 
 
315
  reachy_mini.goto_target(np.eye(4), [0.0, 0.0], body_yaw=0.0, duration=1.0)
316
 
 
 
 
317
  tracking_thread = threading.Thread(
318
  target=self.track, args=(reachy_mini, stop_event)
319
  )
320
  tracking_thread.start()
321
 
 
 
322
  while not stop_event.is_set():
323
  t0 = time.time()
324
  im = reachy_mini.media.get_frame()
 
7
  from hand_tracker_v2.hand_tracker import HandTracker
8
  from scipy.spatial.transform import Rotation as R
9
  from hand_tracker_v2.utils import finger_orientation_deg, allow_multiturn
10
+ # from hand_tracker_v2.recorded_moves import RecordedMoves
11
  from fastapi.responses import StreamingResponse
12
  from pydantic import BaseModel
13
+ from reachy_mini.motion.recorded_move import RecordedMove, RecordedMoves
14
 
15
 
16
  DEBUG = True
 
53
  self.preferred_side = "Left"
54
  self.antenna_mode = "Same Movement"
55
 
56
+ self.app_ready = False
57
+
 
 
58
  self.last_frame = None
59
 
60
  @self.settings_app.get("/video_feed")
61
  def video_feed():
62
  return StreamingResponse(self.frame_generator(),
63
  media_type="multipart/x-mixed-replace; boundary=frame")
64
+
65
+ @self.settings_app.get("/ready")
66
+ async def ready():
67
+ print("[READY ENDPOINT] called, app_ready =", self.app_ready)
68
+ return {"ready": self.app_ready}
69
+
70
  class UIState(BaseModel):
71
  video: bool | None = None
72
  tracking: bool | None = None
 
212
 
213
 
214
  while not stop_event.is_set():
215
+ t0 = time.time()
216
+
217
  self.hand_count_history.append(self.number_hands)
218
  if len(self.hand_count_history) > self.hand_count_buffer_size:
219
  self.hand_count_history.pop(0)
 
228
 
229
  self.previous_number_hands = stable_hand_count
230
 
 
231
  time_since_last_hand = time.time() - self.last_hand_seen
232
 
233
  if time_since_last_hand > self.idle_timeout or (not self.track_mode and not self.antenna_tracking):
 
315
 
316
  time.sleep(max(0, (1.0 / FREQUENCY) - (time.time() - t0)))
317
 
318
+ def play_recorded_move(self, reachy_mini: ReachyMini, move_name: str):
319
+ move: RecordedMove = self.recorded_moves.get(move_name)
320
+ reachy_mini.play_move(move, initial_goto_duration=1.0)
321
+
322
+
323
 
324
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
325
+ self.recorded_moves = RecordedMoves(
326
+ "pollen-robotics/reachy-mini-emotions-library"
327
+ )
328
+ self.play_recorded_move(reachy_mini, "success2")
329
  reachy_mini.goto_target(np.eye(4), [0.0, 0.0], body_yaw=0.0, duration=1.0)
330
 
331
+
332
+ self.app_ready = True
333
+
334
  tracking_thread = threading.Thread(
335
  target=self.track, args=(reachy_mini, stop_event)
336
  )
337
  tracking_thread.start()
338
 
339
+
340
+
341
  while not stop_event.is_set():
342
  t0 = time.time()
343
  im = reachy_mini.media.get_frame()
hand_tracker_v2/static/index.html CHANGED
@@ -30,7 +30,7 @@
30
  Blue and green dots: used to calculate the index finger angle to determine antenna orientation.
31
  </p>
32
 
33
- <img id="video-feed" src="/video_feed" class="video-display">
34
  </section>
35
 
36
  <!-- Two side cards -->
@@ -86,8 +86,15 @@
86
 
87
  </div>
88
  </main>
89
-
 
 
 
 
 
90
  <script src="/static/main.js"></script>
91
  </body>
92
 
 
 
93
  </html>
 
30
  Blue and green dots: used to calculate the index finger angle to determine antenna orientation.
31
  </p>
32
 
33
+ <img id="video-feed" src="" class="video-display">
34
  </section>
35
 
36
  <!-- Two side cards -->
 
86
 
87
  </div>
88
  </main>
89
+ <div id="loading-overlay">
90
+ <div class="loader">
91
+ <div class="spinner"></div>
92
+ <p>Starting Hand Tracking…</p>
93
+ </div>
94
+ </div>
95
  <script src="/static/main.js"></script>
96
  </body>
97
 
98
+
99
+
100
  </html>
hand_tracker_v2/static/main.js CHANGED
@@ -5,15 +5,12 @@ const toggleAntenna = document.getElementById("toggle-antennas");
5
 
6
 
7
 
8
- // Sauvegarder l'URL du flux
9
  const videoSrc = "/video_feed";
10
 
11
  toggleVideo.addEventListener("change", () => {
12
  if (toggleVideo.checked) {
13
- // reconnecter le flux
14
  videoFeed.src = videoSrc;
15
  } else {
16
- // couper le flux
17
  videoFeed.src = "";
18
  }
19
  });
@@ -33,19 +30,35 @@ async function updateToggleState() {
33
  })
34
  });
35
  const data = await resp.json();
36
- if (data.status === "ok") {
37
- document.getElementById("status").textContent =
38
- `✔ Settings updated`;
39
- } else {
40
- document.getElementById("status").textContent =
41
- `✘ Failed to update settings`;
42
- }
43
  } catch (e) {
44
  console.error(e);
45
  document.getElementById("status").textContent = "Server error";
46
  }
47
  }
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  // Ajouter un event listener à chaque toggle
50
  [toggleVideo, toggleTracking, toggleAntenna].forEach(toggle => {
51
  toggle.addEventListener("change", updateToggleState);
 
5
 
6
 
7
 
 
8
  const videoSrc = "/video_feed";
9
 
10
  toggleVideo.addEventListener("change", () => {
11
  if (toggleVideo.checked) {
 
12
  videoFeed.src = videoSrc;
13
  } else {
 
14
  videoFeed.src = "";
15
  }
16
  });
 
30
  })
31
  });
32
  const data = await resp.json();
33
+
 
 
 
 
 
 
34
  } catch (e) {
35
  console.error(e);
36
  document.getElementById("status").textContent = "Server error";
37
  }
38
  }
39
 
40
+ async function waitForAppReady() {
41
+ while (true) {
42
+ try {
43
+ const resp = await fetch("/ready");
44
+ console.log("ready status:", resp.status);
45
+ const data = await resp.json();
46
+ console.log("ready payload:", data);
47
+ if (data.ready) {
48
+ document.getElementById("loading-overlay").style.display = "none";
49
+ return;
50
+ }
51
+ } catch (e) {
52
+ console.error("ready fetch error", e);
53
+ }
54
+ await new Promise(r => setTimeout(r, 500));
55
+ }
56
+ }
57
+
58
+
59
+ waitForAppReady();
60
+
61
+
62
  // Ajouter un event listener à chaque toggle
63
  [toggleVideo, toggleTracking, toggleAntenna].forEach(toggle => {
64
  toggle.addEventListener("change", updateToggleState);
hand_tracker_v2/static/style.css CHANGED
@@ -1,39 +1,49 @@
1
- /* General Layout */
2
  body {
3
  margin: 0;
4
  padding: 0;
5
- background: #1f2937;
6
  font-family: "Inter", sans-serif;
7
  color: #222;
 
 
 
 
 
8
  }
9
 
10
- /* Header */
11
  .header {
12
  display: flex;
13
  justify-content: center;
14
  align-items: center;
15
  gap: 16px;
16
- padding: 17px 0; /* reduced height */
17
  color: white;
18
  }
19
 
 
 
 
 
 
 
20
  .header .logo {
21
- height: 50px; /* slightly smaller */
22
  }
23
 
24
- /* Main container */
25
  .container {
26
  max-width: 1100px;
27
  margin: auto;
28
  padding: 20px;
29
  }
30
 
31
- /* Cards */
32
  .card {
33
  background: white;
34
  padding: 22px;
35
- border-radius: 14px;
36
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
 
 
37
  margin-bottom: 24px;
38
  }
39
 
@@ -49,23 +59,22 @@ body {
49
  .card h3 {
50
  margin: 0;
51
  font-size: 18px;
 
52
  }
53
 
54
- /* Video Feed */
55
  .video-display {
56
  width: 100%;
57
- border-radius: 12px;
58
  margin-top: 12px;
 
59
  }
60
 
61
- /* Card header with toggle */
62
  .card-header {
63
  display: flex;
64
  justify-content: space-between;
65
  align-items: center;
66
  }
67
 
68
- /* Switch toggle */
69
  .toggle {
70
  position: relative;
71
  width: 46px;
@@ -81,10 +90,10 @@ body {
81
  .slider {
82
  position: absolute;
83
  cursor: pointer;
84
- background: #ccc;
85
  border-radius: 24px;
86
  inset: 0;
87
- transition: 0.3s;
88
  }
89
 
90
  .slider::before {
@@ -96,7 +105,7 @@ body {
96
  bottom: 3px;
97
  background: white;
98
  border-radius: 50%;
99
- transition: 0.3s;
100
  }
101
 
102
  input:checked + .slider {
@@ -107,25 +116,70 @@ input:checked + .slider::before {
107
  transform: translateX(22px);
108
  }
109
 
110
- /* Options */
111
  .option {
112
  margin-top: 12px;
113
  display: flex;
114
  justify-content: space-between;
 
115
  background: #f3f4f6;
116
  padding: 10px 14px;
117
- border-radius: 10px;
118
  }
119
 
120
  select {
121
  border: none;
122
  background: transparent;
123
  font-size: 14px;
 
 
124
  }
125
 
126
- /* Description text */
127
  .desc {
128
  margin: 10px 0 14px;
129
  font-size: 14px;
130
- color: #444;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  }
 
1
+
2
  body {
3
  margin: 0;
4
  padding: 0;
5
+ min-height: 100vh;
6
  font-family: "Inter", sans-serif;
7
  color: #222;
8
+
9
+ /* Reachy-style dark gradient */
10
+ background:
11
+ radial-gradient(1200px 600px at 50% -200px, #374151 0%, transparent 60%),
12
+ linear-gradient(180deg, #0f172a 0%, #1f2937 100%);
13
  }
14
 
 
15
  .header {
16
  display: flex;
17
  justify-content: center;
18
  align-items: center;
19
  gap: 16px;
20
+ padding: 14px 0;
21
  color: white;
22
  }
23
 
24
+ .header h1 {
25
+ font-size: 30px;
26
+ font-weight: 700;
27
+ letter-spacing: 0.4px;
28
+ }
29
+
30
  .header .logo {
31
+ height: 48px;
32
  }
33
 
 
34
  .container {
35
  max-width: 1100px;
36
  margin: auto;
37
  padding: 20px;
38
  }
39
 
 
40
  .card {
41
  background: white;
42
  padding: 22px;
43
+ border-radius: 16px;
44
+ box-shadow:
45
+ 0 10px 25px rgba(0, 0, 0, 0.15),
46
+ 0 2px 6px rgba(0, 0, 0, 0.08);
47
  margin-bottom: 24px;
48
  }
49
 
 
59
  .card h3 {
60
  margin: 0;
61
  font-size: 18px;
62
+ font-weight: 600;
63
  }
64
 
 
65
  .video-display {
66
  width: 100%;
67
+ border-radius: 14px;
68
  margin-top: 12px;
69
+ background: #000;
70
  }
71
 
 
72
  .card-header {
73
  display: flex;
74
  justify-content: space-between;
75
  align-items: center;
76
  }
77
 
 
78
  .toggle {
79
  position: relative;
80
  width: 46px;
 
90
  .slider {
91
  position: absolute;
92
  cursor: pointer;
93
+ background: #d1d5db;
94
  border-radius: 24px;
95
  inset: 0;
96
+ transition: background 0.25s ease;
97
  }
98
 
99
  .slider::before {
 
105
  bottom: 3px;
106
  background: white;
107
  border-radius: 50%;
108
+ transition: transform 0.25s ease;
109
  }
110
 
111
  input:checked + .slider {
 
116
  transform: translateX(22px);
117
  }
118
 
 
119
  .option {
120
  margin-top: 12px;
121
  display: flex;
122
  justify-content: space-between;
123
+ align-items: center;
124
  background: #f3f4f6;
125
  padding: 10px 14px;
126
+ border-radius: 12px;
127
  }
128
 
129
  select {
130
  border: none;
131
  background: transparent;
132
  font-size: 14px;
133
+ outline: none;
134
+ cursor: pointer;
135
  }
136
 
 
137
  .desc {
138
  margin: 10px 0 14px;
139
  font-size: 14px;
140
+ color: #4b5563;
141
+ line-height: 1.45;
142
+ }
143
+
144
+ #loading-overlay {
145
+ position: fixed;
146
+ inset: 0;
147
+
148
+ /* semi-transparent to see UI behind */
149
+ background: rgba(15, 23, 42, 0.75);
150
+ backdrop-filter: blur(6px);
151
+
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ z-index: 9999;
156
+ }
157
+
158
+ /* Loader content */
159
+ .loader {
160
+ text-align: center;
161
+ color: white;
162
+ }
163
+
164
+ .loader p {
165
+ margin-top: 16px;
166
+ font-size: 16px;
167
+ opacity: 0.9;
168
+ letter-spacing: 0.3px;
169
+ }
170
+
171
+ .spinner {
172
+ width: 48px;
173
+ height: 48px;
174
+ border: 4px solid rgba(255, 255, 255, 0.2);
175
+ border-top: 4px solid #3b82f6;
176
+ border-radius: 50%;
177
+ animation: spin 1s linear infinite;
178
+ margin: auto;
179
+ }
180
+
181
+ @keyframes spin {
182
+ to {
183
+ transform: rotate(360deg);
184
+ }
185
  }