mirror of
https://github.com/taigrr/elevenlabs.git
synced 2026-04-02 03:08:57 -07:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
261398509a | ||
|
|
b925ef1471 | ||
|
e095a7ec13
|
|||
| 32972d4ff2 | |||
| 84e59417d6 | |||
|
3e3b7004b8
|
|||
|
6fcc65115d
|
|||
|
5451bcd0b1
|
|||
|
58581c3c46
|
|||
|
544f24cab1
|
|||
|
35f72a819b
|
|||
|
0d72be9d50
|
|||
|
899a127e7a
|
|||
|
8a27f7df64
|
|||
|
fb146435fd
|
|||
|
e2bb856589
|
|||
|
48f074cc9c
|
12
LICENSE
Normal file
12
LICENSE
Normal file
@@ -0,0 +1,12 @@
|
||||
Copyright (C) 2023 by Tai Groot <tai@taigrr.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
78
README.md
78
README.md
@@ -1,5 +1,11 @@
|
||||
# elevenlabs
|
||||
Unofficial [elevenlabs.io](https://beta.elevenlabs.io/) ([11.ai](11.ai)) voice synthesis client
|
||||
[](https://opensource.org/licenses/0BSD)
|
||||
[](https://godoc.org/github.com/taigrr/elevenlabs)
|
||||
[](go.mod)
|
||||
[](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.
|
||||
|
||||
@@ -12,7 +18,75 @@ 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 you API key [from here](https://help.elevenlabs.io/hc/en-us/articles/14599447207697-How-to-authorize-yourself-using-your-xi-api-key-).
|
||||
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-).
|
||||
|
||||
## Test Program
|
||||
|
||||
To test out an example `say` program, run:
|
||||
|
||||
`go install github.com/taigrr/elevenlabs/cmd/say@latest`
|
||||
|
||||
Set the `XI_API_KEY` environment variable, and pipe it some text to give it a whirl!
|
||||
|
||||
## Example Code
|
||||
|
||||
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
|
||||
to an mp3 on-disk.
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/beep"
|
||||
"github.com/faiface/beep/mp3"
|
||||
"github.com/faiface/beep/speaker"
|
||||
|
||||
"github.com/taigrr/elevenlabs/client"
|
||||
"github.com/taigrr/elevenlabs/client/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
// load in an API key to create a client
|
||||
client := client.New(os.Getenv("XI_API_KEY"))
|
||||
// fetch a list of voice IDs from elevenlabs
|
||||
ids, err := client.GetVoiceIDs(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// prepare a pipe for streaming audio directly to beep
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
text, _ := reader.ReadString('\n')
|
||||
go func() {
|
||||
// stream audio from elevenlabs using the first voice we found
|
||||
err = client.TTSStream(ctx, pipeWriter, text, ids[0], types.SynthesisOptions{Stability: 0.75, SimilarityBoost: 0.75})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pipeWriter.Close()
|
||||
}()
|
||||
// decode and prepare the streaming mp3 as it comes through
|
||||
streamer, format, err := mp3.Decode(pipeReader)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer streamer.Close()
|
||||
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
|
||||
done := make(chan bool)
|
||||
// play the audio
|
||||
speaker.Play(beep.Seq(streamer, beep.Callback(func() {
|
||||
done <- true
|
||||
})))
|
||||
<-done
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -13,11 +12,12 @@ import (
|
||||
"github.com/taigrr/elevenlabs/client/types"
|
||||
)
|
||||
|
||||
func (c Client) TTSWriter(ctx context.Context, w io.Writer, text, voiceID string, options types.SynthesisOptions) error {
|
||||
func (c Client) TTSWriter(ctx context.Context, w io.Writer, text, modelID, voiceID string, options types.SynthesisOptions) error {
|
||||
options.Clamp()
|
||||
url := fmt.Sprintf(c.endpoint+"/v1/text-to-speech/%s", voiceID)
|
||||
opts := types.TTS{
|
||||
Text: text,
|
||||
ModelID: modelID,
|
||||
VoiceSettings: options,
|
||||
}
|
||||
b, _ := json.Marshal(opts)
|
||||
@@ -30,14 +30,13 @@ func (c Client) TTSWriter(ctx context.Context, w io.Writer, text, voiceID string
|
||||
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
|
||||
req.Header.Set("accept", "audio/mpeg")
|
||||
res, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch res.StatusCode {
|
||||
case 401:
|
||||
return ErrUnauthorized
|
||||
case 200:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
io.Copy(w, res.Body)
|
||||
return nil
|
||||
@@ -56,12 +55,13 @@ func (c Client) TTSWriter(ctx context.Context, w io.Writer, text, voiceID string
|
||||
}
|
||||
}
|
||||
|
||||
func (c Client) TTS(ctx context.Context, text, voiceID string, options types.SynthesisOptions) ([]byte, error) {
|
||||
func (c Client) TTS(ctx context.Context, text, voiceID, modelID string, options types.SynthesisOptions) ([]byte, error) {
|
||||
options.Clamp()
|
||||
url := fmt.Sprintf(c.endpoint+"/v1/text-to-speech/%s", voiceID)
|
||||
client := &http.Client{}
|
||||
opts := types.TTS{
|
||||
Text: text,
|
||||
ModelID: modelID,
|
||||
VoiceSettings: options,
|
||||
}
|
||||
b, _ := json.Marshal(opts)
|
||||
@@ -73,19 +73,17 @@ func (c Client) TTS(ctx context.Context, text, voiceID string, options types.Syn
|
||||
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
|
||||
req.Header.Set("accept", "audio/mpeg")
|
||||
res, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
switch res.StatusCode {
|
||||
case 401:
|
||||
return []byte{}, ErrUnauthorized
|
||||
case 200:
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
b := bytes.Buffer{}
|
||||
w := bufio.NewWriter(&b)
|
||||
|
||||
defer res.Body.Close()
|
||||
io.Copy(w, res.Body)
|
||||
io.Copy(&b, res.Body)
|
||||
return b.Bytes(), nil
|
||||
case 422:
|
||||
fallthrough
|
||||
@@ -119,14 +117,13 @@ func (c Client) TTSStream(ctx context.Context, w io.Writer, text, voiceID string
|
||||
req.Header.Set("User-Agent", "github.com/taigrr/elevenlabs")
|
||||
req.Header.Set("accept", "audio/mpeg")
|
||||
res, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch res.StatusCode {
|
||||
case 401:
|
||||
return ErrUnauthorized
|
||||
case 200:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
io.Copy(w, res.Body)
|
||||
return nil
|
||||
|
||||
@@ -19,6 +19,7 @@ type Voice struct {
|
||||
Labels string `json:"labels,omitempty"` // Serialized labels dictionary for the voice.
|
||||
}
|
||||
type TTS struct {
|
||||
ModelID string `json:"model_id,omitempty"`
|
||||
Text string `json:"text"` // The text that will get converted into speech. Currently only English text is supported.
|
||||
VoiceSettings SynthesisOptions `json:"voice_settings,omitempty"` // Voice settings are applied only on the given TTS request.
|
||||
}
|
||||
@@ -103,6 +104,22 @@ type LanguageResponseModel struct {
|
||||
IsoCode string `json:"iso_code"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
type Language struct {
|
||||
LanguageID string `json:"language_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ModelResponseModel struct {
|
||||
ModelID string `json:"model_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CanBeFinetuned bool `json:"can_be_finetuned"`
|
||||
CanDoTextToSpeech bool `json:"can_do_text_to_speech"`
|
||||
CanDoVoiceConversion bool `json:"can_do_voice_conversion"`
|
||||
TokenCostFactor float64 `json:"token_cost_factor"`
|
||||
Languages []Language `json:"languages"`
|
||||
}
|
||||
type RecordingResponseModel struct {
|
||||
RecordingID string `json:"recording_id"`
|
||||
MimeType string `json:"mime_type"`
|
||||
|
||||
1
cmd/say/.gitignore
vendored
1
cmd/say/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
*.mp3
|
||||
main
|
||||
say
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/beep"
|
||||
"github.com/faiface/beep/mp3"
|
||||
"github.com/faiface/beep/speaker"
|
||||
|
||||
"github.com/taigrr/elevenlabs/client"
|
||||
"github.com/taigrr/elevenlabs/client/types"
|
||||
@@ -15,13 +23,30 @@ func main() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
saveFile, err := os.Create("sample.mp3")
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
b, _ := io.ReadAll(reader)
|
||||
text := string(b)
|
||||
|
||||
go func() {
|
||||
err = client.TTSStream(ctx, pipeWriter, text, ids[0], types.SynthesisOptions{Stability: 0.75, SimilarityBoost: 0.75})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer saveFile.Close()
|
||||
err = client.TTSWriter(ctx, saveFile, "hello, golang", ids[0], types.SynthesisOptions{Stability: 0.75, SimilarityBoost: 0.75})
|
||||
pipeWriter.Close()
|
||||
}()
|
||||
streamer, format, err := mp3.Decode(pipeReader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer streamer.Close()
|
||||
|
||||
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
|
||||
done := make(chan bool)
|
||||
speaker.Play(beep.Seq(streamer, beep.Callback(func() {
|
||||
done <- true
|
||||
})))
|
||||
|
||||
<-done
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
}
|
||||
12
go.mod
12
go.mod
@@ -1,3 +1,15 @@
|
||||
module github.com/taigrr/elevenlabs
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/faiface/beep v1.1.0
|
||||
|
||||
require (
|
||||
github.com/hajimehoshi/go-mp3 v0.3.0 // indirect
|
||||
github.com/hajimehoshi/oto v0.7.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect
|
||||
)
|
||||
|
||||
38
go.sum
38
go.sum
@@ -0,0 +1,38 @@
|
||||
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 h1:fTM5DXjp/DL2G74HHAs/aBGiS9Tg7wnp+jkU38bHy4g=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
|
||||
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||
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=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/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 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
Reference in New Issue
Block a user