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..d040397 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.GetId(), + 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 {