// 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 }