完善生图5.0的功能
This commit is contained in:
1 parent
88a4268d40
commit
678d213f40
31 files changed
+3281
-107
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -23,8 +24,9 @@ type FencedSettlementStateWriter interface {
|
||||
WriteBillingFenced(context.Context, string, json.RawMessage, jobs.Status, string) error
|
||||
}
|
||||
|
||||
// SettlementProcessor adds Seedance actual-usage settlement to a jobs
|
||||
// processor. Other providers and non-successful advances pass through.
|
||||
// SettlementProcessor adds actual-usage settlement for Seedance videos and
|
||||
// Seedream layer decomposition. Other providers and non-successful advances
|
||||
// pass through.
|
||||
type SettlementProcessor struct {
|
||||
inner jobs.Processor
|
||||
ledger SettlementLedger
|
||||
@@ -44,9 +46,15 @@ func (p *SettlementProcessor) Advance(ctx context.Context, job jobs.Job) (jobs.J
|
||||
return jobs.Job{}, errors.New("advance generation job")
|
||||
}
|
||||
advanced, err := p.inner.Advance(ctx, job)
|
||||
if err != nil || advanced.Provider != "seedance" || advanced.Status != jobs.StatusSucceeded || len(advanced.Billing) == 0 {
|
||||
if err != nil || advanced.Status != jobs.StatusSucceeded || len(advanced.Billing) == 0 {
|
||||
return advanced, err
|
||||
}
|
||||
if advanced.Provider == "seedream" && seedreamLayerRequest(advanced.RequestPayload) {
|
||||
return p.settleSeedreamLayers(ctx, advanced)
|
||||
}
|
||||
if advanced.Provider != "seedance" {
|
||||
return advanced, nil
|
||||
}
|
||||
|
||||
var charge billingState
|
||||
if json.Unmarshal(advanced.Billing, &charge) != nil {
|
||||
@@ -131,13 +139,84 @@ func (p *SettlementProcessor) Advance(ctx context.Context, job jobs.Job) (jobs.J
|
||||
return p.write(ctx, advanced, charge.raw)
|
||||
}
|
||||
|
||||
func (p *SettlementProcessor) settleSeedreamLayers(ctx context.Context, job jobs.Job) (jobs.Job, error) {
|
||||
var charge billingState
|
||||
if json.Unmarshal(job.Billing, &charge) != nil {
|
||||
return jobs.Job{}, errors.New("settle seedream layer charge")
|
||||
}
|
||||
status, _ := charge.raw["settlementStatus"].(string)
|
||||
if status == "settled" || status == "estimated" {
|
||||
return job, nil
|
||||
}
|
||||
var use usageContext
|
||||
if len(job.UsageContext) != 0 && json.Unmarshal(job.UsageContext, &use) != nil {
|
||||
return jobs.Job{}, errors.New("settle seedream layer charge")
|
||||
}
|
||||
quotaExempt := charge.QuotaExempt || use.Source == "platform" && use.Role == "super_admin"
|
||||
chargeReady := charge.Status == "charged" || quotaExempt && charge.Status == "not_charged"
|
||||
if !chargeReady || !quotaExempt && use.OrganizationID == "" {
|
||||
return job, nil
|
||||
}
|
||||
|
||||
images := seedreamLayerImages(job.ResponsePayload)
|
||||
if len(images) == 0 {
|
||||
charge.raw["settlementStatus"] = "estimated"
|
||||
charge.raw["settlementReason"] = "provider_layer_metadata_unavailable"
|
||||
charge.raw["settledAt"] = p.now().UTC().Format(time.RFC3339Nano)
|
||||
return p.write(ctx, job, charge.raw)
|
||||
}
|
||||
actualAmount, err := billing.CalculateSeedreamLayerAmountFen(images, numberOrZero(charge.raw["markupMultiplier"]))
|
||||
if err != nil {
|
||||
return jobs.Job{}, errors.New("settle seedream layer charge")
|
||||
}
|
||||
delta := actualAmount - charge.AmountFen
|
||||
settledAt := p.now().UTC()
|
||||
var settlementLedgerID string
|
||||
if delta != 0 && !quotaExempt {
|
||||
if p.ledger == nil {
|
||||
return jobs.Job{}, errors.New("settle seedream layer charge")
|
||||
}
|
||||
description := capabilityLabel(job.Capability) + "图层拆分实际费用退回"
|
||||
if delta > 0 {
|
||||
description = capabilityLabel(job.Capability) + "图层拆分实际费用补扣"
|
||||
}
|
||||
posting, postErr := p.ledger.Settle(ctx, billing.SettlementRequest{
|
||||
OrganizationID: use.OrganizationID, AccountID: use.AccountID, JobID: job.ID,
|
||||
DeltaFen: delta, Description: description,
|
||||
Metadata: map[string]any{
|
||||
"operation": "seedream_layer_actual_settlement", "reservedAmountFen": charge.AmountFen,
|
||||
"actualAmountFen": actualAmount, "outputCount": len(images), "model": job.ReqKey,
|
||||
},
|
||||
})
|
||||
if postErr != nil {
|
||||
return jobs.Job{}, errors.New("settle seedream layer charge")
|
||||
}
|
||||
settlementLedgerID = posting.LedgerID
|
||||
if !posting.CreatedAt.IsZero() {
|
||||
settledAt = posting.CreatedAt.UTC()
|
||||
}
|
||||
}
|
||||
charge.raw["amountFen"] = actualAmount
|
||||
charge.raw["settlementStatus"] = "settled"
|
||||
charge.raw["settledAt"] = settledAt.Format(time.RFC3339Nano)
|
||||
if settlementLedgerID != "" {
|
||||
charge.raw["settlementLedgerEntryId"] = settlementLedgerID
|
||||
} else {
|
||||
delete(charge.raw, "settlementLedgerEntryId")
|
||||
}
|
||||
charge.raw["providerUsage"] = map[string]any{
|
||||
"model": job.ReqKey, "layerDecomposition": true, "generatedImages": len(images),
|
||||
}
|
||||
return p.write(ctx, job, charge.raw)
|
||||
}
|
||||
|
||||
func (p *SettlementProcessor) write(ctx context.Context, job jobs.Job, snapshot map[string]any) (jobs.Job, error) {
|
||||
if p.state == nil {
|
||||
return jobs.Job{}, errors.New("persist seedance generation settlement")
|
||||
return jobs.Job{}, errors.New("persist generation settlement")
|
||||
}
|
||||
encoded, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
return jobs.Job{}, errors.New("persist seedance generation settlement")
|
||||
return jobs.Job{}, errors.New("persist generation settlement")
|
||||
}
|
||||
var writeErr error
|
||||
if fenced, ok := p.state.(FencedSettlementStateWriter); ok && job.LockedBy != "" {
|
||||
@@ -163,6 +242,51 @@ func seedanceCompletionTokens(payload json.RawMessage) int64 {
|
||||
return value
|
||||
}
|
||||
|
||||
func seedreamLayerRequest(payload json.RawMessage) bool {
|
||||
var request struct {
|
||||
Settings map[string]any `json:"settings"`
|
||||
}
|
||||
if json.Unmarshal(payload, &request) != nil {
|
||||
return false
|
||||
}
|
||||
value, _ := request.Settings["layerDecomposition"].(bool)
|
||||
creationMode, _ := request.Settings["creationMode"].(string)
|
||||
return value || strings.EqualFold(strings.TrimSpace(creationMode), "layers")
|
||||
}
|
||||
|
||||
func seedreamLayerImages(payload json.RawMessage) []billing.SeedreamLayerImage {
|
||||
var response struct {
|
||||
Raw json.RawMessage `json:"raw"`
|
||||
}
|
||||
if json.Unmarshal(payload, &response) != nil || len(response.Raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
var providerResponse struct {
|
||||
Data []struct {
|
||||
Size string `json:"size"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if json.Unmarshal(response.Raw, &providerResponse) != nil || len(providerResponse.Data) == 0 || len(providerResponse.Data) > 17 {
|
||||
return nil
|
||||
}
|
||||
images := make([]billing.SeedreamLayerImage, 0, len(providerResponse.Data))
|
||||
for _, output := range providerResponse.Data {
|
||||
parts := strings.FieldsFunc(strings.TrimSpace(output.Size), func(character rune) bool {
|
||||
return character == 'x' || character == 'X' || character == '×' || character == '*'
|
||||
})
|
||||
if len(parts) != 2 {
|
||||
return nil
|
||||
}
|
||||
width, widthErr := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
height, heightErr := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||
if widthErr != nil || heightErr != nil || width <= 0 || height <= 0 {
|
||||
return nil
|
||||
}
|
||||
images = append(images, billing.SeedreamLayerImage{Width: width, Height: height})
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
func seedanceResolution(snapshot map[string]any) string {
|
||||
parameters, _ := snapshot["parameters"].(map[string]any)
|
||||
resolution, _ := parameters["resolution"].(string)
|
||||
|
||||
Reference in new issue
Block a user