mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:29:37 +00:00
feat(voice): speech-interrupted latch in the TTS streaming core
mark_speech_interrupted() / take_speech_interrupted(): a one-shot, TTL'd (120s) latch plus SPEECH_INTERRUPTED_NOTE. Barge-in paths mark it when they cut live speech; the next turn's submit path pops it and prepends the note to the model-bound message — API-call local, never persisted, so history and prompt caching are untouched.
This commit is contained in:
parent
83456d0bc6
commit
393c100a92
2 changed files with 49 additions and 0 deletions
|
|
@ -53,6 +53,26 @@ class TestSentenceChunker:
|
|||
]
|
||||
|
||||
|
||||
# ── Interruption latch ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestSpeechInterruptedLatch:
|
||||
def test_take_pops_and_reports_recent_barge(self):
|
||||
ts.mark_speech_interrupted()
|
||||
assert ts.take_speech_interrupted() is True
|
||||
assert ts.take_speech_interrupted() is False # one-shot
|
||||
|
||||
def test_untouched_latch_is_false(self):
|
||||
ts._interrupted_at = None
|
||||
assert ts.take_speech_interrupted() is False
|
||||
|
||||
def test_stale_barge_expires(self, monkeypatch):
|
||||
ts.mark_speech_interrupted()
|
||||
at = ts._interrupted_at
|
||||
monkeypatch.setattr(ts.time, "monotonic", lambda: at + ts._INTERRUPT_TTL_S + 1)
|
||||
assert ts.take_speech_interrupted() is False
|
||||
|
||||
|
||||
# ── Registry + resolver ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Dict, Iterator, List, Optional
|
||||
|
||||
|
|
@ -30,6 +31,34 @@ from tools.tts_tool import _get_provider, get_env_value
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interruption latch — lets the model know it was cut off mid-speech
|
||||
# ---------------------------------------------------------------------------
|
||||
# When the user barges in on a spoken reply (talks over it, types, hits the
|
||||
# record key), the surface marks the latch; the next turn's submit path takes
|
||||
# it and prepends SPEECH_INTERRUPTED_NOTE to the model-bound message (API-call
|
||||
# local — never persisted, same as the CLI's model-switch notes). The TTL
|
||||
# keeps a stale barge from annotating an unrelated message minutes later.
|
||||
|
||||
SPEECH_INTERRUPTED_NOTE = (
|
||||
"[Note: the user interrupted your previous spoken reply before it finished.]"
|
||||
)
|
||||
_INTERRUPT_TTL_S = 120.0
|
||||
_interrupted_at: Optional[float] = None
|
||||
|
||||
|
||||
def mark_speech_interrupted() -> None:
|
||||
global _interrupted_at
|
||||
_interrupted_at = time.monotonic()
|
||||
|
||||
|
||||
def take_speech_interrupted() -> bool:
|
||||
"""Pop the latch; True when a barge happened within the TTL."""
|
||||
global _interrupted_at
|
||||
at, _interrupted_at = _interrupted_at, None
|
||||
return at is not None and time.monotonic() - at < _INTERRUPT_TTL_S
|
||||
|
||||
# Sentence boundary: after .!? followed by whitespace, or a blank line.
|
||||
SENTENCE_BOUNDARY_RE = re.compile(r"(?<=[.!?])(?:\s|\n)|(?:\n\n)")
|
||||
_THINK_BLOCK_RE = re.compile(r"<think[\s>].*?</think>", flags=re.DOTALL)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue