feat(gateway): 补齐桌面端高级媒体直连接口
ci / verify (pull_request) Successful in 15m34s

新增图片矢量化、视频超分、每日用量、计价与任务隔离能力,并通过环境变量解析平台凭据。

已通过 Go 全量门禁、迁移检查、镜像构建以及 Vectorizer 五格式和 Topaz 3 秒视频真实 DEV 验收。
This commit is contained in:
2026-07-22 14:02:53 +08:00
parent cbebfd7baa
commit 3056cf8fca
37 changed files with 3336 additions and 29 deletions
+169
View File
@@ -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 {