Desmond-Dong commited on
Commit
581cea0
·
1 Parent(s): e6a323c

refactor: 移除硬编码动作,动画完全由JSON配置驱动

Browse files

- motion.py: 移除所有PendingAction调用,只保留set_state()
- animation_player.py: 移除未使用的ActionParams类和_actions字典
- 状态动画参数(offset)由conversation_animations.json控制

reachy_mini_ha_voice/animation_player.py CHANGED
@@ -85,19 +85,22 @@ class AnimationPlayer:
85
  self._phase_x: float = 0.0
86
  self._phase_y: float = 0.0
87
  self._phase_z: float = 0.0
88
- self._load_animations()
89
 
90
- def _load_animations(self) -> None:
91
- """Load animations from JSON file."""
92
  if not _ANIMATIONS_FILE.exists():
93
  _LOGGER.warning("Animations file not found: %s", _ANIMATIONS_FILE)
94
  return
95
  try:
96
  with open(_ANIMATIONS_FILE, "r", encoding="utf-8") as f:
97
  data = json.load(f)
 
98
  settings = data.get("settings", {})
99
  self._amplitude_scale = settings.get("amplitude_scale", 1.0)
100
  self._transition_duration = settings.get("transition_duration_s", 0.3)
 
 
101
  animations = data.get("animations", {})
102
  for name, params in animations.items():
103
  self._animations[name] = AnimationParams(
@@ -126,6 +129,7 @@ class AnimationPlayer:
126
  z_frequency_hz=params.get("z_frequency_hz", 0.0),
127
  phase_offset=params.get("phase_offset", 0.0),
128
  )
 
129
  _LOGGER.info("Loaded %d animations", len(self._animations))
130
  except Exception as e:
131
  _LOGGER.error("Failed to load animations: %s", e)
 
85
  self._phase_x: float = 0.0
86
  self._phase_y: float = 0.0
87
  self._phase_z: float = 0.0
88
+ self._load_config()
89
 
90
+ def _load_config(self) -> None:
91
+ """Load animations and actions from JSON file."""
92
  if not _ANIMATIONS_FILE.exists():
93
  _LOGGER.warning("Animations file not found: %s", _ANIMATIONS_FILE)
94
  return
95
  try:
96
  with open(_ANIMATIONS_FILE, "r", encoding="utf-8") as f:
97
  data = json.load(f)
98
+
99
  settings = data.get("settings", {})
100
  self._amplitude_scale = settings.get("amplitude_scale", 1.0)
101
  self._transition_duration = settings.get("transition_duration_s", 0.3)
102
+
103
+ # Load animations
104
  animations = data.get("animations", {})
105
  for name, params in animations.items():
106
  self._animations[name] = AnimationParams(
 
129
  z_frequency_hz=params.get("z_frequency_hz", 0.0),
130
  phase_offset=params.get("phase_offset", 0.0),
131
  )
132
+
133
  _LOGGER.info("Loaded %d animations", len(self._animations))
134
  except Exception as e:
135
  _LOGGER.error("Failed to load animations: %s", e)
reachy_mini_ha_voice/animations/conversation_animations.json CHANGED
@@ -7,27 +7,27 @@
7
  "frequency_hz": 0.0
8
  },
9
  "listening": {
10
- "description": "Attentive pose while listening to user",
11
- "pitch_amplitude_rad": 0.05,
12
- "pitch_offset_rad": -0.03,
13
  "z_amplitude_m": 0.003,
14
  "antenna_amplitude_rad": 0.2,
15
  "antenna_move_name": "both",
16
  "frequency_hz": 0.6
17
  },
18
  "thinking": {
19
- "description": "Processing/thinking animation - gentle head movement",
 
20
  "pitch_amplitude_rad": 0.03,
21
  "yaw_amplitude_rad": 0.05,
22
- "roll_amplitude_rad": 0.06,
23
- "roll_offset_rad": 0.05,
24
  "z_amplitude_m": 0.003,
25
  "antenna_amplitude_rad": 0.25,
26
  "antenna_move_name": "wiggle",
27
  "frequency_hz": 0.4
28
  },
29
  "speaking": {
30
- "description": "Speaking animation - multi-frequency natural head sway (inspired by reachy_mini_conversation_app)",
31
  "pitch_amplitude_rad": 0.08,
32
  "pitch_frequency_hz": 2.2,
33
  "yaw_amplitude_rad": 0.13,
@@ -54,10 +54,10 @@
54
  },
55
  "sad": {
56
  "description": "Sad/negative response - head droops",
57
- "pitch_amplitude_rad": 0.04,
58
  "pitch_offset_rad": 0.1,
59
- "z_amplitude_m": 0.002,
60
  "z_offset_m": -0.01,
 
61
  "antenna_amplitude_rad": 0.1,
62
  "antenna_move_name": "both",
63
  "frequency_hz": 0.3
 
7
  "frequency_hz": 0.0
8
  },
