Files
Bolt-Assistant/bolt_assistant/wake.py
T

51 lines
2.1 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.
"""
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]) -> Tuple[bool, str]:
"""(heard, remainder): whether a wake phrase opens *transcript*, and the
request that followed 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 False, ""