NithinReddyG commited on
Commit
5a72b21
·
1 Parent(s): 1c13793

remove emotion-library moves + praise; tune detection thresholds

Browse files

- main.py: drop _PERSONALITY_*_EMOTIONS dicts and play_move(emo, ...)
calls — they hammered the motor controller (Serial I/O errors) and
fought TTS audio. Shame path keeps TTS + lightweight count-based
animation. Putdown handler is silent (just streak timer).
- audio.py auto-path now prefers Groq Orpheus when groq_key is set
(ElevenLabs > Groq > Edge). enable_orpheus_tags fires when Groq is
in play (provider=groq or auto+key), so the LLM emits [angry] etc
whenever Orpheus will consume them.
- config.py: PICKUP_THRESHOLD 3 -> 2 frames (Pi 5 hits ~3 fps so 3
frames was a full second), DETECTION_CONFIDENCE 0.3 -> 0.2 (more
eager pickup detection).
- UI: drop emotion-animations + praise checkboxes from settings modal
(along with the JS handlers). praise field still accepted for
back-compat but ignored server-side.

judgy_reachy_no_phone/audio.py CHANGED
@@ -413,7 +413,8 @@ class TextToSpeech:
413
  # For ElevenLabs and Edge (auto path), strip Orpheus tags so they aren't spoken verbatim
414
  text_no_tags = strip_orpheus_tags(text)
415
 
416
- # Explicit ElevenLabs OR auto with ElevenLabs key: try ElevenLabs
 
417
  # Try ElevenLabs first if available and under limit
418
  if self.eleven_client and (self.chars_used + len(text_no_tags)) < self.MONTHLY_LIMIT:
419
  # Check cache first
@@ -441,9 +442,20 @@ class TextToSpeech:
441
  continue
442
 
443
  # All voices failed
444
- logger.warning(f"All ElevenLabs voices failed for {self.personality}, falling back to Edge TTS")
445
 
446
- # Fallback to Edge TTS (always works, unlimited)
 
 
 
 
 
 
 
 
 
 
 
447
  logger.info(f"Using Edge TTS with voice: {edge_voice}")
448
  return await self._synthesize_edge(text_no_tags, output_path, edge_voice)
449
 
 
413
  # For ElevenLabs and Edge (auto path), strip Orpheus tags so they aren't spoken verbatim
414
  text_no_tags = strip_orpheus_tags(text)
415
 
416
+ # Auto path: ElevenLabs (if works) > Groq Orpheus (if key) > Edge (fallback).
417
+ # Explicit "elevenlabs": same as auto-with-eleven, falls through to Edge on failure.
418
  # Try ElevenLabs first if available and under limit
419
  if self.eleven_client and (self.chars_used + len(text_no_tags)) < self.MONTHLY_LIMIT:
420
  # Check cache first
 
442
  continue
443
 
444
  # All voices failed
445
+ logger.warning(f"All ElevenLabs voices failed for {self.personality}, falling back to Groq/Edge")
446
 
447
+ # In the auto path, prefer Groq Orpheus over Edge when groq_key is available.
448
+ # (Explicit provider="edge"/"groq"/"elevenlabs" already short-circuited above.)
449
+ if self.groq_key:
450
+ try:
451
+ wav_path = output_path.rsplit(".", 1)[0] + ".wav"
452
+ logger.info(f"Auto-path -> Groq Orpheus voice={self.groq_tts_voice}")
453
+ # Tags pass through to Orpheus
454
+ return await self._synthesize_groq(text, wav_path)
455
+ except Exception as e:
456
+ logger.warning(f"Groq TTS failed in auto-path: {e}, falling back to Edge TTS")
457
+
458
+ # Final fallback: Edge TTS (always works, unlimited)
459
  logger.info(f"Using Edge TTS with voice: {edge_voice}")
460
  return await self._synthesize_edge(text_no_tags, output_path, edge_voice)
461
 
