fix: normalize video duration steps
This commit is contained in:
@@ -3,6 +3,7 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -518,7 +519,7 @@ func (durationProcessor) Process(params map[string]any, modelType string, contex
|
|||||||
resolution := firstNonEmptyString(stringFromAny(params["resolution"]), context.resolution)
|
resolution := firstNonEmptyString(stringFromAny(params["resolution"]), context.resolution)
|
||||||
modeKey := videoModeKey(params)
|
modeKey := videoModeKey(params)
|
||||||
if options := scopedNumberList(capability["duration_options"], resolution, modeKey); len(options) > 0 {
|
if options := scopedNumberList(capability["duration_options"], resolution, modeKey); len(options) > 0 {
|
||||||
normalized := closestNumber(duration, options)
|
normalized := nextAllowedNumber(duration, options)
|
||||||
params["duration"] = normalized
|
params["duration"] = normalized
|
||||||
syncDurationSeconds(params)
|
syncDurationSeconds(params)
|
||||||
if normalized != duration {
|
if normalized != duration {
|
||||||
@@ -528,7 +529,7 @@ func (durationProcessor) Process(params map[string]any, modelType string, contex
|
|||||||
"duration",
|
"duration",
|
||||||
duration,
|
duration,
|
||||||
normalized,
|
normalized,
|
||||||
"duration 不在模型固定时长选项内,已调整为最近的允许值。",
|
"duration 不在模型固定时长选项内,已向上调整为允许值。",
|
||||||
capabilityPath(modelType, "duration_options"),
|
capabilityPath(modelType, "duration_options"),
|
||||||
capability["duration_options"],
|
capability["duration_options"],
|
||||||
)
|
)
|
||||||
@@ -555,6 +556,23 @@ func (durationProcessor) Process(params map[string]any, modelType string, contex
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
step := durationStep(capability["duration_step"], resolution, modeKey)
|
||||||
|
normalized := normalizeDurationByStep(duration, step)
|
||||||
|
params["duration"] = normalized
|
||||||
|
syncDurationSeconds(params)
|
||||||
|
if normalized != duration {
|
||||||
|
context.recordChange(
|
||||||
|
"DurationProcessor",
|
||||||
|
"adjust",
|
||||||
|
"duration",
|
||||||
|
duration,
|
||||||
|
normalized,
|
||||||
|
"duration 不符合模型时长步进,已按步进向上归一。",
|
||||||
|
capabilityPath(modelType, "duration_step"),
|
||||||
|
capability["duration_step"],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -1044,28 +1062,38 @@ func durationStep(value any, scopes ...string) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func normalizeDurationByRange(target float64, minValue float64, maxValue float64, step float64) float64 {
|
func normalizeDurationByRange(target float64, minValue float64, maxValue float64, step float64) float64 {
|
||||||
clamped := math.Min(math.Max(target, minValue), maxValue)
|
if minValue > maxValue {
|
||||||
if step <= 0 {
|
minValue, maxValue = maxValue, minValue
|
||||||
return clamped
|
|
||||||
}
|
}
|
||||||
snapped := math.Round((clamped-minValue)/step)*step + minValue
|
if step <= 0 {
|
||||||
|
step = 1
|
||||||
|
}
|
||||||
|
clamped := math.Min(math.Max(target, minValue), maxValue)
|
||||||
|
snapped := math.Ceil(((clamped-minValue)/step)-1e-9)*step + minValue
|
||||||
|
snapped = math.Min(math.Max(snapped, minValue), maxValue)
|
||||||
return math.Round(snapped*1_000_000) / 1_000_000
|
return math.Round(snapped*1_000_000) / 1_000_000
|
||||||
}
|
}
|
||||||
|
|
||||||
func closestNumber(target float64, values []float64) float64 {
|
func normalizeDurationByStep(target float64, step float64) float64 {
|
||||||
|
if step <= 0 {
|
||||||
|
step = 1
|
||||||
|
}
|
||||||
|
snapped := math.Ceil((target/step)-1e-9) * step
|
||||||
|
return math.Round(snapped*1_000_000) / 1_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextAllowedNumber(target float64, values []float64) float64 {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
closest := values[0]
|
sorted := append([]float64(nil), values...)
|
||||||
minDiff := math.Abs(target - closest)
|
sort.Float64s(sorted)
|
||||||
for _, value := range values[1:] {
|
for _, value := range sorted {
|
||||||
diff := math.Abs(target - value)
|
if value >= target || math.Abs(value-target) < 1e-9 {
|
||||||
if diff < minDiff {
|
return value
|
||||||
minDiff = diff
|
|
||||||
closest = value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return closest
|
return sorted[len(sorted)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoModeKey(params map[string]any) string {
|
func videoModeKey(params map[string]any) string {
|
||||||
|
|||||||
@@ -180,6 +180,100 @@ func TestParamProcessorVideoCapabilitiesNormalizeAndFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParamProcessorDurationRangeRoundsFractionalSecondsUp(t *testing.T) {
|
||||||
|
body := map[string]any{
|
||||||
|
"model": "Seedance",
|
||||||
|
"prompt": "animate it",
|
||||||
|
"duration": 5.5,
|
||||||
|
}
|
||||||
|
candidate := store.RuntimeModelCandidate{
|
||||||
|
ModelType: "video_generate",
|
||||||
|
Capabilities: map[string]any{
|
||||||
|
"video_generate": map[string]any{
|
||||||
|
"duration_range": []any{3, 12},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := preprocessRequestWithLog("videos.generations", body, candidate)
|
||||||
|
if result.Body["duration"] != float64(6) && result.Body["duration"] != 6 {
|
||||||
|
t.Fatalf("fractional duration should be rounded up to default 1s step, got %+v", result.Body["duration"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamProcessorDurationWithoutRangeStillRoundsUp(t *testing.T) {
|
||||||
|
body := map[string]any{
|
||||||
|
"model": "Seedance",
|
||||||
|
"prompt": "animate it",
|
||||||
|
"duration": 5.2,
|
||||||
|
}
|
||||||
|
candidate := store.RuntimeModelCandidate{
|
||||||
|
ModelType: "video_generate",
|
||||||
|
Capabilities: map[string]any{
|
||||||
|
"video_generate": map[string]any{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := preprocessRequestWithLog("videos.generations", body, candidate)
|
||||||
|
if result.Body["duration"] != float64(6) && result.Body["duration"] != 6 {
|
||||||
|
t.Fatalf("duration should default to a 1s upward step without range, got %+v", result.Body["duration"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamProcessorDurationRangeUsesStepCeilingAndRange(t *testing.T) {
|
||||||
|
body := map[string]any{
|
||||||
|
"model": "Seedance",
|
||||||
|
"prompt": "animate it",
|
||||||
|
"duration": 6.1,
|
||||||
|
"duration_seconds": 6.1,
|
||||||
|
}
|
||||||
|
candidate := store.RuntimeModelCandidate{
|
||||||
|
ModelType: "image_to_video",
|
||||||
|
Capabilities: map[string]any{
|
||||||
|
"image_to_video": map[string]any{
|
||||||
|
"duration_range": []any{5, 10},
|
||||||
|
"duration_step": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := preprocessRequestWithLog("videos.generations", body, candidate)
|
||||||
|
if result.Body["duration"] != float64(7) && result.Body["duration"] != 7 {
|
||||||
|
t.Fatalf("duration should be rounded up by configured step, got %+v", result.Body["duration"])
|
||||||
|
}
|
||||||
|
if result.Body["duration_seconds"] != result.Body["duration"] {
|
||||||
|
t.Fatalf("duration_seconds should sync with normalized duration, got %+v", result.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
body["duration"] = 10.1
|
||||||
|
body["duration_seconds"] = 10.1
|
||||||
|
result = preprocessRequestWithLog("videos.generations", body, candidate)
|
||||||
|
if result.Body["duration"] != float64(10) && result.Body["duration"] != 10 {
|
||||||
|
t.Fatalf("duration should be capped by range max, got %+v", result.Body["duration"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamProcessorDurationOptionsChooseNextAllowedValue(t *testing.T) {
|
||||||
|
body := map[string]any{
|
||||||
|
"model": "Seedance",
|
||||||
|
"prompt": "animate it",
|
||||||
|
"duration": 8.1,
|
||||||
|
}
|
||||||
|
candidate := store.RuntimeModelCandidate{
|
||||||
|
ModelType: "image_to_video",
|
||||||
|
Capabilities: map[string]any{
|
||||||
|
"image_to_video": map[string]any{
|
||||||
|
"duration_options": []any{4, 8, 12},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := preprocessRequestWithLog("videos.generations", body, candidate)
|
||||||
|
if result.Body["duration"] != float64(12) && result.Body["duration"] != 12 {
|
||||||
|
t.Fatalf("duration should use next allowed option, got %+v", result.Body["duration"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParamProcessorVideoGenerateLogsFirstFrameRemoval(t *testing.T) {
|
func TestParamProcessorVideoGenerateLogsFirstFrameRemoval(t *testing.T) {
|
||||||
body := map[string]any{
|
body := map[string]any{
|
||||||
"model": "Seedance T2V",
|
"model": "Seedance T2V",
|
||||||
|
|||||||
Reference in New Issue
Block a user