4 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
11 changed files with 882 additions and 128 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"

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

@@ -60,6 +60,7 @@ 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")
@@ -103,6 +104,7 @@ 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")

View File

@@ -78,6 +78,7 @@ 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")

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,7 +75,7 @@ func (c *Client) ConvertSpeechToTextFromReader(ctx context.Context, reader io.Re
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
}
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)

View File

@@ -93,6 +93,7 @@ func (c Client) requestTTS(ctx context.Context, params types.TTS, options types.
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")

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

@@ -31,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 {
@@ -107,6 +107,7 @@ func (c Client) EditVoiceSettings(ctx context.Context, voiceID string, settings
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")
@@ -157,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 {
@@ -242,14 +243,13 @@ func (c Client) GetVoice(ctx context.Context, voiceID string) (types.VoiceRespon
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
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)
@@ -284,13 +284,13 @@ func (c Client) GetVoices(ctx context.Context) ([]types.VoiceResponseModel, erro
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
req.Header.Set("accept", "application/json")
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-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/image v0.26.0 // indirect
golang.org/x/mobile v0.0.0-20250408133729-978277e7eaf7 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/sys v0.41.0 // indirect
)

110
go.sum
View File

@@ -1,100 +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/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80=
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/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/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
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/mobile v0.0.0-20250408133729-978277e7eaf7 h1:8MGTx39304caZ/OMsjPfuxUoDGI2tRas92F5x97tIYc=
golang.org/x/mobile v0.0.0-20250408133729-978277e7eaf7/go.mod h1:ftACcHgQ7vaOnQbHOHvXt9Y6bEPHrs5Ovk67ClwrPJA=
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/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=