itsMarco-G commited on
Commit
fe7b296
Β·
1 Parent(s): 18c7353

Implemented simple reachy quips

Browse files
reachy_phone_home/assets/{beep1.mp3 β†’ ambient_beep1.mp3} RENAMED
File without changes
reachy_phone_home/assets/{beep2.mp3 β†’ ambient_beep2.mp3} RENAMED
File without changes
reachy_phone_home/assets/{reach_curious.mp3 β†’ curious_reach.mp3} RENAMED
File without changes
reachy_phone_home/assets/{reachy_reachy.mp3 β†’ happy_reachy_reachy.mp3} RENAMED
File without changes
reachy_phone_home/main.py CHANGED
@@ -20,11 +20,13 @@ from .web_ui import WEB_UI
20
 
21
  try:
22
  from .movements import MovementScheduler, SituationMovements
 
23
  except ImportError: # allow running as a script
24
  import sys
25
 
26
  sys.path.append(str(Path(__file__).resolve().parents[1]))
27
  from reachy_phone_home.movements import MovementScheduler, SituationMovements
 
28
 
29
 
30
  PHONE_CLASS_ID = 67 # COCO "cell phone"
@@ -177,6 +179,9 @@ class TrackerConfig:
177
  antenna_happy_amp: float = 0.2
178
  antenna_happy_duration: float = 0.5
179
  good_job_heartbeats: int = 3
 
 
 
180
  phone_use_bad_sec: float = 10.0
181
  movement_restore_sec: float = 0.6
182
  no_display: bool = True
@@ -211,8 +216,10 @@ def run_tracker(
211
  head_duration=config.head_duration,
212
  )
213
  movements = SituationMovements(reachy)
 
