90 lines
2.5 KiB
Go
90 lines
2.5 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// The zero value preserves normal startup and the saved voice configuration.
|
|
// Modes last for this run only; they never alter enableSpeech or profiles.
|
|
type speechMode uint8
|
|
|
|
const (
|
|
speechModeSpeech speechMode = iota
|
|
speechModeBeeps
|
|
speechModeOff
|
|
)
|
|
|
|
func (mode speechMode) announcement() string {
|
|
switch mode {
|
|
case speechModeBeeps:
|
|
return "Speech Mode Beeps"
|
|
case speechModeOff:
|
|
return "Speech off"
|
|
default:
|
|
return "Speech Mode Speech"
|
|
}
|
|
}
|
|
|
|
func (worker *speechWorker) cycle_speech_mode() speechMode {
|
|
worker.mutex.Lock()
|
|
defer worker.mutex.Unlock()
|
|
worker.mode = (worker.mode + 1) % 3
|
|
worker.startupUntil = time.Time{}
|
|
worker.interruptLocked()
|
|
worker.logger.Debug("speech mode changed", "mode", worker.mode.announcement())
|
|
return worker.mode
|
|
}
|
|
|
|
// Capture mode and beep cancellation together so a concurrent mode change or
|
|
// key press cannot leave a beep running behind its interruption barrier.
|
|
func (worker *speechWorker) deliver_mode(ctx context.Context, request speechRequest) (bool, error) {
|
|
worker.mutex.Lock()
|
|
mode := worker.mode
|
|
if request.ForceSpeech || mode == speechModeSpeech {
|
|
worker.mutex.Unlock()
|
|
return false, nil
|
|
}
|
|
if mode == speechModeOff {
|
|
worker.mutex.Unlock()
|
|
worker.log_mode_delivery(ctx, request, mode)
|
|
return true, nil
|
|
}
|
|
beepContext, cancelBeep := context.WithCancel(ctx)
|
|
worker.beepCancel = cancelBeep
|
|
worker.mutex.Unlock()
|
|
worker.log_mode_delivery(ctx, request, mode)
|
|
defer func() {
|
|
cancelBeep()
|
|
worker.mutex.Lock()
|
|
worker.beepCancel = nil
|
|
worker.mutex.Unlock()
|
|
}()
|
|
if worker.beep == nil {
|
|
return true, errors.New("speech beep backend unavailable")
|
|
}
|
|
return true, worker.beep(beepContext)
|
|
}
|
|
|
|
func (worker *speechWorker) log_mode_delivery(ctx context.Context, request speechRequest, mode speechMode) {
|
|
// Say All depends on synth completion. In beeps/off, do not run those
|
|
// callbacks and race the reading cursor to the end of the document.
|
|
text := request.Text
|
|
if len(request.ReadAll) != 0 {
|
|
text = request.ReadAll[0].Text
|
|
}
|
|
if worker.history != nil && !request.ExcludeFromHistory && request.Cause != "speech-history" {
|
|
worker.history.add(text, request.Role)
|
|
}
|
|
if request.Role == "password text" {
|
|
text = "[REDACTED]"
|
|
}
|
|
worker.logger.DebugContext(ctx, "speech replaced by mode", "mode", mode.announcement(),
|
|
"cause", request.Cause, "role", request.Role, "utterance", text)
|
|
}
|