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
10 changes: 2 additions & 8 deletions app/tts/engine/elevenlabs/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,13 @@ func FetchModels() (map[string]engine.Model, error) {

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)
httpResponse, err := httpClient.Do(request)
if err != nil {
response.Error(util.MessageData{
Summary: "Failed to fetch elevenlabs models",
Expand Down Expand Up @@ -73,17 +70,14 @@ func FetchVoices() ([]engine.Voice, error) {
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)
httpResponse, err := httpClient.Do(request)
if err != nil {
return make([]engine.Voice, 0), response.Err(fmt.Errorf("performing request failed: %w", err))
}
Expand Down
12 changes: 8 additions & 4 deletions app/tts/engine/elevenlabs/elevenlabs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,22 @@ import (
"nstudio/app/common/util/fileIndex"
"nstudio/app/config"
"nstudio/app/tts/engine"
"time"
)

// requestTimeout bounds each HTTP call so a hung endpoint cannot block a
// pooled engine instance forever.
const requestTimeout = 30 * time.Second

type ElevenLabs struct {
Models map[string]Model
outputType string
}

var voices = make([]engine.Voice, 0)

var httpClient = &http.Client{Timeout: requestTimeout}

// <editor-fold desc="Engine Interface">
func (labs *ElevenLabs) Initialize() error {
var err error
Expand Down Expand Up @@ -201,10 +208,7 @@ func (labs *ElevenLabs) sendRequest(voiceID string, data ElevenLabsRequest) ([]b
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)
httpResponse, err := httpClient.Do(httpRequest)
if err != nil {
return nil, response.Err(fmt.Errorf("failed to send HTTP request: %v", err))
}
Expand Down
5 changes: 4 additions & 1 deletion app/tts/engine/gemini/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ func (gemini *Gemini) sendRequest(request GeminiRequest, modelName string) ([]by
return nil, response.Err(err)
}

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

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

// 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.
httpRequest.Header.Set("x-goog-api-key", apiKey)
httpRequest.Header.Set("Content-Type", "application/json")

client := &http.Client{Timeout: 30 * time.Second}
Expand Down
21 changes: 4 additions & 17 deletions app/tts/engine/google/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,19 @@ import (
"fmt"
"nstudio/app/common/response"
"nstudio/app/common/util"
"nstudio/app/config"
"nstudio/app/tts/engine"
"strings"

texttospeech "cloud.google.com/go/texttospeech/apiv1"
"cloud.google.com/go/texttospeech/apiv1/texttospeechpb"
"google.golang.org/api/option"
)

func (google *Google) sendRequest(data GoogleRequest) ([]byte, error) {
ctx := context.Background()
apiKey := config.GetEngine().Api.Google.ApiKey
if apiKey == "" {
return nil, response.Err(fmt.Errorf("Google Cloud API key is not set"))
}

client, err := texttospeech.NewClient(ctx, option.WithAPIKey(apiKey))
client, err := google.getClient(ctx)
if err != nil {
return nil, response.Err(fmt.Errorf("Failed to create Google TTS client: %v", err))
return nil, err
}
defer client.Close()

audioEncoding := texttospeechpb.AudioEncoding_MP3
if data.AudioConfig.AudioEncoding == "LINEAR16" {
Expand Down Expand Up @@ -103,16 +95,11 @@ func (google *Google) fetchVoices(model string) ([]engine.Voice, error) {
}

ctx := context.Background()
apiKey := config.GetEngine().Api.Google.ApiKey
if apiKey == "" {
return nil, response.Err(fmt.Errorf("Google Cloud API key is not set"))
}

client, err := texttospeech.NewClient(ctx, option.WithAPIKey(apiKey))
client, err := google.getClient(ctx)
if err != nil {
return nil, response.Err(fmt.Errorf("Failed to create Google TTS client: %v", err))
return nil, err
}
defer client.Close()

request := &texttospeechpb.ListVoicesRequest{}
dataResponse, err := client.ListVoices(ctx, request)
Expand Down
39 changes: 39 additions & 0 deletions app/tts/engine/google/google.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package google

import (
"context"
"encoding/json"
"fmt"
"nstudio/app/common/audio"
Expand All @@ -11,12 +12,42 @@ import (
"nstudio/app/tts/engine"
"strings"
"sync"

texttospeech "cloud.google.com/go/texttospeech/apiv1"
"google.golang.org/api/option"
)

type Google struct {
Models map[string]Model
voiceCache map[string][]engine.Voice
mu sync.RWMutex

clientMu sync.Mutex
client *texttospeech.Client
}

// getClient lazily creates and caches a single TextToSpeech client, reusing
// it across requests so each call does not repeat auth and connection setup.
func (google *Google) getClient(ctx context.Context) (*texttospeech.Client, error) {
google.clientMu.Lock()
defer google.clientMu.Unlock()

if google.client != nil {
return google.client, nil
}

apiKey := config.GetEngine().Api.Google.ApiKey
if apiKey == "" {
return nil, response.Err(fmt.Errorf("Google Cloud API key is not set"))
}

client, err := texttospeech.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
return nil, response.Err(fmt.Errorf("Failed to create Google TTS client: %v", err))
}

google.client = client
return client, nil
}

func (google *Google) Initialize() error {
Expand All @@ -32,6 +63,14 @@ func (google *Google) Start(modelName string) error {
}

func (google *Google) Stop(modelName string) error {
google.clientMu.Lock()
defer google.clientMu.Unlock()
if google.client != nil {
if err := google.client.Close(); err != nil {
return response.Err(err)
}
google.client = nil
}
return nil
}

Expand Down
12 changes: 8 additions & 4 deletions app/tts/engine/openai/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ import (
"nstudio/app/common/response"
"nstudio/app/common/util"
"nstudio/app/config"
"time"
)

// requestTimeout bounds each HTTP call so a hung endpoint cannot block a
// pooled engine instance forever.
const requestTimeout = 30 * time.Second

var httpClient = &http.Client{Timeout: requestTimeout}

func (openAI *OpenAI) sendRequest(data OpenAIRequest) ([]byte, error) {
apiKey := config.GetEngine().Api.OpenAI.ApiKey
if apiKey == "" {
Expand All @@ -30,10 +37,7 @@ func (openAI *OpenAI) sendRequest(data OpenAIRequest) ([]byte, error) {
httpRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
httpRequest.Header.Set("Content-Type", "application/json")

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

httpResponse, err := client.Do(httpRequest)
httpResponse, err := httpClient.Do(httpRequest)
if err != nil {
return nil, response.Err(fmt.Errorf("Failed to send HTTP httpRequest: %v", err))
}
Expand Down