feat: 支持 MiniMax 音色克隆和 2.8 语音模型
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
)
|
||||
|
||||
type ClonedVoice struct {
|
||||
ID string `json:"id"`
|
||||
GatewayUserID string `json:"gatewayUserId,omitempty"`
|
||||
UserID string `json:"userId"`
|
||||
GatewayTenantID string `json:"gatewayTenantId,omitempty"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
TenantKey string `json:"tenantKey,omitempty"`
|
||||
Provider string `json:"provider"`
|
||||
PlatformID string `json:"platformId,omitempty"`
|
||||
PlatformName string `json:"platformName,omitempty"`
|
||||
PlatformModelID string `json:"platformModelId,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
PreviewModel string `json:"previewModel,omitempty"`
|
||||
VoiceID string `json:"voiceId"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
DemoAudioURL string `json:"demoAudioUrl,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt string `json:"expiresAt,omitempty"`
|
||||
LastUsedAt string `json:"lastUsedAt,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ClonedVoiceInput struct {
|
||||
GatewayUserID string
|
||||
UserID string
|
||||
GatewayTenantID string
|
||||
TenantID string
|
||||
TenantKey string
|
||||
Provider string
|
||||
PlatformID string
|
||||
PlatformModelID string
|
||||
SourceTaskID string
|
||||
SourceAttemptID string
|
||||
Model string
|
||||
PreviewModel string
|
||||
VoiceID string
|
||||
DisplayName string
|
||||
DemoAudioURL string
|
||||
Status string
|
||||
ExpiresAt *time.Time
|
||||
Metadata map[string]any
|
||||
}
|
||||
|
||||
const clonedVoiceColumns = `
|
||||
v.id::text, COALESCE(v.gateway_user_id::text, ''), v.user_id,
|
||||
COALESCE(v.gateway_tenant_id::text, ''), COALESCE(v.tenant_id, ''), COALESCE(v.tenant_key, ''),
|
||||
v.provider, COALESCE(v.platform_id::text, ''), COALESCE(p.name, ''),
|
||||
COALESCE(v.platform_model_id::text, ''), COALESCE(v.model, ''), COALESCE(v.preview_model, ''),
|
||||
v.voice_id, COALESCE(v.display_name, ''), COALESCE(v.demo_audio_url, ''), v.status,
|
||||
COALESCE(v.expires_at::text, ''), COALESCE(v.last_used_at::text, ''),
|
||||
COALESCE(v.metadata, '{}'::jsonb), v.created_at, v.updated_at`
|
||||
|
||||
func (s *Store) UpsertClonedVoice(ctx context.Context, input ClonedVoiceInput) (ClonedVoice, error) {
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
status := strings.TrimSpace(input.Status)
|
||||
if status == "" {
|
||||
status = "active"
|
||||
}
|
||||
return scanClonedVoice(s.pool.QueryRow(ctx, `
|
||||
WITH upsert AS (
|
||||
INSERT INTO gateway_cloned_voices (
|
||||
gateway_user_id, user_id, gateway_tenant_id, tenant_id, tenant_key,
|
||||
provider, platform_id, platform_model_id, source_task_id, source_attempt_id,
|
||||
model, preview_model, voice_id, display_name, demo_audio_url, status, expires_at, metadata
|
||||
)
|
||||
VALUES (
|
||||
NULLIF($1, '')::uuid, $2, NULLIF($3, '')::uuid, NULLIF($4, ''), NULLIF($5, ''),
|
||||
$6, NULLIF($7, '')::uuid, NULLIF($8, '')::uuid, NULLIF($9, '')::uuid, NULLIF($10, '')::uuid,
|
||||
$11, $12, $13, $14, $15, $16, $17, $18::jsonb
|
||||
)
|
||||
ON CONFLICT (platform_id, voice_id) WHERE platform_id IS NOT NULL AND voice_id <> ''
|
||||
DO UPDATE SET
|
||||
gateway_user_id = EXCLUDED.gateway_user_id,
|
||||
user_id = EXCLUDED.user_id,
|
||||
gateway_tenant_id = EXCLUDED.gateway_tenant_id,
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
tenant_key = EXCLUDED.tenant_key,
|
||||
provider = EXCLUDED.provider,
|
||||
platform_model_id = EXCLUDED.platform_model_id,
|
||||
source_task_id = EXCLUDED.source_task_id,
|
||||
source_attempt_id = EXCLUDED.source_attempt_id,
|
||||
model = EXCLUDED.model,
|
||||
preview_model = EXCLUDED.preview_model,
|
||||
display_name = EXCLUDED.display_name,
|
||||
demo_audio_url = EXCLUDED.demo_audio_url,
|
||||
status = EXCLUDED.status,
|
||||
expires_at = EXCLUDED.expires_at,
|
||||
metadata = gateway_cloned_voices.metadata || EXCLUDED.metadata,
|
||||
updated_at = now()
|
||||
RETURNING *
|
||||
)
|
||||
SELECT `+clonedVoiceColumns+`
|
||||
FROM upsert v
|
||||
LEFT JOIN integration_platforms p ON p.id = v.platform_id`,
|
||||
input.GatewayUserID,
|
||||
input.UserID,
|
||||
input.GatewayTenantID,
|
||||
input.TenantID,
|
||||
input.TenantKey,
|
||||
input.Provider,
|
||||
input.PlatformID,
|
||||
input.PlatformModelID,
|
||||
input.SourceTaskID,
|
||||
input.SourceAttemptID,
|
||||
input.Model,
|
||||
input.PreviewModel,
|
||||
input.VoiceID,
|
||||
input.DisplayName,
|
||||
input.DemoAudioURL,
|
||||
status,
|
||||
input.ExpiresAt,
|
||||
string(metadata),
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) ListClonedVoices(ctx context.Context, user *auth.User) ([]ClonedVoice, error) {
|
||||
gatewayUserID, userID := clonedVoiceUserKeys(user)
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT `+clonedVoiceColumns+`
|
||||
FROM gateway_cloned_voices v
|
||||
LEFT JOIN integration_platforms p ON p.id = v.platform_id
|
||||
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
|
||||
)
|
||||
ORDER BY v.created_at DESC`, gatewayUserID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]ClonedVoice, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanClonedVoice(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) FindClonedVoiceForUser(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, `
|
||||
SELECT `+clonedVoiceColumns+`
|
||||
FROM gateway_cloned_voices v
|
||||
LEFT JOIN integration_platforms p ON p.id = v.platform_id
|
||||
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)
|
||||
)
|
||||
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
|
||||
LIMIT 1`, 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 {
|
||||
if strings.TrimSpace(clonedVoiceID) == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE gateway_cloned_voices
|
||||
SET last_used_at = now(), expires_at = now() + interval '7 days', updated_at = now()
|
||||
WHERE id = $1::uuid`, clonedVoiceID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) MarkClonedVoiceStatus(ctx context.Context, clonedVoiceID string, status string) error {
|
||||
if strings.TrimSpace(clonedVoiceID) == "" || strings.TrimSpace(status) == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE gateway_cloned_voices
|
||||
SET status = $2, updated_at = now()
|
||||
WHERE id = $1::uuid`, clonedVoiceID, status)
|
||||
return err
|
||||
}
|
||||
|
||||
func clonedVoiceUserKeys(user *auth.User) (string, string) {
|
||||
if user == nil {
|
||||
return "", ""
|
||||
}
|
||||
gatewayUserID := strings.TrimSpace(user.GatewayUserID)
|
||||
if gatewayUserID == "" && user.Source == "gateway" {
|
||||
gatewayUserID = strings.TrimSpace(user.ID)
|
||||
}
|
||||
userID := strings.TrimSpace(user.ID)
|
||||
return gatewayUserID, userID
|
||||
}
|
||||
|
||||
type clonedVoiceScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanClonedVoice(scanner clonedVoiceScanner) (ClonedVoice, error) {
|
||||
var item ClonedVoice
|
||||
var metadata []byte
|
||||
if err := scanner.Scan(
|
||||
&item.ID,
|
||||
&item.GatewayUserID,
|
||||
&item.UserID,
|
||||
&item.GatewayTenantID,
|
||||
&item.TenantID,
|
||||
&item.TenantKey,
|
||||
&item.Provider,
|
||||
&item.PlatformID,
|
||||
&item.PlatformName,
|
||||
&item.PlatformModelID,
|
||||
&item.Model,
|
||||
&item.PreviewModel,
|
||||
&item.VoiceID,
|
||||
&item.DisplayName,
|
||||
&item.DemoAudioURL,
|
||||
&item.Status,
|
||||
&item.ExpiresAt,
|
||||
&item.LastUsedAt,
|
||||
&metadata,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
); err != nil {
|
||||
return ClonedVoice{}, err
|
||||
}
|
||||
item.Metadata = decodeObject(metadata)
|
||||
return item, nil
|
||||
}
|
||||
Reference in New Issue
Block a user