...
This commit is contained in:
@@ -81,6 +81,44 @@ def test_petctl_nap_also_flips_the_controller_state(ctrl):
|
||||
|
||||
# ── barge-in ────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_the_interrupt_log_reports_what_fired_not_the_reset_counters(monkeypatch, ctrl):
|
||||
"""_speak resets the detector after playback so the pet's own voice
|
||||
doesn't linger in the wake model's window. Reading the stats after that
|
||||
reset reports 0.000 at frame 0 for every interruption, which is worse
|
||||
than no instrumentation — it looks like hard evidence and isn't."""
|
||||
from bolt_pet.audio import barge_in as barge_in_mod
|
||||
|
||||
class Detector(barge_in_mod.WakeWordBargeIn):
|
||||
def __init__(self):
|
||||
self._frames, self._peak, self._last, self._last_threshold = 0, 0.0, 0.0, 0.5
|
||||
self._frame_len = 1280
|
||||
self.reset_calls = 0
|
||||
|
||||
def reset(self):
|
||||
self.reset_calls += 1
|
||||
self._frames, self._peak, self._last = 0, 0.0, 0.0
|
||||
|
||||
detector = Detector()
|
||||
ctrl._barge_in = detector
|
||||
logs = _capture(ctrl.log)
|
||||
|
||||
def interrupted_playback(text, on_error=None, should_stop=None):
|
||||
# What really happens: frames get scored during playback, then one
|
||||
# clears the threshold and playback aborts.
|
||||
detector._frames, detector._peak, detector._last = 7, 0.81, 0.81
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(controller_mod.tts, "speak", interrupted_playback)
|
||||
|
||||
ctrl._speak("a very long explanation")
|
||||
|
||||
interrupted = next(m for m in logs if "Interrupted" in m)
|
||||
assert "0.810" in interrupted and "frame 7" in interrupted
|
||||
# Once before playback (clear the window) and once after (drop the pet's
|
||||
# own voice) — the point is that the *read* happens between them.
|
||||
assert detector.reset_calls == 2
|
||||
|
||||
|
||||
def test_interrupted_playback_queues_an_immediate_next_turn(monkeypatch, ctrl):
|
||||
logs = _capture(ctrl.log)
|
||||
monkeypatch.setattr(controller_mod.tts, "speak",
|
||||
@@ -99,6 +137,110 @@ def test_uninterrupted_playback_does_not_queue_a_turn(monkeypatch, ctrl):
|
||||
assert not ctrl._talk_now.is_set()
|
||||
|
||||
|
||||
# ── follow-up listening ─────────────────────────────────────────────────────
|
||||
|
||||
@pytest.fixture
|
||||
def spoke(monkeypatch):
|
||||
"""Playback that always completes, so only the follow-up rule decides
|
||||
whether another turn is queued."""
|
||||
monkeypatch.setattr(controller_mod.tts, "speak",
|
||||
lambda text, on_error=None, should_stop=None: True)
|
||||
|
||||
|
||||
def test_a_reply_ending_in_a_question_keeps_listening(spoke, ctrl):
|
||||
logs = _capture(ctrl.log)
|
||||
|
||||
ctrl._speak("You're still in ~/Documents/bolt-pet. Ready to run a command?")
|
||||
|
||||
assert ctrl._talk_now.is_set() # no wake word needed for the answer
|
||||
assert ctrl._pending_follow_up # and the next turn knows it's an answer
|
||||
assert any("listening for your answer" in message for message in logs)
|
||||
|
||||
|
||||
def test_a_statement_does_not_keep_listening(spoke, ctrl):
|
||||
ctrl._speak("It's 7:15 AM on July 23, 2026.")
|
||||
assert not ctrl._talk_now.is_set()
|
||||
assert not ctrl._pending_follow_up
|
||||
|
||||
|
||||
def test_a_question_in_passing_does_not_count(spoke, ctrl):
|
||||
"""Only a reply that *ends* on a question is waiting for an answer."""
|
||||
ctrl._speak("What time is it? It's 7:15 AM.")
|
||||
assert not ctrl._talk_now.is_set()
|
||||
|
||||
|
||||
def test_follow_ups_stop_at_the_cap(spoke, monkeypatch, ctrl):
|
||||
monkeypatch.setattr(controller_mod.config, "FOLLOW_UP_MAX_TURNS", 2)
|
||||
logs = _capture(ctrl.log)
|
||||
|
||||
for _ in range(2):
|
||||
ctrl._speak("Want me to keep going?")
|
||||
ctrl._talk_now.clear()
|
||||
assert ctrl._follow_ups == 2
|
||||
|
||||
ctrl._speak("Want me to keep going?")
|
||||
|
||||
assert not ctrl._talk_now.is_set() # chain broken until you re-trigger it
|
||||
assert any("Follow-up limit reached" in message for message in logs)
|
||||
|
||||
|
||||
def test_the_cap_is_not_announced_on_ordinary_replies(spoke, monkeypatch, ctrl):
|
||||
monkeypatch.setattr(controller_mod.config, "FOLLOW_UP_MAX_TURNS", 1)
|
||||
ctrl._follow_ups = 1
|
||||
logs = _capture(ctrl.log)
|
||||
|
||||
ctrl._speak("Done — the file is saved.")
|
||||
|
||||
assert not any("Follow-up limit" in message for message in logs)
|
||||
|
||||
|
||||
def test_starting_a_turn_yourself_resets_the_chain(spoke, monkeypatch, ctrl):
|
||||
monkeypatch.setattr(controller_mod.mic, "record_utterance",
|
||||
lambda *a, **kw: None) # you said nothing
|
||||
ctrl._follow_ups = 3
|
||||
|
||||
ctrl._handle_conversation_turn()
|
||||
|
||||
assert ctrl._follow_ups == 0
|
||||
|
||||
|
||||
def test_an_answered_question_gets_a_longer_grace_period(spoke, monkeypatch, ctrl):
|
||||
"""You were just asked something — you get longer to think than when you
|
||||
deliberately said the wake word."""
|
||||
grace = []
|
||||
monkeypatch.setattr(controller_mod.mic, "record_utterance",
|
||||
lambda *a, **kw: grace.append(kw.get("grace_s")) or None)
|
||||
|
||||
ctrl._handle_conversation_turn() # you started this one
|
||||
ctrl._pending_follow_up = True
|
||||
ctrl._handle_conversation_turn() # this one answers a question
|
||||
|
||||
assert grace == [None, controller_mod.config.FOLLOW_UP_GRACE_SECONDS]
|
||||
|
||||
|
||||
def test_muting_stops_follow_ups(spoke, ctrl):
|
||||
ctrl._muted = True
|
||||
ctrl._speak("Shall I continue?")
|
||||
assert not ctrl._talk_now.is_set() # mute means don't listen, question or not
|
||||
|
||||
|
||||
def test_follow_up_can_be_turned_off(spoke, monkeypatch, ctrl):
|
||||
monkeypatch.setattr(controller_mod.config, "FOLLOW_UP_LISTEN", False)
|
||||
ctrl._speak("Shall I continue?")
|
||||
assert not ctrl._talk_now.is_set()
|
||||
|
||||
|
||||
def test_being_interrupted_restarts_the_chain(monkeypatch, ctrl):
|
||||
monkeypatch.setattr(controller_mod.tts, "speak",
|
||||
lambda text, on_error=None, should_stop=None: False)
|
||||
ctrl._follow_ups = 3
|
||||
|
||||
ctrl._speak("a very long explanation")
|
||||
|
||||
assert ctrl._follow_ups == 0 # you're clearly engaged
|
||||
assert ctrl._talk_now.is_set()
|
||||
|
||||
|
||||
def test_speech_is_recorded_in_the_history(monkeypatch, ctrl):
|
||||
monkeypatch.setattr(controller_mod.tts, "speak",
|
||||
lambda text, on_error=None, should_stop=None: True)
|
||||
|
||||
Reference in New Issue
Block a user