judgy_reachy_no_phone/config.py CHANGED
@@ -8,9 +8,11 @@ from dataclasses import dataclass
8
  class Config:
9
  """App configuration."""
10
  # Detection settings
11
- PICKUP_THRESHOLD: int = 3 # Frames to confirm phone pickup
 
 
12
  PUTDOWN_THRESHOLD: int = 15 # Frames to confirm phone put down (~3 sec)
13
- DETECTION_CONFIDENCE: float = 0.3 # Higher = fewer false positives
14
  COOLDOWN_SECONDS: float = 10.0 # Min time between shames
15
 
16
  # API Keys (optional - leave empty for free defaults)
 
8
  class Config:
9
  """App configuration."""
10
  # Detection settings
11
+ # Pi 5 camera path yields ~3 fps; threshold=2 -> ~0.7 s held phone before
12
+ # first shame (vs 1 s with the upstream default of 3 frames).
13
+ PICKUP_THRESHOLD: int = 2 # Frames to confirm phone pickup
14
  PUTDOWN_THRESHOLD: int = 15 # Frames to confirm phone put down (~3 sec)
15
+ DETECTION_CONFIDENCE: float = 0.2 # Higher = fewer false positives
16
  COOLDOWN_SECONDS: float = 10.0 # Min time between shames
17
 
18
  # API Keys (optional - leave empty for free defaults)
judgy_reachy_no_phone/main.py CHANGED
@@ -408,85 +408,35 @@ class JudgyReachyNoPhone(ReachyMiniApp):
408
  text = self.llm.get_response(count)
409
  logger.info(f"Response: {text}")
410
 
411
- # Generate and play audio
 
 
412
  try:
413
  loop = asyncio.new_event_loop()
414
  asyncio.set_event_loop(loop)
415
  audio_path = loop.run_until_complete(self.tts.synthesize(text))
416
  loop.close()
417
 
418
- # Play audio
419
  reachy.media.play_sound(audio_path)
420
-
421
- # Emotion-library move (motion only — sound=False so it doesn't fight TTS)
422
- # falls back to count-based generic animation if emotions disabled or unavailable.
423
- emo_name = _pick_emotion(self.llm.personality, "shame") if self.emotions_enabled else None
424
- if emo_name and self.emotions:
425
- try:
426
- emo = self.emotions.get(emo_name)
427
- logger.info(f"Playing shame emotion: {emo_name} (personality={self.llm.personality})")
428
- reachy.play_move(emo, sound=False)
429
- except Exception as e:
430
- logger.warning(f"Emotion '{emo_name}' failed: {e}; falling back to generic animation")
431
- get_animation_for_count(count)(reachy)
432
- else:
433
- animation = get_animation_for_count(count)
434
- animation(reachy)
435
 
436
  except Exception as e:
437
  logger.error(f"Shame response error: {e}")
438
- # Fallback: just animate
439
  play_sound_safe(reachy, "confused1.wav")
440
  disappointed_shake(reachy)
441
 
442
  def _handle_phone_putdown(self, reachy: ReachyMini):
443
- """Handle phone put down event."""
444
- logger.info("Phone put down!")
445
-
446
- # Start new streak
 
 
 
 
447
  self.current_streak_start = time.time()
448
 
