|
| 1 | +package activities |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "math" |
| 7 | + "strconv" |
| 8 | + "strings" |
| 9 | +) |
| 10 | + |
| 11 | +type CropShortInput struct { |
| 12 | + InputVideoPath string |
| 13 | + AudioVideoPath string |
| 14 | + OutputVideoPath string |
| 15 | + KeyFrames []Keyframe |
| 16 | + InSeconds float64 |
| 17 | + OutSeconds float64 |
| 18 | +} |
| 19 | + |
| 20 | +type CropShortResult struct { |
| 21 | + Arguments []string |
| 22 | +} |
| 23 | + |
| 24 | +func (ua UtilActivities) CropShortActivity(ctx context.Context, params CropShortInput) (*CropShortResult, error) { |
| 25 | + cropFilter := buildCropFilter(params.KeyFrames) |
| 26 | + |
| 27 | + args := []string{ |
| 28 | + "-i", params.InputVideoPath, |
| 29 | + "-i", params.AudioVideoPath, |
| 30 | + "-filter_complex", |
| 31 | + fmt.Sprintf( |
| 32 | + "[0:v]%s[v]; [1:a]atrim=start=%.3f:end=%.3f,asetpts=PTS-STARTPTS[a]", |
| 33 | + cropFilter, params.InSeconds, params.OutSeconds, |
| 34 | + ), |
| 35 | + "-map", "[v]", |
| 36 | + "-map", "[a]", |
| 37 | + "-c:v", "libx264", |
| 38 | + "-c:a", "aac", |
| 39 | + "-pix_fmt", "yuv420p", |
| 40 | + "-y", |
| 41 | + params.OutputVideoPath, |
| 42 | + } |
| 43 | + return &CropShortResult{Arguments: args}, nil |
| 44 | +} |
| 45 | + |
| 46 | +func buildCropFilter(keyframes []Keyframe) string { |
| 47 | + if len(keyframes) == 0 { |
| 48 | + return "crop=960:540:489:29" |
| 49 | + } |
| 50 | + if len(keyframes) == 1 { |
| 51 | + kf := keyframes[0] |
| 52 | + return fmt.Sprintf("crop=%d:%d:%d:%d", kf.W, kf.H, kf.X, kf.Y) |
| 53 | + } |
| 54 | + |
| 55 | + cropW := keyframes[0].W |
| 56 | + cropH := keyframes[0].H |
| 57 | + |
| 58 | + xExpr := buildSmoothTransitionExpression(keyframes, "X") |
| 59 | + yExpr := buildSmoothTransitionExpression(keyframes, "Y") |
| 60 | + |
| 61 | + return fmt.Sprintf("crop=%d:%d:x='%s':y='%s'", cropW, cropH, xExpr, yExpr) |
| 62 | +} |
| 63 | + |
| 64 | +func buildSmoothTransitionExpression(keyframes []Keyframe, param string) string { |
| 65 | + var conditions []string |
| 66 | + for i := len(keyframes) - 1; i >= 1; i-- { |
| 67 | + currentKf := keyframes[i] |
| 68 | + if currentKf.JumpCut { |
| 69 | + target := getParamValue(currentKf, param) |
| 70 | + conditions = append(conditions, |
| 71 | + fmt.Sprintf("if(gte(t,%.3f),%d,", currentKf.StartTimestamp, target)) |
| 72 | + } else { |
| 73 | + prev := getParamValue(keyframes[i-1], param) |
| 74 | + target := getParamValue(currentKf, param) |
| 75 | + |
| 76 | + dist := calculateDistance(keyframes[i-1], currentKf) |
| 77 | + panDur := calculatePanDuration(dist) |
| 78 | + end := currentKf.StartTimestamp + panDur |
| 79 | + |
| 80 | + norm := fmt.Sprintf("(t-%.3f)/%.3f", currentKf.StartTimestamp, panDur) |
| 81 | + ease := "(1-pow(1-(" + norm + "),2))" |
| 82 | + |
| 83 | + smooth := fmt.Sprintf("if(lte(t,%.3f),%d+(%d-%d)*%s,%d)", |
| 84 | + end, prev, target, prev, ease, target) |
| 85 | + conditions = append(conditions, |
| 86 | + fmt.Sprintf("if(gte(t,%.3f),%s,", currentKf.StartTimestamp, smooth)) |
| 87 | + } |
| 88 | + } |
| 89 | + result := strings.Join(conditions, "") |
| 90 | + first := getParamValue(keyframes[0], param) |
| 91 | + result += strconv.Itoa(first) + strings.Repeat(")", len(conditions)) |
| 92 | + return result |
| 93 | +} |
| 94 | + |
| 95 | +func calculateDistance(kf1, kf2 Keyframe) float64 { |
| 96 | + dx := float64(kf2.X - kf1.X) |
| 97 | + dy := float64(kf2.Y - kf1.Y) |
| 98 | + return math.Sqrt(dx*dx + dy*dy) |
| 99 | +} |
| 100 | + |
| 101 | +func calculatePanDuration(distance float64) float64 { |
| 102 | + const ( |
| 103 | + minDur = 0.1 |
| 104 | + maxDur = 3.0 |
| 105 | + speed = 200.0 |
| 106 | + ) |
| 107 | + d := distance / speed |
| 108 | + if d < minDur { |
| 109 | + d = minDur |
| 110 | + } |
| 111 | + if d > maxDur { |
| 112 | + d = maxDur |
| 113 | + } |
| 114 | + return d |
| 115 | +} |
| 116 | + |
| 117 | +func getParamValue(kf Keyframe, param string) int { |
| 118 | + switch param { |
| 119 | + case "X": |
| 120 | + return kf.X |
| 121 | + case "Y": |
| 122 | + return kf.Y |
| 123 | + } |
| 124 | + return 0 |
| 125 | +} |
0 commit comments