Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 74 additions & 118 deletions app/tts/engine/elevenlabs/api.go
Original file line number Diff line number Diff line change
@@ -1,118 +1,74 @@
package elevenlabs

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"nstudio/app/common/response"
"nstudio/app/common/util"
"nstudio/app/config"
"nstudio/app/tts/engine"
)

func FetchModels() (map[string]engine.Model, error) {
apiKey := config.GetEngine().Api.ElevenLabs.ApiKey
if apiKey == "" {
return make(map[string]engine.Model, 0), response.Err(fmt.Errorf("Api key is empty"))
}

modelsMap := make(map[string]engine.Model)

client := &http.Client{}
defer client.CloseIdleConnections()

request, err := http.NewRequest("GET", "https://api.elevenlabs.io/v1/models", nil)
if err != nil {
return modelsMap, response.Err(err)
}
request.Header.Set("xi-api-key", apiKey)

httpResponse, err := client.Do(request)
if err != nil {
response.Error(util.MessageData{
Summary: "Failed to fetch elevenlabs models",
Detail: err.Error(),
})
return modelsMap, response.Err(err)
}
defer httpResponse.Body.Close()

if httpResponse.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(httpResponse.Body)
return make(map[string]engine.Model, 0), response.Err(errors.New(string(bodyBytes)))
}

bodyBytes, err := io.ReadAll(httpResponse.Body)
if err != nil {
return make(map[string]engine.Model, 0), response.Err(err)
}

var modelsResponse []ModelResponse
err = json.Unmarshal(bodyBytes, &modelsResponse)
if err != nil {
return make(map[string]engine.Model, 0), response.Err(err)
}

for _, m := range modelsResponse {
model := engine.Model{
ID: m.ModelID,
Name: m.Name,
Engine: "elevenlabs",
}
modelsMap[m.ModelID] = model
}

return modelsMap, nil
}

func FetchVoices() ([]engine.Voice, error) {
apiKey := config.GetEngine().Api.ElevenLabs.ApiKey
if apiKey == "" {
return make([]engine.Voice, 0), response.Err(fmt.Errorf("api key is empty"))
}

client := &http.Client{}
defer client.CloseIdleConnections()

request, err := http.NewRequest("GET", "https://api.elevenlabs.io/v1/voices", nil)
if err != nil {
return make([]engine.Voice, 0), response.Err(fmt.Errorf("creating request failed: %w", err))
}

request.Header.Set("xi-api-key", apiKey)

httpResponse, err := client.Do(request)
if err != nil {
return make([]engine.Voice, 0), response.Err(fmt.Errorf("performing request failed: %w", err))
}
defer httpResponse.Body.Close()

if httpResponse.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(httpResponse.Body)
return make([]engine.Voice, 0), response.Err(fmt.Errorf("unexpected status code: %d, response: %s", httpResponse.StatusCode, string(bodyBytes)))
}

bodyBytes, err := io.ReadAll(httpResponse.Body)
if err != nil {
return make([]engine.Voice, 0), response.Err(fmt.Errorf("reading response body failed: %w", err))
}

var voicesResp VoicesResponse
err = json.Unmarshal(bodyBytes, &voicesResp)
if err != nil {
return make([]engine.Voice, 0), response.Err(fmt.Errorf("parsing JSON failed: %w", err))
}

responseVoices := make([]engine.Voice, 0, len(voicesResp.Voices))
for _, vd := range voicesResp.Voices {
voice := engine.Voice{
ID: vd.VoiceID,
Name: vd.Name,
Gender: vd.Labels.Gender,
}
responseVoices = append(responseVoices, voice)
}
return responseVoices, nil
}
package elevenlabs

import (
"encoding/json"
"fmt"
"nstudio/app/common/response"
"nstudio/app/config"
"nstudio/app/tts/engine"
"nstudio/app/tts/engine/internal/httpapi"
)

func FetchModels() (map[string]engine.Model, error) {
apiKey := config.GetEngine().Api.ElevenLabs.ApiKey
if apiKey == "" {
return make(map[string]engine.Model, 0), response.Err(fmt.Errorf("Api key is empty"))
}

body, err := httpapi.Do(httpapi.Request{
Method: "GET",
URL: "https://api.elevenlabs.io/v1/models",
Headers: map[string]string{"xi-api-key": apiKey},
})
if err != nil {
return make(map[string]engine.Model, 0), err
}

var modelsResponse []ModelResponse
if err := json.Unmarshal(body, &modelsResponse); err != nil {
return make(map[string]engine.Model, 0), response.Err(err)
}

modelsMap := make(map[string]engine.Model)
for _, m := range modelsResponse {
modelsMap[m.ModelID] = engine.Model{
ID: m.ModelID,
Name: m.Name,
Engine: "elevenlabs",
}
}

return modelsMap, nil
}

func FetchVoices() ([]engine.Voice, error) {
apiKey := config.GetEngine().Api.ElevenLabs.ApiKey
if apiKey == "" {
return make([]engine.Voice, 0), response.Err(fmt.Errorf("api key is empty"))
}

body, err := httpapi.Do(httpapi.Request{
Method: "GET",
URL: "https://api.elevenlabs.io/v1/voices",
Headers: map[string]string{"xi-api-key": apiKey},
})
if err != nil {
return make([]engine.Voice, 0), err
}

var voicesResp VoicesResponse
if err := json.Unmarshal(body, &voicesResp); err != nil {
return make([]engine.Voice, 0), response.Err(fmt.Errorf("parsing JSON failed: %w", err))
}

responseVoices := make([]engine.Voice, 0, len(voicesResp.Voices))
for _, vd := range voicesResp.Voices {
responseVoices = append(responseVoices, engine.Voice{
ID: vd.VoiceID,
Name: vd.Name,
Gender: vd.Labels.Gender,
})
}

return responseVoices, nil
}
51 changes: 9 additions & 42 deletions app/tts/engine/elevenlabs/elevenlabs.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package elevenlabs

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
commonAudio "nstudio/app/common/audio"
"nstudio/app/common/response"
"nstudio/app/common/util"
"nstudio/app/common/util/fileIndex"
"nstudio/app/config"
"nstudio/app/tts/engine"
"nstudio/app/tts/engine/internal/httpapi"
)

