108 lines
3.5 KiB
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
|
|
}
|