Skip to content

Commit

Permalink
test setup, use cobra cmd
Browse files Browse the repository at this point in the history
  • Loading branch information
sgerhardt committed Jul 24, 2024
1 parent 7d4b9a4 commit 23573c9
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 62 deletions.
75 changes: 24 additions & 51 deletions cmd/chatter/main.go
Original file line number Diff line number Diff line change
@@ -1,68 +1,41 @@
package main

import (
"flag"
"github.com/joho/godotenv"
"github.com/sgerhardt/chatter/internal/client"
"github.com/sgerhardt/chatter/internal/config"
"github.com/sgerhardt/chatter/internal/setup"
"github.com/spf13/cobra"
"log"
"net"
"net/http"
"os"
"time"
)

func main() {
run()
}

func readEnvFile() (string, string) {
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
func run() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
return os.Getenv("XI_API_KEY"), os.Getenv("OUTPUT")
}

func run() {
app, httpClient := setup()

client.New(app, httpClient).Run()
var rootCmd = &cobra.Command{
Use: "app",
Short: "an eleven labs client for text to voice",
Run: func(_ *cobra.Command, _ []string) {
cfg, c, err := setup.New(".env", voiceID, textInput, siteInput)
if err != nil {
log.Fatal(err)
}
client.New(cfg, c).Run()
},
}
var (
voiceID string
textInput string
siteInput string
)

func setup() (config.AppConfig, client.HTTP) {
var app config.AppConfig
key, dir := readEnvFile()
if key == "" {
log.Fatal("API Key not found")
}
app.APIKey = key
app.OutputDir = dir
app.CharacterRequestLimit = 10000

textInput := flag.String("t", "", "Text to convert to voice")
siteInput := flag.String("s", "", "Website to read text from")
voiceID := flag.String("v", "", "Voice ID to use")
flag.Parse()
if *voiceID == "" {
log.Fatal("Voice ID is required")
}
app.VoiceID = *voiceID

app.TextInput = *textInput
app.WebsiteURL = *siteInput
if app.TextInput != "" && app.WebsiteURL != "" {
log.Fatal("Only one of text or site can be provided")
}

httpClient := &http.Client{
Timeout: time.Second * 310,
Transport: &http.Transport{
DialContext: (&net.Dialer{Timeout: time.Second * 3}).DialContext,
TLSHandshakeTimeout: time.Second * 3,
ResponseHeaderTimeout: time.Second * 300, // eleven labs doesn't appear to respond with the header until the request completes
},
}

return app, httpClient
func init() {
rootCmd.PersistentFlags().StringVarP(&textInput, "text", "t", "", "Text to convert to voice")
rootCmd.PersistentFlags().StringVarP(&siteInput, "site", "s", "", "Website to read text from")
rootCmd.PersistentFlags().StringVarP(&voiceID, "voice", "v", "", "Voice ID to use")
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ go 1.22
require (
github.com/PuerkitoBio/goquery v1.9.2
github.com/joho/godotenv v1.5.1
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
)

require (
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/net v0.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
Expand Down
18 changes: 9 additions & 9 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type PronunciationDictionaryLocators struct {

type ElevenLabs struct {
httpClient HTTP
Config config.AppConfig
Config *config.AppConfig
}

type HTTP interface {
Expand All @@ -59,7 +59,7 @@ func (c *ElevenLabs) fileWithTimestamp() string {
return prefix + formattedTime + ".mp3"
}

func (c *ElevenLabs) Write(data []byte) (int, error) {
func (c *ElevenLabs) write(data []byte) (int, error) {
err := os.WriteFile(c.fileWithTimestamp(), data, 0644)
if err != nil {
return 0, err
Expand All @@ -68,7 +68,7 @@ func (c *ElevenLabs) Write(data []byte) (int, error) {
return len(data), nil
}

func New(cfg config.AppConfig, httpClient HTTP) *ElevenLabs {
func New(cfg *config.AppConfig, httpClient HTTP) *ElevenLabs {
return &ElevenLabs{
Config: cfg,
httpClient: httpClient,
Expand All @@ -77,26 +77,26 @@ func New(cfg config.AppConfig, httpClient HTTP) *ElevenLabs {

func (c *ElevenLabs) Run() {
if c.Config.TextInput != "" {
c.fromText()
c.handleText()
}

if c.Config.WebsiteURL != "" {
c.fromSite()
c.handleSite()
}
}

func (c *ElevenLabs) fromText() {
func (c *ElevenLabs) handleText() {
fromText, err := c.FromText(c.Config.TextInput, c.Config.VoiceID)
if err != nil {
log.Fatal(err)
}
_, err = c.Write(fromText)
_, err = c.write(fromText)
if err != nil {
log.Fatal(err)
}
}

func (c *ElevenLabs) fromSite() {
func (c *ElevenLabs) handleSite() {
texts, err := c.FromWebsite(c.Config.WebsiteURL)
if err != nil {
log.Fatal(err)
Expand All @@ -106,7 +106,7 @@ func (c *ElevenLabs) fromSite() {
if tErr != nil {
log.Fatal(tErr)
}
_, err = c.Write(fromText)
_, err = c.write(fromText)
if err != nil {
log.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestClient_GenerateVoiceFromText(t *testing.T) {
t.Parallel()
mockClient := mocks.NewHTTP(t)
tt.mockSetup(mockClient)
cfg := config.AppConfig{
cfg := &config.AppConfig{
CharacterRequestLimit: 100,
OutputDir: tt.fields.outputFilePath,
APIKey: tt.fields.apiKey,
Expand Down
2 changes: 1 addition & 1 deletion internal/client/web_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestWebReader(t *testing.T) {
t.Parallel()
mockClient := mocks.NewHTTP(t)
tt.mockSetup(mockClient)
appConfig := config.AppConfig{
appConfig := &config.AppConfig{
CharacterRequestLimit: tt.charLimit,
APIKey: "testkey",
VoiceID: "testvoice",
Expand Down
65 changes: 65 additions & 0 deletions internal/setup/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package setup

import (
"errors"
"fmt"
"github.com/joho/godotenv"
"github.com/sgerhardt/chatter/internal/client"
"github.com/sgerhardt/chatter/internal/config"
"net"
"net/http"
"os"
"strings"
"time"
)

func readEnvFile(filename string) (string, string, error) {
err := godotenv.Load(filename)
if err != nil {
return "", "", fmt.Errorf("error loading .env file: %v", err)
}
return os.Getenv("XI_API_KEY"), os.Getenv("OUTPUT"), nil
}

func New(filename string, voiceID string, textInput string, siteInput string) (*config.AppConfig, client.HTTP, error) {
if filename == "" || !strings.HasSuffix(filename, ".env") {
return nil, nil, errors.New(".env file not found")
}
key, dir, err := readEnvFile(filename)
if err != nil {
return nil, nil, fmt.Errorf("error reading env file: %w", err)
}
if key == "" {
return nil, nil, fmt.Errorf("API Key not found")
}

app := &config.AppConfig{}
app.APIKey = key
app.OutputDir = dir
app.CharacterRequestLimit = 10000

if voiceID == "" {
return nil, nil, errors.New("voice ID is required")
}
app.VoiceID = voiceID

if textInput == "" && siteInput == "" {
return nil, nil, errors.New("text or site is required")
}
if textInput != "" && siteInput != "" {
return nil, nil, errors.New("only one of text or site can be provided")
}
app.TextInput = textInput
app.WebsiteURL = siteInput

httpClient := &http.Client{
Timeout: time.Second * 310,
Transport: &http.Transport{
DialContext: (&net.Dialer{Timeout: time.Second * 3}).DialContext,
TLSHandshakeTimeout: time.Second * 3,
ResponseHeaderTimeout: time.Second * 300, // eleven labs doesn't appear to respond with the header until the request completes
},
}

return app, httpClient, nil
}
97 changes: 97 additions & 0 deletions internal/setup/setup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package setup

import (
"github.com/sgerhardt/chatter/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"testing"
)

func TestNew(t *testing.T) {
// nolint:paralleltest
// This test deals with setting os-level env vars, which is not supported in parallel tests
type expected struct {
errString string
cfg *config.AppConfig
}

tests := []struct {
name string

envFile string

expected expected
}{
{
name: "errors when .env file not present",
expected: expected{
errString: ".env file not found",
cfg: &config.AppConfig{},
},
envFile: "",
},
{
name: ".env file exists and is empty",
expected: expected{
errString: "API Key not found",
cfg: &config.AppConfig{},
},
envFile: ".env",
},
{
name: "API key is in .env file and voice ID is populated",
expected: expected{
errString: "text or site is required",
cfg: &config.AppConfig{APIKey: "123", VoiceID: "testVoiceID"},
},
envFile: ".env",
},
{
name: "API key is in .env file and voice ID is populated and text input is populated",
expected: expected{
errString: "",
cfg: &config.AppConfig{APIKey: "123", VoiceID: "testVoiceID", TextInput: "hello world!", CharacterRequestLimit: 10000},
},
envFile: ".env",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// nolint:paralleltest
// This test deals with setting os-level env vars, which is not supported in parallel tests

dir := t.TempDir()
envFile := ""
if tt.envFile != "" {
file, err := os.CreateTemp(dir, "*"+tt.envFile)
require.NoError(t, err)
_, err = file.WriteString("XI_API_KEY=" + tt.expected.cfg.APIKey)
t.Setenv("XI_API_KEY", tt.expected.cfg.APIKey)
require.NoError(t, file.Close())
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, os.Remove(file.Name()))
})
envFile = file.Name()
}

// Run test
cfg, client, err := New(envFile, tt.expected.cfg.VoiceID, tt.expected.cfg.TextInput, tt.expected.cfg.WebsiteURL)

// Assert expectations
if tt.expected.errString != "" {
require.Error(t, err)
assert.ErrorContains(t, err, tt.expected.errString)
assert.Nil(t, cfg)
assert.Nil(t, client)
return
}
require.NoError(t, err)
assert.NotNil(t, cfg)
assert.NotNil(t, client)
assert.Equal(t, tt.expected.cfg, cfg)
})
}
}

0 comments on commit 23573c9

Please sign in to comment.