214
  scheduler = MovementScheduler(
215
  movements,
 
216
  good_job_heartbeats=config.good_job_heartbeats,
217
  phone_use_bad_sec=config.phone_use_bad_sec,
218
  phone_use_clear_sec=config.phone_use_clear_sec,
@@ -244,8 +251,14 @@ def run_tracker(
244
  last_oscillate_update = 0.0
245
  look_down_cycles = 0
246
  last_prompt = 0.0
 
 
247
  mode = "tracking_phone"
248
  mode_start = time.time()
 
 
 
 
249
 
250
  while True:
251
  if stop_event is not None and getattr(stop_event, "is_set", lambda: False)():
@@ -438,6 +451,14 @@ def run_tracker(
438
  phone_home_start = None
439
  phone_home_stop = None
440
 
 
 
 
 
 
 
 
 
441
  if time.time() - last_heartbeat >= 10.0:
442
  msg = f"[heartbeat] phone_detected={'yes' if phone_seen else 'no'}"
443
  logger.info(msg)
@@ -450,13 +471,34 @@ def run_tracker(
450
  ):
451
  oscillate_start = time.time()
452
  last_heartbeat = time.time()
 
453
  scheduler.good_job_heartbeats = WEB_UI.get_good_job_heartbeats(
454
  config.good_job_heartbeats
455
  )
 
 
456
  scheduler.on_heartbeat(
457
  phone_tracked=(last_track_state == "tracking_phone"),
458
  phone_use=last_phone_use_state,
459
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
  if not config.no_antenna:
462
  if last_phone_use_state:
@@ -631,6 +673,8 @@ def main() -> None:
631
  parser.add_argument("--weights", type=str, default="yolo26l")
632
  parser.add_argument("--conf", type=float, default=0.15)
633
  parser.add_argument("--good-job-heartbeats", type=int, default=3)
 
 
634
  parser.add_argument("--display", action="store_true", help="Show the OpenCV window")
635
  parser.add_argument("--no-web-ui", action="store_true", help="Disable the web UI server")
636
  parser.set_defaults(no_display=True)
@@ -647,6 +691,8 @@ def main() -> None:
647
  weights=args.weights,
648
  conf=args.conf,
649
  good_job_heartbeats=args.good_job_heartbeats,
 
 
650
  no_display=no_display,
651
  )
652
 
 
20
 
21
  try:
22
  from .movements import MovementScheduler, SituationMovements
23
+ from .voice_quips import VoiceQuips
24
  except ImportError: # allow running as a script
25
  import sys
26
 
27
  sys.path.append(str(Path(__file__).resolve().parents[1]))
28
  from reachy_phone_home.movements import MovementScheduler, SituationMovements
29
+ from reachy_phone_home.voice_quips import VoiceQuips
30
 
31
 
32
  PHONE_CLASS_ID = 67 # COCO "cell phone"
 
179
  antenna_happy_amp: float = 0.2
180
  antenna_happy_duration: float = 0.5
181
  good_job_heartbeats: int = 3
182
+ ambient_interval_sec: float = 30.0
183
+ curious_interval_sec: float = 5.0
184
+ quips_enabled: bool = True
185
  phone_use_bad_sec: float = 10.0
186
  movement_restore_sec: float = 0.6
187
  no_display: bool = True
 
216
  head_duration=config.head_duration,
217
  )
218
  movements = SituationMovements(reachy)
219
+ quips = VoiceQuips(reachy)
220
  scheduler = MovementScheduler(
221
  movements,
222
+ quips=quips if config.quips_enabled else None,
223
  good_job_heartbeats=config.good_job_heartbeats,
224
  phone_use_bad_sec=config.phone_use_bad_sec,
225
  phone_use_clear_sec=config.phone_use_clear_sec,
 
251
  last_oscillate_update = 0.0
252
  look_down_cycles = 0
253
  last_prompt = 0.0
254
+ last_curious = 0.0
255
+ heartbeat_count = 0
256
  mode = "tracking_phone"
257
  mode_start = time.time()
258
+ quips_enabled = config.quips_enabled
259
+ last_quips_enabled = quips_enabled
260
+ ambient_interval_sec = config.ambient_interval_sec
261
+ curious_interval_sec = config.curious_interval_sec
262
 
263
  while True:
264
  if stop_event is not None and getattr(stop_event, "is_set", lambda: False)():
 
451
  phone_home_start = None
452
  phone_home_stop = None
453
 
454
+ quips_enabled = WEB_UI.get_quips_enabled(config.quips_enabled)
455
+ if quips_enabled != last_quips_enabled:
456
+ state = "enabled" if quips_enabled else "disabled"
457
+ logger.info("[settings] Reachy Audio %s", state)
458
+ WEB_UI.append_log(f"[settings] Reachy Audio {state}")
459
+ last_quips_enabled = quips_enabled
460
+ scheduler.quips = quips if quips_enabled else None
461
+
462
  if time.time() - last_heartbeat >= 10.0:
463
  msg = f"[heartbeat] phone_detected={'yes' if phone_seen else 'no'}"
464
  logger.info(msg)
 
471
  ):
472
  oscillate_start = time.time()
473
  last_heartbeat = time.time()
474
+ heartbeat_count += 1
475
  scheduler.good_job_heartbeats = WEB_UI.get_good_job_heartbeats(
476
  config.good_job_heartbeats
477
  )
478
+ ambient_interval_sec = WEB_UI.get_ambient_interval_sec(config.ambient_interval_sec)
479
+ curious_interval_sec = WEB_UI.get_curious_interval_sec(config.curious_interval_sec)
480
  scheduler.on_heartbeat(
481
  phone_tracked=(last_track_state == "tracking_phone"),
482
  phone_use=last_phone_use_state,
483
  )
484
+ if (
485
+ quips_enabled
486
+ and quips is not None
487
+ and phone_seen
488
+ and ambient_interval_sec >= 10.0
489
+ and heartbeat_count % max(1, int(round(ambient_interval_sec / 10.0))) == 0
490
+ ):
491
+ quips.ambient()
492
+
493
+ if phone_seen:
494
+ last_curious = time.time()
495
+ elif (
496
+ quips_enabled
497
+ and quips is not None
498
+ and time.time() - last_curious >= curious_interval_sec
499
+ ):
500
+ quips.curious()
501
+ last_curious = time.time()
502
 
503
  if not config.no_antenna:
504
  if last_phone_use_state:
 
673
  parser.add_argument("--weights", type=str, default="yolo26l")
674
  parser.add_argument("--conf", type=float, default=0.15)
675
  parser.add_argument("--good-job-heartbeats", type=int, default=3)
676
+ parser.add_argument("--ambient-interval-sec", type=float, default=30.0)
677
+ parser.add_argument("--curious-interval-sec", type=float, default=5.0)
678
  parser.add_argument("--display", action="store_true", help="Show the OpenCV window")
679
  parser.add_argument("--no-web-ui", action="store_true", help="Disable the web UI server")
680
  parser.set_defaults(no_display=True)
 
691
  weights=args.weights,
692
  conf=args.conf,
693
  good_job_heartbeats=args.good_job_heartbeats,
694
+ ambient_interval_sec=args.ambient_interval_sec,
695
+ curious_interval_sec=args.curious_interval_sec,
696
  no_display=no_display,
697
  )
698
 
reachy_phone_home/movements.py CHANGED
@@ -7,6 +7,8 @@ import random
7
  import time
8
  from typing import Callable, Dict, List, Optional
9
 
 
 
10
  from reachy_mini import ReachyMini
11
  from reachy_mini.motion.recorded_move import RecordedMove, RecordedMoves
12
 
@@ -142,12 +144,14 @@ class MovementScheduler:
142
  def __init__(
143
  self,
144
  movements: SituationMovements,
 
145
  good_job_heartbeats: int = 3,
146
  phone_use_bad_sec: float = 10.0,
147
  phone_use_clear_sec: float = 3.0,
148
  restore_head_duration: float = 0.6,
149
  ) -> None:
150
  self.movements = movements
 
151
  self.good_job_heartbeats = int(good_job_heartbeats)
152
  self.phone_use_bad_sec = float(phone_use_bad_sec)
153
  self.phone_use_clear_sec = float(phone_use_clear_sec)
@@ -176,6 +180,8 @@ class MovementScheduler:
176
  if phone_tracked and not phone_use:
177
  self._good_job_streak += 1
178
  if self._good_job_streak >= self.good_job_heartbeats:
 
 
179
  self._pick_and_run(self._good_job_moves)
180
  self._good_job_streak = 0
181
  else:
@@ -186,6 +192,8 @@ class MovementScheduler:
186
  if self._phone_use_start is None:
187
  self._phone_use_start = now
188
  elif now - self._phone_use_start >= self.phone_use_bad_sec:
 
 
189
  self._pick_and_run(self._bad_moves)
190
  self._phone_use_start = now
191
  else:
 
7
  import time
8
  from typing import Callable, Dict, List, Optional
9
 
10
+ from .voice_quips import VoiceQuips
11
+
12
  from reachy_mini import ReachyMini
13
  from reachy_mini.motion.recorded_move import RecordedMove, RecordedMoves
14
 
 
144
  def __init__(
145
  self,
146
  movements: SituationMovements,
147
+ quips: Optional[VoiceQuips] = None,
148
  good_job_heartbeats: int = 3,
149
  phone_use_bad_sec: float = 10.0,
150
  phone_use_clear_sec: float = 3.0,
151
  restore_head_duration: float = 0.6,
152
  ) -> None:
153
  self.movements = movements
154
+ self.quips = quips
155
  self.good_job_heartbeats = int(good_job_heartbeats)
156
  self.phone_use_bad_sec = float(phone_use_bad_sec)
157
  self.phone_use_clear_sec = float(phone_use_clear_sec)
 
180
  if phone_tracked and not phone_use:
181
  self._good_job_streak += 1
182
  if self._good_job_streak >= self.good_job_heartbeats:
183
+ if self.quips is not None:
184
+ self.quips.happy()
185
  self._pick_and_run(self._good_job_moves)
186
  self._good_job_streak = 0
187
  else:
 
192
  if self._phone_use_start is None:
193
  self._phone_use_start = now
194
  elif now - self._phone_use_start >= self.phone_use_bad_sec:
195
+ if self.quips is not None:
196
+ self.quips.angry()
197
  self._pick_and_run(self._bad_moves)
198
  self._phone_use_start = now
199
  else:
reachy_phone_home/static/index.html CHANGED
@@ -311,6 +311,11 @@
311
  .log-line {
312
  margin-bottom: 6px;
313
  }
 
 
 
 
 
314
  </style>
315
  </head>
316
  <body>
@@ -325,6 +330,9 @@
325
  <button class="tab-btn" id="tabSettings">Settings</button>
326
  </div>
327
  <div class="panel" id="panelStatus">
 
 
 
328
  <div class="cards">
329
  <div class="card" id="cardPhoneHome">
330
  <div class="card-header">
@@ -400,12 +408,28 @@
400
  <option value="600">10 min</option>
401
  </select>
402
  </div>
403
- <div class="settings-actions">
404
- <button id="saveSettingsBtn" class="tab-btn" type="button">Save</button>
405
- <span class="muted" id="settingsStatus"></span>
 
 
 
 
 
 
 
 
 
 
 
 
406
  </div>
407
  </div>
408
  </div>
 
 
 
 
409
  </div>
410
  </div>
411
  <script>
@@ -422,6 +446,8 @@
422
  const downloadWeightsBtn = document.getElementById("downloadWeightsBtn");
423
  const confInput = document.getElementById("confInput");
424
  const goodJobSelect = document.getElementById("goodJobSelect");
 
 
425
  const saveSettingsBtn = document.getElementById("saveSettingsBtn");
426
  const settingsStatus = document.getElementById("settingsStatus");
427
  let videoEnabled = false;
@@ -440,6 +466,8 @@
440
  const statusReachy = document.getElementById("statusReachy");
441
  const subReachy = document.getElementById("subReachy");
442
  const statusPhoneHome = document.getElementById("statusPhoneHome");
 
 
443
  let aspectTimer = null;
444
  let snapshotTimer = null;
445
 
@@ -540,6 +568,16 @@
540
  const seconds = data.good_job_heartbeats * 10;
541
  goodJobSelect.value = String(seconds);
542
  }
 
 
 
 
 
 
 
 
 
 
