159 lines
5.4 KiB
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")
|
|
}
|