feat: Enhance local command handling and introduce local intents
- Refactor `run_local_command` to manage subprocesses more effectively, ensuring child processes are terminated on timeout. - Introduce `_terminate` function to handle process group termination and capture output. - Implement `_command_output` to format command results with a character limit. - Add local intent recognition in `intents.py` to handle commands like "stop", "go to sleep", and "come here" without server interaction. - Normalize user input to match local intents while stripping filler words. - Update tests to cover new local intent functionality and ensure proper command handling. - Enhance speech processing to handle abbreviations and improve spoken output clarity.
This commit is contained in:
+39
-10
@@ -67,6 +67,27 @@ _SPOKEN_SYMBOLS = {
|
||||
"=": " equals ",
|
||||
}
|
||||
|
||||
# Abbreviations a voice spells out letter by letter ("eee gee") because the
|
||||
# periods make them look like sentence boundaries. Written out instead — this
|
||||
# has to run before _UNSPEAKABLE strips anything, and the trailing \.? keeps
|
||||
# "etc" working with or without its period. Word-bounded so "vs" inside a
|
||||
# filename is left alone.
|
||||
_SPOKEN_ABBREVIATIONS = (
|
||||
(re.compile(r"\be\.g\.?(?=\s|$)", re.IGNORECASE), "for example"),
|
||||
(re.compile(r"\bi\.e\.?(?=\s|$)", re.IGNORECASE), "that is"),
|
||||
(re.compile(r"\betc\.?(?=\s|$)", re.IGNORECASE), "and so on"),
|
||||
(re.compile(r"\bvs\.?(?=\s|$)", re.IGNORECASE), "versus"),
|
||||
(re.compile(r"\baka\b", re.IGNORECASE), "also known as"),
|
||||
(re.compile(r"\bw/(?=\s)", re.IGNORECASE), "with"),
|
||||
# "PR #42" -> "PR number 42"; a bare "#" is markup and _UNSPEAKABLE drops it.
|
||||
(re.compile(r"#(?=\d)"), "number "),
|
||||
# A long option's dashes are punctuation to the eye and syllables to the ear
|
||||
# ("dash dash force"). Only the doubled form: a single hyphen has to survive
|
||||
# for "bolt-pet" and "up-to-date", and requiring a word character after it
|
||||
# keeps a "---" horizontal rule intact for _RULE to strip.
|
||||
(re.compile(r"(?<!\w)--(?=\w)"), ""),
|
||||
)
|
||||
|
||||
_MULTI_SPACE = re.compile(r"[ \t]+")
|
||||
_MULTI_PUNCT = re.compile(r"(?:\s*\.){2,}")
|
||||
|
||||
@@ -105,6 +126,10 @@ def for_speech(text: str) -> str:
|
||||
return ""
|
||||
for symbol, spoken in _PRE_SPOKEN_SYMBOLS.items():
|
||||
text = text.replace(symbol, spoken)
|
||||
# Before the markdown pass, so "#42" still has its "#" to word and a real
|
||||
# "## Heading" (no digit after the hashes) is left for _HEADING to strip.
|
||||
for pattern, spoken in _SPOKEN_ABBREVIATIONS:
|
||||
text = pattern.sub(spoken, text)
|
||||
text = _strip_markdown(text, keep_emoji=False)
|
||||
text = _URL.sub(" link ", text)
|
||||
text = _TABLE_PIPE.sub(", ", text)
|
||||
@@ -119,17 +144,21 @@ def for_speech(text: str) -> str:
|
||||
|
||||
|
||||
def is_question(text: str) -> bool:
|
||||
"""True if the spoken reply contains a question mark — the cue for the
|
||||
pet to keep listening instead of making you say the wake word again.
|
||||
"""True if the reply asks the user anything — the cue for the pet to keep
|
||||
listening instead of making you say the wake word again.
|
||||
|
||||
The check runs on the spoken form, so a '?' that only exists inside a
|
||||
stripped code block or a URL doesn't count, and trailing decoration
|
||||
(emoji, quotes, brackets) is peeled off first so "Ready to go? 🚀"
|
||||
still reads as a question."""
|
||||
spoken = for_speech(text)
|
||||
while spoken and not (spoken[-1].isalnum() or spoken[-1] in "?."):
|
||||
spoken = spoken[:-1]
|
||||
return "?" in spoken
|
||||
Anywhere in the reply counts, not only the end. An earlier version required
|
||||
a *trailing* '?' on the theory that "What time is it? It's 7:15." isn't
|
||||
waiting on an answer, and that's true of that sentence but wrong far more
|
||||
often: Bolt routinely asks first and then keeps talking ("Want me to fix
|
||||
it? I'd start with the config."), and refusing to listen there is the case
|
||||
that actually costs you a wake word. The cheap failure is the other
|
||||
direction — an unwanted extra listen ends itself on `VAD_GRACE_SECONDS` of
|
||||
silence, and `FOLLOW_UP_MAX_TURNS` caps the chain.
|
||||
|
||||
The test runs on the *spoken* form, so a '?' that only exists inside a
|
||||
stripped code block, a URL, or a markdown link target doesn't count."""
|
||||
return "?" in for_speech(text)
|
||||
|
||||
|
||||
def for_display(text: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user