543
  } catch (err) {
544
  settingsStatus.textContent = "Failed to load settings";
545
  }
@@ -554,7 +592,28 @@
554
  await fetch("/settings", {
555
  method: "POST",
556
  headers: { "Content-Type": "application/json" },
557
- body: JSON.stringify({ conf: confVal, good_job_heartbeats: beats }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  });
559
  settingsStatus.textContent = "Saved";
560
  } catch (err) {
 
311
  .log-line {
312
  margin-bottom: 6px;
313
  }
314
+ .status-actions {
315
+ display: flex;
316
+ justify-content: center;
317
+ margin: 8px 0 16px;
318
+ }
319
  </style>
320
  </head>
321
  <body>
 
330
  <button class="tab-btn" id="tabSettings">Settings</button>
331
  </div>
332
  <div class="panel" id="panelStatus">
333
+ <div class="status-actions">
334
+ <button id="quipsToggleBtn" class="tab-btn" type="button">Reachy Audio: On</button>
335
+ </div>
336
  <div class="cards">
337
  <div class="card" id="cardPhoneHome">
338
  <div class="card-header">
 
408
  <option value="600">10 min</option>
409
  </select>
410
  </div>
411
+ </div>
412
+ <div class="settings-card">
413
+ <h4>Reachy Audio</h4>
414
+ <div class="settings-row">
415
+ <label for="ambientSelect">Ambient interval</label>
416
+ <select id="ambientSelect">
417
+ <option value="10">10s</option>
418
+ <option value="30" selected>30s</option>
419
+ <option value="60">1 min</option>
420
+ <option value="600">10 min</option>
421
+ </select>
422
+ </div>
423
+ <div class="settings-row">
424
+ <label for="curiousInput">Curious interval (sec)</label>
425
+ <input id="curiousInput" type="number" min="1" max="30" step="1" value="5" />
426
  </div>
427
  </div>
428
  </div>
429
+ <div class="settings-actions">
430
+ <button id="saveSettingsBtn" class="tab-btn" type="button">Save</button>
431
+ <span class="muted" id="settingsStatus"></span>
432
+ </div>
433
  </div>
434
  </div>
435
  <script>
 
446
  const downloadWeightsBtn = document.getElementById("downloadWeightsBtn");
447
  const confInput = document.getElementById("confInput");
448
  const goodJobSelect = document.getElementById("goodJobSelect");
449
+ const ambientSelect = document.getElementById("ambientSelect");
450
+ const curiousInput = document.getElementById("curiousInput");
451
  const saveSettingsBtn = document.getElementById("saveSettingsBtn");
452
  const settingsStatus = document.getElementById("settingsStatus");
453
  let videoEnabled = false;
 
466
  const statusReachy = document.getElementById("statusReachy");
467
  const subReachy = document.getElementById("subReachy");
468
  const statusPhoneHome = document.getElementById("statusPhoneHome");
469
+ const quipsToggleBtn = document.getElementById("quipsToggleBtn");
470
+ let quipsEnabled = true;
471
  let aspectTimer = null;
472
  let snapshotTimer = null;
473
 
 
568
  const seconds = data.good_job_heartbeats * 10;
569
  goodJobSelect.value = String(seconds);
570
  }
