feat: support cloned voice deletion
This commit is contained in:
parent
c4341335d7
commit
6089aa6085
@ -398,6 +398,36 @@ func (c MinimaxClient) runVoiceClone(ctx context.Context, request Request) (Resp
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c MinimaxClient) DeleteVoiceClone(ctx context.Context, request VoiceCloneDeleteRequest) (VoiceCloneDeleteResponse, error) {
|
||||||
|
startedAt := time.Now()
|
||||||
|
voiceID := strings.TrimSpace(request.VoiceID)
|
||||||
|
if voiceID == "" {
|
||||||
|
return VoiceCloneDeleteResponse{}, &ClientError{Code: "bad_request", Message: "voice_id is required", Retryable: false}
|
||||||
|
}
|
||||||
|
client := httpClient(request.HTTPClient, c.HTTPClient)
|
||||||
|
payload := map[string]any{
|
||||||
|
"voice_type": "voice_cloning",
|
||||||
|
"voice_id": voiceID,
|
||||||
|
}
|
||||||
|
result, requestID, err := providerPostJSON(ctx, client, providerURL(request.Candidate.BaseURL, "/delete_voice"), payload, request.Candidate.Credentials, "bearer")
|
||||||
|
finishedAt := time.Now()
|
||||||
|
if err != nil {
|
||||||
|
return VoiceCloneDeleteResponse{}, annotateResponseError(err, requestID, startedAt, finishedAt)
|
||||||
|
}
|
||||||
|
if isProviderTaskFailure(providerTaskSpec{Name: "minimax"}, result) {
|
||||||
|
return VoiceCloneDeleteResponse{}, providerTaskFailure(providerTaskSpec{Name: "minimax"}, result, firstNonEmptyString(requestID, requestIDFromResult(result)), startedAt)
|
||||||
|
}
|
||||||
|
deletedVoiceID := firstNonEmptyString(valueAtPath(result, "voice_id"), voiceID)
|
||||||
|
return VoiceCloneDeleteResponse{
|
||||||
|
VoiceID: deletedVoiceID,
|
||||||
|
RequestID: firstNonEmptyString(requestID, requestIDFromResult(result)),
|
||||||
|
Result: result,
|
||||||
|
StartedAt: startedAt,
|
||||||
|
FinishedAt: finishedAt,
|
||||||
|
DurationMS: responseDurationMS(startedAt, finishedAt),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c MinimaxClient) minimaxVoiceCloneFileID(ctx context.Context, client *http.Client, request Request, body map[string]any, purpose string, fileIDKey string, sourceKeys ...string) (any, string, error) {
|
func (c MinimaxClient) minimaxVoiceCloneFileID(ctx context.Context, client *http.Client, request Request, body map[string]any, purpose string, fileIDKey string, sourceKeys ...string) (any, string, error) {
|
||||||
if value := firstPresent(body[fileIDKey], nil); value != nil {
|
if value := firstPresent(body[fileIDKey], nil); value != nil {
|
||||||
return normalizeMinimaxFileID(value), "", nil
|
return normalizeMinimaxFileID(value), "", nil
|
||||||
|
|||||||
@ -60,6 +60,25 @@ type Client interface {
|
|||||||
Run(ctx context.Context, request Request) (Response, error)
|
Run(ctx context.Context, request Request) (Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VoiceCloneDeleteRequest struct {
|
||||||
|
VoiceID string
|
||||||
|
Candidate store.RuntimeModelCandidate
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoiceCloneDeleteResponse struct {
|
||||||
|
VoiceID string
|
||||||
|
RequestID string
|
||||||
|
Result map[string]any
|
||||||
|
StartedAt time.Time
|
||||||
|
FinishedAt time.Time
|
||||||
|
DurationMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoiceCloneDeleter interface {
|
||||||
|
DeleteVoiceClone(ctx context.Context, request VoiceCloneDeleteRequest) (VoiceCloneDeleteResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
type ClientError struct {
|
type ClientError struct {
|
||||||
Code string
|
Code string
|
||||||
Message string
|
Message string
|
||||||
|
|||||||
@ -1306,7 +1306,7 @@ func scopeForTaskKind(kind string) string {
|
|||||||
|
|
||||||
func statusFromRunError(err error) int {
|
func statusFromRunError(err error) int {
|
||||||
switch {
|
switch {
|
||||||
case clients.ErrorCode(err) == "bad_request" || clients.ErrorCode(err) == "cloned_voice_expired" || clients.ErrorCode(err) == "cloned_voice_unavailable":
|
case clients.ErrorCode(err) == "bad_request" || clients.ErrorCode(err) == "cloned_voice_expired" || clients.ErrorCode(err) == "cloned_voice_unavailable" || clients.ErrorCode(err) == "cloned_voice_platform_unavailable" || clients.ErrorCode(err) == "unsupported_operation" || clients.ErrorCode(err) == "invalid_proxy":
|
||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
case clients.ErrorCode(err) == "cloned_voice_not_found":
|
case clients.ErrorCode(err) == "cloned_voice_not_found":
|
||||||
return http.StatusNotFound
|
return http.StatusNotFound
|
||||||
|
|||||||
@ -145,6 +145,7 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
|||||||
mux.Handle("POST /api/v1/speech/generations", server.auth.Require(auth.PermissionBasic, server.createTask("speech.generations", true)))
|
mux.Handle("POST /api/v1/speech/generations", server.auth.Require(auth.PermissionBasic, server.createTask("speech.generations", true)))
|
||||||
mux.Handle("POST /api/v1/voice_clone", server.auth.Require(auth.PermissionBasic, server.createTask("voice.clone", true)))
|
mux.Handle("POST /api/v1/voice_clone", server.auth.Require(auth.PermissionBasic, server.createTask("voice.clone", true)))
|
||||||
mux.Handle("GET /api/v1/voice_clone/voices", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listClonedVoices)))
|
mux.Handle("GET /api/v1/voice_clone/voices", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listClonedVoices)))
|
||||||
|
mux.Handle("DELETE /api/v1/voice_clone/voices/{voiceID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.deleteClonedVoice)))
|
||||||
mux.Handle("POST /api/v1/files/upload", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.uploadFile)))
|
mux.Handle("POST /api/v1/files/upload", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.uploadFile)))
|
||||||
mux.Handle("GET /api/v1/tasks", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listTasks)))
|
mux.Handle("GET /api/v1/tasks", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listTasks)))
|
||||||
mux.Handle("GET /api/v1/tasks/{taskID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.getTask)))
|
mux.Handle("GET /api/v1/tasks/{taskID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.getTask)))
|
||||||
@ -178,6 +179,8 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
|||||||
mux.Handle("POST /v1/voice_clone", server.auth.Require(auth.PermissionBasic, server.createTask("voice.clone", true)))
|
mux.Handle("POST /v1/voice_clone", server.auth.Require(auth.PermissionBasic, server.createTask("voice.clone", true)))
|
||||||
mux.Handle("GET /voice_clone/voices", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listClonedVoices)))
|
mux.Handle("GET /voice_clone/voices", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listClonedVoices)))
|
||||||
mux.Handle("GET /v1/voice_clone/voices", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listClonedVoices)))
|
mux.Handle("GET /v1/voice_clone/voices", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listClonedVoices)))
|
||||||
|
mux.Handle("DELETE /voice_clone/voices/{voiceID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.deleteClonedVoice)))
|
||||||
|
mux.Handle("DELETE /v1/voice_clone/voices/{voiceID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.deleteClonedVoice)))
|
||||||
mux.Handle("POST /v1/files/upload", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.uploadFile)))
|
mux.Handle("POST /v1/files/upload", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.uploadFile)))
|
||||||
mux.Handle("POST /v1/tasks/{taskID}/cancel", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.cancelTask)))
|
mux.Handle("POST /v1/tasks/{taskID}/cancel", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.cancelTask)))
|
||||||
|
|
||||||
|
|||||||
@ -36,3 +36,41 @@ func (s *Server) listClonedVoices(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deleteClonedVoice godoc
|
||||||
|
// @Summary 删除当前用户克隆音色
|
||||||
|
// @Description 先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。
|
||||||
|
// @Tags voice-clone
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param voiceID path string true "克隆音色记录 ID 或上游 voice_id"
|
||||||
|
// @Success 200 {object} map[string]any
|
||||||
|
// @Failure 400 {object} ErrorEnvelope
|
||||||
|
// @Failure 401 {object} ErrorEnvelope
|
||||||
|
// @Failure 403 {object} ErrorEnvelope
|
||||||
|
// @Failure 404 {object} ErrorEnvelope
|
||||||
|
// @Failure 502 {object} ErrorEnvelope
|
||||||
|
// @Router /api/v1/voice_clone/voices/{voiceID} [delete]
|
||||||
|
// @Router /v1/voice_clone/voices/{voiceID} [delete]
|
||||||
|
// @Router /voice_clone/voices/{voiceID} [delete]
|
||||||
|
func (s *Server) deleteClonedVoice(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, ok := auth.UserFromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !apiKeyScopeAllowed(user, "voice.clone") {
|
||||||
|
writeError(w, http.StatusForbidden, "api key scope does not allow this capability")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := s.runner.DeleteClonedVoice(r.Context(), user, r.PathValue("voiceID"))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorWithDetails(w, statusFromRunError(err), runErrorMessage(err), runErrorDetails(err), runErrorCode(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
|
"data": result.Voice,
|
||||||
|
"upstream": result.Upstream,
|
||||||
|
"requestId": result.RequestID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,12 @@ type clonedVoiceBinding struct {
|
|||||||
Explicit bool
|
Explicit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeletedClonedVoiceResult struct {
|
||||||
|
Voice store.ClonedVoice `json:"voice"`
|
||||||
|
Upstream map[string]any `json:"upstream,omitempty"`
|
||||||
|
RequestID string `json:"requestId,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func validateVoiceCloneRequest(body map[string]any) error {
|
func validateVoiceCloneRequest(body map[string]any) error {
|
||||||
voiceID := firstNonEmptyString(stringFromMap(body, "voice_id"), stringFromMap(body, "voiceId"))
|
voiceID := firstNonEmptyString(stringFromMap(body, "voice_id"), stringFromMap(body, "voiceId"))
|
||||||
if !validMiniMaxVoiceID(voiceID) {
|
if !validMiniMaxVoiceID(voiceID) {
|
||||||
@ -184,6 +190,62 @@ func (s *Service) touchClonedVoiceUsage(ctx context.Context, user *auth.User, bo
|
|||||||
_ = s.store.TouchClonedVoiceUsage(ctx, voice.ID)
|
_ = s.store.TouchClonedVoiceUsage(ctx, voice.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteClonedVoice(ctx context.Context, user *auth.User, rawID string) (DeletedClonedVoiceResult, error) {
|
||||||
|
id := strings.TrimSpace(rawID)
|
||||||
|
if id == "" {
|
||||||
|
return DeletedClonedVoiceResult{}, &clients.ClientError{Code: "bad_request", Message: "voice id is required", StatusCode: 400, Retryable: false}
|
||||||
|
}
|
||||||
|
clonedVoiceID := ""
|
||||||
|
voiceID := id
|
||||||
|
if looksLikeUUID(id) {
|
||||||
|
clonedVoiceID = id
|
||||||
|
voiceID = ""
|
||||||
|
}
|
||||||
|
voice, found, err := s.store.FindClonedVoiceForUser(ctx, user, clonedVoiceID, voiceID)
|
||||||
|
if err != nil {
|
||||||
|
return DeletedClonedVoiceResult{}, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return DeletedClonedVoiceResult{}, &clients.ClientError{Code: "cloned_voice_not_found", Message: "cloned voice not found", StatusCode: 404, Retryable: false}
|
||||||
|
}
|
||||||
|
candidate, ok, err := s.store.GetRuntimeModelCandidateForVoiceCloneDeletion(ctx, voice.PlatformModelID, voice.PlatformID)
|
||||||
|
if err != nil {
|
||||||
|
return DeletedClonedVoiceResult{}, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return DeletedClonedVoiceResult{}, &clients.ClientError{Code: "cloned_voice_platform_unavailable", Message: "cloned voice platform binding is unavailable", StatusCode: 400, Retryable: false}
|
||||||
|
}
|
||||||
|
requestHTTPClient, err := s.httpClientForCandidate(candidate, false)
|
||||||
|
if err != nil {
|
||||||
|
return DeletedClonedVoiceResult{}, err
|
||||||
|
}
|
||||||
|
deleter, ok := s.clientFor(candidate, false).(clients.VoiceCloneDeleter)
|
||||||
|
if !ok {
|
||||||
|
return DeletedClonedVoiceResult{}, &clients.ClientError{Code: "unsupported_operation", Message: "voice clone deletion is not supported by this provider", StatusCode: 400, Retryable: false}
|
||||||
|
}
|
||||||
|
upstream, err := deleter.DeleteVoiceClone(ctx, clients.VoiceCloneDeleteRequest{
|
||||||
|
VoiceID: voice.VoiceID,
|
||||||
|
Candidate: candidate,
|
||||||
|
HTTPClient: requestHTTPClient,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return DeletedClonedVoiceResult{}, err
|
||||||
|
}
|
||||||
|
deleted, found, err := s.store.DeleteClonedVoiceForUser(ctx, user, voice.ID, voice.VoiceID)
|
||||||
|
if err != nil {
|
||||||
|
return DeletedClonedVoiceResult{}, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
deleted = voice
|
||||||
|
deleted.Status = "deleted"
|
||||||
|
}
|
||||||
|
return DeletedClonedVoiceResult{
|
||||||
|
Voice: deleted,
|
||||||
|
Upstream: upstream.Result,
|
||||||
|
RequestID: upstream.RequestID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func firstAudioURLFromResult(result map[string]any) string {
|
func firstAudioURLFromResult(result map[string]any) string {
|
||||||
items, _ := result["data"].([]any)
|
items, _ := result["data"].([]any)
|
||||||
for _, raw := range items {
|
for _, raw := range items {
|
||||||
|
|||||||
@ -308,6 +308,68 @@ ORDER BY effective_priority ASC,
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetRuntimeModelCandidateForVoiceCloneDeletion(ctx context.Context, platformModelID string, platformID string) (RuntimeModelCandidate, bool, error) {
|
||||||
|
platformModelID = strings.TrimSpace(platformModelID)
|
||||||
|
platformID = strings.TrimSpace(platformID)
|
||||||
|
if platformModelID == "" && platformID == "" {
|
||||||
|
return RuntimeModelCandidate{}, false, nil
|
||||||
|
}
|
||||||
|
var item RuntimeModelCandidate
|
||||||
|
var credentials []byte
|
||||||
|
var platformConfig []byte
|
||||||
|
err := s.pool.QueryRow(ctx, `
|
||||||
|
SELECT p.id::text, p.platform_key, p.name, p.provider,
|
||||||
|
COALESCE(NULLIF(p.config->>'specType', ''), NULLIF(cp.provider_type, ''), NULLIF(p.config->>'sourceSpecType', ''), p.provider) AS spec_type,
|
||||||
|
COALESCE(p.base_url, ''), p.auth_type, p.credentials, p.config,
|
||||||
|
COALESCE(m.id::text, ''), COALESCE(NULLIF(m.provider_model_name, ''), m.model_name, 'voice_clone'),
|
||||||
|
COALESCE(m.model_name, 'voice_clone'), COALESCE(m.model_alias, '')
|
||||||
|
FROM integration_platforms p
|
||||||
|
LEFT JOIN platform_models m
|
||||||
|
ON m.platform_id = p.id
|
||||||
|
AND (
|
||||||
|
(NULLIF($1, '')::uuid IS NOT NULL AND m.id = NULLIF($1, '')::uuid)
|
||||||
|
OR (
|
||||||
|
NULLIF($1, '') IS NULL
|
||||||
|
AND m.model_type @> jsonb_build_array('voice_clone'::text)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
LEFT JOIN model_catalog_providers cp ON cp.provider_key = p.provider OR cp.provider_code = p.provider
|
||||||
|
WHERE p.deleted_at IS NULL
|
||||||
|
AND (
|
||||||
|
(NULLIF($1, '')::uuid IS NOT NULL AND m.id = NULLIF($1, '')::uuid)
|
||||||
|
OR (NULLIF($2, '')::uuid IS NOT NULL AND p.id = NULLIF($2, '')::uuid)
|
||||||
|
)
|
||||||
|
ORDER BY CASE WHEN NULLIF($1, '')::uuid IS NOT NULL AND m.id = NULLIF($1, '')::uuid THEN 0 ELSE 1 END,
|
||||||
|
m.created_at DESC
|
||||||
|
LIMIT 1`, platformModelID, platformID).Scan(
|
||||||
|
&item.PlatformID,
|
||||||
|
&item.PlatformKey,
|
||||||
|
&item.PlatformName,
|
||||||
|
&item.Provider,
|
||||||
|
&item.SpecType,
|
||||||
|
&item.BaseURL,
|
||||||
|
&item.AuthType,
|
||||||
|
&credentials,
|
||||||
|
&platformConfig,
|
||||||
|
&item.PlatformModelID,
|
||||||
|
&item.ProviderModelName,
|
||||||
|
&item.ModelName,
|
||||||
|
&item.ModelAlias,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if IsNotFound(err) {
|
||||||
|
return RuntimeModelCandidate{}, false, nil
|
||||||
|
}
|
||||||
|
return RuntimeModelCandidate{}, false, err
|
||||||
|
}
|
||||||
|
item.Credentials = decodeObject(credentials)
|
||||||
|
item.PlatformConfig = decodeObject(platformConfig)
|
||||||
|
item.ModelType = "voice_clone"
|
||||||
|
item.ClientID = item.PlatformKey + ":voice_clone:" + firstNonEmpty(item.ProviderModelName, item.ModelName)
|
||||||
|
item.QueueKey = item.ClientID
|
||||||
|
return item, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
type runtimeCandidateLoadInput struct {
|
type runtimeCandidateLoadInput struct {
|
||||||
Policy map[string]any
|
Policy map[string]any
|
||||||
ConcurrentActive float64
|
ConcurrentActive float64
|
||||||
|
|||||||
@ -133,6 +133,7 @@ SELECT `+clonedVoiceColumns+`
|
|||||||
FROM gateway_cloned_voices v
|
FROM gateway_cloned_voices v
|
||||||
LEFT JOIN integration_platforms p ON p.id = v.platform_id
|
LEFT JOIN integration_platforms p ON p.id = v.platform_id
|
||||||
WHERE (
|
WHERE (
|
||||||
|
(
|
||||||
NULLIF($1, '')::uuid IS NOT NULL
|
NULLIF($1, '')::uuid IS NOT NULL
|
||||||
AND v.gateway_user_id = NULLIF($1, '')::uuid
|
AND v.gateway_user_id = NULLIF($1, '')::uuid
|
||||||
)
|
)
|
||||||
@ -140,6 +141,8 @@ WHERE (
|
|||||||
NULLIF($2, '') IS NOT NULL
|
NULLIF($2, '') IS NOT NULL
|
||||||
AND v.user_id = $2
|
AND v.user_id = $2
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
AND v.status <> 'deleted'
|
||||||
ORDER BY v.created_at DESC`, gatewayUserID, userID)
|
ORDER BY v.created_at DESC`, gatewayUserID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -181,6 +184,7 @@ WHERE (
|
|||||||
(NULLIF($3, '')::uuid IS NOT NULL AND v.id = NULLIF($3, '')::uuid)
|
(NULLIF($3, '')::uuid IS NOT NULL AND v.id = NULLIF($3, '')::uuid)
|
||||||
OR (NULLIF($4, '') IS NOT NULL AND v.voice_id = $4)
|
OR (NULLIF($4, '') IS NOT NULL AND v.voice_id = $4)
|
||||||
)
|
)
|
||||||
|
AND v.status NOT IN ('deleted', 'failed')
|
||||||
ORDER BY CASE WHEN NULLIF($3, '')::uuid IS NOT NULL AND v.id = NULLIF($3, '')::uuid THEN 0 ELSE 1 END,
|
ORDER BY CASE WHEN NULLIF($3, '')::uuid IS NOT NULL AND v.id = NULLIF($3, '')::uuid THEN 0 ELSE 1 END,
|
||||||
v.created_at DESC
|
v.created_at DESC
|
||||||
LIMIT 1`, gatewayUserID, userID, clonedVoiceID, voiceID))
|
LIMIT 1`, gatewayUserID, userID, clonedVoiceID, voiceID))
|
||||||
@ -193,6 +197,46 @@ LIMIT 1`, gatewayUserID, userID, clonedVoiceID, voiceID))
|
|||||||
return item, true, nil
|
return item, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteClonedVoiceForUser(ctx context.Context, user *auth.User, clonedVoiceID string, voiceID string) (ClonedVoice, bool, error) {
|
||||||
|
gatewayUserID, userID := clonedVoiceUserKeys(user)
|
||||||
|
clonedVoiceID = strings.TrimSpace(clonedVoiceID)
|
||||||
|
voiceID = strings.TrimSpace(voiceID)
|
||||||
|
if clonedVoiceID == "" && voiceID == "" {
|
||||||
|
return ClonedVoice{}, false, nil
|
||||||
|
}
|
||||||
|
item, err := scanClonedVoice(s.pool.QueryRow(ctx, `
|
||||||
|
WITH updated AS (
|
||||||
|
UPDATE gateway_cloned_voices v
|
||||||
|
SET status = 'deleted', updated_at = now()
|
||||||
|
WHERE (
|
||||||
|
(
|
||||||
|
NULLIF($1, '')::uuid IS NOT NULL
|
||||||
|
AND v.gateway_user_id = NULLIF($1, '')::uuid
|
||||||
|
)
|
||||||
|
OR (
|
||||||
|
NULLIF($2, '') IS NOT NULL
|
||||||
|
AND v.user_id = $2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
(NULLIF($3, '')::uuid IS NOT NULL AND v.id = NULLIF($3, '')::uuid)
|
||||||
|
OR (NULLIF($4, '') IS NOT NULL AND v.voice_id = $4)
|
||||||
|
)
|
||||||
|
AND v.status <> 'deleted'
|
||||||
|
RETURNING *
|
||||||
|
)
|
||||||
|
SELECT `+clonedVoiceColumns+`
|
||||||
|
FROM updated v
|
||||||
|
LEFT JOIN integration_platforms p ON p.id = v.platform_id`, gatewayUserID, userID, clonedVoiceID, voiceID))
|
||||||
|
if err != nil {
|
||||||
|
if IsNotFound(err) {
|
||||||
|
return ClonedVoice{}, false, nil
|
||||||
|
}
|
||||||
|
return ClonedVoice{}, false, err
|
||||||
|
}
|
||||||
|
return item, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) TouchClonedVoiceUsage(ctx context.Context, clonedVoiceID string) error {
|
func (s *Store) TouchClonedVoiceUsage(ctx context.Context, clonedVoiceID string) error {
|
||||||
if strings.TrimSpace(clonedVoiceID) == "" {
|
if strings.TrimSpace(clonedVoiceID) == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user