6 Commits

Author SHA1 Message Date
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
fba03ed0be update error code for 400 2025-03-13 20:39:03 -07:00
aa701237ff add new error type 2025-03-13 14:04:06 -07:00
Jose Ramirez
29fa401714 s/speeh/speech (#11) 2025-03-04 00:20:55 -08:00
14 changed files with 98 additions and 154 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"
@@ -14,12 +15,14 @@ var (
type Client struct {
apiKey string
endpoint string
httpClient *http.Client
}
func New(apiKey string) Client {
return Client{
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
}

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)
@@ -66,7 +63,7 @@ func (c Client) HistoryDownloadZipWriter(ctx context.Context, w io.Writer, id1,
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 +96,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)
@@ -110,7 +106,7 @@ func (c Client) HistoryDownloadZip(ctx context.Context, id1, id2 string, additio
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 +138,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 +145,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 +174,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 +181,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 +213,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 +220,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
@@ -84,7 +82,7 @@ func (c Client) requestSoundGeneration(ctx context.Context, params types.SoundGe
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

@@ -75,7 +75,6 @@ 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")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
@@ -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)
}
@@ -102,9 +101,19 @@ func (c *Client) ConvertSpeechToTextFromReader(ctx context.Context, reader io.Re
return &sttResponse, nil
case 422:
ve := types.ValidationError{}
defer res.Body.Close()
jerr := json.NewDecoder(res.Body).Decode(&ve)
if jerr != nil {
err = errors.Join(err, jerr)
} else {
err = errors.Join(err, ve)
}
return nil, err
case 400:
fallthrough
default:
ve := types.ValidationError{}
ve := types.ParamError{}
defer res.Body.Close()
jerr := json.NewDecoder(res.Body).Decode(&ve)
if jerr != nil {

View File

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

@@ -202,6 +202,17 @@ func (ve ValidationError) Error() string {
return fmt.Sprintf("%s %s: ", ve.Type_, ve.Msg)
}
type ParamError struct {
Detail struct {
Status string `json:"status"`
Message string `json:"message"`
} `json:"detail"`
}
func (pe ParamError) Error() string {
return fmt.Sprintf("%s %s: ", pe.Detail.Status, pe.Detail.Message)
}
type VerificationAttemptResponseModel struct {
Text string `json:"text"`
DateUnix int32 `json:"date_unix"`
@@ -242,16 +253,16 @@ const (
TimestampsGranularityCharacter TimestampsGranularity = "character"
)
type SpeehToTextModel string
type SpeechToTextModel string
const (
SpeehToTextModelScribeV1 SpeehToTextModel = "scribe_v1"
SpeechToTextModelScribeV1 SpeechToTextModel = "scribe_v1"
)
// SpeechToTextRequest represents a request to the speech-to-text API
type SpeechToTextRequest struct {
// The ID of the model to use for transcription (currently only 'scribe_v1')
ModelID SpeehToTextModel `json:"model_id"`
ModelID SpeechToTextModel `json:"model_id"`
// ISO-639-1 or ISO-639-3 language code. If not specified, language is auto-detected
LanguageCode string `json:"language_code,omitempty"`
// Whether to tag audio events like (laughter), (footsteps), etc.

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 {
@@ -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,7 +100,7 @@ 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))
@@ -113,7 +110,7 @@ func (c Client) EditVoiceSettings(ctx context.Context, voiceID string, settings
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 +142,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)
@@ -171,7 +167,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 +193,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 +201,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 +233,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,7 +241,7 @@ 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)
switch res.StatusCode {
case 401:
return types.VoiceResponseModel{}, ErrUnauthorized
@@ -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,7 +283,7 @@ 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)
switch res.StatusCode {
case 401:
return []types.VoiceResponseModel{}, ErrUnauthorized

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"

View File

@@ -17,7 +17,7 @@ func main() {
filePath := os.Args[1]
resp, err := client.ConvertSpeechToText(ctx, filePath, types.SpeechToTextRequest{
ModelID: types.SpeehToTextModelScribeV1,
ModelID: types.SpeechToTextModelScribeV1,
TimestampsGranularity: types.TimestampsGranularityWord,
Diarize: true,
})

11
go.mod
View File

@@ -4,15 +4,12 @@ go 1.23.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.3.2 // indirect
github.com/ebitengine/purego v0.8.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.32.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.3.2 h1:VTWBsKX9eb+dXzaF4jEwQbs4yWIdXukJ0K40KgkpYlg=
github.com/ebitengine/oto/v3 v3.3.2/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=