Fixed a bug where key echo voice was getting cut off when reviewing text.
This commit is contained in:
1 parent
0834b47b55
commit
4be35fcf9c
4 files changed
+124
No files matched your search
@@ -159,6 +159,22 @@ func test_echo_speaker(t *testing.T, wrapped bool) {
|
||||
}
|
||||
check(false, "", "")
|
||||
check(true, "echo-module", "62")
|
||||
// Key names use completion notifications on the selected voice's real
|
||||
// SSIP connection, so following navigation cannot cancel an active echo.
|
||||
keySpeech := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
||||
for _, custom := range []bool{false, true} {
|
||||
if err := keySpeech.deliver(ctx, speechRequest{Text: "right", Role: "key", Cause: "key-echo", CustomEcho: custom}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := <-spoken
|
||||
wantModule, wantRate := "", ""
|
||||
if custom {
|
||||
wantModule, wantRate = "echo-module", "62"
|
||||
}
|
||||
if got.text != "right" || got.module != wantModule || got.rate != wantRate {
|
||||
t.Fatalf("key echo custom=%v: %+v", custom, got)
|
||||
}
|
||||
}
|
||||
// Preview unsaved echo selections through the real SSIP clients. Only the
|
||||
// numeric request uses that endpoint; subsequent dialog speech stays main.
|
||||
previewWorker := newPreferencesWorker(slog.New(slog.DiscardHandler), settings, speaker)
|
||||
|
||||
@@ -174,6 +174,22 @@ func (worker *speechWorker) deliver(ctx context.Context, request speechRequest)
|
||||
speak = echo.SpeakEcho
|
||||
}
|
||||
}
|
||||
// A key name and its resulting caret/focus feedback arrive separately.
|
||||
// Wait for the key name's END before the next request can cancel it,
|
||||
// including when the echo voice uses a different synthesizer. New input
|
||||
// still cancels active speech through the worker's cancellation barrier.
|
||||
if request.Cause == "key-echo" {
|
||||
if voiced, ok := worker.speaker.(interface {
|
||||
SpeakVoiceAndWait(context.Context, string, string, func()) error
|
||||
}); ok {
|
||||
if request.CustomEcho {
|
||||
kind = "echo"
|
||||
}
|
||||
speak = func(ctx context.Context, text string) error {
|
||||
return voiced.SpeakVoiceAndWait(ctx, text, kind, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
chunks := request.Chunks
|
||||
if len(chunks) == 0 {
|
||||
chunks = []string{request.Text}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
)
|
||||
|
||||
type keyEchoCompletionSpeaker struct{ controlledSpeaker }
|
||||
|
||||
func (speaker *keyEchoCompletionSpeaker) SpeakEcho(ctx context.Context, text string) error {
|
||||
return speaker.call(ctx, "queued:echo:"+text)
|
||||
}
|
||||
|
||||
func (speaker *keyEchoCompletionSpeaker) SpeakVoiceAndWait(ctx context.Context, text, voice string, _ func()) error {
|
||||
return speaker.call(ctx, "completed:"+voice+":"+text)
|
||||
}
|
||||
|
||||
func TestSpeechWorkerKeyEchoCompletesBeforeCaretSpeech(t *testing.T) {
|
||||
for _, custom := range []bool{false, true} {
|
||||
t.Run(fmt.Sprint(custom), func(t *testing.T) {
|
||||
speaker := &keyEchoCompletionSpeaker{controlledSpeaker{calls: make(chan speechCall, 8)}}
|
||||
worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
||||
if err := worker.Start(t.Context()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { worker.Stop(context.Background()) })
|
||||
worker.Enqueue(speechRequest{Text: "right", Role: "key", Cause: "key-echo", CustomEcho: custom})
|
||||
close(expectSpeechCall(t, &speaker.controlledSpeaker, "cancel").done)
|
||||
voice := ""
|
||||
if custom {
|
||||
voice = "echo"
|
||||
}
|
||||
echo := expectSpeechCall(t, &speaker.controlledSpeaker, "completed:"+voice+":right")
|
||||
worker.Enqueue(speechRequest{Text: "h", Role: "entry", Cause: "object:text-caret-moved", Character: true})
|
||||
select {
|
||||
case call := <-speaker.calls:
|
||||
t.Fatalf("caret output overtook key echo completion: %s", call.text)
|
||||
default:
|
||||
}
|
||||
close(echo.done)
|
||||
close(expectSpeechCall(t, &speaker.controlledSpeaker, "cancel").done)
|
||||
close(expectSpeechCall(t, &speaker.controlledSpeaker, "h").done)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpeechWorkerKeyEchoWaitStillAllowsInputInterruption(t *testing.T) {
|
||||
speaker := &keyEchoCompletionSpeaker{controlledSpeaker{calls: make(chan speechCall, 8)}}
|
||||
worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
||||
if err := worker.Start(t.Context()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { worker.Stop(context.Background()) })
|
||||
worker.Enqueue(speechRequest{Text: "right", Role: "key", Cause: "key-echo", CustomEcho: true})
|
||||
close(expectSpeechCall(t, &speaker.controlledSpeaker, "cancel").done)
|
||||
echo := expectSpeechCall(t, &speaker.controlledSpeaker, "completed:echo:right")
|
||||
worker.Enqueue(speechRequest{Text: "stale character", Cause: "object:text-caret-moved"})
|
||||
if !worker.InterruptForInput(input.Event{Pressed: true, Name: "Left", Timestamp: 100}) {
|
||||
t.Fatal("new input did not interrupt key echo")
|
||||
}
|
||||
close(expectSpeechCall(t, &speaker.controlledSpeaker, "cancel").done)
|
||||
close(echo.done) // Backend reports cancellation of the old echo.
|
||||
worker.Enqueue(speechRequest{Text: "left", Role: "key", Cause: "key-echo", CustomEcho: true})
|
||||
for {
|
||||
select {
|
||||
case call := <-speaker.calls:
|
||||
close(call.done)
|
||||
if call.text == "completed:echo:left" {
|
||||
return
|
||||
}
|
||||
if call.text != "cancel" {
|
||||
t.Fatalf("stale speech survived new input: %s", call.text)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("new key echo did not resume after interruption")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user