Files
alice-backend/station/keyword.go
T

161 lines
3.6 KiB
Go

package main
import (
"context"
"encoding/binary"
"errors"
"io"
"log"
"os"
"os/exec"
"time"
)
type keywordDetector struct {
cmd *exec.Cmd
input io.WriteCloser
output *os.File
packet [1 + hop*2]byte
}
func startKeywordDetector(helper, model string) (*keywordDetector, error) {
cmd := exec.Command(helper, model)
cmd.Stderr = os.Stderr
terminateWithClient(cmd)
input, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
output, err := cmd.StdoutPipe()
if err != nil {
input.Close()
return nil, err
}
if err := cmd.Start(); err != nil {
input.Close()
output.Close()
return nil, err
}
detector := &keywordDetector{cmd: cmd, input: input, output: output.(*os.File)}
if _, err := detector.result(5 * time.Second); err != nil {
detector.close()
return nil, err
}
return detector, nil
}
func (d *keywordDetector) result(timeout time.Duration) (bool, error) {
if err := d.output.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return false, err
}
var result [1]byte
if _, err := io.ReadFull(d.output, result[:]); err != nil {
return false, err
}
if result[0] > 1 {
return false, errors.New("invalid keyword detector response")
}
return result[0] == 1, nil
}
func (d *keywordDetector) process(samples []int16) (bool, error) {
d.packet[0] = 1
for i, sample := range samples {
binary.LittleEndian.PutUint16(d.packet[1+i*2:], uint16(sample))
}
if _, err := d.input.Write(d.packet[:]); err != nil {
return false, err
}
return d.result(time.Second)
}
func (d *keywordDetector) reset() error {
if _, err := d.input.Write([]byte{0}); err != nil {
return err
}
_, err := d.result(time.Second)
return err
}
func (d *keywordDetector) close() {
d.input.Close()
d.cmd.Process.Kill()
d.cmd.Wait()
}
func runKeywordWake(live *liveReader, detector *keywordDetector, panelWake <-chan struct{}, activate func([]int16) error, once bool) error {
if detector == nil {
log.Print("Listening locally with the Mini 2 firmware spotter")
} else {
log.Print("Listening locally with microWakeWord")
}
// Keep 800 ms locally so recognition can see the whole wake word and
// any beginning of the command that overlaps the detector decision.
preroll := make([]int16, 8*rate/10)
next := 0
available := 0
muted := false
for {
frame, err := live.next()
if err != nil {
return err
}
if frame.muted || !unmuted() {
next, available = 0, 0
if detector != nil && !muted {
if err := detector.reset(); err != nil {
return err
}
muted = true
}
continue
}
muted = false
button := false
select {
case <-panelWake:
button = true
default:
}
copy(preroll[next:], frame.pcm)
next = (next + hop) % len(preroll)
available = min(available+hop, len(preroll))
detected := frame.wake
if detector != nil {
detected, err = detector.process(frame.pcm)
if err != nil {
return err
}
}
if !detected && !button {
continue
}
audio := make([]int16, 0, available)
if button {
log.Print("touch panel activated assistant")
live.discardPending()
} else {
log.Print("wake word detected")
if available == len(preroll) {
audio = append(audio, preroll[next:]...)
}
audio = append(audio, preroll[:next]...)
}
if err := activate(audio); err != nil {
log.Printf("session: %v", err)
if !errors.Is(err, errMuted) && !errors.Is(err, errNoSpeech) && !errors.Is(err, context.Canceled) {
lights.flashError()
}
} else if once {
return nil
}
live.discardPending()
next, available = 0, 0
if detector != nil {
if err := detector.reset(); err != nil {
return err
}
}
}
}