type ElevenLabs struct {
Expand Down Expand Up @@ -72,8 +70,6 @@ func (labs *ElevenLabs) Play(message util.CharacterMessage) error {
return response.Success(util.MessageData{
Summary: "ElevenLabs finished playing audio",
})

return nil
}

func (labs *ElevenLabs) Save(messages []util.CharacterMessage, play bool) error {
Expand Down Expand Up @@ -186,46 +182,17 @@ func (labs *ElevenLabs) sendRequest(voiceID string, data ElevenLabsRequest) ([]b
return nil, response.Err(fmt.Errorf("Elevenlabs API Key is not set"))
}

jsonData, err := json.Marshal(data)
if err != nil {
return nil, response.Err(fmt.Errorf("failed to marshal request body: %v", err))
}

url := fmt.Sprintf("https://api.elevenlabs.io/v1/text-to-speech/%s?output_format=%s", voiceID, labs.outputType)

httpRequest, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, response.Err(fmt.Errorf("failed to create HTTP request: %v", err))
}

httpRequest.Header.Set("xi-api-key", apiKey)
httpRequest.Header.Set("Content-Type", "application/json")

client := &http.Client{}
defer client.CloseIdleConnections()

httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, response.Err(fmt.Errorf("failed to send HTTP request: %v", err))
}
defer httpResponse.Body.Close()

if httpResponse.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(httpResponse.Body)
return nil, response.Err(fmt.Errorf("request failed with status %d: %s", httpResponse.StatusCode, string(bodyBytes)))
}

responseData, err := io.ReadAll(httpResponse.Body)
if err != nil {
return nil, response.Err(fmt.Errorf("failed to read response body: %v", err))
}

response.Success(util.MessageData{
Summary: "ElevenLabs request succeeded",
Detail: "Response Status: " + httpResponse.Status,
return httpapi.Do(httpapi.Request{
Method: "POST",
URL: url,
Headers: map[string]string{
"xi-api-key": apiKey,
"Content-Type": "application/json",
},
Body: data,
})

return responseData, nil
}

// </editor-fold>
122 changes: 55 additions & 67 deletions app/tts/engine/gemini/api.go
Original file line number Diff line number Diff line change
@@ -1,67 +1,55 @@
package gemini

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"nstudio/app/common/response"
"nstudio/app/config"
"time"
)

func (gemini *Gemini) sendRequest(request GeminiRequest, modelName string) ([]byte, error) {
apiKey := config.GetEngine().Api.Gemini.ApiKey
if apiKey == "" {
return nil, fmt.Errorf("Gemini API key is not configured")
}

jsonData, err := json.Marshal(request)
if err != nil {
return nil, response.Err(err)
}

url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s", modelName, apiKey)

httpRequest, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, response.Err(err)
}

httpRequest.Header.Set("Content-Type", "application/json")

client := &http.Client{Timeout: 30 * time.Second}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, response.Err(err)
}
defer httpResponse.Body.Close()

if httpResponse.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(httpResponse.Body)
return nil, fmt.Errorf("Gemini API error: %s - %s", httpResponse.Status, string(bodyBytes))
}

var geminiResponse GeminiResponse
if err := json.NewDecoder(httpResponse.Body).Decode(&geminiResponse); err != nil {
return nil, response.Err(err)
}

if len(geminiResponse.Candidates) == 0 || len(geminiResponse.Candidates[0].Content.Parts) == 0 {
return nil, fmt.Errorf("no content in Gemini response")
}

base64Data := geminiResponse.Candidates[0].Content.Parts[0].InlineData.Data
if base64Data == "" {
return nil, fmt.Errorf("no audio data in Gemini response")
}

audioData, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return nil, response.Err(err)
}

return audioData, nil
}
package gemini

import (
"encoding/base64"
"encoding/json"
"fmt"
"nstudio/app/common/response"
"nstudio/app/config"
"nstudio/app/tts/engine/internal/httpapi"
)

func (gemini *Gemini) sendRequest(request GeminiRequest, modelName string) ([]byte, error) {
apiKey := config.GetEngine().Api.Gemini.ApiKey
if apiKey == "" {
return nil, fmt.Errorf("Gemini API key is not configured")
}

url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent", modelName)

// Send the API key via the documented header instead of a URL query param
// so the secret does not leak into logs, proxies, or error bodies.
body, err := httpapi.Do(httpapi.Request{
Method: "POST",
URL: url,
Headers: map[string]string{
"x-goog-api-key": apiKey,
"Content-Type": "application/json",
},
Body: request,
})
if err != nil {
return nil, err
}

var geminiResponse GeminiResponse
if err := json.Unmarshal(body, &geminiResponse); err != nil {
return nil, response.Err(err)
}

if len(geminiResponse.Candidates) == 0 || len(geminiResponse.Candidates[0].Content.Parts) == 0 {
return nil, fmt.Errorf("no content in Gemini response")
}

base64Data := geminiResponse.Candidates[0].Content.Parts[0].InlineData.Data
if base64Data == "" {
return nil, fmt.Errorf("no audio data in Gemini response")
}

audioData, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return nil, response.Err(err)
}

return audioData, nil
}
Loading