449
- # Check if using Pure Reachy mode (no TTS, just emotions)
450
- if self.llm.personality == "pure_reachy" and self.emotions:
451
- # Randomly pick a praise emotion from the config list
452
- import random
453
- personality_data = PERSONALITIES["pure_reachy"]
454
- praise_emotions = personality_data.get("praise_emotions", ["yes1"])
455
- emotion_name = random.choice(praise_emotions)
456
- emotion = self.emotions.get(emotion_name)
457
- logger.info(f"Pure Reachy praise: {emotion_name}")
458
-
459
- # Play emotion (includes sound + animation automatically)
460
- reachy.play_move(emotion)
461
- else:
462
- # Normal mode: Get praise via TTS
463
- text = self.llm.get_praise()
464
- logger.info(f"Praise: {text}")
465
-
466
- try:
467
- loop = asyncio.new_event_loop()
468
- asyncio.set_event_loop(loop)
469
- audio_path = loop.run_until_complete(self.tts.synthesize(text))
470
- loop.close()
471
-
472
- reachy.media.play_sound(audio_path)
473
-
474
- emo_name = _pick_emotion(self.llm.personality, "praise") if self.emotions_enabled else None
475
- if emo_name and self.emotions:
476
- try:
477
- emo = self.emotions.get(emo_name)
478
- logger.info(f"Playing praise emotion: {emo_name} (personality={self.llm.personality})")
479
- reachy.play_move(emo, sound=False)
480
- except Exception as e:
481
- logger.warning(f"Emotion '{emo_name}' failed: {e}; falling back to approving_nod")
482
- approving_nod(reachy)
483
- else:
484
- approving_nod(reachy)
485
-
486
- except Exception as e:
487
- logger.debug(f"Praise error: {e}")
488
- approving_nod(reachy)
489
-
490
  def _run_ui(self, reachy_mini: ReachyMini, stop_event: threading.Event):
491
  """Setup FastAPI routes for the UI."""
492
 
@@ -606,7 +556,7 @@ class JudgyReachyNoPhone(ReachyMiniApp):
606
  user_name=req.user_name,
607
  custom_prompt=req.custom_prompt,
608
  model=req.model,
609
- enable_orpheus_tags=(req.tts_provider == "groq"),
610
  )
611
  self.emotions_enabled = req.emotions_enabled
612
 
@@ -776,7 +726,7 @@ class JudgyReachyNoPhone(ReachyMiniApp):
776
  user_name=req.user_name,
777
  custom_prompt=req.custom_prompt,
778
  model=req.model,
779
- enable_orpheus_tags=(req.tts_provider == "groq"),
780
  )
781
  self.emotions_enabled = req.emotions_enabled
782
 
@@ -812,7 +762,8 @@ class JudgyReachyNoPhone(ReachyMiniApp):
812
  text = self.llm.get_response(self.detector.phone_count)
813
  logger.info(f"Test response: {text}")
814
 
815
- # Play audio and animate
 
816
  try:
817
  loop = asyncio.new_event_loop()
818
  asyncio.set_event_loop(loop)
@@ -820,20 +771,8 @@ class JudgyReachyNoPhone(ReachyMiniApp):
820
  loop.close()
821
 
822
  reachy_mini.media.play_sound(audio_path)
823
-
824
- # Emotion-library move for the personality (motion only, sound=False)
825
- emo_name = _pick_emotion(req.personality, "shame") if req.emotions_enabled else None
826
- if emo_name and self.emotions:
827
- try:
828
- emo = self.emotions.get(emo_name)
829
- logger.info(f"Test playing emotion: {emo_name} (personality={req.personality})")
830
- reachy_mini.play_move(emo, sound=False)
831
- except Exception as e:
832
- logger.warning(f"Test emotion '{emo_name}' failed: {e}; generic animation")
833
- get_animation_for_count(self.detector.phone_count)(reachy_mini)
834
- else:
835
- animation = get_animation_for_count(self.detector.phone_count)
836
- animation(reachy_mini)
837
  except Exception as e:
838
  logger.error(f"Test error: {e}")
839
  play_sound_safe(reachy_mini, "confused1.wav")
@@ -852,7 +791,7 @@ class JudgyReachyNoPhone(ReachyMiniApp):
852
  user_name=req.user_name,
853
  custom_prompt=req.custom_prompt,
854
  model=req.model,
855
- enable_orpheus_tags=(req.tts_provider == "groq"),
856
  )
857
  self.emotions_enabled = req.emotions_enabled
