迁移音频生成与语音合成到 gateway 并补充 simulation 测试

This commit is contained in:
2026-06-07 10:26:57 +08:00
parent 78ab867a9f
commit dc14866210
22 changed files with 2475 additions and 55 deletions
+18
View File
@@ -112,6 +112,24 @@ func (s *Service) billings(ctx context.Context, user *auth.User, kind string, bo
"durationUnitCount": durationUnits,
})}
}
if kind == "song.generations" || kind == "music.generations" {
resource = "music"
unit = "song"
baseKey = "musicBase"
amount := float64(count) * resourcePrice(config, resource, baseKey, "basePrice") * discount
return []any{billingLine(candidate, resource, unit, count, roundPrice(amount), discount, simulated)}
}
if kind == "speech.generations" {
resource = "audio"
unit = "character"
baseKey = "audioBase"
quantity := len([]rune(stringFromMap(body, "text")))
if quantity <= 0 {
quantity = 1
}
amount := float64(quantity) * resourcePrice(config, resource, baseKey, "basePrice") * discount
return []any{billingLine(candidate, resource, unit, quantity, roundPrice(amount), discount, simulated)}
}
amount := float64(count) * resourcePrice(config, resource, baseKey, "basePrice") * resourceWeight(config, resource, "qualityWeights", stringFromMap(body, "quality")) * resourceWeight(config, resource, "sizeWeights", stringFromMap(body, "size")) * resourceWeight(config, resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size"))) * discount
return []any{billingLine(candidate, resource, unit, count, roundPrice(amount), discount, simulated)}
}
+60
View File
@@ -84,6 +84,66 @@ func TestVideoBillingEstimateUsesFiveSecondUnitsAndDynamicWeights(t *testing.T)
}
}
func TestMusicBillingUsesSongResourceAndOutputCount(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
ModelName: "suno-model",
BaseBillingConfig: map[string]any{
"musicBase": 6,
"music": map[string]any{"basePrice": 9},
},
}
items := service.billings(context.Background(), nil, "song.generations", map[string]any{
"prompt": "city lights",
"count": 3,
}, candidate, clients.Response{}, true)
line := firstBillingLine(t, items)
if got, want := line["resourceType"], "music"; got != want {
t.Fatalf("music resource type = %v, want %v", got, want)
}
if got, want := line["unit"], "song"; got != want {
t.Fatalf("music billing unit = %v, want %v", got, want)
}
if got, want := line["quantity"], 3; got != want {
t.Fatalf("music quantity = %v, want %v", got, want)
}
if got, want := floatFromAny(line["amount"]), 18.0; got != want {
t.Fatalf("music amount = %v, want %v", got, want)
}
}
func TestSpeechBillingUsesAudioCharacters(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
ModelName: "speech-model",
BaseBillingConfig: map[string]any{
"audioBase": 0.5,
"audio": map[string]any{"basePrice": 0.8},
},
}
items := service.billings(context.Background(), nil, "speech.generations", map[string]any{
"text": "你好abc",
"voice_id": "female-shaonv",
}, candidate, clients.Response{}, true)
line := firstBillingLine(t, items)
if got, want := line["resourceType"], "audio"; got != want {
t.Fatalf("speech resource type = %v, want %v", got, want)
}
if got, want := line["unit"], "character"; got != want {
t.Fatalf("speech billing unit = %v, want %v", got, want)
}
if got, want := line["quantity"], 5; got != want {
t.Fatalf("speech character quantity = %v, want %v", got, want)
}
if got, want := floatFromAny(line["amount"]), 2.5; got != want {
t.Fatalf("speech amount = %v, want %v", got, want)
}
}
func TestVideoBillingPrefersGeneratedDuration(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
+21 -1
View File
@@ -64,6 +64,7 @@ func New(cfg config.Config, db *store.Store, logger *slog.Logger) *Service {
"midjourney": clients.MidjourneyClient{HTTPClient: httpClients.none},
"minimax": clients.MinimaxClient{HTTPClient: httpClients.none},
"newapi": clients.NewAPIClient{HTTPClient: httpClients.none},
"suno": clients.SunoClient{HTTPClient: httpClients.none},
"tencent-hunyuan-image": clients.HunyuanImageClient{HTTPClient: httpClients.none},
"tencent-hunyuan-video": clients.HunyuanVideoClient{HTTPClient: httpClients.none},
"vidu": clients.ViduClient{HTTPClient: httpClients.none},
@@ -957,6 +958,10 @@ func modelTypeFromKind(kind string, body map[string]any) string {
return "image_to_video"
}
return "video_generate"
case "song.generations", "music.generations":
return "audio_generate"
case "speech.generations":
return "text_to_speech"
default:
return "task"
}
@@ -979,6 +984,10 @@ func canonicalModelType(value string) string {
return "text_embedding"
case "rerank", "reranks":
return "text_rerank"
case "audio", "music", "music_generate", "song", "songs":
return "audio_generate"
case "speech", "tts":
return "text_to_speech"
default:
return normalized
}
@@ -986,7 +995,7 @@ func canonicalModelType(value string) string {
func isKnownModelType(value string) bool {
switch value {
case "text_generate", "text_embedding", "text_rerank", "image_generate", "image_edit", "video_generate", "image_to_video", "text_to_video", "video_edit", "video_reference", "video_first_last_frame", "omni_video", "omni":
case "text_generate", "text_embedding", "text_rerank", "image_generate", "image_edit", "video_generate", "image_to_video", "text_to_video", "video_edit", "video_reference", "video_first_last_frame", "omni_video", "omni", "audio_generate", "text_to_speech":
return true
default:
return false
@@ -1171,6 +1180,17 @@ func validateRequest(kind string, body map[string]any) error {
if strings.TrimSpace(stringFromMap(body, "prompt")) == "" {
return errors.New("prompt is required")
}
case "song.generations", "music.generations":
if strings.TrimSpace(stringFromMap(body, "prompt")) == "" {
return errors.New("prompt is required")
}
case "speech.generations":
if strings.TrimSpace(stringFromMap(body, "text")) == "" && strings.TrimSpace(stringFromMap(body, "text_file_id")) == "" {
return errors.New("text or text_file_id is required")
}
if strings.TrimSpace(stringFromMap(body, "voice_id")) == "" {
return errors.New("voice_id is required")
}
}
return nil
}
+3
View File
@@ -943,6 +943,9 @@ func mediaKindForAsset(taskKind string, item map[string]any, sourceKey string, c
if strings.Contains(kind, "video") {
return "video"
}
if strings.Contains(kind, "audio") || strings.Contains(kind, "song") || strings.Contains(kind, "music") || strings.Contains(kind, "speech") {
return "audio"
}
if strings.Contains(kind, "image") {
return "image"
}