pearlyjam21's picture
Reachy Mini multimodal emotion app
d626f8b verified
Raw History Blame
2.73 kB
"""Reaction policy and SER engine behaviour."""
import random
import numpy as np
from reachy_mini_multimodal_emotion.engine.labels import EMOTIONS
from reachy_mini_multimodal_emotion.reactions import EMPATHY_MOVES, ReactionPolicy, pick_move
def probs_for(emotion, p):
x = np.full(7, (1 - p) / 6)
x[EMOTIONS.index(emotion)] = p
return x
def test_policy_needs_sustained_confidence_then_cools_down():
pol = ReactionPolicy(hold_s=1.5, cooldown_s=6.0)
happy = probs_for("happy", 0.8)
assert pol.update(happy, False, 0.0) is None # first sighting starts the hold timer
assert pol.update(happy, False, 1.0) is None # not held long enough
assert pol.update(happy, False, 1.6) == "happy" # fires after 1.5 s
assert pol.update(happy, False, 2.0) is None # restarts hold
assert pol.update(happy, False, 4.0) is None # held, but inside the 6 s cooldown
assert pol.update(happy, False, 7.7) == "happy" # cooldown over
def test_policy_ignores_neutral_idle_low_confidence_and_resets_on_switch():
pol = ReactionPolicy(hold_s=1.0, cooldown_s=0.0)
assert pol.update(probs_for("neutral", 0.9), False, 0.0) is None
assert pol.update(probs_for("neutral", 0.9), False, 5.0) is None # neutral never reacts
assert pol.update(probs_for("angry", 0.4), False, 6.0) is None # below threshold
pol.update(probs_for("angry", 0.8), False, 7.0)
assert pol.update(probs_for("angry", 0.8), True, 8.5) is None # idle interrupts
pol.update(probs_for("angry", 0.8), False, 9.0)
pol.update(probs_for("fear", 0.8), False, 9.5) # switch restarts hold
assert pol.update(probs_for("fear", 0.8), False, 10.0) is None
assert pol.update(probs_for("fear", 0.8), False, 10.6) == "fear"
def test_every_reactable_emotion_has_moves():
rng = random.Random(0)
for emo in EMOTIONS[1:]:
assert pick_move(emo, rng) in EMPATHY_MOVES[emo]
assert pick_move("neutral") is None
def test_mic_source_returns_with_continuous_audio():
"""Regression from SER_Reachy_merli_v2: a chunk is always ready, so read() must not drain 'until None'."""
import threading
from types import SimpleNamespace
from reachy_mini_multimodal_emotion.main import MicSource
class EndlessMic:
def start_recording(self): pass
def stop_recording(self): pass
def get_audio_sample(self): return np.full((256, 2), 0.1, np.float32)
src = MicSource(SimpleNamespace(media=EndlessMic()))
out = []
t = threading.Thread(target=lambda: out.append(src.read()), daemon=True)
t.start(); t.join(timeout=1.0)
assert not t.is_alive() and out[0].shape == (256,)