feat: support cloned voice deletion

This commit is contained in:
2026-06-17 12:03:13 +08:00
parent c4341335d7
commit 6089aa6085
8 changed files with 259 additions and 1 deletions
+62
View File
@@ -18,6 +18,12 @@ type clonedVoiceBinding struct {
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 {
voiceID := firstNonEmptyString(stringFromMap(body, "voice_id"), stringFromMap(body, "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)
}
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 {
items, _ := result["data"].([]any)
for _, raw := range items {