增加即梦图片5.0和视频2.5的能力
This commit is contained in:
1 parent
542246e4ce
commit
88a4268d40
55 files changed
+1412
-200
No files matched your search
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
const (
|
||||
DefaultBillingMarkupMultiplier = 1.2
|
||||
BillingCatalogObservedAt = "2026-08-11"
|
||||
BillingCatalogObservedAt = "2026-08-21"
|
||||
)
|
||||
|
||||
// PriceRuleSeeder is an optional store capability. Existing Store
|
||||
@@ -24,12 +24,16 @@ func DefaultBillingPriceRules() []PriceRule {
|
||||
{ID: "base-volcengine-jimeng-seedream46", Provider: "volcengine-visual", Capability: "image.generate", ReqKey: "jimeng_seedream46_cvtob", Unit: UnitImage, StandardUnitPriceFen: 20, Note: "即梦4.6公开资源包基准。", Source: catalogSource("https://www.volcengine.com/activity/jimeng")},
|
||||
{ID: "base-evolink-gpt-image-2", Provider: "evolink", Capability: "image.generate", ReqKey: "gpt-image-2", Unit: UnitImage, StandardUnitPriceFen: 34, Note: "medium / 1K / 1:1 / 无参考图基准。", Source: catalogSource("https://evolink.ai/zh/gpt-image-2"), Dimensions: evolinkDimensions()},
|
||||
{ID: "base-bailian-wan27-image-pro", Provider: "bailian", Capability: "image.generate", ReqKey: "wan2.7-image-pro", Unit: UnitImage, StandardUnitPriceFen: 50, Source: catalogSource("https://help.aliyun.com/zh/model-studio/wan2-7-image-pro")},
|
||||
{ID: "base-seedream-5-0-pro", Provider: "seedream", Capability: "image.generate", ReqKey: Seedream50ProModel, Unit: UnitImage, StandardUnitPriceFen: 30, Note: "基础生图:1K/1.5K ¥0.30,2K ¥0.60;首张输入图免费,额外输入图 ¥0.02/张。", Source: catalogSource("https://docs.volcengine.com/docs/82379/1544106?lang=zh"), Dimensions: seedreamDimensions()},
|
||||
{ID: "base-bailian-wan27-i2v-720p", Provider: "bailian", Capability: "video.generate", ReqKey: "wan2.7-i2v-2026-04-25", VariantKey: "resolution=720p", Unit: UnitVideoSecond, StandardUnitPriceFen: 60, Source: catalogSource("https://help.aliyun.com/zh/model-studio/wan2-7-i2v")},
|
||||
{ID: "base-bailian-wan27-i2v-1080p", Provider: "bailian", Capability: "video.generate", ReqKey: "wan2.7-i2v-2026-04-25", VariantKey: "resolution=1080p", Unit: UnitVideoSecond, StandardUnitPriceFen: 100, Source: catalogSource("https://help.aliyun.com/zh/model-studio/wan2-7-i2v")},
|
||||
{ID: "base-seedance-2-0-480p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-0-260128", VariantKey: "resolution=480p", Unit: UnitVideoSecond, StandardUnitPriceFen: 46, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh")},
|
||||
{ID: "base-seedance-2-0-720p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-0-260128", VariantKey: "resolution=720p", Unit: UnitVideoSecond, StandardUnitPriceFen: 99, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh")},
|
||||
{ID: "base-seedance-2-0-1080p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-0-260128", VariantKey: "resolution=1080p", Unit: UnitVideoSecond, StandardUnitPriceFen: 248, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh")},
|
||||
{ID: "base-seedance-2-0-4k", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-0-260128", VariantKey: "resolution=4k", Unit: UnitVideoSecond, StandardUnitPriceFen: 505, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh")},
|
||||
{ID: "base-seedance-2-5-480p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-5-260628", VariantKey: "resolution=480p", Unit: UnitVideoSecond, StandardUnitPriceFen: 67, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh#sd25_price")},
|
||||
{ID: "base-seedance-2-5-720p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-5-260628", VariantKey: "resolution=720p", Unit: UnitVideoSecond, StandardUnitPriceFen: 151, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh#sd25_price")},
|
||||
{ID: "base-seedance-2-5-1080p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-5-260628", VariantKey: "resolution=1080p", Unit: UnitVideoSecond, StandardUnitPriceFen: 374, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh#sd25_price")},
|
||||
}
|
||||
for i := range rules {
|
||||
rules[i].Enabled = true
|
||||
@@ -38,6 +42,17 @@ func DefaultBillingPriceRules() []PriceRule {
|
||||
return rules
|
||||
}
|
||||
|
||||
func seedreamDimensions() []ParameterDimension {
|
||||
return []ParameterDimension{{
|
||||
Key: "size", Label: "分辨率", BaselineValue: "1.5K", DefaultValue: "1.5K",
|
||||
Tiers: []ParameterTier{
|
||||
{Value: "1K", Label: "1K", StandardFactor: 1, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true},
|
||||
{Value: "1.5K", Label: "1.5K", StandardFactor: 1, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true},
|
||||
{Value: "2K", Label: "2K", StandardFactor: 2, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func catalogSource(url string) map[string]any {
|
||||
return map[string]any{"url": url, "observedAt": BillingCatalogObservedAt}
|
||||
}
|
||||
@@ -81,12 +96,14 @@ func NormalizeBillingParameters(payload map[string]any, fallback Parameters) Par
|
||||
setText(out, "resolution", first(settings["resolution"], providerParameters["resolution"], providerPayload["resolution"], inputSettings["resolution"], input["resolution"]))
|
||||
setText(out, "quality", first(input["quality"], settings["quality"], providerPayload["quality"], providerParameters["quality"]))
|
||||
setNumber(out, "duration", first(settings["duration"], providerParameters["duration"], providerPayload["duration"], inputSettings["duration"], input["duration"]), false)
|
||||
setNumber(out, "imageCount", first(providerPayload["n"], providerParameters["n"], input["n"], input["imageCount"]), true)
|
||||
setNumber(out, "imageCount", first(settings["imageCount"], providerPayload["n"], providerParameters["n"], input["n"], input["imageCount"]), true)
|
||||
setNumber(out, "scale", first(input["scale"], settings["scale"]), false)
|
||||
setText(out, "outputFormat", first(settings["outputFormat"], providerPayload["output_format"], inputSettings["outputFormat"], input["outputFormat"]))
|
||||
setText(out, "optimizeMode", first(settings["optimizeMode"], record(providerPayload["optimize_prompt_options"])["mode"], inputSettings["optimizeMode"], input["optimizeMode"]))
|
||||
|
||||
width, widthOK := finiteNumber(first(providerPayload["width"], input["width"]))
|
||||
height, heightOK := finiteNumber(first(providerPayload["height"], input["height"]))
|
||||
if size := normalizedText(first(providerPayload["size"], providerParameters["size"], inputSettings["size"], input["size"])); size != "" {
|
||||
if size := normalizedText(first(settings["size"], providerPayload["size"], providerParameters["size"], inputSettings["size"], input["size"])); size != "" {
|
||||
out["size"] = strings.ReplaceAll(size, "×", "*")
|
||||
} else if widthOK && heightOK {
|
||||
out["size"] = fmt.Sprintf("%g*%g", width, height)
|
||||
@@ -164,13 +181,23 @@ func aspectRatio(width, height float64) string {
|
||||
return fmt.Sprintf("%g:%g", width, height)
|
||||
}
|
||||
func referenceCount(payload, input, providerPayload map[string]any) int {
|
||||
count := 0
|
||||
for _, value := range []any{payload["imageUrls"], payload["inputUrls"], input["imageUrls"], providerPayload["image_urls"], providerPayload["imageUrls"]} {
|
||||
switch items := value.(type) {
|
||||
case []any:
|
||||
count += len(items)
|
||||
if len(items) > 0 {
|
||||
return len(items)
|
||||
}
|
||||
case []string:
|
||||
count += len(items)
|
||||
if len(items) > 0 {
|
||||
return len(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
count := 0
|
||||
for _, material := range billingMaterials(payload) {
|
||||
materialType := strings.ToLower(strings.TrimSpace(fmt.Sprint(material["type"])))
|
||||
if materialType == "" || materialType == "image" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
|
||||
@@ -8,12 +8,16 @@ func TestDefaultBillingPriceRulesFreezeOfficialCatalogMatchKeys(t *testing.T) {
|
||||
"base-volcengine-jimeng-seedream46": false,
|
||||
"base-evolink-gpt-image-2": false,
|
||||
"base-bailian-wan27-image-pro": false,
|
||||
"base-seedream-5-0-pro": false,
|
||||
"base-bailian-wan27-i2v-720p": false,
|
||||
"base-bailian-wan27-i2v-1080p": false,
|
||||
"base-seedance-2-0-480p": false,
|
||||
"base-seedance-2-0-720p": false,
|
||||
"base-seedance-2-0-1080p": false,
|
||||
"base-seedance-2-0-4k": false,
|
||||
"base-seedance-2-5-480p": false,
|
||||
"base-seedance-2-5-720p": false,
|
||||
"base-seedance-2-5-1080p": false,
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if _, ok := want[rule.ID]; !ok {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package billing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Seedream50ProModel = "doubao-seedream-5-0-pro-260628"
|
||||
|
||||
// CalculateSeedreamAmountFen applies Seedream's additive input-image price
|
||||
// before the platform markup: the first reference image is free and every
|
||||
// additional reference image costs 2 fen.
|
||||
func CalculateSeedreamAmountFen(outputPriceFen int64, referenceImageCount int, markupMultiplier float64) (int64, error) {
|
||||
if outputPriceFen <= 0 {
|
||||
return 0, errors.New("seedream output price must be positive")
|
||||
}
|
||||
if referenceImageCount < 0 || referenceImageCount > 10 {
|
||||
return 0, errors.New("seedream reference image count must be between 0 and 10")
|
||||
}
|
||||
if math.IsNaN(markupMultiplier) || math.IsInf(markupMultiplier, 0) || markupMultiplier < 1 {
|
||||
return 0, errors.New("seedream markup multiplier must be at least 1")
|
||||
}
|
||||
extraInputPriceFen := int64(max(0, referenceImageCount-1) * 2)
|
||||
return int64(math.Ceil(float64(outputPriceFen+extraInputPriceFen) * markupMultiplier)), nil
|
||||
}
|
||||
|
||||
func IsSeedreamBillingModel(model string) bool {
|
||||
return strings.TrimSpace(model) == Seedream50ProModel
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -77,11 +78,32 @@ func (s *Service) Quote(ctx context.Context, command QuoteCommand) (*Quote, erro
|
||||
}
|
||||
parameters := NormalizeBillingParameters(command.Payload, command.Parameters)
|
||||
quote, err := (Catalog{Rules: rules}).Quote(QuoteInput{Provider: command.Provider, Capability: command.Capability, ReqKey: command.ReqKey, Source: "platform", Role: command.Role, Parameters: parameters})
|
||||
if err != nil || quote == nil || command.Provider != "seedance" || command.ReqKey != "doubao-seedance-2-0-260128" {
|
||||
if err != nil || quote == nil {
|
||||
return quote, err
|
||||
}
|
||||
inputVideo, inputDuration := seedanceInputVideo(command.Payload)
|
||||
if command.Provider == "seedream" && IsSeedreamBillingModel(command.ReqKey) {
|
||||
referenceImageCount := 0
|
||||
if count, ok := number(parameters["referenceImageCount"]); ok {
|
||||
referenceImageCount = int(math.Ceil(count))
|
||||
}
|
||||
amount, calculateErr := CalculateSeedreamAmountFen(quote.StandardUnitPriceFen, referenceImageCount, quote.MarkupMultiplier)
|
||||
if calculateErr != nil {
|
||||
return nil, calculateErr
|
||||
}
|
||||
quote.AmountFen = amount
|
||||
if quote.Parameters == nil {
|
||||
quote.Parameters = Parameters{}
|
||||
}
|
||||
quote.Parameters["referenceImageCount"] = float64(referenceImageCount)
|
||||
quote.Parameters["inputReferencePriceFen"] = float64(max(0, referenceImageCount-1) * 2)
|
||||
return quote, nil
|
||||
}
|
||||
if command.Provider != "seedance" || !IsSeedanceBillingModel(command.ReqKey) {
|
||||
return quote, nil
|
||||
}
|
||||
inputVideo, inputDuration := seedanceInputVideo(command.Payload, command.ReqKey)
|
||||
estimated, err := EstimateSeedanceAmountFen(SeedanceEstimateInput{
|
||||
Model: command.ReqKey,
|
||||
Resolution: fmt.Sprint(parameters["resolution"]), AspectRatio: fmt.Sprint(parameters["aspectRatio"]),
|
||||
OutputDurationSeconds: quote.Quantity, InputVideo: inputVideo, InputVideoDurationSeconds: inputDuration,
|
||||
MarkupMultiplier: quote.MarkupMultiplier,
|
||||
|
||||
@@ -78,20 +78,92 @@ func TestServiceQuoteFreezesConservativeSeedanceReserve(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceQuoteUsesSeedance25CatalogAndTokenEstimate(t *testing.T) {
|
||||
store := "eStoreStub{rules: []PriceRule{{
|
||||
ID: "seedance-25-720", Provider: "seedance", Capability: "video.generate",
|
||||
ReqKey: Seedance25Model, VariantKey: "resolution=720p", Unit: UnitVideoSecond,
|
||||
StandardUnitPriceFen: 151, MarkupMultiplier: 1.2, Enabled: true,
|
||||
}}}
|
||||
quote, err := NewService(store, nil).Quote(context.Background(), QuoteCommand{
|
||||
Provider: "seedance", Capability: "video.generate", ReqKey: Seedance25Model,
|
||||
Payload: map[string]any{"settings": map[string]any{"duration": 5.0, "resolution": "720p", "ratio": "16:9"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if quote.AmountFen != 908 || quote.ReservedAmountFen != 908 || quote.MarkupMultiplier != 1.2 {
|
||||
t.Fatalf("quote = %#v", quote)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceQuoteAppliesSeedreamSizeAndReferenceInputPricing(t *testing.T) {
|
||||
store := "eStoreStub{rules: []PriceRule{{
|
||||
ID: "seedream-5", Provider: "seedream", Capability: "image.generate", ReqKey: Seedream50ProModel,
|
||||
Unit: UnitImage, StandardUnitPriceFen: 30, MarkupMultiplier: 1.2, Enabled: true, Dimensions: seedreamDimensions(),
|
||||
}}}
|
||||
quote, err := NewService(store, nil).Quote(context.Background(), QuoteCommand{
|
||||
Provider: "seedream", Capability: "image.generate", ReqKey: Seedream50ProModel,
|
||||
Payload: map[string]any{
|
||||
"settings": map[string]any{"size": "2K", "imageCount": 1.0},
|
||||
"materials": []any{
|
||||
map[string]any{"type": "image", "url": "https://in.test/1.png"},
|
||||
map[string]any{"type": "image", "url": "https://in.test/2.png"},
|
||||
map[string]any{"type": "image", "url": "https://in.test/3.png"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 2K output = 60 fen, two billable references = 4 fen, then 1.2x.
|
||||
if quote.StandardUnitPriceFen != 60 || quote.AmountFen != 77 || quote.Parameters["referenceImageCount"] != float64(3) || quote.Parameters["inputReferencePriceFen"] != float64(4) {
|
||||
t.Fatalf("quote=%#v", quote)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateSeedreamAmountFenOfficialVectors(t *testing.T) {
|
||||
tests := []struct {
|
||||
output, references int
|
||||
want int64
|
||||
}{{30, 0, 36}, {30, 1, 36}, {30, 3, 41}, {60, 3, 77}, {60, 10, 94}}
|
||||
for _, test := range tests {
|
||||
got, err := CalculateSeedreamAmountFen(int64(test.output), test.references, 1.2)
|
||||
if err != nil || got != test.want {
|
||||
t.Fatalf("output=%d refs=%d got=%d err=%v want=%d", test.output, test.references, got, err, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeedanceEstimateMatchesExecutableTypeScriptVectors(t *testing.T) {
|
||||
withoutVideo, err := EstimateSeedanceAmountFen(SeedanceEstimateInput{
|
||||
Model: Seedance20Model,
|
||||
Resolution: "720p", AspectRatio: "16:9", OutputDurationSeconds: 5, MarkupMultiplier: 1.2,
|
||||
})
|
||||
if err != nil || withoutVideo != 597 {
|
||||
t.Fatalf("without video = %d, %v", withoutVideo, err)
|
||||
}
|
||||
withUnknownVideo, err := EstimateSeedanceAmountFen(SeedanceEstimateInput{
|
||||
Model: Seedance20Model,
|
||||
Resolution: "720p", AspectRatio: "9:16", OutputDurationSeconds: 5,
|
||||
InputVideo: true, MarkupMultiplier: 1.2,
|
||||
})
|
||||
if err != nil || withUnknownVideo != 1452 {
|
||||
t.Fatalf("with video = %d, %v", withUnknownVideo, err)
|
||||
}
|
||||
seedance25, err := EstimateSeedanceAmountFen(SeedanceEstimateInput{
|
||||
Model: Seedance25Model, Resolution: "720p", AspectRatio: "16:9", OutputDurationSeconds: 5,
|
||||
MarkupMultiplier: 1.2,
|
||||
})
|
||||
if err != nil || seedance25 != 908 {
|
||||
t.Fatalf("seedance 2.5 without video = %d, %v", seedance25, err)
|
||||
}
|
||||
seedance25WithShortVideo, err := EstimateSeedanceAmountFen(SeedanceEstimateInput{
|
||||
Model: Seedance25Model, Resolution: "720p", AspectRatio: "16:9", OutputDurationSeconds: 5,
|
||||
InputVideo: true, InputVideoDurationSeconds: 2, MarkupMultiplier: 1.2,
|
||||
})
|
||||
if err != nil || seedance25WithShortVideo != 980 {
|
||||
t.Fatalf("seedance 2.5 with short video = %d, %v", seedance25WithShortVideo, err)
|
||||
}
|
||||
}
|
||||
|
||||
type quoteStoreStub struct {
|
||||
|
||||
@@ -12,11 +12,13 @@ import (
|
||||
const seedanceTokenScale = 1_000_000
|
||||
|
||||
const (
|
||||
seedanceFPS = 24
|
||||
seedanceMaximumInputDuration = 15
|
||||
Seedance20Model = "doubao-seedance-2-0-260128"
|
||||
Seedance25Model = "doubao-seedance-2-5-260628"
|
||||
seedanceFPS = 24
|
||||
)
|
||||
|
||||
type SeedanceEstimateInput struct {
|
||||
Model string
|
||||
Resolution, AspectRatio string
|
||||
OutputDurationSeconds float64
|
||||
InputVideoDurationSeconds float64
|
||||
@@ -25,24 +27,34 @@ type SeedanceEstimateInput struct {
|
||||
}
|
||||
|
||||
type SeedanceActualAmountInput struct {
|
||||
Model string
|
||||
Resolution string
|
||||
InputVideo bool
|
||||
CompletionTokens int64
|
||||
MarkupMultiplier float64
|
||||
}
|
||||
|
||||
func SeedanceTokenPriceFenPerMillion(resolution string, inputVideo bool) int64 {
|
||||
func SeedanceTokenPriceFenPerMillion(model, resolution string, inputVideo bool) int64 {
|
||||
normalized := strings.ToLower(strings.TrimSpace(resolution))
|
||||
var withoutVideo, withVideo int64
|
||||
switch normalized {
|
||||
case "480p", "720p":
|
||||
withoutVideo, withVideo = 4600, 2800
|
||||
case "1080p":
|
||||
withoutVideo, withVideo = 5100, 3100
|
||||
case "4k":
|
||||
withoutVideo, withVideo = 2600, 1600
|
||||
default:
|
||||
withoutVideo, withVideo = 4600, 2800
|
||||
if strings.TrimSpace(model) == Seedance25Model {
|
||||
switch normalized {
|
||||
case "1080p":
|
||||
withoutVideo, withVideo = 7700, 4600
|
||||
default:
|
||||
withoutVideo, withVideo = 7000, 4200
|
||||
}
|
||||
} else {
|
||||
switch normalized {
|
||||
case "480p", "720p":
|
||||
withoutVideo, withVideo = 4600, 2800
|
||||
case "1080p":
|
||||
withoutVideo, withVideo = 5100, 3100
|
||||
case "4k":
|
||||
withoutVideo, withVideo = 2600, 1600
|
||||
default:
|
||||
withoutVideo, withVideo = 4600, 2800
|
||||
}
|
||||
}
|
||||
if inputVideo {
|
||||
return withVideo
|
||||
@@ -57,7 +69,7 @@ func CalculateSeedanceActualAmountFen(input SeedanceActualAmountInput) (int64, e
|
||||
if math.IsNaN(input.MarkupMultiplier) || math.IsInf(input.MarkupMultiplier, 0) || input.MarkupMultiplier < 1 {
|
||||
return 0, errors.New("seedance markup multiplier must be at least 1")
|
||||
}
|
||||
amount := math.Ceil(float64(input.CompletionTokens) * float64(SeedanceTokenPriceFenPerMillion(input.Resolution, input.InputVideo)) * input.MarkupMultiplier / seedanceTokenScale)
|
||||
amount := math.Ceil(float64(input.CompletionTokens) * float64(SeedanceTokenPriceFenPerMillion(input.Model, input.Resolution, input.InputVideo)) * input.MarkupMultiplier / seedanceTokenScale)
|
||||
if amount < 1 {
|
||||
amount = 1
|
||||
}
|
||||
@@ -75,13 +87,13 @@ func EstimateSeedanceAmountFen(input SeedanceEstimateInput) (int64, error) {
|
||||
if input.InputVideo {
|
||||
inputDuration = input.InputVideoDurationSeconds
|
||||
if math.IsNaN(inputDuration) || math.IsInf(inputDuration, 0) || inputDuration <= 0 {
|
||||
inputDuration = seedanceMaximumInputDuration
|
||||
inputDuration = seedanceMaximumInputDuration(input.Model)
|
||||
}
|
||||
inputDuration = math.Min(seedanceMaximumInputDuration, inputDuration)
|
||||
inputDuration = normalizeSeedanceInputDuration(input.Model, inputDuration)
|
||||
}
|
||||
width, height := seedanceOutputDimensions(input.Resolution, input.AspectRatio)
|
||||
tokens := math.Ceil((inputDuration + input.OutputDurationSeconds) * float64(width*height*seedanceFPS) / 1024)
|
||||
amount := math.Ceil(tokens * float64(SeedanceTokenPriceFenPerMillion(input.Resolution, input.InputVideo)) * input.MarkupMultiplier / seedanceTokenScale)
|
||||
amount := math.Ceil(tokens * float64(SeedanceTokenPriceFenPerMillion(input.Model, input.Resolution, input.InputVideo)) * input.MarkupMultiplier / seedanceTokenScale)
|
||||
return int64(math.Max(1, amount)), nil
|
||||
}
|
||||
|
||||
@@ -108,7 +120,7 @@ func seedanceOutputDimensions(resolution, aspectRatio string) (int, int) {
|
||||
return max(1, int(math.Round(math.Sqrt(area*ratio)))), max(1, int(math.Round(math.Sqrt(area/ratio))))
|
||||
}
|
||||
|
||||
func seedanceInputVideo(payload map[string]any) (bool, float64) {
|
||||
func seedanceInputVideo(payload map[string]any, model string) (bool, float64) {
|
||||
materials := billingMaterials(payload)
|
||||
total, known := 0.0, false
|
||||
for _, material := range materials {
|
||||
@@ -136,7 +148,26 @@ func seedanceInputVideo(payload map[string]any) (bool, float64) {
|
||||
if !known {
|
||||
return inputVideo, 0
|
||||
}
|
||||
return inputVideo, math.Min(seedanceMaximumInputDuration, total)
|
||||
return inputVideo, normalizeSeedanceInputDuration(model, total)
|
||||
}
|
||||
|
||||
func IsSeedanceBillingModel(model string) bool {
|
||||
model = strings.TrimSpace(model)
|
||||
return model == Seedance20Model || model == Seedance25Model
|
||||
}
|
||||
|
||||
func seedanceMaximumInputDuration(model string) float64 {
|
||||
if strings.TrimSpace(model) == Seedance25Model {
|
||||
return 30
|
||||
}
|
||||
return 15
|
||||
}
|
||||
|
||||
func normalizeSeedanceInputDuration(model string, duration float64) float64 {
|
||||
if strings.TrimSpace(model) == Seedance25Model && duration < 4 {
|
||||
duration = 4
|
||||
}
|
||||
return math.Min(seedanceMaximumInputDuration(model), duration)
|
||||
}
|
||||
|
||||
func billingMaterials(payload map[string]any) []map[string]any {
|
||||
|
||||
@@ -8,20 +8,23 @@ import (
|
||||
|
||||
func TestSeedanceActualAmountUsesFrozenTokenPricingAndRoundsUp(t *testing.T) {
|
||||
tests := []struct {
|
||||
model string
|
||||
resolution string
|
||||
inputVideo bool
|
||||
wantPrice int64
|
||||
wantAmount int64
|
||||
}{
|
||||
{resolution: "720P", wantPrice: 4600, wantAmount: 6},
|
||||
{resolution: "1080p", inputVideo: true, wantPrice: 3100, wantAmount: 4},
|
||||
{resolution: "4K", wantPrice: 2600, wantAmount: 4},
|
||||
{model: Seedance20Model, resolution: "720P", wantPrice: 4600, wantAmount: 6},
|
||||
{model: Seedance20Model, resolution: "1080p", inputVideo: true, wantPrice: 3100, wantAmount: 4},
|
||||
{model: Seedance20Model, resolution: "4K", wantPrice: 2600, wantAmount: 4},
|
||||
{model: Seedance25Model, resolution: "720P", wantPrice: 7000, wantAmount: 9},
|
||||
{model: Seedance25Model, resolution: "1080p", inputVideo: true, wantPrice: 4600, wantAmount: 6},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := SeedanceTokenPriceFenPerMillion(test.resolution, test.inputVideo); got != test.wantPrice {
|
||||
if got := SeedanceTokenPriceFenPerMillion(test.model, test.resolution, test.inputVideo); got != test.wantPrice {
|
||||
t.Fatalf("price(%q, %v) = %d, want %d", test.resolution, test.inputVideo, got, test.wantPrice)
|
||||
}
|
||||
got, err := CalculateSeedanceActualAmountFen(SeedanceActualAmountInput{Resolution: test.resolution, InputVideo: test.inputVideo, CompletionTokens: 1001, MarkupMultiplier: 1.2})
|
||||
got, err := CalculateSeedanceActualAmountFen(SeedanceActualAmountInput{Model: test.model, Resolution: test.resolution, InputVideo: test.inputVideo, CompletionTokens: 1001, MarkupMultiplier: 1.2})
|
||||
if err != nil || got != test.wantAmount {
|
||||
t.Fatalf("amount(%q, %v) = %d, %v; want %d", test.resolution, test.inputVideo, got, err, test.wantAmount)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user