9
  "listening": {
10
+ "description": "Attentive pose while listening to user - slight forward lean",
11
+ "pitch_offset_rad": -0.05,
12
+ "pitch_amplitude_rad": 0.03,
13
  "z_amplitude_m": 0.003,
14
  "antenna_amplitude_rad": 0.2,
15
  "antenna_move_name": "both",
16
  "frequency_hz": 0.6
17
  },
18
  "thinking": {
19
+ "description": "Processing/thinking animation - head tilted with gentle sway",
20
+ "roll_offset_rad": 0.08,
21
  "pitch_amplitude_rad": 0.03,
22
  "yaw_amplitude_rad": 0.05,
23
+ "roll_amplitude_rad": 0.04,
 
24
  "z_amplitude_m": 0.003,
25
  "antenna_amplitude_rad": 0.25,
26
  "antenna_move_name": "wiggle",
27
  "frequency_hz": 0.4
28
  },
29
  "speaking": {
30
+ "description": "Speaking animation - multi-frequency natural head sway",
31
  "pitch_amplitude_rad": 0.08,
32
  "pitch_frequency_hz": 2.2,
33
  "yaw_amplitude_rad": 0.13,
 
54
  },
55
  "sad": {
56
  "description": "Sad/negative response - head droops",
 
57
  "pitch_offset_rad": 0.1,
58
+ "pitch_amplitude_rad": 0.04,
59
  "z_offset_m": -0.01,
60
+ "z_amplitude_m": 0.002,
61
  "antenna_amplitude_rad": 0.1,
62
  "antenna_move_name": "both",
63
  "frequency_hz": 0.3
reachy_mini_ha_voice/motion.py CHANGED
@@ -5,10 +5,9 @@ MovementManager for unified 5Hz control with face tracking.
5
  """
6
 
7
  import logging
8
- import math
9
  from typing import Optional
10
 
11
- from .movement_manager import MovementManager, RobotState, PendingAction
12
 
13
  _LOGGER = logging.getLogger(__name__)
14
 
@@ -25,7 +24,7 @@ class ReachyMiniMotion:
25
  self._movement_manager: Optional[MovementManager] = None
26
  self._camera_server = None # Reference to camera server for face tracking control
27
  self._is_speaking = False
28
-
29
  _LOGGER.debug("ReachyMiniMotion.__init__ called with reachy_mini=%s", reachy_mini)
30
 
31
  # Initialize movement manager if robot is available
@@ -49,7 +48,7 @@ class ReachyMiniMotion:
49
 
50
  def set_camera_server(self, camera_server):
51
  """Set the camera server for face tracking.
52
-
53
  Args:
54
  camera_server: MJPEGCameraServer instance with face tracking enabled
55
  """
@@ -111,48 +110,32 @@ class ReachyMiniMotion:
111
 
112
  def on_continue_listening(self):
113
  """Called when continuing to listen in tap conversation mode.
114
-
115
- Performs a small nod to indicate ready for next input.
116
  Non-blocking: command sent to MovementManager.
117
  """
118
  if self._movement_manager is None:
119
  return
120
 
121
  self._movement_manager.set_state(RobotState.LISTENING)
122
-
123
- # Small nod to indicate ready
124
- action = PendingAction(
125
- name="continue_nod",
126
- target_pitch=math.radians(8), # Small nod down
127
- duration=0.25,
128
- )
129
- self._movement_manager.queue_action(action)
130
- _LOGGER.debug("Reachy Mini: Continue listening (nod)")
131
 
132
  def on_thinking(self):
133
  """Called when processing speech - thinking pose.
134
 
135
  Non-blocking: command sent to MovementManager.
 
136
  """
137
  if self._movement_manager is None:
138
  return
139
 
140
  self._movement_manager.set_state(RobotState.THINKING)
141
-
142
- # Slight head tilt (thinking gesture) - avoid looking up too much
143
- action = PendingAction(
144
- name="thinking",
145
- target_pitch=math.radians(-3), # Very slight look up (was -10, too much)
146
- target_roll=math.radians(5), # Slight head tilt for "thinking" look
147
- duration=0.4,
148
- )
149
- self._movement_manager.queue_action(action)
150
  _LOGGER.debug("Reachy Mini: Thinking pose")
151
 
152
  def on_speaking_start(self):
153
  """Called when TTS starts - start speech-reactive motion.
154
 
155
  Non-blocking: command sent to MovementManager.
 
156
  """
157
  if self._movement_manager is None:
158
  _LOGGER.warning("MovementManager not initialized, skipping speaking animation")
@@ -160,15 +143,7 @@ class ReachyMiniMotion:
160
 
161
  self._is_speaking = True
162
  self._movement_manager.set_state(RobotState.SPEAKING)
163
-
164
- # Gentle nod to indicate speaking
165
- action = PendingAction(
166
- name="speaking_start",
167
- target_pitch=math.radians(5), # Slight nod down
168
- duration=0.3,
169
- )
170
- self._movement_manager.queue_action(action)
171
- _LOGGER.info("Reachy Mini: Speaking animation queued")
172
 
173
  def on_speaking_end(self):
174
  """Called when TTS ends - stop speech-reactive motion.
@@ -226,25 +201,15 @@ class ReachyMiniMotion:
226
  def wiggle_antennas(self, happy: bool = True):
227
  """Wiggle antennas to show emotion.
228
 
229
- Non-blocking: command sent to MovementManager.
230
  """
231
  if self._movement_manager is None:
232
  return
233
 
234
- # Queue antenna wiggle action
 
235
  if happy:
236
- action = PendingAction(
237
- name="antenna_happy",
238
- duration=0.2,
239
- )
240
- # Note: antenna control is handled in MovementManager state
241
- else:
242
- action = PendingAction(
243
- name="antenna_sad",
244
- duration=0.2,
245
- )
246
-
247
- self._movement_manager.queue_action(action)
248
  _LOGGER.debug("Reachy Mini: Antenna wiggle (%s)", "happy" if happy else "sad")
249
 
250
  def update_audio_loudness(self, loudness_db: float):
 
5
  """
6
 
7
  import logging
 
8
  from typing import Optional
9
 
10
+ from .movement_manager import MovementManager, RobotState
11
 
12
  _LOGGER = logging.getLogger(__name__)
13
 
 
24
  self._movement_manager: Optional[MovementManager] = None
25
  self._camera_server = None # Reference to camera server for face tracking control
26
  self._is_speaking = False
27
+
28
  _LOGGER.debug("ReachyMiniMotion.__init__ called with reachy_mini=%s", reachy_mini)
29
 
30
  # Initialize movement manager if robot is available
 
48
 
49
  def set_camera_server(self, camera_server):
50
  """Set the camera server for face tracking.
51
+
52
  Args:
53
  camera_server: MJPEGCameraServer instance with face tracking enabled
54
  """
 
110
 
111
  def on_continue_listening(self):
112
  """Called when continuing to listen in tap conversation mode.
113
+
 
114
  Non-blocking: command sent to MovementManager.
115
  """
116
  if self._movement_manager is None:
117
  return
118
 
119
  self._movement_manager.set_state(RobotState.LISTENING)
120
+ _LOGGER.debug("Reachy Mini: Continue listening")
 
 
 
 
 
 
 
 
121
 
122
  def on_thinking(self):
123
  """Called when processing speech - thinking pose.
124
 
125
  Non-blocking: command sent to MovementManager.
126
+ Animation offsets are defined in conversation_animations.json.
127
  """
128
  if self._movement_manager is None:
129
  return
130
 
131
  self._movement_manager.set_state(RobotState.THINKING)
 
 
 
 
 
 
 
 
 
132
  _LOGGER.debug("Reachy Mini: Thinking pose")
133
 
134
  def on_speaking_start(self):
135
  """Called when TTS starts - start speech-reactive motion.
136
 
137
  Non-blocking: command sent to MovementManager.
138
+ Animation is defined in conversation_animations.json.
139
  """
140
  if self._movement_manager is None:
141
  _LOGGER.warning("MovementManager not initialized, skipping speaking animation")
 
143
 
144
  self._is_speaking = True
145
  self._movement_manager.set_state(RobotState.SPEAKING)
146
+ _LOGGER.info("Reachy Mini: Speaking animation started")
 
 
 
 
 
 
 
 
147
 
148
  def on_speaking_end(self):
149
  """Called when TTS ends - stop speech-reactive motion.
 
201
  def wiggle_antennas(self, happy: bool = True):
202
  """Wiggle antennas to show emotion.
203
 
204
+ Non-blocking: antenna movement is handled by animation system.
205
  """
206
  if self._movement_manager is None:
207
  return
208
 
209
+ # Antenna movement is handled by animation system
210
+ # Set appropriate animation state
211
  if happy:
212
+ self._movement_manager.set_state(RobotState.SPEAKING)
 
 
 
 
 
 
 
 
 
 
 
213
  _LOGGER.debug("Reachy Mini: Antenna wiggle (%s)", "happy" if happy else "sad")
214
 
215
  def update_audio_loudness(self, loudness_db: float):