858
  logger.info(
 
408
  text = self.llm.get_response(count)
409
  logger.info(f"Response: {text}")
410
 
411
+ # Generate and play audio + generic animation only.
412
+ # (Emotion-library play_move() was triggering motor I/O errors —
413
+ # removed in favor of the lightweight personality-agnostic animation.)
414
  try:
415
  loop = asyncio.new_event_loop()
416
  asyncio.set_event_loop(loop)
417
  audio_path = loop.run_until_complete(self.tts.synthesize(text))
418
  loop.close()
419
 
 
420
  reachy.media.play_sound(audio_path)
421
+ animation = get_animation_for_count(count)
422
+ animation(reachy)
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
  except Exception as e:
425
  logger.error(f"Shame response error: {e}")
 
426
  play_sound_safe(reachy, "confused1.wav")
427
  disappointed_shake(reachy)
428
 
429
  def _handle_phone_putdown(self, reachy: ReachyMini):
430
+ """Handle phone put down event.
431
+
432
+ Praise was removed per user preference — the robot should react when
433
+ the phone is picked up, but stay silent when it's put down (the user
434
+ doesn't need a reward for the absence of a problem).
435
+ Only the streak timer restarts; no audio, no motion.
436
+ """
437
+ logger.info("Phone put down (silent — praise disabled)")
438
  self.current_streak_start = time.time()
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  def _run_ui(self, reachy_mini: ReachyMini, stop_event: threading.Event):
441
  """Setup FastAPI routes for the UI."""
442
 
 
556
  user_name=req.user_name,
557
  custom_prompt=req.custom_prompt,
558
  model=req.model,
559
+ enable_orpheus_tags=bool(req.groq_key) and req.tts_provider in ("groq", "auto"),
560
  )
561
  self.emotions_enabled = req.emotions_enabled
562
 
 
726
  user_name=req.user_name,
727
  custom_prompt=req.custom_prompt,
728
  model=req.model,
729
+ enable_orpheus_tags=bool(req.groq_key) and req.tts_provider in ("groq", "auto"),
730
  )
731
  self.emotions_enabled = req.emotions_enabled
732
 
 
762
  text = self.llm.get_response(self.detector.phone_count)
763
  logger.info(f"Test response: {text}")
764
 
765
+ # Play audio + lightweight count-based animation. Emotion-library
766
+ # play_move() removed — was the source of motor I/O errors.
767
  try:
768
  loop = asyncio.new_event_loop()
769
  asyncio.set_event_loop(loop)
 
771
  loop.close()
772
 
773
  reachy_mini.media.play_sound(audio_path)
774
+ animation = get_animation_for_count(self.detector.phone_count)
775
+ animation(reachy_mini)
 
 
 
 
 
 
 
 
 
 
 
 
776
  except Exception as e:
777
  logger.error(f"Test error: {e}")
778
  play_sound_safe(reachy_mini, "confused1.wav")
 
791
  user_name=req.user_name,
792
  custom_prompt=req.custom_prompt,
793
  model=req.model,
794
+ enable_orpheus_tags=bool(req.groq_key) and req.tts_provider in ("groq", "auto"),
795
  )
796
  self.emotions_enabled = req.emotions_enabled
