Refactored into a cobra appliction
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=
|
Loading…
Reference in New Issue