Skip to content

Commit 3e97e0c

Browse files
authored
Merge pull request #14 from sumerc/elevenlabs-support
Add elevenlabs `scribe v2` support
2 parents 9f3c303 + 032fe1c commit 3e97e0c

5 files changed

Lines changed: 198 additions & 46 deletions

File tree

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ func run() {
361361
openaiKey := os.Getenv("OPENAI_API_KEY")
362362
dgKey := os.Getenv("DEEPGRAM_API_KEY")
363363
mistralKey := os.Getenv("MISTRAL_API_KEY")
364+
elevenLabsKey := os.Getenv("ELEVENLABS_API_KEY")
364365

365366
type providerDef struct {
366367
name, label, key string
@@ -372,6 +373,7 @@ func run() {
372373
{"openai", "OpenAI", openaiKey, transcriber.OpenAIModels, func() transcriber.Transcriber { return transcriber.NewOpenAI(openaiKey) }},
373374
{"deepgram", "Deepgram", dgKey, transcriber.DeepgramModels, func() transcriber.Transcriber { return transcriber.NewDeepgram(dgKey) }},
374375
{"mistral", "Mistral", mistralKey, transcriber.MistralModels, func() transcriber.Transcriber { return transcriber.NewMistral(mistralKey) }},
376+
{"elevenlabs", "ElevenLabs", elevenLabsKey, transcriber.ElevenLabsModels, func() transcriber.Transcriber { return transcriber.NewElevenLabs(elevenLabsKey) }},
375377
}
376378

377379
var trayModels []tray.Model

transcriber/elevenlabs.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package transcriber
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"mime/multipart"
9+
"net/http"
10+
)
11+
12+
const ModelScribeV2 = "scribe_v2"
13+
14+
var scribeV2Langs = langsFromCodes([]string{
15+
"af", "am", "ar", "hy", "as", "az", "be", "bn", "bs", "bg",
16+
"my", "ca", "ny", "hr", "cs", "da", "nl", "en", "et", "fi",
17+
"fr", "gl", "ka", "de", "el", "gu", "ha", "he", "hi", "hu",
18+
"is", "ig", "id", "ga", "it", "ja", "jv", "kn", "kk", "km",
19+
"ko", "ku", "ky", "lo", "lv", "ln", "lt", "lb", "mk", "ms",
20+
"ml", "mt", "zh", "mi", "mr", "mn", "ne", "no", "oc", "or",
21+
"ps", "fa", "pl", "pt", "pa", "ro", "ru", "sr", "sn", "sd",
22+
"sk", "sl", "so", "es", "sw", "sv", "ta", "tg", "te", "th",
23+
"tr", "uk", "ur", "uz", "vi", "cy", "wo", "xh", "zu",
24+
})
25+
26+
var ElevenLabsModels = []ModelInfo{
27+
{ID: ModelScribeV2, Label: "Scribe V2", Stream: false, Languages: scribeV2Langs},
28+
}
29+
30+
type ElevenLabs struct {
31+
baseTranscriber
32+
apiKey string
33+
}
34+
35+
func NewElevenLabs(apiKey string) *ElevenLabs {
36+
apiURL := "https://api.elevenlabs.io/v1/speech-to-text"
37+
return &ElevenLabs{
38+
baseTranscriber: baseTranscriber{
39+
client: NewTracedClient(apiURL),
40+
apiURL: apiURL,
41+
model: ModelScribeV2,
42+
},
43+
apiKey: apiKey,
44+
}
45+
}
46+
47+
func (e *ElevenLabs) SupportedLanguages() []Language {
48+
return modelLanguages(ElevenLabsModels, e.GetModel())
49+
}
50+
func (e *ElevenLabs) Name() string { return "elevenlabs" }
51+
func (e *ElevenLabs) Models() []ModelInfo { return ElevenLabsModels }
52+
53+
func (e *ElevenLabs) NewSession(_ context.Context, cfg SessionConfig) (Session, error) {
54+
go e.client.Warm()
55+
if cfg.Stream {
56+
return nil, fmt.Errorf("elevenlabs does not support streaming transcription")
57+
}
58+
return newBatchSession(cfg, e.transcribe)
59+
}
60+
61+
type elevenLabsResponse struct {
62+
Text string `json:"text"`
63+
LanguageCode string `json:"language_code"`
64+
LanguageProbability float64 `json:"language_probability"`
65+
Words []struct {
66+
Text string `json:"text"`
67+
Type string `json:"type"`
68+
Start float64 `json:"start"`
69+
End float64 `json:"end"`
70+
LogProb float64 `json:"logprob"`
71+
} `json:"words"`
72+
}
73+
74+
func (e *ElevenLabs) transcribe(audioData []byte, format, lang string) (*Result, error) {
75+
var body bytes.Buffer
76+
writer := multipart.NewWriter(&body)
77+
78+
part, err := writer.CreateFormFile("file", "audio."+format)
79+
if err != nil {
80+
return nil, err
81+
}
82+
if _, err := part.Write(audioData); err != nil {
83+
return nil, err
84+
}
85+
86+
writer.WriteField("model_id", e.GetModel())
87+
if lang != "" {
88+
writer.WriteField("language_code", lang)
89+
}
90+
writer.WriteField("tag_audio_events", "false")
91+
writer.Close()
92+
93+
req, err := http.NewRequest("POST", e.apiURL, &body)
94+
if err != nil {
95+
return nil, err
96+
}
97+
98+
req.Header.Set("xi-api-key", e.apiKey)
99+
req.Header.Set("Content-Type", writer.FormDataContentType())
100+
101+
resp, err := e.client.Do(req)
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
if resp.StatusCode != 200 {
107+
return nil, fmt.Errorf("elevenlabs API error %d: %s", resp.StatusCode, string(resp.Body))
108+
}
109+
110+
var elResp elevenLabsResponse
111+
if err := json.Unmarshal(resp.Body, &elResp); err != nil {
112+
return nil, fmt.Errorf("elevenlabs response parse error: %w", err)
113+
}
114+
115+
var avgLogProb float64
116+
var wordCount int
117+
var duration float64
118+
for _, w := range elResp.Words {
119+
if w.Type == "word" {
120+
avgLogProb += w.LogProb
121+
wordCount++
122+
if w.End > duration {
123+
duration = w.End
124+
}
125+
}
126+
}
127+
if wordCount > 0 {
128+
avgLogProb /= float64(wordCount)
129+
}
130+
131+
return &Result{
132+
Text: elResp.Text,
133+
Metrics: resp.Metrics,
134+
Confidence: elResp.LanguageProbability,
135+
AvgLogProb: avgLogProb,
136+
Duration: duration,
137+
}, nil
138+
}

transcriber/transcriber.go

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,29 @@ type Transcriber interface {
8484

8585
// langLabels maps ISO-639-1 codes to display names.
8686
var langLabels = map[string]string{
87-
"af": "Afrikaans", "ar": "Arabic", "hy": "Armenian", "az": "Azerbaijani",
88-
"be": "Belarusian", "bs": "Bosnian", "bg": "Bulgarian", "ca": "Catalan",
89-
"zh": "Chinese", "hr": "Croatian", "cs": "Czech", "da": "Danish",
90-
"nl": "Dutch", "en": "English", "et": "Estonian", "fi": "Finnish",
91-
"fr": "French", "gl": "Galician", "de": "German", "el": "Greek",
87+
"af": "Afrikaans", "am": "Amharic", "ar": "Arabic", "hy": "Armenian",
88+
"as": "Assamese", "az": "Azerbaijani", "be": "Belarusian", "bn": "Bengali",
89+
"bs": "Bosnian", "bg": "Bulgarian", "my": "Burmese", "ca": "Catalan",
90+
"ny": "Chichewa", "zh": "Chinese", "hr": "Croatian", "cs": "Czech",
91+
"da": "Danish", "nl": "Dutch", "en": "English", "et": "Estonian",
92+
"fi": "Finnish", "fr": "French", "gl": "Galician", "ka": "Georgian",
93+
"de": "German", "el": "Greek", "gu": "Gujarati", "ha": "Hausa",
9294
"he": "Hebrew", "hi": "Hindi", "hu": "Hungarian", "is": "Icelandic",
93-
"id": "Indonesian", "it": "Italian", "ja": "Japanese", "kn": "Kannada",
94-
"kk": "Kazakh", "ko": "Korean", "lv": "Latvian", "lt": "Lithuanian",
95-
"mk": "Macedonian", "ms": "Malay", "mr": "Marathi", "mi": "Maori",
96-
"ne": "Nepali", "no": "Norwegian", "fa": "Persian", "pl": "Polish",
97-
"pt": "Portuguese", "ro": "Romanian", "ru": "Russian", "sr": "Serbian",
98-
"sk": "Slovak", "sl": "Slovenian", "es": "Spanish", "sw": "Swahili",
99-
"sv": "Swedish", "tl": "Tagalog", "ta": "Tamil", "th": "Thai",
100-
"tr": "Turkish", "uk": "Ukrainian", "ur": "Urdu", "vi": "Vietnamese",
101-
"cy": "Welsh",
95+
"ig": "Igbo", "id": "Indonesian", "ga": "Irish", "it": "Italian",
96+
"ja": "Japanese", "jv": "Javanese", "kn": "Kannada", "kk": "Kazakh",
97+
"km": "Khmer", "ko": "Korean", "ku": "Kurdish", "ky": "Kyrgyz",
98+
"lo": "Lao", "lv": "Latvian", "ln": "Lingala", "lt": "Lithuanian",
99+
"lb": "Luxembourgish", "mk": "Macedonian", "ms": "Malay", "ml": "Malayalam",
100+
"mt": "Maltese", "mi": "Maori", "mr": "Marathi", "mn": "Mongolian",
101+
"ne": "Nepali", "no": "Norwegian", "oc": "Occitan", "or": "Odia",
102+
"ps": "Pashto", "fa": "Persian", "pl": "Polish", "pt": "Portuguese",
103+
"pa": "Punjabi", "ro": "Romanian", "ru": "Russian", "sr": "Serbian",
104+
"sn": "Shona", "sd": "Sindhi", "sk": "Slovak", "sl": "Slovenian",
105+
"so": "Somali", "es": "Spanish", "sw": "Swahili", "sv": "Swedish",
106+
"ta": "Tamil", "tg": "Tajik", "te": "Telugu", "th": "Thai",
107+
"tl": "Tagalog", "tr": "Turkish", "uk": "Ukrainian", "ur": "Urdu",
108+
"uz": "Uzbek", "vi": "Vietnamese", "cy": "Welsh", "wo": "Wolof",
109+
"xh": "Xhosa", "zu": "Zulu",
102110
}
103111

104112
func langsFromCodes(codes []string) []Language {
@@ -170,6 +178,7 @@ func New() (Transcriber, error) {
170178
openaiKey := os.Getenv("OPENAI_API_KEY")
171179
groqKey := os.Getenv("GROQ_API_KEY")
172180
mistralKey := os.Getenv("MISTRAL_API_KEY")
181+
elevenLabsKey := os.Getenv("ELEVENLABS_API_KEY")
173182

174183
if dgKey != "" {
175184
return NewDeepgram(dgKey), nil
@@ -183,6 +192,9 @@ func New() (Transcriber, error) {
183192
if mistralKey != "" {
184193
return NewMistral(mistralKey), nil
185194
}
195+
if elevenLabsKey != "" {
196+
return NewElevenLabs(elevenLabsKey), nil
197+
}
186198

187-
return nil, fmt.Errorf("set DEEPGRAM_API_KEY, OPENAI_API_KEY, GROQ_API_KEY, or MISTRAL_API_KEY environment variable")
199+
return nil, fmt.Errorf("set DEEPGRAM_API_KEY, OPENAI_API_KEY, GROQ_API_KEY, MISTRAL_API_KEY, or ELEVENLABS_API_KEY environment variable")
188200
}

tray/tray.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ func statusText() string {
145145
lang = langCode
146146
}
147147
if provider == "" {
148-
return "n/a"
148+
return "𝘻𝘦𝘦"
149149
}
150-
return provider + " · " + model + " · " + lang
150+
return "𝘻𝘦𝘦 — " + provider + " · " + model + " · " + lang
151151
}
152152

153153
func updateStatus() {

tray/tray_darwin.go

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func updateRecordingIcon(rec bool) {
4848
if rec {
4949
systray.SetIcon(iconRecHi)
5050
if mRecord != nil {
51-
mRecord.SetTitle("🔴 Stop Recording (Shift+Control+Space)")
51+
mRecord.SetTitle(" Stop Recording (Shift+Control+Space)")
5252
}
5353
} else {
5454
systray.SetTemplateIcon(iconIdleHi, iconIdle)
@@ -176,8 +176,6 @@ func onReady() {
176176
}
177177
})
178178

179-
systray.AddSeparator()
180-
181179
mCopy = systray.AddMenuItem("Copy Last Recorded Text", "Copy last transcription to clipboard")
182180
mCopy.Disable()
183181
mCopy.Click(func() {
@@ -186,34 +184,8 @@ func onReady() {
186184
}
187185
})
188186

189-
systray.AddSeparator()
190-
191187
mSettings = systray.AddMenuItem("Settings", "Settings")
192188

193-
mDevices = mSettings.AddSubMenuItem("Devices", "Select input device")
194-
195-
deviceMu.Lock()
196-
mDefaultDevice = mDevices.AddSubMenuItemCheckbox("System Default", "Use system default device", deviceSel == "")
197-
mDefaultDevice.Click(func() {
198-
deviceMu.Lock()
199-
cb := deviceCb
200-
deviceMu.Unlock()
201-
if cb != nil {
202-
cb("")
203-
}
204-
deviceMu.Lock()
205-
for _, it := range deviceItems {
206-
it.Uncheck()
207-
}
208-
mDefaultDevice.Check()
209-
deviceMu.Unlock()
210-
})
211-
deviceItems = make([]*systray.MenuItem, 0, len(deviceNames))
212-
for i, name := range deviceNames {
213-
item := addDeviceItem(mDevices, i, name, name == deviceSel)
214-
deviceItems = append(deviceItems, item)
215-
}
216-
deviceMu.Unlock()
217189
mAutoPaste = mSettings.AddSubMenuItemCheckbox("Auto-paste", "Auto-paste transcribed text", autoPasteOn)
218190
mAutoPaste.Click(func() {
219191
if mAutoPaste.Checked() {
@@ -241,6 +213,34 @@ func onReady() {
241213
}
242214
})
243215

216+
sep := mSettings.AddSubMenuItem("─────────", "")
217+
sep.Disable()
218+
219+
mDevices = mSettings.AddSubMenuItem("Microphone", "Select input device")
220+
221+
deviceMu.Lock()
222+
mDefaultDevice = mDevices.AddSubMenuItemCheckbox("System Default", "Use system default device", deviceSel == "")
223+
mDefaultDevice.Click(func() {
224+
deviceMu.Lock()
225+
cb := deviceCb
226+
deviceMu.Unlock()
227+
if cb != nil {
228+
cb("")
229+
}
230+
deviceMu.Lock()
231+
for _, it := range deviceItems {
232+
it.Uncheck()
233+
}
234+
mDefaultDevice.Check()
235+
deviceMu.Unlock()
236+
})
237+
deviceItems = make([]*systray.MenuItem, 0, len(deviceNames))
238+
for i, name := range deviceNames {
239+
item := addDeviceItem(mDevices, i, name, name == deviceSel)
240+
deviceItems = append(deviceItems, item)
241+
}
242+
deviceMu.Unlock()
243+
244244
modelMu.Lock()
245245
if len(models) > 0 {
246246
mBackend = mSettings.AddSubMenuItem("Model", "Select transcription model")

0 commit comments

Comments
 (0)