11 Commits

Author SHA1 Message Date
bel
4a86d2b6ca update readme for STT setting Quiet mode 2023-03-26 14:27:42 -06:00
bel
c663b1a12c expose PATCH /config 2023-03-26 14:17:33 -06:00
bel
af42db6803 v01.config accepts and applies json patch 2023-03-26 10:20:19 -06:00
bel
f9dc4cff9f todo 2023-03-26 10:08:04 -06:00
bel
37050f3d87 test quiet mode 2023-03-26 10:06:22 -06:00
bel
74717609ec v01.yaml has .quiet=true to cause all button pushes to become releases 2023-03-26 10:00:10 -06:00
bel
24ae45896f todo 2023-03-26 09:57:03 -06:00
bel
8b29648c50 add hotwords file for stt 2023-03-26 09:55:50 -06:00
bel
1eba008efe readme order 2023-03-26 09:49:51 -06:00
bel
d48c545030 update host.README for tts and stt integrated 2023-03-26 09:47:19 -06:00
bel
323ca466ad update configs in host.d for tts 2023-03-26 09:29:02 -06:00
15 changed files with 292 additions and 16 deletions

1
go.mod
View File

@@ -9,6 +9,7 @@ require (
)
require (
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/micmonay/keybd_event v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect

3
go.sum
View File

@@ -1,5 +1,7 @@
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
@@ -15,6 +17,7 @@ github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=

View File

@@ -36,6 +36,14 @@ See `./config.d/rusty-pipe.d`
# Server
## TTS
`cd /home/breel/Go/src/gogs.inhome.blapointe.com/tts/larynx.d; bash run.sh`
## STT
`cd /home/breel/Go/src/gogs.inhome.blapointe.com/stt.d/whisper-2023; HOTWORDS=/home/breel/Go/src/gogs.inhome.blapointe.com/mayhem-party.d/host.d/config.d/stt.d/hotwords.txt MIC_TIMEOUT=2 URL=http://localhost:17071/config HEADERS=say='Eye herd {{hotword}}' BODY='[{"op":"replace", "path":"/Quiet", "value":true}]' python3 ./hotwords.py'
## `mayhem-party`
### Configs
@@ -52,7 +60,3 @@ See `./config.d/rusty-pipe.d`
Foreground
## `stt`
TODO pipe stt detecting relevant strings -> change the `./config.d/mayhem-party.d/remap.d/live.yaml` link -> `SIGUSR1` to `mayhem-party`

View File

@@ -1,7 +1,11 @@
export DEBUG=true
export RAW_UDP=17070
export BUTTON_V01=true
export WRAP_REFRESH_ON_SIGUSR1=true
export MAIN_INTERVAL_DURATION=5ms
export RAW_UDP=17070
export PARSE_V01=true
export V01_CONFIG=./config.d/mayhem-party.d/v01.yaml
export WRAP_REFRESH_ON_SIGUSR1=true
export OUTPUT_KEYBOARD=false
export BUTTON_V01_CONFIG=./config.d/mayhem-party.d/v01.yaml

View File

@@ -1 +1 @@
players_offset_4.yaml
players_offset_2.yaml

View File

@@ -1,5 +1,6 @@
feedback:
addr: :17071
ttsurl: http://localhost:15002
users:
bel:
player: 2
@@ -19,3 +20,4 @@ players:
2: 6
3: 7
4: 8
quiet: false

View File

@@ -0,0 +1,5 @@
mario
party
yo
win
die

View File

@@ -1,15 +1,48 @@
package v01
type config struct {
Feedback struct {
import (
"encoding/json"
patch "github.com/evanphx/json-patch/v5"
)
type (
config struct {
Feedback configFeedback
Users map[string]configUser
Players []configPlayer
Quiet bool
}
configFeedback struct {
Addr string
TTSURL string
}
Users map[string]struct {
configUser struct {
Player int
Message string
}
Players []struct {
configPlayer struct {
Transformation transformation
}
)
func (cfg config) WithPatch(v interface{}) config {
originalData, _ := json.Marshal(cfg)
patchData, _ := json.Marshal(v)
patcher, err := patch.DecodePatch(patchData)
if err != nil {
return cfg
}
patchedData, err := patcher.Apply(originalData)
if err != nil {
return cfg
}
var patched config
if err := json.Unmarshal(patchedData, &patched); err != nil {
return cfg
}
return patched
}

View File

@@ -0,0 +1,79 @@
package v01
import (
"fmt"
"testing"
)
func TestConfigPatch(t *testing.T) {
cases := map[string]struct {
cfg config
patch interface{}
want config
}{
"nil patch": {
cfg: config{Quiet: true},
patch: nil,
want: config{Quiet: true},
},
"[] patch": {
cfg: config{Quiet: true},
patch: []interface{}{},
want: config{Quiet: true},
},
"set fake field": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "add", "path": "/Fake", "value": true},
},
want: config{Quiet: true},
},
"remove field": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "remove", "path": "/Quiet"},
},
want: config{Quiet: false},
},
"replace field with valid": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
},
want: config{Quiet: false},
},
"replace field with invalid": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": "teehee"},
},
want: config{Quiet: true},
},
"test and noop": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "test", "path": "/Quiet", "value": false},
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
},
want: config{Quiet: true},
},
"test and apply": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "test", "path": "/Quiet", "value": true},
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
},
want: config{Quiet: false},
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
got := c.cfg.WithPatch(c.patch)
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
t.Errorf("(%+v).Patch(%+v) want %+v, got %+v", c.cfg, c.patch, c.want, got)
}
})
}
}

View File

@@ -1,12 +1,16 @@
package v01
import (
"encoding/json"
"io"
"log"
"mayhem-party/src/device/input/wrap"
"net/http"
"os"
"sync"
"syscall"
"gopkg.in/yaml.v2"
)
func (v01 *V01) listen() {
@@ -55,6 +59,8 @@ func (v01 *V01) serveHTTP(w http.ResponseWriter, r *http.Request) {
v01.getUserFeedback(w, r)
case "/broadcast":
v01.putBroadcast(w, r)
case "/config":
v01.patchConfig(w, r)
}
}
@@ -73,6 +79,21 @@ func (v01 *V01) putBroadcast(w http.ResponseWriter, r *http.Request) {
v01.cfg.Users["broadcast"] = v
}
func (v01 *V01) patchConfig(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var v []interface{}
if err := json.Unmarshal(b, &v); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
v01.cfg = v01.cfg.WithPatch(v)
if b, err := yaml.Marshal(v01.cfg); err == nil && FlagParseV01Config != "" {
if err := os.WriteFile(FlagParseV01Config, b, os.ModePerm); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
func (v01 *V01) globalQueries(r *http.Request) {
v01.globalQuerySay(r)
v01.globalQueryRefresh(r)

View File

@@ -0,0 +1,74 @@
package v01
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"strings"
"testing"
"gopkg.in/yaml.v2"
)
func TestPatchConfig(t *testing.T) {
dir := t.TempDir()
p := path.Join(dir, t.Name()+".yaml")
cases := map[string]struct {
was config
patch string
want config
}{
"replace entire doc": {
was: config{
Feedback: configFeedback{Addr: "a", TTSURL: "a"},
Users: map[string]configUser{"a": configUser{Player: 1, Message: "a"}},
Players: []configPlayer{configPlayer{Transformation: transformation{"a": "a"}}},
Quiet: true,
},
patch: `[{"op": "replace", "path": "", "value": {
"Feedback": {"Addr": "b", "TTSURL": "b"},
"Users": {"b": {"Player": 2, "Message": "b"}},
"Players": [{"Transformation": {"b": "b"}}],
"Quiet": false
}}]`,
want: config{
Feedback: configFeedback{Addr: "b", TTSURL: "b"},
Users: map[string]configUser{"b": configUser{Player: 2, Message: "b"}},
Players: []configPlayer{configPlayer{Transformation: transformation{"b": "b"}}},
Quiet: false,
},
},
}
for name, d := range cases {
c := d
for _, usesdisk := range []bool{false, true} {
t.Run(fmt.Sprintf("%s disk=%v", name, usesdisk), func(t *testing.T) {
b, _ := yaml.Marshal(c.was)
os.WriteFile(p, b, os.ModePerm)
FlagParseV01Config = ""
if usesdisk {
FlagParseV01Config = p
}
v01 := &V01{cfg: c.was}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPatch, "/config", strings.NewReader(c.patch))
v01.patchConfig(w, r)
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
}
if usesdisk {
b, _ := os.ReadFile(p)
var got config
yaml.Unmarshal(b, &got)
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
}
}
})
}
}
}

View File

@@ -17,3 +17,4 @@ players:
b: "2"
x: "3"
y: "4"
quiet: false

View File

@@ -15,7 +15,7 @@ import (
var (
FlagDebug = os.Getenv("DEBUG") == "true"
FlagParseV01Config = os.Getenv("PARSE_V01_CONFIG")
FlagParseV01Config = os.Getenv("V01_CONFIG")
)
type (
@@ -60,7 +60,13 @@ func (v01 *V01) Read() []button.Button {
}
v01.telemetry(msg)
return v01.transform(msg).buttons()
buttons := v01.transform(msg).buttons()
if v01.cfg.Quiet {
for i := range buttons {
buttons[i].Down = false
}
}
return buttons
}
func (v01 *V01) telemetry(msg message) {

View File

@@ -1,6 +1,7 @@
package v01
import (
"context"
"testing"
)
@@ -49,3 +50,43 @@ func TestV01TransformationPipe(t *testing.T) {
})
}
}
func TestV01Quiet(t *testing.T) {
ctx, can := context.WithCancel(context.Background())
defer can()
v01 := NewV01(ctx, constSrc(`{"Y":"a", "N":"b"}`))
v01.cfg.Quiet = false
if got := v01.Read(); len(got) != 2 {
t.Error(len(got))
} else if got[0].Char != 'a' {
t.Error(got[0].Char)
} else if got[0].Down != true {
t.Error(got[0].Down)
} else if got[1].Char != 'b' {
t.Error(got[1].Char)
} else if got[1].Down != false {
t.Error(got[1].Down)
}
v01.cfg.Quiet = true
if got := v01.Read(); len(got) != 2 {
t.Error(len(got))
} else if got[0].Char != 'a' {
t.Error(got[0].Char)
} else if got[0].Down != false {
t.Error(got[0].Down)
} else if got[1].Char != 'b' {
t.Error(got[1].Char)
} else if got[1].Down != false {
t.Error(got[1].Down)
}
}
type constSrc string
func (c constSrc) Close() {}
func (c constSrc) Read() []byte {
return []byte(c)
}

View File

@@ -1,6 +1,6 @@
todo:
- https via home.blapointe and rproxy
- tts for when someone said the word via larynx docker + http.get + https://pkg.go.dev/github.com/faiface/beep@v1.1.0/wav
- json patch endpoint for config
- endpoint for v01 to start read-only mode so when hotword spoken, players are dcd
without losing players; press a hotkey that is bound to dolphin emulator pause
- todo: rotation triggers
@@ -78,3 +78,5 @@ done:
- todo: endpoint for v01 to start read-only mode so when hotword spoken, players are
dcd without losing players
ts: Sat Mar 25 23:16:47 MDT 2023
- todo: tts for when someone said the word via larynx docker + http.get + https://pkg.go.dev/github.com/faiface/beep@v1.1.0/wav
ts: Sun Mar 26 09:57:02 MDT 2023