Refactored into a cobra appliction

master
Drew Bednar 6 days ago
parent 5f141dbd3e
commit 5cc06fd5d4

@ -0,0 +1,31 @@
# Go Say
You write it, we say it.
## Learning goals
- Use of "os/exec" to both run and manage an external Text to Speech program and to play the output from the host's audio card
- Use of os.Pipe
- Use of https://github.com/spf13/viper and https://github.com/spf13/cobra
- Queue structure for TTS
- Cancel context for os.CommandCtx operations
- Packaging a golang binary into a .deb archive for amd64 and arm64
## Resources
- https://www.dolthub.com/blog/2022-11-28-go-os-exec-patterns/
- https://github.com/spf13/cobra/blob/v1.7.0/user_guide.md#user-guide
- Piper
- Aplay
## CLI API design
- `gosay "will say this text"`: Uses Piper TTS and aplay to read the argument aloud
- arg `-m` || `-model`: use provided model by name or path. Assumed model config resides next to model
- `echo "Will also say this text" | gosay`: Same as above but reading from stdin
- `gosay list models`: Prints a list of available models sourced from piper project. Should print an * for models already present in model directory
- `gosay list models --installed`: Prints a list of models present in the user's `GOSAY_MODELS` directory. Defaults to `${HOME}/.config/gosay/model`
`gosay download model <model name>`: Downloads the piper model and .config file from piper to `GOSAY_MODELS` directroy
`gosay set-default <model name>`: Sets model as default model and persists setting to `${HOME}/.config/gosay/config.json`
`gosay server -port 6543`: Starts an HTTP server on port (default 6543)
`gosay check`: Checks that all dependencies and configuration are setup. If an error occurs a helpful message is printed to the console for the user.

@ -0,0 +1,64 @@
package cmd
import (
"errors"
"fmt"
"io"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "gosay",
Short: "gosay is a cli tool for performing basic tts",
Long: `gosay is a cli tool for performing basic tts.
It uses Piper TTS and Aplay to perform speech to text from the cmdline.
Usage:
gosay "Your message" # Say a message directly from an argument
echo "Another message | gosay # Pipe a message to gosay
gosay < input.txt # Redirect from file
`,
Args: cobra.MaximumNArgs(1),
Run: rootRun,
}
// checkStdInSource checks if the source of stdin is a pipe, redirect or file. If true the source can be read.
func checkStdInSource() (bool, error) {
fileInfo, statErr := os.Stdin.Stat()
if statErr != nil {
return false, fmt.Errorf("could not determine stdin source. %s", statErr)
}
// perform a bitwise check on to check. True if os.Stdin is not a character device(ie. tty)
isPiped := (fileInfo.Mode() & os.ModeCharDevice) == 0
return isPiped, nil
}
// rootRun provides the default command of using gosay "your args"
func rootRun(cmd *cobra.Command, args []string) {
var err error
if !(len(args) > 0) {
isPiped, err := checkStdInSource()
if !isPiped || err != nil {
err = errors.New("Must use pipe, redirect from stdin, or provide direct argument")
cobra.CheckErr(err)
}
buf, err := io.ReadAll(os.Stdin)
cobra.CheckErr(err)
err = Say(buf)
cobra.CheckErr(err)
} else {
// assume its input is comming from stdin
err = Say([]byte(args[0]))
}
cobra.CheckErr(err)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "An error occurred while executing gosay: %s", err)
os.Exit(1)
}
}

@ -0,0 +1,34 @@
package cmd
import (
"bytes"
"os"
"os/exec"
"strings"
)
var rawPiperCmd = strings.Split("piper --model /home/toor/piper_models/en_US-ryan-medium.onnx --output-raw", " ")
var rawAplayCmd = strings.Split("aplay -r 22050 -f S16_LE -t raw -", " ")
func Say(msg []byte) error {
buf := new(bytes.Buffer)
buf.Write(msg)
r, w, err := os.Pipe()
if err != nil {
return err
}
defer r.Close()
piperCmd := exec.Command(rawPiperCmd[0], rawPiperCmd[1:]...)
piperCmd.Stdin = buf
piperCmd.Stdout = w
err = piperCmd.Start()
if err != nil {
return err
}
w.Close()
defer piperCmd.Wait()
aplayCmd := exec.Command(rawAplayCmd[0], rawAplayCmd[1:]...)
aplayCmd.Stdin = r
return aplayCmd.Run()
}

@ -1,3 +1,9 @@
module gosay
go 1.24.1
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
)

@ -0,0 +1,10 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -3,57 +3,25 @@
package main
import (
"bufio"
"bytes"
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
"gosay/cmd"
)
var rawPiperCmd = strings.Split("piper --model /home/toor/piper_models/en_US-ryan-medium.onnx --output-raw", " ")
var rawAplayCmd = strings.Split("aplay -r 22050 -f S16_LE -t raw -", " ")
func say(msg []byte) error {
buf := new(bytes.Buffer)
buf.Write(msg)
r, w, err := os.Pipe()
if err != nil {
return err
}
defer r.Close()
piperCmd := exec.Command(rawPiperCmd[0], rawPiperCmd[1:]...)
piperCmd.Stdin = buf
piperCmd.Stdout = w
err = piperCmd.Start()
if err != nil {
return err
}
w.Close()
defer piperCmd.Wait()
aplayCmd := exec.Command(rawAplayCmd[0], rawAplayCmd[1:]...)
aplayCmd.Stdin = r
return aplayCmd.Run()
}
func main() {
fmt.Println("Go say!")
reader := bufio.NewReader(os.Stdin)
for {
fmt.Println("Give me something to say: ")
msg, err := reader.ReadString('\n')
if err != nil {
slog.Error("encoutered error reading from stdin", "error", err)
continue
}
contents := strings.TrimSpace(msg)
err = say([]byte(contents))
if err != nil {
slog.Error("encountered error in say command", "error", err)
}
}
cmd.Execute()
//reader := bufio.NewReader(os.Stdin)
//for {
// fmt.Println("Give me something to say: ")
// msg, err := reader.ReadString('\n')
// if err != nil {
// slog.Error("encoutered error reading from stdin", "error", err)
// continue
// }
// contents := strings.TrimSpace(msg)
// err = say([]byte(contents))
// if err != nil {
// slog.Error("encountered error in say command", "error", err)
// }
// }
}

Loading…
Cancel
Save