797
  logger.info(
judgy_reachy_no_phone/static/index.html CHANGED
@@ -180,18 +180,6 @@
180
  <label for="cooldown">Cooldown between shames (seconds): <span id="cooldown-value">10</span></label>
181
  <input type="range" id="cooldown" min="10" max="120" value="10" step="5">
182
  </div>
183
- <div class="form-group">
184
- <label class="checkbox-label">
185
- <input type="checkbox" id="praise-toggle" checked>
186
- Praise when phone is put down
187
- </label>
188
- </div>
189
- <div class="form-group">
190
- <label class="checkbox-label">
191
- <input type="checkbox" id="emotions-toggle" checked>
192
- Emotion animations (head/antenna moves from Reachy emotion library)
193
- </label>
194
- </div>
195
  </div>
196
  <div class="settings-footer">
197
  <button class="btn btn-done" id="done-settings">✓ Done</button>
 
180
  <label for="cooldown">Cooldown between shames (seconds): <span id="cooldown-value">10</span></label>
181
  <input type="range" id="cooldown" min="10" max="120" value="10" step="5">
182
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
183
  </div>
184
  <div class="settings-footer">
185
  <button class="btn btn-done" id="done-settings">✓ Done</button>
judgy_reachy_no_phone/static/main.js CHANGED
@@ -16,22 +16,17 @@ function getCustomSettings() {
16
  const model = (document.getElementById('groq-model')?.value || '').trim();
17
  const ttsProvider = (document.getElementById('tts-provider')?.value || 'auto').trim();
18
  const groqTtsVoice = (document.getElementById('groq-tts-voice')?.value || '').trim();
19
- // Default to true if checkbox missing in older DOM
20
- const emotionsEl = document.getElementById('emotions-toggle');
21
- const emotionsEnabled = emotionsEl ? !!emotionsEl.checked : true;
22
  localStorage.setItem('userName', userName);
23
  localStorage.setItem('customPrompt', customPrompt);
24
  localStorage.setItem('groqModel', model);
25
  localStorage.setItem('ttsProvider', ttsProvider);
26
  localStorage.setItem('groqTtsVoice', groqTtsVoice);
27
- localStorage.setItem('emotionsEnabled', emotionsEnabled ? '1' : '0');
28
  return {
29
  user_name: userName,
30
  custom_prompt: customPrompt,
31
  model: model,
32
  tts_provider: ttsProvider,
33
  groq_tts_voice: groqTtsVoice,
34
- emotions_enabled: emotionsEnabled,
35
  };
36
  }
37
 
@@ -50,12 +45,6 @@ function hydrateCustomSettings() {
50
  if (v !== null) el.value = v;
51
  }
52
  }
53
- // Hydrate emotions toggle (default ON if never set)
54
- const emo = document.getElementById('emotions-toggle');
55
- if (emo) {
56
- const stored = localStorage.getItem('emotionsEnabled');
57
- emo.checked = stored === null ? true : stored === '1';
58
- }
59
  // Also hydrate groq-key & eleven-key from localStorage if present (existing UX gap).
60
  const gk = document.getElementById('groq-key');
61
  if (gk && localStorage.getItem('groqKey')) gk.value = localStorage.getItem('groqKey');
