The face now treats /hermes/state as the agent's heartbeat: when nothing has been heard for agent_timeout_s (10 s) it logs "agent gone" and sleeps, so "sleeping" really means "no agent connected". It also logs every state change, which is how a missed wake message was found: a one-off publish before discovery matches is lost under volatile QoS. The live log showed the face flickering between listening and idle about twice a second, because the detector let go after 0.4 s. Hold is now 1.2 s so pauses between words do not drop it, and the margin 7 dB over the adaptive floor (quiet room noise peaks sit about 3 dB over it), with tests for both. Confirmed by the owner on the real display: gaze directions, and a mouth that follows their live voice proportionally. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
35 lines
1.2 KiB
Python
35 lines
1.2 KiB
Python
import pytest
|
|
|
|
from hermes_face.arbiter import Heartbeat, resolve_state
|
|
|
|
|
|
@pytest.mark.parametrize('agent,audio,expected', [
|
|
('sleeping', 'quiet', 'sleeping'),
|
|
('sleeping', 'listening', 'sleeping'), # room sound never wakes the face
|
|
('sleeping', 'speaking', 'sleeping'),
|
|
('idle', 'quiet', 'idle'),
|
|
('idle', 'listening', 'listening'),
|
|
('idle', 'speaking', 'speaking'),
|
|
('thinking', 'quiet', 'thinking'),
|
|
('thinking', 'listening', 'listening'), # a voice interrupts the thinking face
|
|
('listening', 'quiet', 'listening'),
|
|
('speaking', 'quiet', 'speaking'), # the agent says it is speaking before the audio shows it
|
|
('thinking', 'speaking', 'speaking'),
|
|
])
|
|
def test_priority(agent, audio, expected):
|
|
assert resolve_state(agent, audio) == expected
|
|
|
|
|
|
def test_heartbeat_expires_without_beats_and_recovers():
|
|
hb = Heartbeat(10.0)
|
|
assert not hb.alive(0.0) # never heard from: not alive
|
|
hb.beat(5.0)
|
|
assert hb.alive(5.0) and hb.alive(15.0)
|
|
assert not hb.alive(15.1)
|
|
hb.beat(20.0)
|
|
assert hb.alive(25.0)
|
|
|
|
|
|
def test_heartbeat_disabled_is_always_alive():
|
|
assert Heartbeat(0).alive(1e9) and Heartbeat(-1).alive(0.0)
|