Files
fenrir/internal/app/speech_selection.go

108 lines
3.5 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"git.stormux.org/storm/fenrir/internal/config"
"git.stormux.org/storm/fenrir/internal/speech"
)
type speechChangeResult struct {
err error
done func(error)
}
func same_speech_selection(a, b config.Speech) bool {
return a.Driver == b.Driver && a.Module == b.Module && a.Language == b.Language && a.Voice == b.Voice
}
func copy_speech_selection(target *config.Speech, source config.Speech) {
target.Module, target.Language, target.Voice = source.Module, source.Language, source.Voice
}
func prepare_speech(selected config.Speech) (speech.Transport, error) {
return speech.Prepare(selected.Driver, speech_options(selected))
}
func speech_options(selected config.Speech) speech.Options {
return speech.Options{Rate: selected.Rate, Pitch: &selected.Pitch, Volume: &selected.Volume, Module: selected.Module, Language: selected.Language, Voice: selected.Voice, Hardware: speech.HardwareOptions{Device: selected.HardwareDevice, BaudRate: selected.HardwareBaudRate}}
}
func (a *application) replace_speech(prepared speech.Transport) error {
if service, ok := a.voice.Speaker.(interface{ Replace(speech.Transport) error }); ok {
return service.Replace(prepared)
}
prepared.Close()
return fmt.Errorf("speech service does not support live selection")
}
// Only the result callback mutates reader state. Preparing a silent candidate
// and committing it through the speech worker never blocks the input loop.
func (a *application) change_speech(selected config.Speech, done func(error)) {
if a.speechPending || a.saver.busy {
done(fmt.Errorf("settings are busy"))
return
}
if !a.settings.speechAvailable {
done(fmt.Errorf("speech was disabled at startup"))
return
}
a.speechPending = true
go func() {
prepared, err := a.prepareSpeech(selected)
if err == nil {
err = a.replace_speech(prepared)
}
select {
case a.speechResults <- speechChangeResult{err: err, done: done}:
case <-a.ctx.Done():
}
}()
}
func speech_choice(source string) bool {
return source == "speech-module" || source == "speech-language" || source == "speech-voice"
}
func (a *application) speech_result(result speechChangeResult) {
a.speechPending = false
result.done(result.err)
// Reapply numeric overrides made while the silent candidate was prepared.
a.voice.Configure(speech_parameters(a.settings.live))
}
type savedSpeechError struct{ err error }
func (e *savedSpeechError) Error() string {
return "settings saved; speech selection was not applied: " + e.err.Error()
}
func (e *savedSpeechError) Unwrap() error { return e.err }
func settings_committed(err error) bool {
var saved *savedSpeechError
return err == nil || errors.As(err, &saved)
}
func open_speech(ctx context.Context, selected config.Speech) (speech.Transport, config.Speech, error) {
options := speech_options(selected)
if selected.Module == "" && selected.Language == "" && selected.Voice == "" {
client, err := speech.Open_driver_context(ctx, selected.Driver, options)
return client, selected, err
}
client, err := speech.Prepare_context(ctx, selected.Driver, options)
if err == nil {
return client, selected, nil
}
slog.Warn("saved speech selection unavailable; using server default", "error", err, "at", time.Now())
selected.Module, selected.Language, selected.Voice = "", "", ""
options.Module, options.Language, options.Voice = "", "", ""
client, err = speech.Open_driver_context(ctx, selected.Driver, options)
return client, selected, err
}