Skip to content

Commit 164ce42

Browse files
committed
Added text-to-speech method
1 parent a95f6ae commit 164ce42

File tree

10 files changed

+413
-92
lines changed

10 files changed

+413
-92
lines changed

pkg/client/clientopts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func OptSkipVerify() ClientOpt {
104104
func OptHeader(key, value string) ClientOpt {
105105
return func(client *Client) error {
106106
if client.headers == nil {
107-
client.headers = make(map[string]string, 1)
107+
client.headers = make(map[string]string, 2)
108108
}
109109
if key == "" {
110110
return ErrBadParameter.With("OptHeader")

pkg/client/transport.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"net/http"
99
"time"
10+
// Packages
1011
)
1112

1213
///////////////////////////////////////////////////////////////////////////////
@@ -62,15 +63,20 @@ func (transport *logtransport) RoundTrip(req *http.Request) (*http.Response, err
6263
fmt.Fprintln(transport.w, " => Error:", err)
6364
} else {
6465
fmt.Fprintln(transport.w, " =>", resp.Status)
65-
66+
for k, v := range resp.Header {
67+
fmt.Fprintf(transport.w, " => %v: %q\n", k, v)
68+
}
6669
// If verbose is switched on, read the body
67-
if transport.v && resp.Body != nil {
68-
defer resp.Body.Close()
69-
body, err := io.ReadAll(resp.Body)
70-
if err == nil {
71-
fmt.Fprintln(transport.w, " ", string(body))
70+
if transport.v && resp.Body != nil && resp.ContentLength > 0 {
71+
contentType := resp.Header.Get("Content-Type")
72+
if contentType == ContentTypeJson || contentType == ContentTypeTextPlain {
73+
defer resp.Body.Close()
74+
body, err := io.ReadAll(resp.Body)
75+
if err == nil {
76+
fmt.Fprintln(transport.w, " ", string(body))
77+
}
78+
resp.Body = io.NopCloser(bytes.NewReader(body))
7279
}
73-
resp.Body = io.NopCloser(bytes.NewReader(body))
7480
}
7581
}
7682

pkg/elevenlabs/client.go

Lines changed: 1 addition & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ elevenlabs implements an API client for elevenlabs (https://elevenlabs.io/docs/a
44
package elevenlabs
55

66
import (
7-
"net/http"
8-
97
// Packages
108
"github.com/mutablelogic/go-client/pkg/client"
119
)
@@ -33,77 +31,16 @@ const (
3331
SampleU8 = "ulaw_8000" // μ-law format (sometimes written mu-law, often approximated as u-law) with 8kHz sample rate
3432
)
3533

36-
///////////////////////////////////////////////////////////////////////////////
37-
// SCHEMA
38-
39-
type Request struct {
40-
client.Payload `json:"-"`
41-
Model string `json:"model_id"`
42-
Text string `json:"text"`
43-
VoiceSettings struct {
44-
SimilarityBoost float64 `json:"similarity_boost"`
45-
Stability float64 `json:"stability"`
46-
Style string `json:"style,omitempty"`
47-
UseSpeakerBoost bool `json:"use_speaker_boost"`
48-
} `json:"voice_settings"`
49-
}
50-
51-
func (r Request) Method() string {
52-
return http.MethodGet
53-
}
54-
55-
func (r Request) Type() string {
56-
return ""
57-
}
58-
59-
func (r Request) Accept() string {
60-
return client.ContentTypeJson
61-
}
62-
63-
type VoicesResponse struct {
64-
Voices []Voice `json:"voices"`
65-
}
66-
67-
type Voice struct {
68-
Id string `json:"voice_id"`
69-
Name string `json:"name"`
70-
Description string `json:"description,omitempty"`
71-
PreviewUrl string `json:"preview_url,omitempty"`
72-
Category string `json:"category,omitempty"`
73-
}
74-
7534
///////////////////////////////////////////////////////////////////////////////
7635
// LIFECYCLE
7736

7837
func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) {
7938
// Create client
80-
client, err := client.New(append(opts, client.OptEndpoint(endPoint), client.OptHeader("Xi-Api-Key", ApiKey))...)
39+
client, err := client.New(append(opts, client.OptEndpoint(endPoint), client.OptHeader("xi-api-key", ApiKey))...)
8140
if err != nil {
8241
return nil, err
8342
}
8443

8544
// Return the client
8645
return &Client{client}, nil
8746
}
88-
89-
///////////////////////////////////////////////////////////////////////////////
90-
// PUBLIC METHODS
91-
92-
// Return current set of voices
93-
func (c *Client) Voices() ([]Voice, error) {
94-
var request Request
95-
var response VoicesResponse
96-
if err := c.Do(request, &response, client.OptPath("voices")); err != nil {
97-
return nil, err
98-
}
99-
return response.Voices, nil
100-
}
101-
102-
// Get returns the current IP address from the API
103-
func (c *Client) TextToSpeech(Text string) ([]byte, error) {
104-
var request Request
105-
if err := c.Do(request, nil, client.OptPath("text-to-speech", "test")); err != nil {
106-
return nil, err
107-
}
108-
return nil, nil
109-
}

pkg/elevenlabs/client_test.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package elevenlabs_test
22

33
import (
4-
"encoding/json"
54
"os"
65
"testing"
76

@@ -11,28 +10,12 @@ import (
1110
assert "github.com/stretchr/testify/assert"
1211
)
1312

14-
func Test_elevenlabs_001(t *testing.T) {
13+
func Test_client_001(t *testing.T) {
1514
assert := assert.New(t)
1615
client, err := elevenlabs.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
1716
assert.NoError(err)
1817
assert.NotNil(client)
19-
response, err := client.Voices()
20-
assert.NoError(err)
21-
assert.NotEmpty(response)
22-
data, err := json.MarshalIndent(response, "", " ")
23-
assert.NoError(err)
24-
t.Log(string(data))
25-
}
26-
27-
func XX_Test_elevenlabs_002(t *testing.T) {
28-
assert := assert.New(t)
29-
client, err := elevenlabs.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
30-
assert.NoError(err)
31-
assert.NotNil(client)
32-
response, err := client.TextToSpeech("test")
33-
assert.NoError(err)
34-
assert.NotEmpty(response)
35-
t.Log(response)
18+
t.Log(client)
3619
}
3720

3821
///////////////////////////////////////////////////////////////////////////////
@@ -44,5 +27,5 @@ func GetApiKey(t *testing.T) string {
4427
t.Skip("ELEVENLABS_API_KEY not set")
4528
t.SkipNow()
4629
}
47-
return ""
30+
return key
4831
}

pkg/elevenlabs/opts.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package elevenlabs
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/djthorpe/go-errors"
7+
)
8+
9+
///////////////////////////////////////////////////////////////////////////////
10+
// TYPES
11+
12+
type TextToSpeechOpt func(*textToSpeechRequest) error
13+
type TextToSpeechFormat string
14+
15+
///////////////////////////////////////////////////////////////////////////////
16+
// GLOBALS
17+
18+
const (
19+
MP3_44100_64 TextToSpeechFormat = "mp3_44100_64" // mp3 with 44.1kHz sample rate at 64kbps
20+
MP3_44100_96 TextToSpeechFormat = "mp3_44100_96" // mp3 with 44.1kHz sample rate at 96kbps
21+
MP3_44100_128 TextToSpeechFormat = "mp3_44100_128" // default output format, mp3 with 44.1kHz sample rate at 128kbps
22+
MP3_44100_192 TextToSpeechFormat = "mp3_44100_192" // mp3 with 44.1kHz sample rate at 192kbps
23+
PCM_16000 TextToSpeechFormat = "pcm_16000" // PCM format (S16LE) with 16kHz sample rate
24+
PCM_22050 TextToSpeechFormat = "pcm_22050" // PCM format (S16LE) with 22.05kHz sample rate
25+
PCM_24000 TextToSpeechFormat = "pcm_24000" // PCM format (S16LE) with 24kHz sample rate
26+
PCM_44100 TextToSpeechFormat = "pcm_44100" // PCM format (S16LE) with 44.1kHz sample rate
27+
ULAW_8000 TextToSpeechFormat = "ulaw_8000" // μ-law format (sometimes written mu-law, often approximated as u-law) with 8kHz sample rate
28+
)
29+
30+
///////////////////////////////////////////////////////////////////////////////
31+
// PUBLIC METHODS
32+
33+
func OptOutput(format TextToSpeechFormat) TextToSpeechOpt {
34+
return func(req *textToSpeechRequest) error {
35+
req.Query.Set("output_format", string(format))
36+
return nil
37+
}
38+
}
39+
40+
func OptOptimizeStreamingLatency(value uint8) TextToSpeechOpt {
41+
return func(req *textToSpeechRequest) error {
42+
req.Query.Set("optimize_streaming_latency", fmt.Sprint(value))
43+
return nil
44+
}
45+
}
46+
47+
func OptModel(id string) TextToSpeechOpt {
48+
return func(req *textToSpeechRequest) error {
49+
if id == "" {
50+
return errors.ErrBadParameter.With("OptModel: id")
51+
}
52+
req.ModelId = id
53+
return nil
54+
}
55+
}
56+
57+
func OptVoiceSettings(settings VoiceSettings) TextToSpeechOpt {
58+
return func(req *textToSpeechRequest) error {
59+
req.Settings = settings
60+
return nil
61+
}
62+
}

pkg/elevenlabs/text-to-speech.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package elevenlabs
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"net/url"
7+
8+
"github.com/djthorpe/go-errors"
9+
"github.com/mutablelogic/go-client/pkg/client"
10+
)
11+
12+
///////////////////////////////////////////////////////////////////////////////
13+
// SCHEMA
14+
15+
type textToSpeechRequest struct {
16+
client.Payload `json:"-"`
17+
Query url.Values `json:"-"`
18+
Text string `json:"text"`
19+
ModelId string `json:"model_id,omitempty"`
20+
Settings VoiceSettings `json:"voice_settings,omitempty"`
21+
}
22+
23+
type textToSpeechResponse struct {
24+
Type string
25+
Bytes []byte
26+
}
27+
28+
///////////////////////////////////////////////////////////////////////////////
29+
// PUBLIC METHODS
30+
31+
// Return speech audio as bytes for text and voice
32+
func (c *Client) TextToSpeech(text, Id string, opts ...TextToSpeechOpt) ([]byte, error) {
33+
var request textToSpeechRequest
34+
var response textToSpeechResponse
35+
36+
// Parse parameters
37+
if text == "" {
38+
return nil, errors.ErrBadParameter.With("text")
39+
} else {
40+
request.Text = text
41+
}
42+
if Id == "" {
43+
return nil, errors.ErrBadParameter.With("Id")
44+
}
45+
46+
// Apply options
47+
request.Query = make(url.Values)
48+
for _, opt := range opts {
49+
if err := opt(&request); err != nil {
50+
return nil, err
51+
}
52+
}
53+
54+
// Perform the request
55+
requestopts := []client.RequestOpt{
56+
client.OptPath("text-to-speech", Id),
57+
client.OptQuery(request.Query),
58+
}
59+
if err := c.Do(request, &response, requestopts...); err != nil {
60+
return nil, err
61+
}
62+
63+
// Return success
64+
return response.Bytes, nil
65+
}
66+
67+
///////////////////////////////////////////////////////////////////////////////
68+
// REQUEST METHODS
69+
70+
func (textToSpeechRequest) Method() string {
71+
return http.MethodPost
72+
}
73+
74+
func (textToSpeechRequest) Type() string {
75+
return client.ContentTypeJson
76+
}
77+
78+
func (textToSpeechRequest) Accept() string {
79+
return client.ContentTypeBinary
80+
}
81+
82+
///////////////////////////////////////////////////////////////////////////////
83+
// RESPONSE METHODS
84+
85+
func (resp *textToSpeechResponse) Unmarshal(mimetype string, r io.Reader) error {
86+
resp.Type = mimetype
87+
if data, err := io.ReadAll(r); err != nil {
88+
return err
89+
} else {
90+
resp.Bytes = data
91+
return nil
92+
}
93+
}

pkg/elevenlabs/text-to-speech_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package elevenlabs_test
2+
3+
import (
4+
"os"
5+
"path"
6+
"testing"
7+
8+
// Packages
9+
opts "github.com/mutablelogic/go-client/pkg/client"
10+
elevenlabs "github.com/mutablelogic/go-client/pkg/elevenlabs"
11+
assert "github.com/stretchr/testify/assert"
12+
)
13+
14+
func Test_tts_001(t *testing.T) {
15+
assert := assert.New(t)
16+
client, err := elevenlabs.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
17+
assert.NoError(err)
18+
assert.NotNil(client)
19+
20+
voices, err := client.Voices()
21+
assert.NoError(err)
22+
assert.NotEmpty(voices)
23+
24+
tmp := t.TempDir()
25+
for n, voice := range voices {
26+
data, err := client.TextToSpeech("The quick brown fox jumped over the lazy dog", voice.Id,
27+
elevenlabs.OptOutput(elevenlabs.MP3_44100_64),
28+
elevenlabs.OptOptimizeStreamingLatency(uint8(n)%5),
29+
)
30+
assert.NoError(err)
31+
assert.NotEmpty(data)
32+
filename := path.Join(tmp, voice.Id+".mp3")
33+
t.Log(voice.Name, "=>", filename)
34+
f, err := os.Create(filename)
35+
assert.NoError(err)
36+
defer f.Close()
37+
_, err = f.Write(data)
38+
assert.NoError(err)
39+
}
40+
}

0 commit comments

Comments
 (0)