// 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) }