mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
The TTS speak/config endpoints were under /mgmt (Basic-Auth protected),
but the soundtouch-web proxy and CLI authenticated with their own
mgmt-password default (empty) while the service defaults to "change_me!",
so speaking from -web returned 401.
This was also inconsistent: the Google API key is configured via the
unauthenticated /setup/settings, and Play URL already proxies to /setup,
so gating only TTS playback behind mgmt auth made no sense. Move
/mgmt/tts/{speak,config} to /setup/tts/{speak,config} (LAN-trust, like
the rest of the setup surface), rename the handlers accordingly, and drop
the now-unused mgmt-credential plumbing from soundtouch-web and the CLI
tts command.
Verified: POST /setup/tts/speak now reaches the handler without auth
(502 only because the test speaker IP is unreachable; previously 401).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
// ttsCommand assembles the `soundtouch-cli tts …` command group. Unlike
|
|
// `speaker tts` (which talks to a speaker directly using the Google Translate
|
|
// URL), these subcommands call the AfterTouch service, which synthesizes audio
|
|
// with the configured provider (e.g. Google Cloud TTS) and plays it on a
|
|
// speaker. They require --service-url.
|
|
func ttsCommand() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "tts",
|
|
Usage: "Text-to-speech via the AfterTouch service (Google Cloud TTS or Google Translate)",
|
|
Description: "Sends text to the AfterTouch service, which synthesizes audio (or builds a\n" +
|
|
"direct URL) and plays it on a speaker via the /speaker endpoint.\n\n" +
|
|
"This differs from 'speaker tts', which talks to a speaker directly using\n" +
|
|
"the Google Translate URL. Use 'tts speak' for the service's configured\n" +
|
|
"provider (e.g. Google Cloud TTS).",
|
|
Subcommands: []*cli.Command{
|
|
ttsSpeakCmd(),
|
|
},
|
|
}
|
|
}
|
|
|
|
func ttsSpeakCmd() *cli.Command {
|
|
return &cli.Command{
|
|
Name: "speak",
|
|
Usage: "Synthesize text and play it on a speaker via the AfterTouch service",
|
|
Flags: append(CloudCommonFlags,
|
|
&cli.StringFlag{
|
|
Name: "text",
|
|
Aliases: []string{"t"},
|
|
Usage: "Text to speak",
|
|
Required: true,
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "device",
|
|
Aliases: []string{"d"},
|
|
Usage: "Target device ID (the service resolves it to an IP)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "speaker-host",
|
|
Usage: "Target speaker IP/hostname (alternative to --device)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "language",
|
|
Aliases: []string{"l"},
|
|
Usage: "Language code (provider-specific; defaults to the service setting)",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "voice",
|
|
Usage: "Voice name (Google Cloud TTS; ignored by the translate provider)",
|
|
},
|
|
&cli.IntFlag{
|
|
Name: "volume",
|
|
Aliases: []string{"v"},
|
|
Usage: "Playback volume (0-100, 0 = service default)",
|
|
},
|
|
),
|
|
Action: ttsSpeak,
|
|
}
|
|
}
|
|
|
|
func ttsSpeak(c *cli.Context) error {
|
|
serviceURL := strings.TrimRight(c.String("service-url"), "/")
|
|
device := c.String("device")
|
|
speakerHost := c.String("speaker-host")
|
|
|
|
if device == "" && speakerHost == "" {
|
|
return fmt.Errorf("one of --device or --speaker-host is required")
|
|
}
|
|
|
|
payload := map[string]interface{}{"text": c.String("text")}
|
|
if device != "" {
|
|
payload["deviceId"] = device
|
|
}
|
|
|
|
if speakerHost != "" {
|
|
payload["host"] = speakerHost
|
|
}
|
|
|
|
if l := c.String("language"); l != "" {
|
|
payload["language"] = l
|
|
}
|
|
|
|
if v := c.String("voice"); v != "" {
|
|
payload["voice"] = v
|
|
}
|
|
|
|
if c.IsSet("volume") {
|
|
payload["volume"] = c.Int("volume")
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPost, serviceURL+"/setup/tts/speak", bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("build request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("request failed: %w", err)
|
|
}
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<12))
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("service returned %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
|
|
}
|
|
|
|
PrintSuccess(fmt.Sprintf("Spoke %q", c.String("text")))
|
|
|
|
return nil
|
|
}
|