66 lines
2.9 KiB
Python
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, ""
|