142 lines
4.2 KiB
Go
142 lines
4.2 KiB
Go
// Fenrir console screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify it
|
|
// under the terms of the GNU General Public License as published by the Free
|
|
// Software Foundation, either version 3 of the License, or (at your option)
|
|
// any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
// more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along with
|
|
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
|
|
|
package speech
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
// Driver binds a stable file/CLI ID to its implementation. Availability must
|
|
// be a cheap dependency/hardware check, never connect, speak or start a service.
|
|
// Connection failures are reported by Open and never select another driver.
|
|
type Driver struct {
|
|
ID, Label string
|
|
Serial bool
|
|
Hardware bool
|
|
Check func() error
|
|
Open func(Options) (Transport, error)
|
|
Modules func(Options) ([]string, error)
|
|
Catalog func(Options) (Catalog, error)
|
|
Voices func(Options) ([]Voice, error)
|
|
}
|
|
|
|
func (d Driver) Availability() error {
|
|
if d.Check != nil {
|
|
return d.Check()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var drivers = append([]Driver{{
|
|
ID: "speech-dispatcher", Label: "Speech Dispatcher",
|
|
Open: func(options Options) (Transport, error) {
|
|
return Open(options)
|
|
},
|
|
Modules: discover_modules,
|
|
Catalog: Discover,
|
|
Voices: discover_voices,
|
|
}}, hardware_drivers()...)
|
|
|
|
func hardware_drivers() []Driver {
|
|
var result []Driver
|
|
for _, entry := range []struct{ id, label string }{
|
|
{"dectalk", "DECtalk"}, {"dectalk-usb", "DECtalk USB"},
|
|
{"litetalk", "LiteTalk"}, {"doubletalk", "DoubleTalk LT"}, {"tripletalk", "TripleTalk"},
|
|
} {
|
|
driver := Driver{ID: entry.id, Label: entry.label,
|
|
Serial: entry.id != "dectalk-usb",
|
|
Hardware: true,
|
|
Open: func(options Options) (Transport, error) { return open_hardware(entry.id, options) },
|
|
}
|
|
if entry.id == "dectalk-usb" {
|
|
driver.Check = usb_available
|
|
}
|
|
result = append(result, driver)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Drivers returns a copy; registering a completed implementation here supplies
|
|
// settings validation, UI choices, discovery and startup from the same entry.
|
|
func Drivers() []Driver { return append([]Driver(nil), drivers...) }
|
|
|
|
func Find_driver(id string) (Driver, error) {
|
|
for _, driver := range drivers {
|
|
if driver.ID == id {
|
|
return driver, nil
|
|
}
|
|
}
|
|
return Driver{}, fmt.Errorf("unknown speech driver %q", id)
|
|
}
|
|
|
|
func Open_driver(id string, options Options) (Transport, error) {
|
|
return Open_driver_context(context.Background(), id, options)
|
|
}
|
|
|
|
// Open_driver_context bounds connection setup without binding the live client to ctx.
|
|
func Open_driver_context(ctx context.Context, id string, options Options) (Transport, error) {
|
|
options.context = ctx
|
|
driver, err := Find_driver(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := Validate_driver_options(id, options); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = driver.Availability(); err != nil {
|
|
return nil, fmt.Errorf("speech driver %s unavailable: %w", id, err)
|
|
}
|
|
client, err := driver.Open(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
// discover_modules uses a separate, bounded connection. It never changes the
|
|
// active speaking client or cancels speech. Only Prepare permits daemon startup.
|
|
func discover_modules(options Options) ([]string, error) {
|
|
wire, err := connect_options(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer wire.close()
|
|
modules, err := wire.command("LIST OUTPUT_MODULES")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, module := range modules {
|
|
if !valid_module(module) || module == "" || seen[module] {
|
|
return nil, fmt.Errorf("invalid Speech Dispatcher module list")
|
|
}
|
|
seen[module] = true
|
|
}
|
|
return modules, nil
|
|
}
|
|
|
|
func valid_module(module string) bool {
|
|
for _, char := range module {
|
|
if !(char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '.' || char == '_' || char == '+' || char == '-') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|