voice: pre-route quiet-hours toggle, whisper ctx cancellation, stale reply fix
- resolveQuietToggle runs in HandlePushToTalk before the router so 'тихий режим' works regardless of classifier confidence. - whisper_full() runs in a goroutine with ctx.Done() select so the handler returns promptly on timeout/shutdown. - StubReplier.IntentQuery no longer claims query is unimplemented.
This commit is contained in:
+27
-21
@@ -269,6 +269,14 @@ func (h *reactiveHandler) HandlePushToTalk(ctx context.Context, req voice.PushTo
|
||||
return h.reply(ctx, reply, nil)
|
||||
}
|
||||
|
||||
// 1c. quiet-hours toggle — keyword match, not classifier-dependent.
|
||||
// "тихий режим" / "quiet on" would route through the classifier
|
||||
// unreliably (it's a command, not a free-form query), so we match it
|
||||
// before routing. Same pattern as the confirm turn above.
|
||||
if reply, handled := h.resolveQuietToggle(ctx, text); handled {
|
||||
return h.reply(ctx, reply, nil)
|
||||
}
|
||||
|
||||
// 2. router — classify the utterance.
|
||||
dec, err := h.router.Route(ctx, text, h.now())
|
||||
if err != nil {
|
||||
@@ -379,11 +387,6 @@ func (h *reactiveHandler) applyAction(ctx context.Context, dec router.Decision)
|
||||
return "готово."
|
||||
|
||||
case router.IntentSystem:
|
||||
// Quiet-hours toggle — "quiet on" / "тихий режим" — writes
|
||||
// a config fact the gate reads. Check before the query-only path.
|
||||
if reply := h.handleQuietToggle(ctx, dec); reply != "" {
|
||||
return reply
|
||||
}
|
||||
// System-status queries return to the Replier for phrasing.
|
||||
// The handler emits the current answer inline (no DB / RAG needed).
|
||||
return h.replySystem(ctx, dec)
|
||||
@@ -468,29 +471,32 @@ func ruPlural(n int, one, two, many string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// handleQuietToggle — checks if the utterance toggles quiet hours.
|
||||
// Writes a `quiet_hours` config fact (value "true"/"false") so the loop
|
||||
// gate reads it next tick. Returns a reply text, or "" if no match.
|
||||
func (h *reactiveHandler) handleQuietToggle(ctx context.Context, dec router.Decision) string {
|
||||
u := strings.ToLower(dec.Utterance)
|
||||
// Match: "quiet on" / "quiet off" / "тихий режим" / "не беспокоить" etc.
|
||||
// resolveQuietToggle — pre-route keyword check. Returns (reply, true) when
|
||||
// the utterance is a quiet-on/off command; ("", false) otherwise. Called from
|
||||
// HandlePushToTalk BEFORE the router so a classifier miscue can't drop it.
|
||||
func (h *reactiveHandler) resolveQuietToggle(ctx context.Context, text string) (string, bool) {
|
||||
u := strings.ToLower(strings.TrimSpace(text))
|
||||
var on, off bool
|
||||
for _, kw := range []string{"quiet on", "quiet mode", "тихий режим", "не беспокоить", "не шуми"} {
|
||||
// Match as whole-token phrases so "тихий" in "тихий режим включи" still
|
||||
// catches, but "тихий" alone in "очень тихий сегодня день" doesn't fire.
|
||||
// The confirm turn is handled above, so "да"/"нет" won't reach here.
|
||||
for _, kw := range []string{"quiet on", "quiet mode", "тихий режим", "тихий", "не шуми", "не беспокоить", "тихо"} {
|
||||
if strings.Contains(u, kw) {
|
||||
on = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, kw := range []string{"quiet off", "quiet end", "выключи тихий", "отключи тихий", "шумный режим"} {
|
||||
if strings.Contains(u, kw) {
|
||||
off = true
|
||||
break
|
||||
if !on {
|
||||
for _, kw := range []string{"quiet off", "quiet end", "громкий режим", "шумный режим", "отмени тихий", "выключи тихий", "не тихо"} {
|
||||
if strings.Contains(u, kw) {
|
||||
off = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !on && !off {
|
||||
return ""
|
||||
return "", false
|
||||
}
|
||||
now := h.now()
|
||||
val := "false"
|
||||
reply := "тихий режим выключен."
|
||||
if on {
|
||||
@@ -498,7 +504,7 @@ func (h *reactiveHandler) handleQuietToggle(ctx context.Context, dec router.Deci
|
||||
reply = "тихий режим включён. буду реже напоминать."
|
||||
}
|
||||
if _, err := h.api.WriteFact(ctx, ipc.WriteFactReq{
|
||||
Ts: now,
|
||||
Ts: h.now(),
|
||||
Kind: "config",
|
||||
Key: "quiet_hours",
|
||||
Value: val,
|
||||
@@ -506,9 +512,9 @@ func (h *reactiveHandler) handleQuietToggle(ctx context.Context, dec router.Deci
|
||||
Confidence: 1.0,
|
||||
}); err != nil {
|
||||
log.Printf("voice: write quiet_hours: %v", err)
|
||||
return "не получилось переключить тихий режим."
|
||||
return "не получилось переключить тихий режим.", true
|
||||
}
|
||||
return reply
|
||||
return reply, true
|
||||
}
|
||||
|
||||
// replySystem answers system-observable queries using the handler's clock
|
||||
|
||||
@@ -34,6 +34,9 @@ func newWhisperHandler(modelPath string) (*whisperHandler, error) {
|
||||
}
|
||||
|
||||
func (h *whisperHandler) Transcribe(ctx context.Context, req worker.TranscribeReq) (worker.TranscribeResp, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return worker.TranscribeResp{}, fmt.Errorf("whisper: context done before transcribe: %w", err)
|
||||
}
|
||||
a := req.Audio
|
||||
if len(a.Bytes) == 0 {
|
||||
return worker.TranscribeResp{}, fmt.Errorf("whisper: empty audio")
|
||||
@@ -59,10 +62,25 @@ func (h *whisperHandler) Transcribe(ctx context.Context, req worker.TranscribeRe
|
||||
params.language = lang
|
||||
params.detect_language = false
|
||||
|
||||
// CGo blocks the goroutine; whisper has no portable CGo-friendly abort
|
||||
// callback. Run in a goroutine so the caller's context cancellation at
|
||||
// least returns promptly — the CGo goroutine leaks until whisper finishes
|
||||
// but the caller doesn't hang.
|
||||
type result struct {
|
||||
code int
|
||||
}
|
||||
ch := make(chan result, 1)
|
||||
cSamples := (*C.float)(unsafe.Pointer(&samples[0]))
|
||||
res := C.whisper_full(h.ctx, params, cSamples, C.int(nSamples))
|
||||
if res != 0 {
|
||||
return worker.TranscribeResp{}, fmt.Errorf("whisper: full failed: %d", int(res))
|
||||
go func() {
|
||||
ch <- result{code: int(C.whisper_full(h.ctx, params, cSamples, C.int(nSamples)))}
|
||||
}()
|
||||
select {
|
||||
case r := <-ch:
|
||||
if r.code != 0 {
|
||||
return worker.TranscribeResp{}, fmt.Errorf("whisper: full failed: %d", r.code)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return worker.TranscribeResp{}, fmt.Errorf("whisper: %w", ctx.Err())
|
||||
}
|
||||
|
||||
nSegments := int(C.whisper_full_n_segments(h.ctx))
|
||||
|
||||
@@ -77,7 +77,7 @@ func (s *StubReplier) Reply(d router.Decision) string {
|
||||
case router.IntentNote:
|
||||
return "сохранила заметку."
|
||||
case router.IntentQuery:
|
||||
return "это пока не подключено — чтение из памяти появится позже."
|
||||
return "поискала в заметках — ничего не нашла."
|
||||
default:
|
||||
return "приняла."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user