9c7bafd5b1
It wired no PushHandler, and SendRequest discards a push frame when there is none. That was not a missing feature but a silent one. mavend routes a nudge to the voice session that spoke most recently, so once mavwaked had spoken once it WAS that session. PushToMostRecent succeeded, the dispatcher counted the nudge delivered and stopped rerouting to the away channels, and mavwaked threw the audio away. He heard nothing, anywhere. It now connects at startup rather than at the first utterance, because the dispatcher has to tell "he is not at the machine" from "he is, and she has nothing to say". The receiver redials on its own clock, since mavend restarts on every deploy. A nudge is queued, not played where it arrives. The capture loop picks it up on the next frame, so the half-duplex gate and barge-in cover it the way they cover a reply. It resets the VAD first: playback is about to suppress every frame, and a half-heard sentence would otherwise splice onto whatever he says next. A nudge arriving while one still waits replaces it, which is the contract internal/voice states for PushHandler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
80 lines
2.7 KiB
Go
80 lines
2.7 KiB
Go
package main
|
|
|
|
// The receiving half of the voice reach (V-671).
|
|
//
|
|
// mavwaked used to send and never listen. It wired no PushHandler, and
|
|
// SendRequest discards a push frame when there is none. The consequence was
|
|
// not a missing feature but a silent one: mavend routes a nudge to the voice
|
|
// session that spoke most recently, and once mavwaked had spoken once it WAS
|
|
// that session. PushToMostRecent succeeded, the dispatcher counted the nudge
|
|
// delivered and stopped rerouting to telegram and ntfy, and mavwaked threw the
|
|
// audio away. He heard nothing, anywhere.
|
|
//
|
|
// So the connection is opened at startup rather than at the first utterance,
|
|
// and it is held open. A client that has never connected has no session, and
|
|
// the dispatcher must be able to tell "he is not at the machine" from "he is,
|
|
// and she has nothing to say".
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/voice"
|
|
)
|
|
|
|
// nudgeRetry is how long to wait before dialling again after the conn ends.
|
|
// mavend restarts on every deploy, and a listener that gives up then is a
|
|
// listener that is deaf until the next reboot.
|
|
const nudgeRetry = 5 * time.Second
|
|
|
|
// nudgeHandler decodes a push and hands the audio to the session, which
|
|
// speaks it through the same player the reply path uses. It does not play
|
|
// anything itself: the half-duplex gate and barge-in live on the capture
|
|
// loop, and a nudge has to sit under both.
|
|
type nudgeHandler struct{ sess *session }
|
|
|
|
func (h *nudgeHandler) OnPush(p voice.Push) {
|
|
if p.Kind != voice.PushKindAudioNudge {
|
|
log.Printf("mavwaked: ignoring push of unknown kind %q", p.Kind)
|
|
return
|
|
}
|
|
var ap voice.AudioNudgePush
|
|
if err := json.Unmarshal(p.Params, &ap); err != nil {
|
|
log.Printf("mavwaked: nudge: decode: %v", err)
|
|
return
|
|
}
|
|
log.Printf("mavwaked: nudge from rule %q (severity %d): %q (%.2fs audio)",
|
|
ap.RuleName, ap.Severity, ap.Text, ap.Audio.Duration())
|
|
if len(ap.Audio.Bytes) == 0 {
|
|
// mavttsd was down or the text was empty. Say so rather than going
|
|
// quiet: the dispatcher already counted this one as delivered.
|
|
log.Printf("mavwaked: nudge %q carried no audio, nothing to speak", ap.RuleName)
|
|
return
|
|
}
|
|
h.sess.Nudge(ap.Audio)
|
|
}
|
|
|
|
// runNudgeReceiver keeps a push handler wired for as long as ctx lives,
|
|
// redialling whenever the conn ends. Returns when ctx is cancelled.
|
|
func runNudgeReceiver(ctx context.Context, vc *voice.Client, sess *session) {
|
|
h := &nudgeHandler{sess: sess}
|
|
for {
|
|
err := vc.RunPushReceiver(ctx, h)
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Printf("mavwaked: nudge receiver: %v", err)
|
|
} else {
|
|
log.Printf("mavwaked: voice connection ended, reconnecting in %s", nudgeRetry)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(nudgeRetry):
|
|
}
|
|
}
|
|
}
|