571
+ if (data.quips_enabled !== null && data.quips_enabled !== undefined) {
572
+ quipsEnabled = Boolean(data.quips_enabled);
573
+ quipsToggleBtn.textContent = quipsEnabled ? "Reachy Audio: On" : "Reachy Audio: Off";
574
+ }
575
+ if (data.ambient_interval_sec !== null && data.ambient_interval_sec !== undefined) {
576
+ ambientSelect.value = String(data.ambient_interval_sec);
577
+ }
578
+ if (data.curious_interval_sec !== null && data.curious_interval_sec !== undefined) {
579
+ curiousInput.value = String(data.curious_interval_sec);
580
+ }
581
  } catch (err) {
582
  settingsStatus.textContent = "Failed to load settings";
583
  }
 
592
  await fetch("/settings", {
593
  method: "POST",
594
  headers: { "Content-Type": "application/json" },
595
+ body: JSON.stringify({
596
+ conf: confVal,
597
+ good_job_heartbeats: beats,
598
+ ambient_interval_sec: parseFloat(ambientSelect.value),
599
+ curious_interval_sec: parseFloat(curiousInput.value),
600
+ }),
601
+ });
602
+ settingsStatus.textContent = "Saved";
603
+ } catch (err) {
604
+ settingsStatus.textContent = "Save failed";
605
+ }
606
+ });
607
+
608
+ quipsToggleBtn.addEventListener("click", async () => {
609
+ quipsEnabled = !quipsEnabled;
610
+ quipsToggleBtn.textContent = quipsEnabled ? "Reachy Audio: On" : "Reachy Audio: Off";
611
+ settingsStatus.textContent = "Saving...";
612
+ try {
613
+ await fetch("/settings", {
614
+ method: "POST",
615
+ headers: { "Content-Type": "application/json" },
616
+ body: JSON.stringify({ quips_enabled: quipsEnabled }),
617
  });
618
  settingsStatus.textContent = "Saved";
619
  } catch (err) {
reachy_phone_home/voice_quips.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Audio quips library for Reachy Phone Home."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional
8
+
9
+ from reachy_mini import ReachyMini
10
+
11
+
12
+ class VoiceQuips:
13
+ """Play short audio quips grouped by mood."""
14
+
15
+ _MOODS = ("happy", "ambient", "sad", "angry", "curious")
16
+
17
+ def __init__(self, reachy: ReachyMini, assets_dir: Optional[Path] = None) -> None:
18
+ self.reachy = reachy
19
+ self.assets_dir = assets_dir or Path(__file__).resolve().parent / "assets"
20
+ self._quips: Dict[str, List[Path]] = {mood: [] for mood in self._MOODS}
21
+ self._index_quips()
22
+
23
+ def _index_quips(self) -> None:
24
+ if not self.assets_dir.exists():
25
+ return
26
+ for path in self.assets_dir.glob("*.mp3"):
27
+ name = path.name.lower()
28
+ for mood in self._MOODS:
29
+ if name.startswith(f"{mood}_"):
30
+ self._quips[mood].append(path)
31
+ break
32
+
33
+ def _play_random(self, mood: str) -> Optional[Path]:
34
+ pool = self._quips.get(mood, [])
35
+ if not pool:
36
+ return None
37
+ choice = random.choice(pool)
38
+ self.reachy.media.play_sound(str(choice))
39
+ return choice
40
+
41
+ def happy(self) -> Optional[Path]:
42
+ return self._play_random("happy")
43
+
44
+ def ambient(self) -> Optional[Path]:
45
+ return self._play_random("ambient")
46
+
47
+ def sad(self) -> Optional[Path]:
48
+ return self._play_random("sad")
49
+
50
+ def angry(self) -> Optional[Path]:
51
+ return self._play_random("angry")
52
+
53
+ def curious(self) -> Optional[Path]:
54
+ return self._play_random("curious")
55
+
reachy_phone_home/web_ui.py CHANGED
@@ -28,6 +28,9 @@ class WebUIState:
28
  self._max_log_lines = 300
29
  self._conf_override: Optional[float] = None
30
  self._good_job_heartbeats: Optional[int] = None
 
 
 
31
  self._download_callback: Optional[Callable[[str], None]] = None
32
 
33
  def append_log(self, line: str) -> None:
@@ -77,6 +80,39 @@ class WebUIState:
77
  return int(default)
78
  return int(self._good_job_heartbeats)
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  def set_download_callback(self, callback: Callable[[str], None]) -> None:
81
  self._download_callback = callback
82
 
@@ -164,6 +200,9 @@ class WebUIState:
164
  {
165
  "conf": self._conf_override,
166
  "good_job_heartbeats": self._good_job_heartbeats,
 
 
 
167
  }
168
  )
169
 
@@ -177,12 +216,24 @@ class WebUIState:
177
  data = {}
178
  conf = data.get("conf")
179
  beats = data.get("good_job_heartbeats")
 
 
 
180
  self.set_conf_override(conf if conf is None else float(conf))
181
  self.set_good_job_heartbeats(beats if beats is None else int(beats))
 
 
 
 
 
 
182
  return JSONResponse(
183
  {
184
  "conf": self._conf_override,
185
  "good_job_heartbeats": self._good_job_heartbeats,
 
 
 
186
  }
187
  )
188
 
 
28
  self._max_log_lines = 300
29
  self._conf_override: Optional[float] = None
30
  self._good_job_heartbeats: Optional[int] = None
31
+ self._ambient_interval_sec: Optional[float] = None
32
+ self._curious_interval_sec: Optional[float] = None
33
+ self._quips_enabled: Optional[bool] = None
34
  self._download_callback: Optional[Callable[[str], None]] = None
35
 
36
  def append_log(self, line: str) -> None:
 
80
  return int(default)
81
  return int(self._good_job_heartbeats)
82
 
83
+ def set_ambient_interval_sec(self, seconds: Optional[float]) -> None:
84
+ if seconds is None:
85
+ self._ambient_interval_sec = None
86
+ else:
87
+ self._ambient_interval_sec = max(10.0, float(seconds))
88
+
89
+ def get_ambient_interval_sec(self, default: float) -> float:
90
+ if self._ambient_interval_sec is None:
91
+ return float(default)
92
+ return float(self._ambient_interval_sec)
93
+
94
+ def set_curious_interval_sec(self, seconds: Optional[float]) -> None:
95
+ if seconds is None:
96
+ self._curious_interval_sec = None
97
+ else:
98
+ self._curious_interval_sec = max(0.5, float(seconds))
99
+
100
+ def get_curious_interval_sec(self, default: float) -> float:
101
+ if self._curious_interval_sec is None:
102
+ return float(default)
103
+ return float(self._curious_interval_sec)
104
+
105
+ def set_quips_enabled(self, enabled: Optional[bool]) -> None:
106
+ if enabled is None:
107
+ self._quips_enabled = None
108
+ else:
109
+ self._quips_enabled = bool(enabled)
110
+
111
+ def get_quips_enabled(self, default: bool) -> bool:
112
+ if self._quips_enabled is None:
113
+ return bool(default)
114
+ return bool(self._quips_enabled)
115
+
116
  def set_download_callback(self, callback: Callable[[str], None]) -> None:
117
  self._download_callback = callback
118
 
 
200
  {
201
  "conf": self._conf_override,
202
  "good_job_heartbeats": self._good_job_heartbeats,
203
+ "ambient_interval_sec": self._ambient_interval_sec,
204
+ "curious_interval_sec": self._curious_interval_sec,
205
+ "quips_enabled": self._quips_enabled,
206
  }
207
  )
208
 
 
216
  data = {}
217
  conf = data.get("conf")
218
  beats = data.get("good_job_heartbeats")
219
+ ambient = data.get("ambient_interval_sec")
220
+ curious = data.get("curious_interval_sec")
221
+ quips_enabled = data.get("quips_enabled")
222
  self.set_conf_override(conf if conf is None else float(conf))
223
  self.set_good_job_heartbeats(beats if beats is None else int(beats))
224
+ self.set_ambient_interval_sec(ambient if ambient is None else float(ambient))
225
+ self.set_curious_interval_sec(curious if curious is None else float(curious))
226
+ self.set_quips_enabled(quips_enabled if quips_enabled is None else bool(quips_enabled))
227
+ if quips_enabled is not None:
228
+ state = "enabled" if bool(quips_enabled) else "disabled"
229
+ self.append_log(f"[settings] Reachy Audio {state}")
230
  return JSONResponse(
231
  {
232
  "conf": self._conf_override,
233
  "good_job_heartbeats": self._good_job_heartbeats,
234
+ "ambient_interval_sec": self._ambient_interval_sec,
235
+ "curious_interval_sec": self._curious_interval_sec,
236
+ "quips_enabled": self._quips_enabled,
237
  }
238
  )
239
 
setup.cfg CHANGED
@@ -15,6 +15,8 @@ install_requires =
15
  [options.package_data]
16
  reachy_phone_home =
17
  static/*.html
 
 
18
 
19
  [options.entry_points]
20
  reachy_mini_apps =
 
15
  [options.package_data]
16
  reachy_phone_home =
17
  static/*.html
18
+ static/*.PNG
19
+ assets/*.mp3
20
 
21
  [options.entry_points]
22
  reachy_mini_apps =
web_log_ui.html CHANGED
@@ -311,6 +311,11 @@
311
  .log-line {
312
  margin-bottom: 6px;
313
  }
 
 
 
 
 
314
  </style>
315
  </head>
316
  <body>
@@ -325,6 +330,9 @@
325
  <button class="tab-btn" id="tabSettings">Settings</button>
326
  </div>
327
  <div class="panel" id="panelStatus">
 
 
 
328
  <div class="cards">
329
  <div class="card" id="cardPhoneHome">
330
  <div class="card-header">
@@ -400,12 +408,28 @@
400
  <option value="600">10 min</option>
401
  </select>
402
  </div>
403
- <div class="settings-actions">
404
- <button id="saveSettingsBtn" class="tab-btn" type="button">Save</button>
405
- <span class="muted" id="settingsStatus"></span>
 
 
 
 
 
 
 
 
 
 
 
 
406
  </div>
407
  </div>
408
  </div>
 
 
 
 
409
  </div>
410
  </div>
411
  <script>
@@ -422,6 +446,8 @@
422
  const downloadWeightsBtn = document.getElementById("downloadWeightsBtn");
423
  const confInput = document.getElementById("confInput");
424
  const goodJobSelect = document.getElementById("goodJobSelect");
 
 
425
  const saveSettingsBtn = document.getElementById("saveSettingsBtn");
426
  const settingsStatus = document.getElementById("settingsStatus");
427
  let videoEnabled = false;
@@ -440,6 +466,8 @@
440
  const statusReachy = document.getElementById("statusReachy");
441
  const subReachy = document.getElementById("subReachy");
442
  const statusPhoneHome = document.getElementById("statusPhoneHome");
 
 
443
  let aspectTimer = null;
444
  let snapshotTimer = null;
445
 
@@ -540,6 +568,16 @@
540
  const seconds = data.good_job_heartbeats * 10;
541
  goodJobSelect.value = String(seconds);
542
  }
 
 
 
 
 
 
 
 
 
 
543
  } catch (err) {
544
  settingsStatus.textContent = "Failed to load settings";
545
  }
@@ -554,7 +592,28 @@
554
  await fetch("/settings", {
555
  method: "POST",
556
  headers: { "Content-Type": "application/json" },
557
- body: JSON.stringify({ conf: confVal, good_job_heartbeats: beats }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  });
559
  settingsStatus.textContent = "Saved";
560
  } catch (err) {
 
311
  .log-line {
312
  margin-bottom: 6px;
313
  }
314
+ .status-actions {
315
+ display: flex;
316
+ justify-content: center;
317
+ margin: 8px 0 16px;
318
+ }
319
  </style>
320
  </head>
321
  <body>
 
330
  <button class="tab-btn" id="tabSettings">Settings</button>
331
  </div>
332
  <div class="panel" id="panelStatus">
333
+ <div class="status-actions">
334
+ <button id="quipsToggleBtn" class="tab-btn" type="button">Reachy Audio: On</button>
335
+ </div>
336
  <div class="cards">
337
  <div class="card" id="cardPhoneHome">
338
  <div class="card-header">
 
408
  <option value="600">10 min</option>
409
  </select>
410
  </div>
411
+ </div>
412
+ <div class="settings-card">
413
+ <h4>Reachy Audio</h4>
414
+ <div class="settings-row">
415
+ <label for="ambientSelect">Ambient interval</label>
416
+ <select id="ambientSelect">
417
+ <option value="10">10s</option>
418
+ <option value="30" selected>30s</option>
419
+ <option value="60">1 min</option>
420
+ <option value="600">10 min</option>
421
+ </select>
422
+ </div>
423
+ <div class="settings-row">
424
+ <label for="curiousInput">Curious interval (sec)</label>
425
+ <input id="curiousInput" type="number" min="1" max="30" step="1" value="5" />
426
  </div>
427
  </div>
428
  </div>
429
+ <div class="settings-actions">
430
+ <button id="saveSettingsBtn" class="tab-btn" type="button">Save</button>
431
+ <span class="muted" id="settingsStatus"></span>
432
+ </div>
433
  </div>
434
  </div>
435
  <script>
 
446
  const downloadWeightsBtn = document.getElementById("downloadWeightsBtn");
447
  const confInput = document.getElementById("confInput");
448
  const goodJobSelect = document.getElementById("goodJobSelect");
449
+ const ambientSelect = document.getElementById("ambientSelect");
450
+ const curiousInput = document.getElementById("curiousInput");
451
  const saveSettingsBtn = document.getElementById("saveSettingsBtn");
452
  const settingsStatus = document.getElementById("settingsStatus");
453
  let videoEnabled = false;
 
466
  const statusReachy = document.getElementById("statusReachy");
467
  const subReachy = document.getElementById("subReachy");
468
  const statusPhoneHome = document.getElementById("statusPhoneHome");
469
+ const quipsToggleBtn = document.getElementById("quipsToggleBtn");
470
+ let quipsEnabled = true;
471
  let aspectTimer = null;
472
  let snapshotTimer = null;
473
 
 
568
  const seconds = data.good_job_heartbeats * 10;
569
  goodJobSelect.value = String(seconds);
570
  }
571
+ if (data.quips_enabled !== null && data.quips_enabled !== undefined) {
572
+ quipsEnabled = Boolean(data.quips_enabled);
573
+ quipsToggleBtn.textContent = quipsEnabled ? "Reachy Audio: On" : "Reachy Audio: Off";
574
+ }
575
+ if (data.ambient_interval_sec !== null && data.ambient_interval_sec !== undefined) {
576
+ ambientSelect.value = String(data.ambient_interval_sec);
577
+ }
578
+ if (data.curious_interval_sec !== null && data.curious_interval_sec !== undefined) {
579
+ curiousInput.value = String(data.curious_interval_sec);
580
+ }
581
  } catch (err) {
582
  settingsStatus.textContent = "Failed to load settings";
583
  }
 
592
  await fetch("/settings", {
593
  method: "POST",
594
  headers: { "Content-Type": "application/json" },
595
+ body: JSON.stringify({
596
+ conf: confVal,
597
+ good_job_heartbeats: beats,
598
+ ambient_interval_sec: parseFloat(ambientSelect.value),
599
+ curious_interval_sec: parseFloat(curiousInput.value),
600
+ }),
601
+ });
602
+ settingsStatus.textContent = "Saved";
603
+ } catch (err) {
604
+ settingsStatus.textContent = "Save failed";
605
+ }
606
+ });
607
+
608
+ quipsToggleBtn.addEventListener("click", async () => {
609
+ quipsEnabled = !quipsEnabled;
610
+ quipsToggleBtn.textContent = quipsEnabled ? "Reachy Audio: On" : "Reachy Audio: Off";
611
+ settingsStatus.textContent = "Saving...";
612
+ try {
613
+ await fetch("/settings", {
614
+ method: "POST",
615
+ headers: { "Content-Type": "application/json" },
616
+ body: JSON.stringify({ quips_enabled: quipsEnabled }),
617
  });
618
  settingsStatus.textContent = "Saved";
619
  } catch (err) {