Files
Bolt-Assistant/bolt_assistant/wake.py
T

66 lines
2.9 KiB
Python

"""Finding the wake phrase in a transcript, and what was said after it.
>>> find_wake("Hey Bolt, what's the weather?", WAKE)
(True, "what's the weather?")
>>> find_wake("tighten that bolt", WAKE)
(False, '')
Rules, all aimed at not waking up on ordinary conversation:
- The phrase has to **open** what was said. Only filler may come before it
("um, hey Bolt", "okay so Bolt"), at most two words of it, so "tighten
that bolt" and "the bolt on the door" never count.
- Whole words only: "boltage" is not "bolt".
- Longest phrase wins, so "hey thunderbolt" isn't read as "hey" + garbage.
- Except *anywhere* words ("assistant"): one of those anywhere in what was
said counts, and the whole transcript is the request ("set a timer,
assistant" asks "set a timer, assistant").
"""
from __future__ import annotations
import re
from typing import Iterable, Tuple
_WORD = re.compile(r"[a-z0-9']+")
_LEAD_SLACK = 2 # filler words allowed before the wake phrase
_FILLER = {"um", "uh", "erm", "er", "so", "oh", "ok", "okay", "well", "and", "right",
"alright", "yeah", "hmm", "hm", "ah", "hi", "hey", "yo"}
def _words(text: str) -> list:
"""Lower-case words with their character spans in the original text."""
return [(m.group(0), m.start(), m.end()) for m in _WORD.finditer(text.lower())]
def find_wake(transcript: str, phrases: Iterable[str],
anywhere: Iterable[str] = ()) -> Tuple[bool, str]:
"""(heard, remainder): whether a wake phrase opens *transcript* (or an
*anywhere* word appears in it), and the request that came with it in the
same breath ("" if it was just the phrase)."""
words = _words(transcript or "")
if not words:
return False, ""
candidates = sorted({tuple(_WORD.findall(p.lower())) for p in phrases if p.strip()},
key=len, reverse=True)
for start in range(min(_LEAD_SLACK + 1, len(words))):
if any(word not in _FILLER for word, _, _ in words[:start]):
break # a real word came first: this isn't addressed to Bolt
for phrase in candidates:
span = words[start:start + len(phrase)]
if len(span) == len(phrase) and all(w == p for (w, _, _), p in zip(span, phrase)):
rest = transcript[span[-1][2]:]
return True, rest.lstrip(" \t,.;:!?-—").strip()
return _find_anywhere(transcript, words, anywhere)
def _find_anywhere(transcript: str, words: list, anywhere: Iterable[str]) -> Tuple[bool, str]:
said = [w for w, _, _ in words]
for phrase in {tuple(_WORD.findall(p.lower())) for p in anywhere if p.strip()}:
n = len(phrase)
if any(tuple(said[i:i + n]) == phrase for i in range(len(said) - n + 1)):
if all(w in phrase or w in _FILLER for w in said):
return True, "" # just the word on its own: wait for the request
return True, transcript.strip()
return False, ""