Skip to content

Commit b9e80f2

Browse files
authored
Merge pull request #386 from bcc-code/feature/bcc-76-shorts-automation-v2-flows
Feature/bcc 76 shorts automation v2 flows
2 parents 0a6909a + 20cc6b9 commit b9e80f2

File tree

4 files changed

+401
-0
lines changed

4 files changed

+401
-0
lines changed

activities/crop_shorts.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
}

activities/shorts.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package activities
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/go-resty/resty/v2"
9+
"go.temporal.io/sdk/activity"
10+
)
11+
12+
var shortServiceURL = os.Getenv("SHORTS_SERVICE_URL")
13+
14+
type SubmitShortJobInput struct {
15+
InputPath string `json:"input_path"`
16+
OutputPath string `json:"output_path"`
17+
Model string `json:"model"`
18+
Debug bool `json:"debug"`
19+
}
20+
21+
type Square struct {
22+
X int `json:"x"`
23+
Y int `json:"y"`
24+
W int `json:"w"`
25+
H int `json:"h"`
26+
}
27+
28+
type Keyframe struct {
29+
EndTimestamp float64 `json:"end_timestamp"`
30+
JumpCut bool `json:"jump_cut"`
31+
StartTimestamp float64 `json:"start_timestamp"`
32+
Square
33+
}
34+
35+
type GenerateShortRequestResult struct {
36+
Debug string `json:"debug"`
37+
Keyframes []Keyframe `json:"keyframes"`
38+
Status string `json:"status"`
39+
}
40+
41+
type SubmitShortJobResult struct {
42+
JobID string `json:"job_id"`
43+
}
44+
45+
func (ua UtilActivities) SubmitShortJobActivity(ctx context.Context, params SubmitShortJobInput) (*SubmitShortJobResult, error) {
46+
log := activity.GetLogger(ctx)
47+
activity.RecordHeartbeat(ctx, "SubmitShortJob")
48+
log.Info("Starting SubmitShortJob activity")
49+
50+
restyClient := resty.New()
51+
var result SubmitShortJobResult
52+
resp, err := restyClient.R().SetContext(ctx).SetBody(params).SetResult(&result).Post(fmt.Sprintf("%s/submit_job", shortServiceURL))
53+
54+
if err != nil {
55+
return nil, fmt.Errorf("resty request failed: %w", err)
56+
}
57+
if resp.StatusCode() != 202 {
58+
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode(), resp.String())
59+
}
60+
61+
return &result, nil
62+
}
63+
64+
type CheckJobStatusInput struct {
65+
JobID string `json:"job_id"`
66+
}
67+
68+
func (ua UtilActivities) CheckJobStatusActivity(ctx context.Context, params CheckJobStatusInput) (*GenerateShortRequestResult, error) {
69+
activity.RecordHeartbeat(ctx, "CheckJobStatus")
70+
71+
restyClient := resty.New()
72+
73+
var result GenerateShortRequestResult
74+
resp, err := restyClient.R().SetContext(ctx).SetResult(&result).Get(fmt.Sprintf("%s/job_status/%s", shortServiceURL, params.JobID))
75+
76+
if err != nil {
77+
return nil, fmt.Errorf("resty request failed: %w", err)
78+
}
79+
if resp.StatusCode() != 200 {
80+
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode(), resp.String())
81+
}
82+
83+
return &result, nil
84+
}

0 commit comments

Comments
 (0)