@@ -130,7 +119,7 @@ async function updateUIForAPIKeys() {
130
  const groqKey = document.getElementById('groq-key').value;
131
  const elevenKey = document.getElementById('eleven-key').value;
132
  const cooldown = document.getElementById('cooldown').value;
133
- const praise = document.getElementById('praise-toggle').checked;
134
 
135
  // If no API keys at all, show default message
136
  if (!groqKey && !elevenKey) {
@@ -379,7 +368,7 @@ async function toggleMonitoring() {
379
  const groqKey = document.getElementById('groq-key').value;
380
  const elevenKey = document.getElementById('eleven-key').value;
381
  const cooldown = document.getElementById('cooldown').value;
382
- const praise = document.getElementById('praise-toggle').checked;
383
 
384
  // Get voice override for selected personality
385
  const voiceOverride = voiceOverrides[selectedPersonality] || {};
@@ -428,7 +417,7 @@ async function testShame() {
428
  const groqKey = document.getElementById('groq-key').value;
429
  const elevenKey = document.getElementById('eleven-key').value;
430
  const cooldown = document.getElementById('cooldown').value;
431
- const praise = document.getElementById('praise-toggle').checked;
432
 
433
  // Get voice override for selected personality
434
  const voiceOverride = voiceOverrides[selectedPersonality] || {};
@@ -460,7 +449,7 @@ async function updatePersonalityWhileRunning() {
460
  const groqKey = document.getElementById('groq-key').value;
461
  const elevenKey = document.getElementById('eleven-key').value;
462
  const cooldown = document.getElementById('cooldown').value;
463
- const praise = document.getElementById('praise-toggle').checked;
464
  const voiceOverride = voiceOverrides[selectedPersonality] || {};
465
 
466
  try {
@@ -580,7 +569,6 @@ async function initialize() {
580
  document.getElementById('groq-model')?.addEventListener('change', () => getCustomSettings());
581
  document.getElementById('tts-provider')?.addEventListener('change', () => getCustomSettings());
582
  document.getElementById('groq-tts-voice')?.addEventListener('change', () => getCustomSettings());
583
- document.getElementById('emotions-toggle')?.addEventListener('change', () => getCustomSettings());
584
 
585
  // Initial UI update
586
  updateDisplay();
 
16
  const model = (document.getElementById('groq-model')?.value || '').trim();
17
  const ttsProvider = (document.getElementById('tts-provider')?.value || 'auto').trim();
18
  const groqTtsVoice = (document.getElementById('groq-tts-voice')?.value || '').trim();
 
 
 
19
  localStorage.setItem('userName', userName);
20
  localStorage.setItem('customPrompt', customPrompt);
21
  localStorage.setItem('groqModel', model);
22
  localStorage.setItem('ttsProvider', ttsProvider);
23
  localStorage.setItem('groqTtsVoice', groqTtsVoice);
 
24
  return {
25
  user_name: userName,
26
  custom_prompt: customPrompt,
27
  model: model,
28
  tts_provider: ttsProvider,
29
  groq_tts_voice: groqTtsVoice,
 
30
  };
31
  }
32
 
 
45
  if (v !== null) el.value = v;
46
  }
47
  }
 
 
 
 
 
 
48
  // Also hydrate groq-key & eleven-key from localStorage if present (existing UX gap).
49
  const gk = document.getElementById('groq-key');
50
  if (gk && localStorage.getItem('groqKey')) gk.value = localStorage.getItem('groqKey');
 
119
  const groqKey = document.getElementById('groq-key').value;
120
  const elevenKey = document.getElementById('eleven-key').value;
121
  const cooldown = document.getElementById('cooldown').value;
122
+ const praise = document.getElementById('praise-toggle')?.checked ?? true;
123
 
124
  // If no API keys at all, show default message
125
  if (!groqKey && !elevenKey) {
 
368
  const groqKey = document.getElementById('groq-key').value;
369
  const elevenKey = document.getElementById('eleven-key').value;
370
  const cooldown = document.getElementById('cooldown').value;
371
+ const praise = document.getElementById('praise-toggle')?.checked ?? true;
372
 
373
  // Get voice override for selected personality
374
  const voiceOverride = voiceOverrides[selectedPersonality] || {};
 
417
  const groqKey = document.getElementById('groq-key').value;
418
  const elevenKey = document.getElementById('eleven-key').value;
419
  const cooldown = document.getElementById('cooldown').value;
420
+ const praise = document.getElementById('praise-toggle')?.checked ?? true;
421
 
422
  // Get voice override for selected personality
423
  const voiceOverride = voiceOverrides[selectedPersonality] || {};
 
449
  const groqKey = document.getElementById('groq-key').value;
450
  const elevenKey = document.getElementById('eleven-key').value;
451
  const cooldown = document.getElementById('cooldown').value;
452
+ const praise = document.getElementById('praise-toggle')?.checked ?? true;
453
  const voiceOverride = voiceOverrides[selectedPersonality] || {};
454
 
455
  try {
 
569
  document.getElementById('groq-model')?.addEventListener('change', () => getCustomSettings());
570
  document.getElementById('tts-provider')?.addEventListener('change', () => getCustomSettings());
571
  document.getElementById('groq-tts-voice')?.addEventListener('change', () => getCustomSettings());
 
572
 
573
  // Initial UI update
574
  updateDisplay();