Files
cthulhu/internal/app/preferences_preview.go

159 lines
5.4 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"
"fmt"
"strings"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/preferences"
)
func (worker *preferencesWorker) preview(ctx context.Context, request preferences.Request) preferences.Response {
if request.EchoPreview != "" {
return worker.preview_echo_voice(ctx, request)
}
next := worker.settings
if worker.editingActive {
next = worker.editing
}
next.DefaultVoice.Rate = request.Values.Rate
next.DefaultVoice.AveragePitch = request.Values.Pitch
next.DefaultVoice.Gain = request.Values.Volume
next.SpeechServerFactory = request.Values.SpeechServerFactory
next.HardwareSpeechDevice = request.Values.HardwareSpeechDevice
next.HardwareSpeechBaudRate = request.Values.HardwareSpeechBaudRate
next.SpeechOutputModule = request.Values.SpeechOutputModule
next.DefaultVoice.Family = request.Values.SpeechVoiceFamily
next.UppercaseVoice = request.Values.UppercaseVoice
next.HyperlinkVoice = request.Values.HyperlinkVoice
next.SystemVoice = request.Values.SystemVoice
// GUI feedback auditions the selected type; Cancel restores the committed
// configuration, while Apply receives the complete unmodified draft.
if kind := request.Values.VoiceType; kind != "" && kind != "default" {
next.DefaultVoice = config.EffectiveVoice(next, kind)
next.SystemVoice = config.Voice{}
}
response := worker.response("Speech settings previewed.", false)
response.Preview = true
if err := config.Validate(next); err != nil {
response.Message = "Speech settings were not previewed: " + err.Error()
return response
}
if err := worker.updateSpeech(ctx, next); err != nil {
response.Message = "Speech settings were not previewed: " + err.Error()
return response
}
response.Accepted = true
return response
}
func (worker *preferencesWorker) preview_echo_voice(ctx context.Context, request preferences.Request) preferences.Response {
response := worker.response("Speech settings previewed.", false)
response.Preview = true
var value *float64
text := ""
label := ""
switch request.EchoPreview {
case "rate":
value = request.Values.EchoVoice.Rate
label = "Key echo rate"
case "pitch":
value = request.Values.EchoVoice.AveragePitch
label = "Key echo pitch"
case "volume":
value = request.Values.EchoVoice.Gain
label = "Key echo volume"
case "synthesizer":
label = "Key echo speech synthesizer"
text = request.Values.EchoSpeechServer
if text == "" {
text = "Default"
}
case "person":
label = "Key echo person"
text = "Default"
if family := request.Values.EchoVoice.Family; family != nil {
text = family.Name
if language := strings.TrimSuffix(family.Lang+"-"+family.Dialect, "-"); language != "" {
text += " (" + language + ")"
}
}
}
if value != nil {
text = fmt.Sprintf("%.1f", *value)
}
if !request.Values.UseCustomEchoVoice || text == "" {
response.Message = "Choose a custom echo voice and a valid echo control value."
return response
}
// Preview only echo settings. Control labels and ordinary output retain
// the active profile's main voice; the draft is never committed here.
next := worker.settings
next.UseCustomEchoVoice = true
next.UseCustomEchoSpeechServer = request.Values.UseCustomEchoSpeechServer
next.EchoSpeechServer = request.Values.EchoSpeechServer
next.EchoVoice = request.Values.EchoVoice
if err := config.Validate(next); err != nil {
response.Message = "Speech settings were not previewed: " + err.Error()
return response
}
if err := worker.updateSpeech(ctx, next); err != nil {
response.Message = "Speech settings were not previewed: " + err.Error()
return response
}
if worker.announceEchoPreview != nil {
if !request.EchoPreviewFocus {
label = ""
}
worker.announceEchoPreview(label, text)
}
response.Accepted = true
return response
}
func echo_preview_speech(label, value string) speechRequest {
parts := []readAllPart{}
if label != "" {
parts = append(parts, readAllPart{Text: label, VoiceType: "default"})
}
parts = append(parts, readAllPart{Text: value, VoiceType: "echo"})
// Both parts belong to one cancellable request and wait for completion.
// No second voice can cancel the first part merely by being queued next.
return speechRequest{Text: value, Role: "text", Cause: "echo-voice-preview", ReadAll: parts}
}
func (worker *preferencesWorker) restorePreview(ctx context.Context) preferences.Response {
response := worker.response("Speech settings restored.", false)
if err := worker.updateSpeech(ctx, worker.effective_settings(worker.settings)); err != nil {
response.Message = "Speech settings could not be restored: " + err.Error()
return response
}
response.Accepted = true
response.Close = true
return response
}
func (worker *preferencesWorker) updateSpeech(ctx context.Context, next config.Settings) error {
if worker.updater == nil {
return errors.New("speech is disabled or this backend cannot change settings")
}
if updater, ok := worker.updater.(interface {
UpdateConfiguration(context.Context, config.Settings) error
}); ok {
return updater.UpdateConfiguration(ctx, next)
}
if updater, ok := worker.updater.(speechSettingsUpdater); ok {
if next.UseCustomEchoVoice {
return errors.New("this speech backend cannot use a custom echo voice")
}
return updater.UpdateSettings(ctx, speechSettingsFromConfig(next))
}
return errors.New("this backend cannot change speech settings")
}