85 lines
2.1 KiB
Go
85 lines
2.1 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package speech
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
type Voice struct{ Name, Language, Variant string }
|
|
type Catalog map[string][]Voice
|
|
|
|
// Discover uses its own connection, so slow modules never hold up speech.
|
|
// The empty module selects the server default.
|
|
func Discover(ctx context.Context, socket string) (Catalog, error) {
|
|
endpoint, err := resolveServerAddress(socket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn, err := (&net.Dialer{Timeout: connectTimeout}).DialContext(ctx, endpoint.network, endpoint.address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
client := &Client{conn: conn, reader: bufio.NewReader(conn), ioTimeout: ioTimeout}
|
|
query := func(command string) ([]string, error) {
|
|
finish, err := prepareIO(ctx, conn, ioTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer finish()
|
|
if _, err = conn.Write([]byte(command + "\r\n")); err != nil {
|
|
return nil, err
|
|
}
|
|
reply, err := client.readReply()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if reply.code < 200 || reply.code >= 300 {
|
|
return nil, fmt.Errorf("voice discovery: %s", reply.text)
|
|
}
|
|
return reply.data, nil
|
|
}
|
|
modules, err := query("LIST OUTPUT_MODULES")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
catalog := Catalog{}
|
|
for _, module := range append([]string{""}, modules...) {
|
|
if strings.ContainsAny(module, "\r\n\x00") {
|
|
continue
|
|
}
|
|
if module != "" {
|
|
if err := client.sendCommand(ctx, []string{"SET", "SELF", "OUTPUT_MODULE", module}); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
rows, err := query("LIST SYNTHESIS_VOICES")
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return catalog, ctx.Err()
|
|
}
|
|
continue
|
|
}
|
|
catalog[module] = nil
|
|
for _, row := range rows {
|
|
fields := strings.Split(row, "\t")
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
voice := Voice{Name: fields[0], Language: fields[1]}
|
|
if len(fields) > 2 {
|
|
voice.Variant = fields[2]
|
|
}
|
|
catalog[module] = append(catalog[module], voice)
|
|
}
|
|
}
|
|
return catalog, nil
|
|
}
|