6 Commits

Author SHA1 Message Date
3e3d8def67 fix(client): fix label field bug, nil deref, receiver consistency, add tests
- CreateVoice/EditVoice: write 'labels' field instead of duplicate 'name'
- GetVoice/GetVoices: check http error before dereferencing response
- ConvertSpeechToText*: use value receiver for consistency
- Add Content-Type: application/json header to all JSON POST requests
- Add comprehensive test suite (35+ tests) using httptest
2026-04-06 07:10:27 +00:00
ae5a1f2218 chore(deps): update Go to 1.26, bump purego, fix lint warning (#15)
- Updated Go version from 1.24.0 to 1.26.0
- Bumped ebitengine/purego v0.9.1 -> v0.10.0
- Removed dead code in SynthesisOptions.Clamp() (staticcheck S1002)
2026-03-04 04:45:43 -05:00
7e25beb0a7 chore: update dependencies, fix vet warning (#14)
- Bump go directive from 1.23 to 1.24
- Update ebitengine/oto/v3 v3.3.2 → v3.4.0
- Update ebitengine/purego v0.8.0 → v0.9.1
- Update golang.org/x/sys v0.32.0 → v0.41.0
- Fix fmt.Sprintf with non-format string in stt.go
2026-02-22 20:51:27 -05:00
b179ed7944 update to newer sound package 2025-07-04 19:22:42 -07:00
c5609d07e9 update dep 2025-04-30 10:26:33 -07:00
Jose Ramirez
d79325ad71 add WithHTTPClient (#13) 2025-04-30 10:21:50 -07:00
14 changed files with 924 additions and 167 deletions

View File

@@ -1,10 +1,10 @@
# elevenlabs
[![License 0BSD](https://img.shields.io/badge/License-0BSD-pink.svg)](https://opensource.org/licenses/0BSD)
[![GoDoc](https://godoc.org/github.com/taigrr/elevenlabs?status.svg)](https://godoc.org/github.com/taigrr/elevenlabs)
[![Go Mod](https://img.shields.io/badge/go.mod-v1.20-blue)](go.mod)
[![Go Report Card](https://goreportcard.com/badge/github.com/taigrr/elevenlabs?branch=master)](https://goreportcard.com/report/github.com/taigrr/elevenlabs)
Unofficial [elevenlabs.io](https://beta.elevenlabs.io/) ([11.ai](http://11.ai)) voice synthesis client
This library is not affiliated with, nor associated with ElevenLabs in any way.
@@ -13,10 +13,10 @@ ElevenLabs' official api documentation, upon which this client has been
derived, [can be found here](https://api.elevenlabs.io/docs).
## Purpose
This go client provides an easy interface to create synthesized voices and
make TTS (text-to-speech) requests to elevenlabs.io
As a prerequisite, you must already have an account with elevenlabs.io.
After creating your account, you can get your API key [from here](https://help.elevenlabs.io/hc/en-us/articles/14599447207697-How-to-authorize-yourself-using-your-xi-api-key-).
@@ -35,8 +35,9 @@ Set the `XI_API_KEY` environment variable, and pipe it some text to give it a wh
To use this library, create a new client and send a TTS request to a voice.
The following code block illustrates how one might replicate the say/espeak
command, using the streaming endpoint.
I've opted to go with faiface's beep package, but you can also save the file
I've opted to go with gopxl's beep package, but you can also save the file
to an mp3 on-disk.
```go
package main
@@ -48,9 +49,9 @@ import (
"os"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/mp3"
"github.com/gopxl/beep/v2/speaker"
"github.com/taigrr/elevenlabs/client"
"github.com/taigrr/elevenlabs/client/types"

View File

@@ -2,6 +2,7 @@ package client
import (
"errors"
"net/http"
)
const apiEndpoint = "https://api.elevenlabs.io"
@@ -12,14 +13,16 @@ var (
)
type Client struct {
apiKey string
endpoint string
apiKey string
endpoint string
httpClient *http.Client
}
func New(apiKey string) Client {
return Client{
apiKey: apiKey,
endpoint: apiEndpoint,
apiKey: apiKey,
endpoint: apiEndpoint,
httpClient: &http.Client{},
}
}
@@ -27,3 +30,14 @@ func (c Client) WithEndpoint(endpoint string) Client {
c.endpoint = endpoint
return c
}
func (c Client) WithAPIKey(apiKey string) Client {
c.apiKey = apiKey
return c
}
// WithHTTPClient allows users to provide their own http.Client
func (c Client) WithHTTPClient(hc *http.Client) Client {
c.httpClient = hc
return c
}

833
client/client_test.go Normal file
View File

@@ -0,0 +1,833 @@
package client
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/taigrr/elevenlabs/client/types"
)
// newTestClient creates a Client pointed at the given test server.
func newTestClient(ts *httptest.Server) Client {
return New("test-api-key").WithEndpoint(ts.URL)
}
// assertAPIKey checks that the request carries the expected API key header.
func assertAPIKey(t *testing.T, r *http.Request) {
t.Helper()
if got := r.Header.Get("xi-api-key"); got != "test-api-key" {
t.Errorf("xi-api-key = %q, want %q", got, "test-api-key")
}
}
func TestNew(t *testing.T) {
c := New("my-key")
if c.apiKey != "my-key" {
t.Fatalf("apiKey = %q, want %q", c.apiKey, "my-key")
}
if c.endpoint != apiEndpoint {
t.Fatalf("endpoint = %q, want %q", c.endpoint, apiEndpoint)
}
if c.httpClient == nil {
t.Fatal("httpClient is nil")
}
}
func TestWithEndpoint(t *testing.T) {
c := New("k").WithEndpoint("http://custom")
if c.endpoint != "http://custom" {
t.Fatalf("endpoint = %q", c.endpoint)
}
}
func TestWithAPIKey(t *testing.T) {
c := New("old").WithAPIKey("new")
if c.apiKey != "new" {
t.Fatalf("apiKey = %q", c.apiKey)
}
}
func TestWithHTTPClient(t *testing.T) {
custom := &http.Client{}
c := New("k").WithHTTPClient(custom)
if c.httpClient != custom {
t.Fatal("httpClient not set")
}
}
// --- TTS tests ---
func TestTTS(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAPIKey(t, r)
if r.Method != http.MethodPost {
t.Errorf("method = %s, want POST", r.Method)
}
if !strings.HasPrefix(r.URL.Path, "/v1/text-to-speech/") {
t.Errorf("path = %s", r.URL.Path)
}
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q, want application/json", ct)
}
var body types.TTS
json.NewDecoder(r.Body).Decode(&body)
if body.Text != "hello" {
t.Errorf("text = %q", body.Text)
}
w.WriteHeader(200)
w.Write([]byte("audio-bytes"))
}))
defer ts.Close()
c := newTestClient(ts)
data, err := c.TTS(context.Background(), "hello", "voice1", "model1", types.SynthesisOptions{Stability: 0.5, SimilarityBoost: 0.5})
if err != nil {
t.Fatal(err)
}
if string(data) != "audio-bytes" {
t.Errorf("data = %q", string(data))
}
}
func TestTTSUnauthorized(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(401)
}))
defer ts.Close()
c := newTestClient(ts)
_, err := c.TTS(context.Background(), "hello", "voice1", "", types.SynthesisOptions{})
if err != ErrUnauthorized {
t.Fatalf("err = %v, want ErrUnauthorized", err)
}
}
func TestTTSStream(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasSuffix(r.URL.Path, "/stream") {
t.Errorf("path = %s, want stream suffix", r.URL.Path)
}
w.WriteHeader(200)
w.Write([]byte("streamed"))
}))
defer ts.Close()
c := newTestClient(ts)
var buf strings.Builder
err := c.TTSStream(context.Background(), &buf, "text", "v1", types.SynthesisOptions{Stability: 0.5, SimilarityBoost: 0.5})
if err != nil {
t.Fatal(err)
}
if buf.String() != "streamed" {
t.Errorf("got = %q", buf.String())
}
}
func TestTTSWithOptionalParams(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body types.TTS
json.NewDecoder(r.Body).Decode(&body)
if body.PreviousText != "prev" {
t.Errorf("previous_text = %q", body.PreviousText)
}
if body.NextText != "next" {
t.Errorf("next_text = %q", body.NextText)
}
w.WriteHeader(200)
w.Write([]byte("ok"))
}))
defer ts.Close()
c := newTestClient(ts)
_, err := c.TTS(context.Background(), "hello", "v1", "m1",
types.SynthesisOptions{Stability: 0.5, SimilarityBoost: 0.5},
WithPreviousText("prev"), WithNextText("next"))
if err != nil {
t.Fatal(err)
}
}
// --- Voice tests ---
func TestGetVoices(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAPIKey(t, r)
if r.URL.Path != "/v1/voices" {
t.Errorf("path = %s", r.URL.Path)
}
resp := types.GetVoicesResponseModel{
Voices: []types.VoiceResponseModel{
{VoiceID: "id1", Name: "Alice"},
{VoiceID: "id2", Name: "Bob"},
},
}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
voices, err := c.GetVoices(context.Background())
if err != nil {
t.Fatal(err)
}
if len(voices) != 2 {
t.Fatalf("len = %d", len(voices))
}
if voices[0].Name != "Alice" {
t.Errorf("name = %s", voices[0].Name)
}
}
func TestGetVoiceIDs(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := types.GetVoicesResponseModel{
Voices: []types.VoiceResponseModel{
{VoiceID: "id1"},
{VoiceID: "id2"},
},
}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
ids, err := c.GetVoiceIDs(context.Background())
if err != nil {
t.Fatal(err)
}
if len(ids) != 2 || ids[0] != "id1" || ids[1] != "id2" {
t.Errorf("ids = %v", ids)
}
}
func TestGetVoice(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAPIKey(t, r)
if r.URL.Path != "/v1/voices/v123" {
t.Errorf("path = %s", r.URL.Path)
}
resp := types.VoiceResponseModel{VoiceID: "v123", Name: "Test"}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
voice, err := c.GetVoice(context.Background(), "v123")
if err != nil {
t.Fatal(err)
}
if voice.VoiceID != "v123" {
t.Errorf("voiceID = %s", voice.VoiceID)
}
}
func TestGetVoiceUnauthorized(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(401)
}))
defer ts.Close()
c := newTestClient(ts)
_, err := c.GetVoice(context.Background(), "v1")
if err != ErrUnauthorized {
t.Fatalf("err = %v, want ErrUnauthorized", err)
}
}
func TestDeleteVoice(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAPIKey(t, r)
if r.Method != http.MethodDelete {
t.Errorf("method = %s", r.Method)
}
w.WriteHeader(200)
}))
defer ts.Close()
c := newTestClient(ts)
err := c.DeleteVoice(context.Background(), "v1")
if err != nil {
t.Fatal(err)
}
}
func TestGetVoiceSettings(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := types.SynthesisOptions{Stability: 0.5, SimilarityBoost: 0.8}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
settings, err := c.GetVoiceSettings(context.Background(), "v1")
if err != nil {
t.Fatal(err)
}
if settings.Stability != 0.5 {
t.Errorf("stability = %f", settings.Stability)
}
}
func TestEditVoiceSettings(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q", ct)
}
w.WriteHeader(200)
json.NewEncoder(w).Encode(types.SynthesisOptions{})
}))
defer ts.Close()
c := newTestClient(ts)
err := c.EditVoiceSettings(context.Background(), "v1", types.SynthesisOptions{Stability: 0.5})
if err != nil {
t.Fatal(err)
}
}
// --- User tests ---
func TestGetUserInfo(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAPIKey(t, r)
resp := types.UserResponseModel{
Subscription: types.Subscription{Tier: "pro"},
IsNewUser: false,
}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
info, err := c.GetUserInfo(context.Background())
if err != nil {
t.Fatal(err)
}
if info.Subscription.Tier != "pro" {
t.Errorf("tier = %s", info.Subscription.Tier)
}
}
func TestGetSubscriptionInfo(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := types.UserResponseModel{
Subscription: types.Subscription{Tier: "starter", CharacterLimit: 10000},
}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
sub, err := c.GetSubscriptionInfo(context.Background())
if err != nil {
t.Fatal(err)
}
if sub.CharacterLimit != 10000 {
t.Errorf("limit = %d", sub.CharacterLimit)
}
}
// --- History tests ---
func TestGetHistoryItemList(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAPIKey(t, r)
resp := types.GetHistoryResponse{
History: []types.HistoryItemList{
{HistoryItemID: "h1", VoiceID: "v1", Text: "hello"},
{HistoryItemID: "h2", VoiceID: "v2", Text: "world"},
},
}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
items, err := c.GetHistoryItemList(context.Background())
if err != nil {
t.Fatal(err)
}
if len(items) != 2 {
t.Fatalf("len = %d", len(items))
}
}
func TestGetHistoryIDs(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := types.GetHistoryResponse{
History: []types.HistoryItemList{
{HistoryItemID: "h1", VoiceID: "v1"},
{HistoryItemID: "h2", VoiceID: "v2"},
{HistoryItemID: "h3", VoiceID: "v1"},
},
}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
// No filter
ids, err := c.GetHistoryIDs(context.Background())
if err != nil {
t.Fatal(err)
}
if len(ids) != 3 {
t.Errorf("unfiltered len = %d", len(ids))
}
}
func TestGetHistoryIDsFiltered(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := types.GetHistoryResponse{
History: []types.HistoryItemList{
{HistoryItemID: "h1", VoiceID: "v1"},
{HistoryItemID: "h2", VoiceID: "v2"},
{HistoryItemID: "h3", VoiceID: "v1"},
},
}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
ids, err := c.GetHistoryIDs(context.Background(), "v1")
if err != nil {
t.Fatal(err)
}
if len(ids) != 2 {
t.Errorf("filtered len = %d, want 2", len(ids))
}
}
func TestHistoryDelete(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("method = %s", r.Method)
}
w.WriteHeader(200)
}))
defer ts.Close()
c := newTestClient(ts)
ok, err := c.HistoryDelete(context.Background(), "h1")
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("expected true")
}
}
func TestHistoryDownloadAudio(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("audio-data"))
}))
defer ts.Close()
c := newTestClient(ts)
data, err := c.HistoryDownloadAudio(context.Background(), "h1")
if err != nil {
t.Fatal(err)
}
if len(data) == 0 {
t.Error("expected non-empty data")
}
}
// --- Samples tests ---
func TestDeleteVoiceSample(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("method = %s", r.Method)
}
w.WriteHeader(200)
}))
defer ts.Close()
c := newTestClient(ts)
ok, err := c.DeleteVoiceSample(context.Background(), "v1", "s1")
if err != nil {
t.Fatal(err)
}
if !ok {
t.Error("expected true")
}
}
func TestDownloadVoiceSample(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("sample-audio"))
}))
defer ts.Close()
c := newTestClient(ts)
data, err := c.DownloadVoiceSample(context.Background(), "v1", "s1")
if err != nil {
t.Fatal(err)
}
if len(data) == 0 {
t.Error("expected non-empty data")
}
}
// --- Sound generation tests ---
func TestSoundGeneration(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAPIKey(t, r)
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q", ct)
}
var body types.SoundGeneration
json.NewDecoder(r.Body).Decode(&body)
if body.Text != "thunder" {
t.Errorf("text = %q", body.Text)
}
w.WriteHeader(200)
w.Write([]byte("sound-bytes"))
}))
defer ts.Close()
c := newTestClient(ts)
data, err := c.SoundGeneration(context.Background(), "thunder", 0, 0)
if err != nil {
t.Fatal(err)
}
if string(data) != "sound-bytes" {
t.Errorf("data = %q", string(data))
}
}
func TestSoundGenerationWriter(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("streamed-sound"))
}))
defer ts.Close()
c := newTestClient(ts)
var buf strings.Builder
err := c.SoundGenerationWriter(context.Background(), &buf, "rain", 5.0, 0.5)
if err != nil {
t.Fatal(err)
}
if buf.String() != "streamed-sound" {
t.Errorf("got = %q", buf.String())
}
}
// --- STT tests ---
func TestConvertSpeechToTextFromReader(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAPIKey(t, r)
if r.Method != http.MethodPost {
t.Errorf("method = %s", r.Method)
}
if !strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") {
t.Errorf("Content-Type = %s", r.Header.Get("Content-Type"))
}
resp := types.SpeechToTextResponse{
Text: "hello world",
LanguageCode: "en",
}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
resp, err := c.ConvertSpeechToTextFromReader(
context.Background(),
strings.NewReader("fake-audio"),
"test.wav",
types.SpeechToTextRequest{ModelID: types.SpeechToTextModelScribeV1},
)
if err != nil {
t.Fatal(err)
}
if resp.Text != "hello world" {
t.Errorf("text = %q", resp.Text)
}
}
func TestConvertSpeechToTextUnauthorized(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(401)
}))
defer ts.Close()
c := newTestClient(ts)
_, err := c.ConvertSpeechToTextFromReader(
context.Background(),
strings.NewReader("fake"),
"test.wav",
types.SpeechToTextRequest{ModelID: types.SpeechToTextModelScribeV1},
)
if err != ErrUnauthorized {
t.Fatalf("err = %v, want ErrUnauthorized", err)
}
}
// --- Types tests ---
func TestSynthesisOptionsClamp(t *testing.T) {
tests := []struct {
name string
in types.SynthesisOptions
want types.SynthesisOptions
}{
{
name: "valid values unchanged",
in: types.SynthesisOptions{Stability: 0.5, SimilarityBoost: 0.8, Style: 0.3},
want: types.SynthesisOptions{Stability: 0.5, SimilarityBoost: 0.8, Style: 0.3},
},
{
name: "out of range clamped",
in: types.SynthesisOptions{Stability: 2.0, SimilarityBoost: -1.0, Style: 5.0},
want: types.SynthesisOptions{Stability: 0.75, SimilarityBoost: 0.75, Style: 0.0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.in.Clamp()
if tt.in.Stability != tt.want.Stability {
t.Errorf("Stability = %f, want %f", tt.in.Stability, tt.want.Stability)
}
if tt.in.SimilarityBoost != tt.want.SimilarityBoost {
t.Errorf("SimilarityBoost = %f, want %f", tt.in.SimilarityBoost, tt.want.SimilarityBoost)
}
if tt.in.Style != tt.want.Style {
t.Errorf("Style = %f, want %f", tt.in.Style, tt.want.Style)
}
})
}
}
func TestValidationErrorString(t *testing.T) {
ve := types.ValidationError{Msg: "bad input", Type_: "value_error"}
s := ve.Error()
if !strings.Contains(s, "bad input") {
t.Errorf("error = %q", s)
}
}
func TestParamErrorString(t *testing.T) {
pe := types.ParamError{}
pe.Detail.Status = "error"
pe.Detail.Message = "invalid param"
s := pe.Error()
if !strings.Contains(s, "invalid param") {
t.Errorf("error = %q", s)
}
}
// --- TTSWriter test ---
func TestTTSWriter(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("written-audio"))
}))
defer ts.Close()
c := newTestClient(ts)
var buf strings.Builder
err := c.TTSWriter(context.Background(), &buf, "hello", "m1", "v1", types.SynthesisOptions{Stability: 0.5, SimilarityBoost: 0.5})
if err != nil {
t.Fatal(err)
}
if buf.String() != "written-audio" {
t.Errorf("got = %q", buf.String())
}
}
// --- DownloadVoiceSampleWriter test ---
func TestDownloadVoiceSampleWriter(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("sample-stream"))
}))
defer ts.Close()
c := newTestClient(ts)
var buf strings.Builder
err := c.DownloadVoiceSampleWriter(context.Background(), &buf, "v1", "s1")
if err != nil {
t.Fatal(err)
}
if buf.String() != "sample-stream" {
t.Errorf("got = %q", buf.String())
}
}
// --- HistoryDownloadAudioWriter test ---
func TestHistoryDownloadAudioWriter(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("audio-stream"))
}))
defer ts.Close()
c := newTestClient(ts)
var buf strings.Builder
err := c.HistoryDownloadAudioWriter(context.Background(), &buf, "h1")
if err != nil {
t.Fatal(err)
}
if buf.String() != "audio-stream" {
t.Errorf("got = %q", buf.String())
}
}
// --- HistoryDownloadZip tests ---
func TestHistoryDownloadZip(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q, want application/json", ct)
}
var body types.HistoryPost
json.NewDecoder(r.Body).Decode(&body)
if len(body.HistoryItemIds) < 2 {
t.Errorf("expected at least 2 ids, got %d", len(body.HistoryItemIds))
}
w.WriteHeader(200)
w.Write([]byte("zip-data"))
}))
defer ts.Close()
c := newTestClient(ts)
data, err := c.HistoryDownloadZip(context.Background(), "h1", "h2")
if err != nil {
t.Fatal(err)
}
_ = data
}
func TestHistoryDownloadZipWriter(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("zip-stream"))
}))
defer ts.Close()
c := newTestClient(ts)
var buf strings.Builder
err := c.HistoryDownloadZipWriter(context.Background(), &buf, "h1", "h2")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(buf.String(), "zip-stream") {
t.Errorf("got = %q", buf.String())
}
}
// Verify CreateVoice sends multipart with correct field names
func TestCreateVoiceFieldNames(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAPIKey(t, r)
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatal(err)
}
name := r.FormValue("name")
if name != "TestVoice" {
t.Errorf("name = %q", name)
}
labels := r.FormValue("labels")
if labels != "english, male" {
t.Errorf("labels = %q, want %q", labels, "english, male")
}
w.WriteHeader(200)
}))
defer ts.Close()
c := newTestClient(ts)
err := c.CreateVoice(context.Background(), "TestVoice", "A test voice", []string{"english", "male"}, nil)
if err != nil {
t.Fatal(err)
}
}
// Verify handling of 422 validation errors
func TestTTS422(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(422)
json.NewEncoder(w).Encode(types.ValidationError{Msg: "invalid", Type_: "value_error"})
}))
defer ts.Close()
c := newTestClient(ts)
_, err := c.TTS(context.Background(), "hello", "v1", "", types.SynthesisOptions{})
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "invalid") {
t.Errorf("error = %q", err.Error())
}
}
// Test that STT properly sends diarize and other fields
func TestSTTFields(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatal(err)
}
if r.FormValue("model_id") != "scribe_v1" {
t.Errorf("model_id = %q", r.FormValue("model_id"))
}
if r.FormValue("diarize") != "true" {
t.Errorf("diarize = %q", r.FormValue("diarize"))
}
if r.FormValue("language_code") != "en" {
t.Errorf("language_code = %q", r.FormValue("language_code"))
}
resp := types.SpeechToTextResponse{Text: "ok"}
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
c := newTestClient(ts)
_, err := c.ConvertSpeechToTextFromReader(
context.Background(),
strings.NewReader("audio"),
"test.wav",
types.SpeechToTextRequest{
ModelID: types.SpeechToTextModelScribeV1,
LanguageCode: "en",
Diarize: true,
},
)
if err != nil {
t.Fatal(err)
}
}
// Test that io.Writer variants correctly proxy
func TestDownloadVoiceSampleWriterProxy(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, _ = io.WriteString(w, "proxy-test")
}))
defer ts.Close()
c := newTestClient(ts)
var buf strings.Builder
err := c.DownloadVoiceSampleWriter(context.Background(), &buf, "v1", "s1")
if err != nil {
t.Fatal(err)
}
if buf.String() != "proxy-test" {
t.Errorf("got = %q", buf.String())
}
}

View File

@@ -15,8 +15,6 @@ import (
func (c Client) HistoryDelete(ctx context.Context, historyItemID string) (bool, error) {
url := fmt.Sprintf(c.endpoint+"/v1/history/%s", historyItemID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return false, err
@@ -24,7 +22,7 @@ func (c Client) HistoryDelete(ctx context.Context, historyItemID string) (bool,
req.Header.Set("accept", "application/json")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
switch res.StatusCode {
case 401:
@@ -55,7 +53,6 @@ func (c Client) HistoryDownloadZipWriter(ctx context.Context, w io.Writer, id1,
toDownload := types.HistoryPost{
HistoryItemIds: downloads,
}
client := &http.Client{}
body, _ := json.Marshal(toDownload)
bodyReader := bytes.NewReader(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader)
@@ -63,10 +60,11 @@ func (c Client) HistoryDownloadZipWriter(ctx context.Context, w io.Writer, id1,
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("accept", "archive/zip")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
switch res.StatusCode {
case 401:
@@ -99,7 +97,6 @@ func (c Client) HistoryDownloadZip(ctx context.Context, id1, id2 string, additio
toDownload := types.HistoryPost{
HistoryItemIds: downloads,
}
client := &http.Client{}
body, _ := json.Marshal(toDownload)
bodyReader := bytes.NewReader(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader)
@@ -107,10 +104,11 @@ func (c Client) HistoryDownloadZip(ctx context.Context, id1, id2 string, additio
return []byte{}, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("accept", "archive/zip")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
switch res.StatusCode {
case 401:
@@ -142,7 +140,6 @@ func (c Client) HistoryDownloadZip(ctx context.Context, id1, id2 string, additio
func (c Client) HistoryDownloadAudioWriter(ctx context.Context, w io.Writer, ID string) error {
url := fmt.Sprintf(c.endpoint+"/v1/history/%s/audio", ID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
@@ -150,7 +147,7 @@ func (c Client) HistoryDownloadAudioWriter(ctx context.Context, w io.Writer, ID
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -179,7 +176,6 @@ func (c Client) HistoryDownloadAudioWriter(ctx context.Context, w io.Writer, ID
func (c Client) HistoryDownloadAudio(ctx context.Context, ID string) ([]byte, error) {
url := fmt.Sprintf(c.endpoint+"/v1/history/%s/audio", ID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return []byte{}, err
@@ -187,7 +183,7 @@ func (c Client) HistoryDownloadAudio(ctx context.Context, ID string) ([]byte, er
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return []byte{}, err
}
@@ -219,7 +215,6 @@ func (c Client) HistoryDownloadAudio(ctx context.Context, ID string) ([]byte, er
func (c Client) GetHistoryItemList(ctx context.Context) ([]types.HistoryItemList, error) {
url := c.endpoint + "/v1/history"
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return []types.HistoryItemList{}, err
@@ -227,7 +222,7 @@ func (c Client) GetHistoryItemList(ctx context.Context) ([]types.HistoryItemList
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return []types.HistoryItemList{}, err
}

View File

@@ -15,7 +15,6 @@ import (
func (c Client) DeleteVoiceSample(ctx context.Context, voiceID, sampleID string) (bool, error) {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/samples/%s", voiceID, sampleID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return false, err
@@ -24,7 +23,7 @@ func (c Client) DeleteVoiceSample(ctx context.Context, voiceID, sampleID string)
req.Header.Set("accept", "application/json")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return false, err
}
@@ -50,7 +49,6 @@ func (c Client) DeleteVoiceSample(ctx context.Context, voiceID, sampleID string)
func (c Client) DownloadVoiceSampleWriter(ctx context.Context, w io.Writer, voiceID, sampleID string) error {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/samples/%s/audio", voiceID, sampleID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
@@ -58,7 +56,7 @@ func (c Client) DownloadVoiceSampleWriter(ctx context.Context, w io.Writer, voic
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -87,7 +85,6 @@ func (c Client) DownloadVoiceSampleWriter(ctx context.Context, w io.Writer, voic
func (c Client) DownloadVoiceSample(ctx context.Context, voiceID, sampleID string) ([]byte, error) {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/samples/%s/audio", voiceID, sampleID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return []byte{}, err
@@ -95,7 +92,7 @@ func (c Client) DownloadVoiceSample(ctx context.Context, voiceID, sampleID strin
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return []byte{}, err
}

View File

@@ -68,8 +68,6 @@ func (c Client) SoundGeneration(ctx context.Context, text string, durationSecond
func (c Client) requestSoundGeneration(ctx context.Context, params types.SoundGeneration) (io.ReadCloser, error) {
url := c.endpoint + "/v1/sound-generation"
client := &http.Client{}
b, err := json.Marshal(params)
if err != nil {
return nil, err
@@ -80,11 +78,12 @@ func (c Client) requestSoundGeneration(ctx context.Context, params types.SoundGe
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

View File

@@ -16,7 +16,7 @@ import (
)
// ConvertSpeechToText converts audio to text using the specified file path
func (c *Client) ConvertSpeechToText(ctx context.Context, audioFilePath string, request types.SpeechToTextRequest) (*types.SpeechToTextResponse, error) {
func (c Client) ConvertSpeechToText(ctx context.Context, audioFilePath string, request types.SpeechToTextRequest) (*types.SpeechToTextResponse, error) {
file, err := os.Open(audioFilePath)
if err != nil {
return nil, fmt.Errorf("failed to open audio file: %w", err)
@@ -27,7 +27,7 @@ func (c *Client) ConvertSpeechToText(ctx context.Context, audioFilePath string,
}
// ConvertSpeechToTextFromReader converts audio to text using the provided reader
func (c *Client) ConvertSpeechToTextFromReader(ctx context.Context, reader io.Reader, filename string, request types.SpeechToTextRequest) (*types.SpeechToTextResponse, error) {
func (c Client) ConvertSpeechToTextFromReader(ctx context.Context, reader io.Reader, filename string, request types.SpeechToTextRequest) (*types.SpeechToTextResponse, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
@@ -75,8 +75,7 @@ func (c *Client) ConvertSpeechToTextFromReader(ctx context.Context, reader io.Re
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
}
client := &http.Client{}
url := fmt.Sprintf(c.endpoint + "/v1/speech-to-text")
url := c.endpoint + "/v1/speech-to-text"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
@@ -86,7 +85,7 @@ func (c *Client) ConvertSpeechToTextFromReader(ctx context.Context, reader io.Re
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("xi-api-key", c.apiKey)
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}

View File

@@ -88,16 +88,16 @@ func (c Client) requestTTS(ctx context.Context, params types.TTS, options types.
if params.Stream {
url += "/stream"
}
client := &http.Client{}
b, _ := json.Marshal(params)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(b))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "audio/mpeg")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

View File

@@ -40,9 +40,8 @@ func (so *SynthesisOptions) Clamp() {
if so.Style > 1 || so.Style < 0 {
so.Style = 0.0
}
if so.UseSpeakerBoost != true && so.UseSpeakerBoost != false {
so.UseSpeakerBoost = true
}
// UseSpeakerBoost defaults to the zero value (false) for bool;
// no conditional needed as the caller sets it explicitly.
}
type SynthesisOptions struct {

View File

@@ -11,7 +11,6 @@ import (
func (c Client) GetUserInfo(ctx context.Context) (types.UserResponseModel, error) {
url := c.endpoint + "/v1/user"
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return types.UserResponseModel{}, err
@@ -19,7 +18,7 @@ func (c Client) GetUserInfo(ctx context.Context) (types.UserResponseModel, error
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return types.UserResponseModel{}, err
}

View File

@@ -17,8 +17,6 @@ import (
func (c Client) CreateVoice(ctx context.Context, name, description string, labels []string, files []*os.File) error {
url := c.endpoint + "/v1/voices/add"
client := &http.Client{}
var b bytes.Buffer
w := multipart.NewWriter(&b)
for _, r := range files {
@@ -33,7 +31,7 @@ func (c Client) CreateVoice(ctx context.Context, name, description string, label
}
w.WriteField("name", name)
w.WriteField("description", description)
w.WriteField("name", strings.Join(labels, ", "))
w.WriteField("labels", strings.Join(labels, ", "))
w.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &b)
if err != nil {
@@ -43,7 +41,7 @@ func (c Client) CreateVoice(ctx context.Context, name, description string, label
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -69,7 +67,6 @@ func (c Client) CreateVoice(ctx context.Context, name, description string, label
func (c Client) DeleteVoice(ctx context.Context, voiceID string) error {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s", voiceID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return err
@@ -77,7 +74,7 @@ func (c Client) DeleteVoice(ctx context.Context, voiceID string) error {
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -103,17 +100,18 @@ func (c Client) DeleteVoice(ctx context.Context, voiceID string) error {
func (c Client) EditVoiceSettings(ctx context.Context, voiceID string, settings types.SynthesisOptions) error {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/settings/edit", voiceID)
client := &http.Client{}
b, _ := json.Marshal(settings)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -145,7 +143,6 @@ func (c Client) EditVoiceSettings(ctx context.Context, voiceID string, settings
func (c Client) EditVoice(ctx context.Context, voiceID, name, description string, labels []string, files []*os.File) error {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/edit", voiceID)
client := &http.Client{}
var b bytes.Buffer
w := multipart.NewWriter(&b)
@@ -161,7 +158,7 @@ func (c Client) EditVoice(ctx context.Context, voiceID, name, description string
}
w.WriteField("name", name)
w.WriteField("description", description)
w.WriteField("name", strings.Join(labels, ", "))
w.WriteField("labels", strings.Join(labels, ", "))
w.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &b)
if err != nil {
@@ -171,7 +168,7 @@ func (c Client) EditVoice(ctx context.Context, voiceID, name, description string
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
@@ -197,7 +194,7 @@ func (c Client) EditVoice(ctx context.Context, voiceID, name, description string
func (c Client) GetVoiceSettings(ctx context.Context, voiceID string) (types.SynthesisOptions, error) {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s/settings", voiceID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return types.SynthesisOptions{}, err
@@ -205,7 +202,7 @@ func (c Client) GetVoiceSettings(ctx context.Context, voiceID string) (types.Syn
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return types.SynthesisOptions{}, err
}
@@ -237,7 +234,7 @@ func (c Client) GetVoiceSettings(ctx context.Context, voiceID string) (types.Syn
func (c Client) GetVoice(ctx context.Context, voiceID string) (types.VoiceResponseModel, error) {
url := fmt.Sprintf(c.endpoint+"/v1/voices/%s", voiceID)
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return types.VoiceResponseModel{}, err
@@ -245,15 +242,14 @@ func (c Client) GetVoice(ctx context.Context, voiceID string) (types.VoiceRespon
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return types.VoiceResponseModel{}, err
}
switch res.StatusCode {
case 401:
return types.VoiceResponseModel{}, ErrUnauthorized
case 200:
if err != nil {
return types.VoiceResponseModel{}, err
}
vrm := types.VoiceResponseModel{}
defer res.Body.Close()
jerr := json.NewDecoder(res.Body).Decode(&vrm)
@@ -279,7 +275,7 @@ func (c Client) GetVoice(ctx context.Context, voiceID string) (types.VoiceRespon
func (c Client) GetVoices(ctx context.Context) ([]types.VoiceResponseModel, error) {
url := c.endpoint + "/v1/voices"
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return []types.VoiceResponseModel{}, err
@@ -287,14 +283,14 @@ func (c Client) GetVoices(ctx context.Context) ([]types.VoiceResponseModel, erro
req.Header.Set("xi-api-key", c.apiKey)
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
res, err := client.Do(req)
res, err := c.httpClient.Do(req)
if err != nil {
return []types.VoiceResponseModel{}, err
}
switch res.StatusCode {
case 401:
return []types.VoiceResponseModel{}, ErrUnauthorized
case 200:
if err != nil {
return []types.VoiceResponseModel{}, err
}
vr := types.GetVoicesResponseModel{}
defer res.Body.Close()
jerr := json.NewDecoder(res.Body).Decode(&vr)

View File

@@ -9,9 +9,9 @@ import (
"strings"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/mp3"
"github.com/gopxl/beep/v2/speaker"
"github.com/taigrr/elevenlabs/client"
"github.com/taigrr/elevenlabs/client/types"

15
go.mod
View File

@@ -1,18 +1,13 @@
module github.com/taigrr/elevenlabs
go 1.23.0
go 1.26.0
toolchain go1.24.0
require github.com/faiface/beep v1.1.0
require github.com/gopxl/beep/v2 v2.1.1
require (
github.com/ebitengine/oto/v3 v3.4.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
github.com/hajimehoshi/oto v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/exp/shiny v0.0.0-20250228200357-dead58393ab7 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/mobile v0.0.0-20250218173827-cd096645fcd3 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/sys v0.41.0 // indirect
)

102
go.sum
View File

@@ -1,92 +1,22 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
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/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gopxl/beep/v2 v2.1.1 h1:6FYIYMm2qPAdWkjX+7xwKrViS1x0Po5kDMdRkq8NVbU=
github.com/gopxl/beep/v2 v2.1.1/go.mod h1:ZAm9TGQ9lvpoiFLd4zf5B1IuyxZhgRACMId1XJbaW0E=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4=
github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp/shiny v0.0.0-20250228200357-dead58393ab7 h1:VxTRg3kpOpYQ+S2PlDH9x2j/ZOQMxVsPgdYYRvkErNY=
golang.org/x/exp/shiny v0.0.0-20250228200357-dead58393ab7/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20230906132913-2077a3224571 h1:QDvQ2KLFHHQWRID6IkZOBf6uLIh9tZ0G+mw61pFQxuo=
golang.org/x/mobile v0.0.0-20230906132913-2077a3224571/go.mod h1:wEyOn6VvNW7tcf+bW/wBz1sehi2s2BZ4TimyR7qZen4=
golang.org/x/mobile v0.0.0-20250218173827-cd096645fcd3 h1:0V/7Y1FEaFdAzb9DkVDh4QFp4vL4yYCiJ5cjk80lZyA=
golang.org/x/mobile v0.0.0-20250218173827-cd096645fcd3/go.mod h1:j5VYNgQ6lZYZlzHFjdgS2UeqRSZunDk+/zXVTAIA3z4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=