From 3876203dcf91f374fd56b646ceb2a0019198773a Mon Sep 17 00:00:00 2001
From: Pavlo Voroniak
Date: Thu, 3 Oct 2024 10:42:33 +0300
Subject: [PATCH 1/2] add voice api
---
app/cognitive_profile.go | 44 ++++++++++++++++++++++
controller/cognitive_profile.go | 14 +++++++
go.mod | 14 +++----
grpc_api/cognitive_profile.go | 38 +++++++++++++++++++
model/cognitive_profile.go | 11 ++++++
tts/elevenlabs.go | 65 +++++++++++++++++++++++++++++++++
tts/google.go | 20 ++++++++++
tts/microsoft.go | 15 ++++++++
8 files changed, 214 insertions(+), 7 deletions(-)
diff --git a/app/cognitive_profile.go b/app/cognitive_profile.go
index 441ad93..a45b6f9 100644
--- a/app/cognitive_profile.go
+++ b/app/cognitive_profile.go
@@ -1,9 +1,22 @@
package app
import (
+ "strings"
+
"github.com/webitel/engine/auth_manager"
engine "github.com/webitel/engine/model"
"github.com/webitel/storage/model"
+ tts2 "github.com/webitel/storage/tts"
+)
+
+type ttsVoiceFunction func(domainId int64, params *model.SearchCognitiveProfileVoice) ([]*model.CognitiveProfileVoice, engine.AppError)
+
+var (
+ ttsVoiceEngine = map[string]ttsVoiceFunction{
+ strings.ToLower(TtsMicrosoft): tts2.MicrosoftVoice,
+ strings.ToLower(TtsGoogle): tts2.GoogleVoice,
+ strings.ToLower(TtsElevenLabs): tts2.ElevenLabsVoice,
+ }
)
func (app *App) CognitiveProfileCheckAccess(domainId, id int64, groups []int, access auth_manager.PermissionAccess) (bool, engine.AppError) {
@@ -32,6 +45,37 @@ func (app *App) SearchCognitiveProfilesByGroups(domainId int64, groups []int, se
return res, search.EndOfList(), nil
}
+func (app *App) SearchCognitiveProfileVoices(domainId int64, search *model.SearchCognitiveProfileVoice) ([]*model.CognitiveProfileVoice, engine.AppError) {
+ var ttsProfile *model.TtsProfile
+ ttsProfile, err := app.Store.CognitiveProfile().SearchTtsProfile(domainId, int(search.Id))
+ if err != nil {
+ return nil, err
+ }
+ if !ttsProfile.Enabled {
+ err = engine.NewBadRequestError("tts.profile.disabled", "Profile is disabled")
+
+ return nil, err
+ }
+
+ provider := ttsProfile.Provider
+ provider = strings.ToLower(provider)
+
+ if fn, ok := ttsVoiceEngine[provider]; ok {
+ res, ttsErr := fn(domainId, search)
+ if ttsErr != nil {
+ switch ttsErr.(type) {
+ case engine.AppError:
+ return nil, engine.NewNotFoundError("tts.valid.not_found", "Not found provider")
+ default:
+ return nil, engine.NewInternalError("tts.app_error", ttsErr.Error())
+ }
+ }
+ return res, nil
+ }
+
+ return nil, engine.NewNotFoundError("tts.valid.not_found", "Not found provider")
+}
+
func (app *App) GetCognitiveProfile(id, domain int64) (*model.CognitiveProfile, engine.AppError) {
return app.Store.CognitiveProfile().Get(id, domain)
}
diff --git a/controller/cognitive_profile.go b/controller/cognitive_profile.go
index 58a0944..1004bda 100644
--- a/controller/cognitive_profile.go
+++ b/controller/cognitive_profile.go
@@ -50,6 +50,20 @@ func (c *Controller) SearchCognitiveProfile(session *auth_manager.Session, domai
return list, endOfList, err
}
+func (c *Controller) SearchCognitiveProfileVoice(session *auth_manager.Session, domainId int64, search *model.SearchCognitiveProfileVoice) ([]*model.CognitiveProfileVoice, engine.AppError) {
+ permission := session.GetPermission(model.PermissionScopeCognitiveProfile)
+ if !permission.CanRead() {
+ return nil, c.app.MakePermissionError(session, permission, auth_manager.PERMISSION_ACCESS_READ)
+ }
+
+ var list []*model.CognitiveProfileVoice
+ var err engine.AppError
+
+ list, err = c.app.SearchCognitiveProfileVoices(session.Domain(domainId), search)
+
+ return list, err
+}
+
func (c *Controller) GetCognitiveProfile(session *auth_manager.Session, id int64, domainId int64) (*model.CognitiveProfile, engine.AppError) {
var err engine.AppError
permission := session.GetPermission(model.PermissionScopeCognitiveProfile)
diff --git a/go.mod b/go.mod
index 1269d83..2c2cbae 100644
--- a/go.mod
+++ b/go.mod
@@ -4,8 +4,8 @@ go 1.19
require (
buf.build/gen/go/webitel/engine/protocolbuffers/go v1.34.2-20240402125447-cb375844242f.2
- buf.build/gen/go/webitel/storage/grpc/go v1.4.0-20240731055617-28b8bce074fa.2
- buf.build/gen/go/webitel/storage/protocolbuffers/go v1.34.2-20240731055617-28b8bce074fa.2
+ buf.build/gen/go/webitel/storage/grpc/go v1.5.1-20241001075334-99db0a72402e.1
+ buf.build/gen/go/webitel/storage/protocolbuffers/go v1.34.2-20241001075334-99db0a72402e.2
cloud.google.com/go/speech v1.23.1
cloud.google.com/go/storage v1.39.1
cloud.google.com/go/texttospeech v1.7.7
@@ -28,7 +28,7 @@ require (
golang.org/x/sync v0.7.0
google.golang.org/api v0.177.0
google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae
- google.golang.org/grpc v1.63.2
+ google.golang.org/grpc v1.64.1
google.golang.org/protobuf v1.34.2
)
@@ -75,12 +75,12 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
- golang.org/x/crypto v0.22.0 // indirect
+ golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
- golang.org/x/net v0.24.0 // indirect
+ golang.org/x/net v0.26.0 // indirect
golang.org/x/oauth2 v0.19.0 // indirect
- golang.org/x/sys v0.19.0 // indirect
- golang.org/x/text v0.14.0 // indirect
+ golang.org/x/sys v0.21.0 // indirect
+ golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect
diff --git a/grpc_api/cognitive_profile.go b/grpc_api/cognitive_profile.go
index e4659d1..6dd49f2 100644
--- a/grpc_api/cognitive_profile.go
+++ b/grpc_api/cognitive_profile.go
@@ -188,6 +188,37 @@ func (api *cognitiveProfile) DeleteCognitiveProfile(ctx context.Context, in *sto
return toGrpcCognitiveProfile(profile), nil
}
+func (api *cognitiveProfile) SearchCognitiveProfileVoices(ctx context.Context, in *storage.SearchCognitiveProfileVoicesRequest) (*storage.ListCognitiveProfileVoices, error) {
+ session, err := api.ctrl.GetSessionFromCtx(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ var list []*model.CognitiveProfileVoice
+
+ rec := &model.SearchCognitiveProfileVoice{
+ ListRequest: model.ListRequest{
+ Q: in.GetQ(),
+ },
+ Id: in.Id,
+ Key: in.Key,
+ }
+
+ list, err = api.ctrl.SearchCognitiveProfileVoice(session, session.Domain(0), rec)
+
+ if err != nil {
+ return nil, err
+ }
+
+ items := make([]*storage.CognitiveProfileVoice, 0, len(list))
+ for _, v := range list {
+ items = append(items, toGrpcCognitiveProfileVoice(v))
+ }
+ return &storage.ListCognitiveProfileVoices{
+ Items: items,
+ }, nil
+}
+
func toGrpcCognitiveProfile(src *model.CognitiveProfile) *storage.CognitiveProfile {
// nullify password
src.Properties.Remove(model.CognitiveProfileKeyField)
@@ -207,6 +238,13 @@ func toGrpcCognitiveProfile(src *model.CognitiveProfile) *storage.CognitiveProfi
}
}
+func toGrpcCognitiveProfileVoice(src *model.CognitiveProfileVoice) *storage.CognitiveProfileVoice {
+ return &storage.CognitiveProfileVoice{
+ Id: src.Id,
+ Name: src.Name,
+ }
+}
+
func getProvider(p string) storage.ProviderType {
switch p {
case storage.ProviderType_Microsoft.String():
diff --git a/model/cognitive_profile.go b/model/cognitive_profile.go
index 4fb16ea..eb8e51d 100644
--- a/model/cognitive_profile.go
+++ b/model/cognitive_profile.go
@@ -29,6 +29,17 @@ type CognitiveProfile struct {
SyncTag int64 `json:"-" db:"-"`
}
+type CognitiveProfileVoice struct {
+ Id string `json:"id" db:"id"`
+ Name string `json:"name" db:"name"`
+}
+
+type SearchCognitiveProfileVoice struct {
+ ListRequest
+ Id int64
+ Key string
+}
+
type SearchCognitiveProfile struct {
ListRequest
Ids []int64
diff --git a/tts/elevenlabs.go b/tts/elevenlabs.go
index 3b40c8c..8b7ad27 100644
--- a/tts/elevenlabs.go
+++ b/tts/elevenlabs.go
@@ -1,6 +1,7 @@
package tts
import (
+ "crypto/tls"
"encoding/json"
"errors"
"fmt"
@@ -9,6 +10,9 @@ import (
"net/http"
"strconv"
"strings"
+
+ engine "github.com/webitel/engine/model"
+ "github.com/webitel/storage/model"
)
type ElevenLabsVoiceSettings struct {
@@ -24,6 +28,16 @@ type ElevenLabsRequest struct {
VoiceSettings ElevenLabsVoiceSettings `json:"voice_settings"`
}
+type Voice struct {
+ VoiceID string `json:"voice_id"`
+ Name string `json:"name"`
+ Category string `json:"category"`
+}
+
+type Response struct {
+ Voices []Voice `json:"voices"`
+}
+
func ElevenLabs(params TTSParams) (io.ReadCloser, *string, *int, error) {
token := string(fixKey(params.Key))
voiceId := ""
@@ -93,3 +107,54 @@ func ElevenLabs(params TTSParams) (io.ReadCloser, *string, *int, error) {
return res.Body, &ct, nil, nil
}
+
+func ElevenLabsVoice(domainId int64, req *model.SearchCognitiveProfileVoice) ([]*model.CognitiveProfileVoice, engine.AppError) {
+ token := req.Key
+ client := &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+ var url string
+ if req.Q != "" {
+ url = fmt.Sprintf("https://api.elevenlabs.io/v1/shared-voices?search=%s", req.Q)
+ } else {
+ url = "https://api.elevenlabs.io/v1/voices"
+ }
+
+ resp, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, engine.NewCustomCodeError("store.cognitive_profile_store.search_voice.app_error", err.Error(), http.StatusInternalServerError)
+ }
+
+ resp.Header.Add("xi-api-key", token)
+
+ res, err := client.Do(resp)
+ if err != nil {
+ return nil, engine.NewCustomCodeError("store.cognitive_profile_store.search_voice.app_error", err.Error(), http.StatusInternalServerError)
+ }
+ defer res.Body.Close()
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return nil, engine.NewCustomCodeError("store.cognitive_profile_store.search_voice.app_error", err.Error(), http.StatusInternalServerError)
+ }
+
+ var response Response
+ err = json.Unmarshal(body, &response)
+ if err != nil {
+ return nil, engine.NewCustomCodeError("store.cognitive_profile_store.search_voice.app_error", err.Error(), http.StatusInternalServerError)
+ }
+
+ var filteredVoices []*model.CognitiveProfileVoice
+ for _, voice := range response.Voices {
+ if req.Q != "" || voice.Category == "generated" {
+ filteredVoices = append(filteredVoices, &model.CognitiveProfileVoice{
+ Id: voice.VoiceID,
+ Name: voice.Name,
+ })
+ }
+ }
+
+ return filteredVoices, nil
+}
diff --git a/tts/google.go b/tts/google.go
index 6496b7f..a47cf4b 100644
--- a/tts/google.go
+++ b/tts/google.go
@@ -8,6 +8,8 @@ import (
"strings"
texttospeech "cloud.google.com/go/texttospeech/apiv1"
+ engine "github.com/webitel/engine/model"
+ "github.com/webitel/storage/model"
"google.golang.org/api/option"
texttospeechpb "google.golang.org/genproto/googleapis/cloud/texttospeech/v1"
)
@@ -108,3 +110,21 @@ func Google(params TTSParams) (io.ReadCloser, *string, *int, error) {
return r, &v, &size, nil
}
+
+func GoogleVoice(domainId int64, req *model.SearchCognitiveProfileVoice) ([]*model.CognitiveProfileVoice, engine.AppError) {
+ var voices []*model.CognitiveProfileVoice
+ voices = append(voices, &model.CognitiveProfileVoice{
+ Id: "FEMALE",
+ Name: "FEMALE",
+ })
+ voices = append(voices, &model.CognitiveProfileVoice{
+ Id: "MALE",
+ Name: "MALE",
+ })
+ voices = append(voices, &model.CognitiveProfileVoice{
+ Id: "NEUTRAL",
+ Name: "NEUTRAL",
+ })
+
+ return voices, nil
+}
diff --git a/tts/microsoft.go b/tts/microsoft.go
index 3750a66..2b142f6 100644
--- a/tts/microsoft.go
+++ b/tts/microsoft.go
@@ -9,6 +9,7 @@ import (
"strings"
engine "github.com/webitel/engine/model"
+ "github.com/webitel/storage/model"
"github.com/webitel/wlog"
)
@@ -72,6 +73,20 @@ func Microsoft(req TTSParams) (io.ReadCloser, *string, *int, error) {
return result.Body, &contentType, nil, nil
}
+func MicrosoftVoice(domainId int64, req *model.SearchCognitiveProfileVoice) ([]*model.CognitiveProfileVoice, engine.AppError) {
+ var voices []*model.CognitiveProfileVoice
+ voices = append(voices, &model.CognitiveProfileVoice{
+ Id: "FEMALE",
+ Name: "FEMALE",
+ })
+ voices = append(voices, &model.CognitiveProfileVoice{
+ Id: "MALE",
+ Name: "MALE",
+ })
+
+ return voices, nil
+}
+
func microsoftToken(key, region string) (string, error) {
req, err := http.NewRequest("POST", fmt.Sprintf("https://%s.api.cognitive.microsoft.com/sts/v1.0/issueToken", region), nil)
if err != nil {
From 69b07f43d95866f149622301ea3d922a8c8d409b Mon Sep 17 00:00:00 2001
From: Pavlo Voroniak
Date: Thu, 3 Oct 2024 10:52:13 +0300
Subject: [PATCH 2/2] fix get id
---
grpc_api/cognitive_profile.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/grpc_api/cognitive_profile.go b/grpc_api/cognitive_profile.go
index 6dd49f2..d040397 100644
--- a/grpc_api/cognitive_profile.go
+++ b/grpc_api/cognitive_profile.go
@@ -200,7 +200,7 @@ func (api *cognitiveProfile) SearchCognitiveProfileVoices(ctx context.Context, i
ListRequest: model.ListRequest{
Q: in.GetQ(),
},
- Id: in.Id,
+ Id: in.GetId(),
Key: in.Key,
}