"""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, ""