Files
alice-backend/station/keyword.go
T

140 lines
3.1 KiB
Go

package main
import (
"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, activate func([]int16) error, once bool) error {
log.Print("Listening locally for the Алиса keyword model")
// 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 {
samples, err := live.next()
if err != nil {
return err
}
if !unmuted() {
next, available = 0, 0
if !muted {
if err := detector.reset(); err != nil {
return err
}
muted = true
}
continue
}
muted = false
copy(preroll[next:], samples)
next = (next + hop) % len(preroll)
available = min(available+hop, len(preroll))
detected, err := detector.process(samples)
if err != nil {
return err
}
if !detected {
continue
}
log.Print("wake word detected")
audio := make([]int16, 0, available)
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) {
lights.flashError()
}
} else if once {
return nil
}
live.discardPending()
next, available = 0, 0
if err := detector.reset(); err != nil {
return err
}
}
}