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:
kami
2026-07-03 10:56:44 +02:00
parent e00cb07658
commit b77f209686
3 changed files with 49 additions and 25 deletions
+27 -21
View File
@@ -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