Skip to content

Commit 51c181f

Browse files
committed
Updated
1 parent 6d1cb2f commit 51c181f

File tree

15 files changed

+648
-35
lines changed

15 files changed

+648
-35
lines changed

cmd/api/anthropic.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ func anthropicParse(flags *Flags, opts ...client.ClientOpt) error {
4040
apiKey := flags.GetString("anthropic-api-key")
4141
if apiKey == "" {
4242
return fmt.Errorf("missing -anthropic-api-key flag")
43-
}
44-
if client, err := anthropic.New(flags.GetString("anthropic-api-key"), opts...); err != nil {
43+
} else if client, err := anthropic.New(apiKey, opts...); err != nil {
4544
return err
4645
} else {
4746
anthropicClient = client

cmd/api/bitwarden.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ func bwFolders(_ context.Context, w *tablewriter.Writer, _ []string) error {
139139
if bwPassword != "" {
140140
opts = append(opts, bitwarden.OptPassword(bwPassword))
141141
}
142+
143+
// Retrieve the folders
142144
folders, err := bwClient.Folders(opts...)
143145
if err != nil {
144146
return err

cmd/api/elevenlabs.go

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"regexp"
9+
"strings"
10+
11+
// Packages
12+
tablewriter "github.com/djthorpe/go-tablewriter"
13+
"github.com/go-audio/audio"
14+
"github.com/go-audio/wav"
15+
client "github.com/mutablelogic/go-client"
16+
elevenlabs "github.com/mutablelogic/go-client/pkg/elevenlabs"
17+
18+
// Namespace imports
19+
. "github.com/djthorpe/go-errors"
20+
)
21+
22+
///////////////////////////////////////////////////////////////////////////////
23+
// GLOBALS
24+
25+
var (
26+
elName = "elevenlabs"
27+
elClient *elevenlabs.Client
28+
elExt = "mp3"
29+
elBitrate = uint64(32) // in kbps
30+
elSamplerate = uint64(44100) // in Hz
31+
reVoiceId = regexp.MustCompile("^[a-z0-9-]{20}$")
32+
)
33+
34+
///////////////////////////////////////////////////////////////////////////////
35+
// LIFECYCLE
36+
37+
func elRegister(flags *Flags) {
38+
// Register flags required
39+
flags.String(elName, "elevenlabs-api-key", "${ELEVENLABS_API_KEY}", "API Key")
40+
flags.Float(elName, "similarity-boost", 0, "Similarity boost")
41+
flags.Float(elName, "stability", 0, "Voice stability")
42+
flags.Bool(elName, "use-speaker-boost", false, "Use speaker boost")
43+
flags.Unsigned(elName, "bitrate", 0, "Bit rate (kbps)")
44+
flags.Unsigned(elName, "samplerate", 0, "Sample rate (kHz)")
45+
46+
// Register command set
47+
flags.Register(Cmd{
48+
Name: elName,
49+
Description: "Elevenlabs API",
50+
Parse: elParse,
51+
Fn: []Fn{
52+
{Name: "voices", Call: elVoices, Description: "Return registered voices"},
53+
{Name: "voice", Call: elVoice, Description: "Return one voice", MinArgs: 1, MaxArgs: 1, Syntax: "<voice-id>"},
54+
{Name: "settings", Call: elGetVoiceSettings, Description: "Return voice settings, or default settings", MaxArgs: 1, Syntax: "(<voice-id>)"},
55+
{Name: "set", Call: elSetVoiceSettings, Description: "Set voice settings from -stability, -similarity-boost and -use-speaker-boost flags", MinArgs: 1, MaxArgs: 1, Syntax: "<voice-id>"},
56+
{Name: "say", Call: elTextToSpeech, Description: "Text to speech", MinArgs: 2, Syntax: "<voice-id> <text>..."},
57+
},
58+
})
59+
}
60+
61+
func elParse(flags *Flags, opts ...client.ClientOpt) error {
62+
// Set defaults
63+
if typ := flags.GetOutExt(); typ != "" {
64+
elExt = strings.ToLower(flags.GetOutExt())
65+
}
66+
67+
// Create the client
68+
apiKey := flags.GetString("elevenlabs-api-key")
69+
if apiKey == "" {
70+
return fmt.Errorf("missing -elevenlabs-api-key flag")
71+
} else if client, err := elevenlabs.New(apiKey, opts...); err != nil {
72+
return err
73+
} else {
74+
elClient = client
75+
}
76+
77+
// Get the bit rate and sample rate
78+
if bitrate, err := flags.GetValue("bitrate"); err == nil {
79+
if bitrate_, ok := bitrate.(uint64); ok && bitrate_ > 0 {
80+
elBitrate = bitrate_
81+
}
82+
}
83+
if samplerate, err := flags.GetValue("samplerate"); err == nil {
84+
if samplerate_, ok := samplerate.(uint64); ok && samplerate_ > 0 {
85+
elSamplerate = samplerate_
86+
}
87+
}
88+
89+
// Return success
90+
return nil
91+
}
92+
93+
/////////////////////////////////////////////////////////////////////
94+
// API CALL FUNCTIONS
95+
96+
func elVoices(ctx context.Context, w *tablewriter.Writer, args []string) error {
97+
voices, err := elClient.Voices()
98+
if err != nil {
99+
return err
100+
}
101+
return w.Write(voices)
102+
}
103+
104+
func elVoice(ctx context.Context, w *tablewriter.Writer, args []string) error {
105+
if voice, err := elVoiceId(args[0]); err != nil {
106+
return err
107+
} else if voice, err := elClient.Voice(voice); err != nil {
108+
return err
109+
} else {
110+
return w.Write(voice)
111+
}
112+
}
113+
114+
func elGetVoiceSettings(ctx context.Context, w *tablewriter.Writer, args []string) error {
115+
var voice string
116+
if len(args) > 0 {
117+
if v, err := elVoiceId(args[0]); err != nil {
118+
return err
119+
} else {
120+
voice = v
121+
}
122+
}
123+
if voice, err := elClient.VoiceSettings(voice); err != nil {
124+
return err
125+
} else {
126+
return w.Write(voice)
127+
}
128+
}
129+
130+
func elSetVoiceSettings(ctx context.Context, w *tablewriter.Writer, args []string) error {
131+
var voice string
132+
if len(args) > 0 {
133+
if v, err := elVoiceId(args[0]); err != nil {
134+
return err
135+
} else {
136+
voice = v
137+
}
138+
}
139+
140+
// Get voice settings
141+
settings, err := elClient.VoiceSettings(voice)
142+
if err != nil {
143+
return err
144+
}
145+
146+
// TODO: Modify settings
147+
fmt.Println("TODO: elSetVoiceSettings: Modify settings")
148+
149+
// Set voice settings
150+
if err := elClient.SetVoiceSettings(voice, settings); err != nil {
151+
return err
152+
} else {
153+
return w.Write(settings)
154+
}
155+
}
156+
157+
func elTextToSpeech(ctx context.Context, w *tablewriter.Writer, args []string) error {
158+
// The voice to use
159+
voice, err := elVoiceId(args[0])
160+
if err != nil {
161+
return err
162+
}
163+
164+
// Output format
165+
opts := []elevenlabs.Opt{}
166+
if format := elOutputFormat(); format != nil {
167+
opts = append(opts, format)
168+
} else {
169+
return ErrBadParameter.Withf("invalid output format %q", elExt)
170+
}
171+
172+
// The text to speak
173+
text := strings.Join(args[1:], " ")
174+
175+
// If wav, then wrap in a header
176+
if elExt == "wav" {
177+
// Create the writer
178+
writer := NewAudioWriter(w.Output().(io.WriteSeeker), int(elSamplerate), 1)
179+
defer writer.Close()
180+
181+
// Read the data
182+
if n, err := elClient.TextToSpeech(writer, voice, text, opts...); err != nil {
183+
return err
184+
} else {
185+
elClient.Debugf("elTextToSpeech: generated %v bytes of PCM data", n)
186+
}
187+
} else if _, err := elClient.TextToSpeech(w.Output(), voice, text, opts...); err != nil {
188+
return err
189+
}
190+
191+
// Return success
192+
return nil
193+
}
194+
195+
/////////////////////////////////////////////////////////////////////
196+
// PRIVATE METHODS
197+
198+
func elVoiceId(q string) (string, error) {
199+
if reVoiceId.MatchString(q) {
200+
return q, nil
201+
} else if voices, err := elClient.Voices(); err != nil {
202+
return "", err
203+
} else {
204+
for _, v := range voices {
205+
if strings.EqualFold(v.Name, q) || v.Id == q {
206+
return v.Id, nil
207+
}
208+
}
209+
}
210+
return "", ErrNotFound.Withf("%q", q)
211+
}
212+
213+
func elOutputFormat() elevenlabs.Opt {
214+
switch elExt {
215+
case "mp3":
216+
return elevenlabs.OptFormatMP3(uint(elBitrate), uint(elSamplerate))
217+
case "wav":
218+
return elevenlabs.OptFormatPCM(uint(elSamplerate))
219+
case "ulaw":
220+
return elevenlabs.OptFormatULAW()
221+
}
222+
return nil
223+
}
224+
225+
/////////////////////////////////////////////////////////////////////
226+
// AUDIO WRITER
227+
228+
type audioWriter struct {
229+
enc *wav.Encoder
230+
buf *bytes.Buffer
231+
pcm *audio.IntBuffer
232+
}
233+
234+
func NewAudioWriter(w io.WriteSeeker, sampleRate, channels int) *audioWriter {
235+
this := new(audioWriter)
236+
237+
// Create a WAV encoder
238+
this.enc = wav.NewEncoder(w, sampleRate, 16, channels, 1)
239+
if this.enc == nil {
240+
return nil
241+
}
242+
243+
// Create a buffer for the incoming byte data
244+
this.buf = bytes.NewBuffer(nil)
245+
246+
// Make a PCM buffer with a capacity of 4096 samples
247+
this.pcm = &audio.IntBuffer{
248+
Format: &audio.Format{
249+
SampleRate: this.enc.SampleRate,
250+
NumChannels: this.enc.NumChans,
251+
},
252+
SourceBitDepth: this.enc.BitDepth,
253+
Data: make([]int, 0, 4096),
254+
}
255+
256+
// Return the writer
257+
return this
258+
}
259+
260+
func (a *audioWriter) Write(data []byte) (int, error) {
261+
// Write the data to the buffer
262+
if n, err := a.buf.Write(data); err != nil {
263+
return 0, err
264+
} else if err := a.Flush(); err != nil {
265+
return 0, err
266+
} else {
267+
return n, nil
268+
}
269+
}
270+
271+
func (a *audioWriter) Flush() error {
272+
var n int
273+
var sample [2]byte
274+
275+
// Read data until we have a full PCM buffer
276+
for {
277+
if a.buf.Len() < len(sample) {
278+
break
279+
} else if n, err := a.buf.Read(sample[:]); err != nil {
280+
return err
281+
} else if n != len(sample) {
282+
return ErrInternalAppError.With("short read")
283+
}
284+
285+
// Append the sample data - Little Endian
286+
a.pcm.Data = append(a.pcm.Data, int(int16(sample[0])|int16(sample[1])<<8))
287+
n += 2
288+
}
289+
290+
// Write the PCM data
291+
if n > 0 {
292+
if err := a.enc.Write(a.pcm); err != nil {
293+
return err
294+
}
295+
}
296+
297+
// Reset the PCM data
298+
a.pcm.Data = a.pcm.Data[:0]
299+
300+
// Return success
301+
return nil
302+
}
303+
304+
func (a *audioWriter) Close() error {
305+
if err := a.Flush(); err != nil {
306+
return err
307+
}
308+
return a.enc.Close()
309+
}

0 commit comments

Comments
 (0)