feat(gateway): 补齐桌面端高级媒体直连接口
ci / verify (pull_request) Successful in 15m34s
ci / verify (pull_request) Successful in 15m34s
新增图片矢量化、视频超分、每日用量、计价与任务隔离能力,并通过环境变量解析平台凭据。 已通过 Go 全量门禁、迁移检查、镜像构建以及 Vectorizer 五格式和 Topaz 3 秒视频真实 DEV 验收。
This commit is contained in:
@@ -3,6 +3,7 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
@@ -131,6 +132,20 @@ func (s *Service) billings(ctx context.Context, user *auth.User, kind string, bo
|
||||
resource = "image_edit"
|
||||
baseKey = "editBase"
|
||||
}
|
||||
if kind == "images.vectorize" {
|
||||
resource = "image_vectorize"
|
||||
unit = "conversion"
|
||||
baseKey = "vectorizeBase"
|
||||
}
|
||||
if kind == "videos.upscales" {
|
||||
resource = "video_enhance"
|
||||
unit = "5s_video"
|
||||
baseKey = "videoEnhanceBase"
|
||||
inputs := resolveVideoEnhancePricingInputs(body, response)
|
||||
amount, details := videoEnhanceAmount(config, inputs, float64(count), resourcePrice(config, resource, baseKey, "basePrice"))
|
||||
amount = math.Ceil(amount*1e8) / 1e8 * discount
|
||||
return []any{billingLineWithDetails(candidate, resource, unit, float64(count)*inputs.DurationUnits, roundPrice(amount), discount, simulated, details)}
|
||||
}
|
||||
if kind == "videos.generations" {
|
||||
resource = "video"
|
||||
unit = "5s_video"
|
||||
@@ -193,6 +208,160 @@ func (s *Service) billings(ctx context.Context, user *auth.User, kind string, bo
|
||||
return []any{billingLine(candidate, resource, unit, count, roundPrice(amount), discount, simulated)}
|
||||
}
|
||||
|
||||
type videoEnhancePricingInputs struct {
|
||||
DurationSeconds, DurationUnits, SourceFrameRate, TargetFrameRate, SlowMotionRate float64
|
||||
SourceResolution, TargetResolution, Model string
|
||||
}
|
||||
|
||||
var videoEnhanceResolutionPixels = map[string]float64{
|
||||
"480p": 854 * 480, "720p": 1280 * 720, "1080p": 1920 * 1080,
|
||||
"1440p": 2560 * 1440, "2160p": 3840 * 2160,
|
||||
}
|
||||
|
||||
var videoEnhanceResolutionBaseline = map[string]float64{"480p": 1, "720p": 1.4, "1080p": 1.8, "1440p": 2.1, "2160p": 2.6}
|
||||
|
||||
func resolveVideoEnhancePricingInputs(body map[string]any, response clients.Response) videoEnhancePricingInputs {
|
||||
merged := cloneMap(body)
|
||||
if data, ok := response.Result["data"].([]any); ok && len(data) > 0 {
|
||||
if item, ok := data[0].(map[string]any); ok {
|
||||
for _, key := range []string{"duration", "source_resolution", "target_resolution", "source_frame_rate", "target_frame_rate", "slow_motion_rate"} {
|
||||
if merged[key] == nil && item[key] != nil {
|
||||
merged[key] = item[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
duration := floatFromAny(merged["duration"])
|
||||
if duration <= 0 {
|
||||
duration = 5
|
||||
}
|
||||
targetValue := firstNonEmptyString(stringFromMap(merged, "target_resolution"), stringFromMap(merged, "output_resolution"), stringFromMap(merged, "resolution"))
|
||||
if dimensions := videoEnhanceDimensionsValue(merged, "output_width", "output_height"); dimensions != "" {
|
||||
targetValue = dimensions
|
||||
}
|
||||
target := normalizeVideoEnhanceResolution(targetValue, "1080p")
|
||||
sourceValue := firstNonEmptyString(stringFromMap(merged, "source_resolution"), stringFromMap(merged, "resolution"))
|
||||
if dimensions := videoEnhanceDimensionsValue(merged, "source_width", "source_height"); dimensions != "" {
|
||||
sourceValue = dimensions
|
||||
}
|
||||
source := normalizeVideoEnhanceResolution(sourceValue, target)
|
||||
sourceFPS := floatFromAny(firstPresentValue(merged, "source_frame_rate", "frame_rate", "frameRate"))
|
||||
if sourceFPS <= 0 {
|
||||
sourceFPS = 24
|
||||
}
|
||||
targetFPS := floatFromAny(firstPresentValue(merged, "target_frame_rate", "output_frame_rate"))
|
||||
if targetFPS <= 0 {
|
||||
targetFPS = sourceFPS
|
||||
}
|
||||
slowMotion := floatFromAny(merged["slow_motion_rate"])
|
||||
if slowMotion <= 0 {
|
||||
slowMotion = 1
|
||||
}
|
||||
return videoEnhancePricingInputs{
|
||||
DurationSeconds: duration, DurationUnits: math.Max(1, math.Round(duration)/5),
|
||||
SourceResolution: source, TargetResolution: target,
|
||||
SourceFrameRate: sourceFPS, TargetFrameRate: targetFPS, SlowMotionRate: slowMotion,
|
||||
Model: firstNonEmptyString(stringFromMap(merged, "model"), stringFromMap(merged, "enhancement_model")),
|
||||
}
|
||||
}
|
||||
|
||||
func videoEnhanceDimensionsValue(values map[string]any, widthKey, heightKey string) string {
|
||||
width := int(math.Round(floatFromAny(values[widthKey])))
|
||||
height := int(math.Round(floatFromAny(values[heightKey])))
|
||||
if width <= 0 || height <= 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(width) + "x" + strconv.Itoa(height)
|
||||
}
|
||||
|
||||
func normalizeVideoEnhanceResolution(value, fallback string) string {
|
||||
normalized := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(value)), "_upscale")
|
||||
if normalized == "2k" {
|
||||
normalized = "1440p"
|
||||
}
|
||||
if normalized == "4k" {
|
||||
normalized = "2160p"
|
||||
}
|
||||
if width, height, ok := parseVideoEnhanceDimensions(normalized); ok {
|
||||
pixels := float64(width * height)
|
||||
for _, bucket := range []string{"480p", "720p", "1080p", "1440p", "2160p"} {
|
||||
if pixels <= videoEnhanceResolutionPixels[bucket] {
|
||||
return bucket
|
||||
}
|
||||
}
|
||||
return "2160p"
|
||||
}
|
||||
if _, ok := videoEnhanceResolutionPixels[normalized]; ok {
|
||||
return normalized
|
||||
}
|
||||
if _, ok := videoEnhanceResolutionPixels[fallback]; ok {
|
||||
return fallback
|
||||
}
|
||||
return "1080p"
|
||||
}
|
||||
|
||||
func parseVideoEnhanceDimensions(value string) (int, int, bool) {
|
||||
parts := strings.Split(strings.ReplaceAll(value, " ", ""), "x")
|
||||
if len(parts) != 2 {
|
||||
return 0, 0, false
|
||||
}
|
||||
width, widthErr := strconv.Atoi(parts[0])
|
||||
height, heightErr := strconv.Atoi(parts[1])
|
||||
return width, height, widthErr == nil && heightErr == nil && width > 0 && height > 0
|
||||
}
|
||||
|
||||
func videoEnhanceAmount(config map[string]any, input videoEnhancePricingInputs, count, basePrice float64) (float64, map[string]any) {
|
||||
transitionKey := input.SourceResolution + "->" + input.TargetResolution
|
||||
minimumResolutionWeight := videoEnhanceResolutionBaseline[input.TargetResolution]
|
||||
resolutionWeight, configuredResolution := pricingDynamicWeight(config, "video_enhance", "resolutionTransitions", transitionKey)
|
||||
if !configuredResolution {
|
||||
resolutionWeight = math.Max(minimumResolutionWeight, videoEnhanceResolutionPixels[input.TargetResolution]/videoEnhanceResolutionPixels[input.SourceResolution])
|
||||
} else {
|
||||
resolutionWeight = math.Max(minimumResolutionWeight, resolutionWeight)
|
||||
}
|
||||
sourceFPS := math.Max(1, math.Round(input.SourceFrameRate))
|
||||
targetFPS := math.Max(1, math.Round(input.TargetFrameRate))
|
||||
frameKey := strconv.Itoa(int(sourceFPS)) + "->" + strconv.Itoa(int(targetFPS))
|
||||
minimumFrameWeight := math.Max(1, targetFPS/24)
|
||||
frameWeight, configuredFrame := pricingDynamicWeight(config, "video_enhance", "frameRateTransitions", frameKey)
|
||||
if !configuredFrame {
|
||||
frameWeight = minimumFrameWeight
|
||||
} else {
|
||||
frameWeight = math.Max(minimumFrameWeight, frameWeight)
|
||||
}
|
||||
modelWeight, configuredModel := pricingDynamicWeight(config, "video_enhance", "modelWeights", input.Model)
|
||||
if !configuredModel {
|
||||
modelWeight = 1
|
||||
}
|
||||
markup, foundMarkup := pricingDynamicWeight(config, "video_enhance", "", "markup")
|
||||
if !foundMarkup {
|
||||
markup = 1
|
||||
}
|
||||
amount := count * input.DurationUnits * basePrice * resolutionWeight * frameWeight * input.SlowMotionRate * modelWeight * markup
|
||||
return amount, map[string]any{
|
||||
"count": count, "durationSeconds": input.DurationSeconds, "durationUnit": "5s", "durationUnitCount": input.DurationUnits,
|
||||
"sourceResolution": input.SourceResolution, "targetResolution": input.TargetResolution,
|
||||
"resolutionTransitionKey": transitionKey, "resolutionTransitionWeight": resolutionWeight, "resolutionTransitionFallback": !configuredResolution,
|
||||
"sourceFrameRate": input.SourceFrameRate, "targetFrameRate": input.TargetFrameRate,
|
||||
"frameRateTransitionKey": frameKey, "frameRateTransitionWeight": frameWeight, "frameRateTransitionFallback": !configuredFrame,
|
||||
"slowMotionRate": input.SlowMotionRate, "model": input.Model, "modelWeight": modelWeight, "markup": markup,
|
||||
}
|
||||
}
|
||||
|
||||
func pricingDynamicWeight(config map[string]any, resource, group, key string) (float64, bool) {
|
||||
resourceConfig, _ := config[resource].(map[string]any)
|
||||
dynamic, _ := resourceConfig["dynamicWeight"].(map[string]any)
|
||||
if group == "" {
|
||||
value, ok := dynamic[key]
|
||||
weight := floatFromAny(value)
|
||||
return weight, ok && weight > 0
|
||||
}
|
||||
values, _ := dynamic[group].(map[string]any)
|
||||
value, ok := values[key]
|
||||
weight := floatFromAny(value)
|
||||
return weight, ok && weight > 0
|
||||
}
|
||||
|
||||
func (s *Service) effectiveBillingConfig(ctx context.Context, candidate store.RuntimeModelCandidate) map[string]any {
|
||||
var inheritedRuleSetConfig map[string]any
|
||||
if ruleSetID := firstNonEmptyString(candidate.BasePricingRuleSetID, candidate.PlatformPricingRuleSetID); ruleSetID != "" && s.store != nil {
|
||||
|
||||
Reference in New Issue
Block a user