refactor(cli): move Cloud TTS under speaker tts-cloud, use global --host

Replaces the awkward top-level `tts speak --speaker-host` with a
`speaker tts-cloud` subcommand that sits alongside the existing
`speaker tts` and uses the global --host flag (--device still works as
an alternative). The two are now clearly related: `speaker tts` sends a
Google Translate URL straight to the speaker, while `speaker tts-cloud`
routes through the service for server-side synthesis (Cloud TTS) and
playback. --speaker-host is gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-31 22:35:31 +02:00
co-authored by Claude Opus 4.8
parent e6d5588b99
commit 22c3142a79
3 changed files with 32 additions and 42 deletions
+22 -35
View File
@@ -11,30 +11,21 @@ import (
"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 {
// ttsCloudCmd is the `speaker tts-cloud` subcommand. Unlike `speaker tts`
// (which sends a Google Translate URL straight to the speaker), this routes
// through the AfterTouch service, which synthesizes the audio with the
// configured provider (e.g. Google Cloud TTS), hosts it, and plays it on the
// speaker. It therefore needs --service-url. Target the speaker with the global
// --host, or with --device (resolved to an IP by the service).
func ttsCloudCmd() *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",
Name: "tts-cloud",
Usage: "Speak text via the AfterTouch service (Google Cloud TTS), synthesized server-side",
Description: "Routes through the AfterTouch service (requires --service-url), which\n" +
"synthesizes the audio with the configured provider, hosts it, and plays it\n" +
"on the speaker. Target the speaker with the global --host or with --device.\n\n" +
"Contrast with 'speaker tts', which sends a Google Translate URL directly to\n" +
"the speaker without involving the service.",
Flags: append(CloudCommonFlags,
&cli.StringFlag{
Name: "text",
@@ -45,11 +36,7 @@ func ttsSpeakCmd() *cli.Command {
&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)",
Usage: "Target device ID (the service resolves it to an IP); alternative to --host",
},
&cli.StringFlag{
Name: "language",
@@ -71,17 +58,17 @@ func ttsSpeakCmd() *cli.Command {
Value: "radio",
},
),
Action: ttsSpeak,
Action: ttsCloud,
}
}
func ttsSpeak(c *cli.Context) error {
func ttsCloud(c *cli.Context) error {
serviceURL := strings.TrimRight(c.String("service-url"), "/")
device := c.String("device")
speakerHost := c.String("speaker-host")
host := c.String("host") // global flag
if device == "" && speakerHost == "" {
return fmt.Errorf("one of --device or --speaker-host is required")
if device == "" && host == "" {
return fmt.Errorf("one of --host or --device is required")
}
payload := map[string]interface{}{"text": c.String("text")}
@@ -89,8 +76,8 @@ func ttsSpeak(c *cli.Context) error {
payload["deviceId"] = device
}
if speakerHost != "" {
payload["host"] = speakerHost
if host != "" {
payload["host"] = host
}
if l := c.String("language"); l != "" {
+1 -1
View File
@@ -1941,6 +1941,7 @@ func main() {
Action: playNotificationBeep,
Before: RequireHost,
},
ttsCloudCmd(),
{
Name: "help",
Usage: "Show detailed help about speaker functionality",
@@ -2315,7 +2316,6 @@ func main() {
// AfterTouch service management (sources, accounts, devices).
// Defined in cmd_cloud.go.
app.Commands = append(app.Commands, cloudCommand())
app.Commands = append(app.Commands, ttsCommand())
// Sort commands alphabetically (including subcommands and flags recursively)
sortCommands(app.Commands)
@@ -317,16 +317,19 @@ curl -X POST http://soundtouch.local:8000/setup/tts/speak \
-d '{"host":"192.0.2.100","text":"Dinner is ready"}'
```
`deviceId` may be used instead of `host` (the service resolves it to an IP from
its datastore). Optional fields: `language`, `voice`, `volume`.
`deviceId` may be used instead of `host` (the service resolves it to an IP from its datastore). Optional fields: `language`, `voice`, `volume`, and `method`
(`radio`, the default LOCAL_INTERNET_RADIO path, or `speaker`, the /speaker
notification path that ducks and resumes playback).
CLI (calls the service, not the speaker directly):
CLI (`speaker tts-cloud` routes through the service for Cloud TTS, in contrast
to `speaker tts` which sends a Google Translate URL straight to the speaker):
```bash
soundtouch-cli tts speak \
soundtouch-cli speaker tts-cloud \
--service-url http://soundtouch.local:8000 \
--speaker-host 192.0.2.100 \
--text "Dinner is ready"
--host 192.0.2.100 \
--text "Dinner is ready" \
--method speaker
```
Web UI: the TTS source view (and the Play URL view) include a "Say something…"