Skip to content

Commit 767e064

Browse files
Hanaclaude
andcommitted
style(review): apply Task 7 code-review cleanups (expression)
From the code quality reviewer's findings: Important: - Added _HAND_POSES constant — 9-pose enum tuple (resting, reaching, holding, gesturing, clasped, writing, guarded, open, fist). Week 2's compute_expression produces 4 of these; the others are defined for NellFace animation authoring in Week 6. ExpressionVector docstring updated to reference _HAND_POSES instead of "a small enum". - test_all_params_in_zero_to_one_range now asserts float-range [0.0, 1.0] on arm_hand values (skipping hand_pose which is str). Previous test only verified type, not range — coverage gap. Minor (comments only): - Inline comment on the grief mouth coefficient (0.5 vs joy's 0.4): intentional asymmetry — mixed states land below neutral, matching human pattern where sadness dominates mouth expression. - Inline comment on body_heat = max(desire, arousal_emotion): arousal decays in hours (half-life 0.5d), desire decays in days. max() keeps the transient arousal signal from being washed out. - Docstring note on compute_expression: Week 2 only drives arousal- tier-gated arm params; arm_openness/wrist_angle/finger_spread/ reach_retract stay at baseline 0.3. NellFace Week 6 handles those via pose-driven animation. Deliberately NOT applied: - brow_furrow saturation at grief+anger extremes: known tuning issue for Week 6 ramp-weight refinement. - _clamp(0.7) vacuous-clamp on literal constant: cosmetic only. Pytest: 113/113 pass. Ruff + format both clean. This closes out all 7 emotion sub-modules. Task 8 (week close-out + tag week-2-complete) is next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 756e26b commit 767e064

2 files changed

Lines changed: 37 additions & 4 deletions

File tree

brain/emotion/expression.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@
6161
"reach_retract",
6262
)
6363

64+
# Canonical hand-pose enum. compute_expression currently produces 4 of these
65+
# (resting, reaching, holding, open); the rest are defined for NellFace
66+
# animation authoring and future consumption (gesturing for explanatory
67+
# scenes, clasped for restraint, writing for creative states, guarded for
68+
# defensive states, fist for anger).
69+
_HAND_POSES: tuple[str, ...] = (
70+
"resting",
71+
"reaching",
72+
"holding",
73+
"gesturing",
74+
"clasped",
75+
"writing",
76+
"guarded",
77+
"open",
78+
"fist",
79+
)
80+
6481

6582
@dataclass
6683
class ExpressionVector:
@@ -69,7 +86,7 @@ class ExpressionVector:
6986
Attributes:
7087
facial: {param_name: value in [0, 1]} for all 24 facial params.
7188
arm_hand: {param_name: value} for all 8 arm/hand params.
72-
hand_pose is a string from a small enum; others are floats [0, 1].
89+
hand_pose is a string from _HAND_POSES; others are floats [0, 1].
7390
arousal_tier: Pass-through of the current arousal tier.
7491
"""
7592

@@ -108,6 +125,12 @@ def compute_expression(state: EmotionalState, arousal_tier: int, energy: int) ->
108125
109126
Returns:
110127
ExpressionVector with facial + arm_hand dicts populated.
128+
129+
Note on arm/hand coverage: Week 2 drives only the arousal-tier-gated
130+
arm params (arm_tension, grip_strength, hand_pose, reach_forward).
131+
The remaining four (arm_openness, wrist_angle, finger_spread,
132+
reach_retract) stay at baseline 0.3 — NellFace Week 6 handles those
133+
via pose-driven animation rather than emotion-driven ramping.
111134
"""
112135
facial, arm_hand = _baseline()
113136

@@ -118,6 +141,9 @@ def compute_expression(state: EmotionalState, arousal_tier: int, energy: int) ->
118141
facial["cheek_raise"] = _clamp(0.3 + 0.5 * joy)
119142

120143
# Grief: lowers mouth, furrows brow, wets eyes.
144+
# Grief's mouth coefficient (0.5) is intentionally heavier than joy's (0.4):
145+
# mixed joy+grief states land below neutral, matching the human pattern
146+
# where sadness tends to dominate the mouth expression even when smiling.
121147
grief = state.emotions.get("grief", 0.0) / 10.0
122148
facial["mouth_curve"] = _clamp(facial["mouth_curve"] - 0.5 * grief)
123149
facial["brow_furrow"] = _clamp(facial["brow_furrow"] + 0.4 * grief)
@@ -144,6 +170,10 @@ def compute_expression(state: EmotionalState, arousal_tier: int, energy: int) ->
144170
facial["blush_opacity"] = _clamp(0.2 + 0.3 * tenderness)
145171

146172
# Desire / arousal: deepens blush, dilates pupils, opens lips, tenses body.
173+
# arousal decays quickly (half-life ~0.5 days, physiological); desire
174+
# decays over days (pull-toward). body_heat = max() so whichever is
175+
# currently higher drives the expression — the transient arousal signal
176+
# doesn't get washed out by slower-moving desire.
147177
desire = state.emotions.get("desire", 0.0) / 10.0
148178
arousal_emotion = state.emotions.get("arousal", 0.0) / 10.0
149179
body_heat = max(desire, arousal_emotion)

tests/unit/brain/emotion/test_expression.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,17 @@ def test_expression_vector_has_8_arm_hand_params() -> None:
7575

7676

7777
def test_all_params_in_zero_to_one_range() -> None:
78-
"""All params stay in [0, 1] even at extreme emotional inputs."""
78+
"""All float params stay in [0, 1] even at extreme emotional inputs."""
7979
vec = compute_expression(
8080
_with(anger=10.0, fear=10.0, grief=10.0), arousal_tier=TIER_DORMANT, energy=2
8181
)
8282
for value in vec.facial.values():
8383
assert 0.0 <= value <= 1.0
84-
for value in vec.arm_hand.values():
85-
assert isinstance(value, (float, str))
84+
for name, value in vec.arm_hand.items():
85+
if isinstance(value, str):
86+
# hand_pose is an enum string, not a float param
87+
continue
88+
assert 0.0 <= value <= 1.0, f"arm_hand[{name}] out of range: {value}"
8689

8790

8891
def test_to_dict_round_trips() -> None:

0 commit comments

Comments
 (0)