vargai 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +7 -0
- package/.env.example +27 -0
- package/.github/workflows/ci.yml +23 -0
- package/.husky/README.md +102 -0
- package/.husky/commit-msg +6 -0
- package/.husky/pre-commit +9 -0
- package/.husky/pre-push +6 -0
- package/.size-limit.json +8 -0
- package/.test-hooks.ts +5 -0
- package/CLAUDE.md +125 -0
- package/CONTRIBUTING.md +150 -0
- package/LICENSE.md +53 -0
- package/README.md +78 -0
- package/SKILLS.md +173 -0
- package/STRUCTURE.md +92 -0
- package/biome.json +34 -0
- package/bun.lock +1254 -0
- package/commitlint.config.js +22 -0
- package/docs/plan.md +66 -0
- package/docs/todo.md +14 -0
- package/docs/varg-sdk.md +812 -0
- package/ffmpeg/CLAUDE.md +68 -0
- package/package.json +69 -0
- package/pipeline/cookbooks/SKILL.md +285 -0
- package/pipeline/cookbooks/remotion-video.md +585 -0
- package/pipeline/cookbooks/round-video-character.md +337 -0
- package/pipeline/cookbooks/scripts/animate-frames-parallel.ts +84 -0
- package/pipeline/cookbooks/scripts/combine-scenes.sh +53 -0
- package/pipeline/cookbooks/scripts/generate-frames-parallel.ts +99 -0
- package/pipeline/cookbooks/scripts/still-to-video.sh +37 -0
- package/pipeline/cookbooks/talking-character.md +59 -0
- package/pipeline/cookbooks/text-to-tiktok.md +669 -0
- package/pipeline/cookbooks/trendwatching.md +156 -0
- package/plan.md +281 -0
- package/scripts/.gitkeep +0 -0
- package/src/ai-sdk/cache.ts +142 -0
- package/src/ai-sdk/examples/cached-generation.ts +53 -0
- package/src/ai-sdk/examples/duet-scene-4.ts +53 -0
- package/src/ai-sdk/examples/duet-scene-5-audio.ts +32 -0
- package/src/ai-sdk/examples/duet-video.ts +56 -0
- package/src/ai-sdk/examples/editly-composition.ts +63 -0
- package/src/ai-sdk/examples/editly-test.ts +57 -0
- package/src/ai-sdk/examples/editly-video-test.ts +52 -0
- package/src/ai-sdk/examples/fal-lipsync.ts +43 -0
- package/src/ai-sdk/examples/higgsfield-image.ts +61 -0
- package/src/ai-sdk/examples/music-generation.ts +19 -0
- package/src/ai-sdk/examples/openai-sora.ts +34 -0
- package/src/ai-sdk/examples/replicate-bg-removal.ts +52 -0
- package/src/ai-sdk/examples/simpsons-scene.ts +61 -0
- package/src/ai-sdk/examples/talking-lion.ts +55 -0
- package/src/ai-sdk/examples/video-generation.ts +39 -0
- package/src/ai-sdk/examples/workflow-animated-girl.ts +104 -0
- package/src/ai-sdk/examples/workflow-before-after.ts +114 -0
- package/src/ai-sdk/examples/workflow-character-grid.ts +112 -0
- package/src/ai-sdk/examples/workflow-slideshow.ts +161 -0
- package/src/ai-sdk/file-cache.ts +112 -0
- package/src/ai-sdk/file.ts +238 -0
- package/src/ai-sdk/generate-element.ts +92 -0
- package/src/ai-sdk/generate-music.ts +46 -0
- package/src/ai-sdk/generate-video.ts +165 -0
- package/src/ai-sdk/index.ts +72 -0
- package/src/ai-sdk/music-model.ts +110 -0
- package/src/ai-sdk/providers/editly/editly.test.ts +1108 -0
- package/src/ai-sdk/providers/editly/ffmpeg.ts +60 -0
- package/src/ai-sdk/providers/editly/index.ts +817 -0
- package/src/ai-sdk/providers/editly/layers.ts +772 -0
- package/src/ai-sdk/providers/editly/plan.md +144 -0
- package/src/ai-sdk/providers/editly/types.ts +328 -0
- package/src/ai-sdk/providers/elevenlabs-provider.ts +255 -0
- package/src/ai-sdk/providers/fal-provider.ts +512 -0
- package/src/ai-sdk/providers/higgsfield.ts +379 -0
- package/src/ai-sdk/providers/openai.ts +251 -0
- package/src/ai-sdk/providers/replicate.ts +16 -0
- package/src/ai-sdk/video-model.ts +185 -0
- package/src/cli/commands/find.tsx +137 -0
- package/src/cli/commands/help.tsx +85 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/list.tsx +238 -0
- package/src/cli/commands/run.tsx +511 -0
- package/src/cli/commands/which.tsx +253 -0
- package/src/cli/index.ts +112 -0
- package/src/cli/quiet.ts +44 -0
- package/src/cli/types.ts +32 -0
- package/src/cli/ui/components/Badge.tsx +29 -0
- package/src/cli/ui/components/DataTable.tsx +51 -0
- package/src/cli/ui/components/Header.tsx +23 -0
- package/src/cli/ui/components/HelpBlock.tsx +44 -0
- package/src/cli/ui/components/KeyValue.tsx +33 -0
- package/src/cli/ui/components/OptionRow.tsx +81 -0
- package/src/cli/ui/components/Separator.tsx +23 -0
- package/src/cli/ui/components/StatusBox.tsx +108 -0
- package/src/cli/ui/components/VargBox.tsx +51 -0
- package/src/cli/ui/components/VargProgress.tsx +36 -0
- package/src/cli/ui/components/VargSpinner.tsx +34 -0
- package/src/cli/ui/components/VargText.tsx +56 -0
- package/src/cli/ui/components/index.ts +19 -0
- package/src/cli/ui/index.ts +12 -0
- package/src/cli/ui/render.ts +35 -0
- package/src/cli/ui/theme.ts +63 -0
- package/src/cli/utils.ts +78 -0
- package/src/core/executor/executor.ts +201 -0
- package/src/core/executor/index.ts +13 -0
- package/src/core/executor/job.ts +214 -0
- package/src/core/executor/pipeline.ts +222 -0
- package/src/core/index.ts +11 -0
- package/src/core/registry/index.ts +9 -0
- package/src/core/registry/loader.ts +149 -0
- package/src/core/registry/registry.ts +221 -0
- package/src/core/registry/resolver.ts +206 -0
- package/src/core/schema/helpers.ts +134 -0
- package/src/core/schema/index.ts +8 -0
- package/src/core/schema/shared.ts +102 -0
- package/src/core/schema/types.ts +279 -0
- package/src/core/schema/validator.ts +92 -0
- package/src/definitions/actions/captions.ts +261 -0
- package/src/definitions/actions/edit.ts +298 -0
- package/src/definitions/actions/image.ts +125 -0
- package/src/definitions/actions/index.ts +114 -0
- package/src/definitions/actions/music.ts +205 -0
- package/src/definitions/actions/sync.ts +128 -0
- package/src/definitions/actions/transcribe.ts +200 -0
- package/src/definitions/actions/upload.ts +111 -0
- package/src/definitions/actions/video.ts +163 -0
- package/src/definitions/actions/voice.ts +119 -0
- package/src/definitions/index.ts +23 -0
- package/src/definitions/models/elevenlabs.ts +50 -0
- package/src/definitions/models/flux.ts +56 -0
- package/src/definitions/models/index.ts +36 -0
- package/src/definitions/models/kling.ts +56 -0
- package/src/definitions/models/llama.ts +54 -0
- package/src/definitions/models/nano-banana-pro.ts +102 -0
- package/src/definitions/models/sonauto.ts +68 -0
- package/src/definitions/models/soul.ts +65 -0
- package/src/definitions/models/wan.ts +54 -0
- package/src/definitions/models/whisper.ts +44 -0
- package/src/definitions/skills/index.ts +12 -0
- package/src/definitions/skills/talking-character.ts +87 -0
- package/src/definitions/skills/text-to-tiktok.ts +97 -0
- package/src/index.ts +118 -0
- package/src/providers/apify.ts +269 -0
- package/src/providers/base.ts +264 -0
- package/src/providers/elevenlabs.ts +217 -0
- package/src/providers/fal.ts +392 -0
- package/src/providers/ffmpeg.ts +544 -0
- package/src/providers/fireworks.ts +193 -0
- package/src/providers/groq.ts +149 -0
- package/src/providers/higgsfield.ts +145 -0
- package/src/providers/index.ts +143 -0
- package/src/providers/replicate.ts +147 -0
- package/src/providers/storage.ts +206 -0
- package/src/tests/all.test.ts +509 -0
- package/src/tests/index.ts +33 -0
- package/src/tests/unit.test.ts +403 -0
- package/tsconfig.json +45 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FillColorLayer,
|
|
3
|
+
ImageLayer,
|
|
4
|
+
ImageOverlayLayer,
|
|
5
|
+
Layer,
|
|
6
|
+
LinearGradientLayer,
|
|
7
|
+
NewsTitleLayer,
|
|
8
|
+
Position,
|
|
9
|
+
RadialGradientLayer,
|
|
10
|
+
RainbowColorsLayer,
|
|
11
|
+
SlideInTextLayer,
|
|
12
|
+
SubtitleLayer,
|
|
13
|
+
TitleBackgroundLayer,
|
|
14
|
+
TitleLayer,
|
|
15
|
+
VideoLayer,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
function escapeDrawText(text: string): string {
|
|
19
|
+
return text
|
|
20
|
+
.replace(/\\/g, "\\\\")
|
|
21
|
+
.replace(/'/g, "'\\''")
|
|
22
|
+
.replace(/:/g, "\\:")
|
|
23
|
+
.replace(/\[/g, "\\[")
|
|
24
|
+
.replace(/\]/g, "\\]");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseSize(val: number | string | undefined, base: number): number {
|
|
28
|
+
if (val === undefined) return base;
|
|
29
|
+
if (typeof val === "number") return Math.round(val);
|
|
30
|
+
if (val.endsWith("%")) {
|
|
31
|
+
return Math.round((parseFloat(val) / 100) * base);
|
|
32
|
+
}
|
|
33
|
+
if (val.endsWith("px")) {
|
|
34
|
+
return Math.round(parseFloat(val));
|
|
35
|
+
}
|
|
36
|
+
return Math.round(parseFloat(val));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FilterInput {
|
|
40
|
+
label: string;
|
|
41
|
+
path?: string;
|
|
42
|
+
duration?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface LayerFilter {
|
|
46
|
+
inputs: FilterInput[];
|
|
47
|
+
filterComplex: string;
|
|
48
|
+
outputLabel: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getVideoFilter(
|
|
52
|
+
layer: VideoLayer,
|
|
53
|
+
index: number,
|
|
54
|
+
width: number,
|
|
55
|
+
height: number,
|
|
56
|
+
clipDuration: number,
|
|
57
|
+
isOverlay = false,
|
|
58
|
+
): LayerFilter {
|
|
59
|
+
const inputLabel = `${index}:v`;
|
|
60
|
+
const outputLabel = `vout${index}`;
|
|
61
|
+
const filters: string[] = [];
|
|
62
|
+
|
|
63
|
+
const start = layer.cutFrom ?? 0;
|
|
64
|
+
const end = layer.cutTo ?? start + clipDuration;
|
|
65
|
+
filters.push(`trim=start=${start}:end=${end}`);
|
|
66
|
+
filters.push("setpts=PTS-STARTPTS");
|
|
67
|
+
|
|
68
|
+
const layerWidth = parseSize(layer.width, width);
|
|
69
|
+
const layerHeight = parseSize(layer.height, height);
|
|
70
|
+
|
|
71
|
+
if (isOverlay) {
|
|
72
|
+
filters.push(
|
|
73
|
+
`scale=${layerWidth}:${layerHeight}:force_original_aspect_ratio=decrease`,
|
|
74
|
+
);
|
|
75
|
+
filters.push("setsar=1");
|
|
76
|
+
filters.push("fps=30");
|
|
77
|
+
filters.push("settb=1/30");
|
|
78
|
+
return {
|
|
79
|
+
inputs: [
|
|
80
|
+
{
|
|
81
|
+
label: inputLabel,
|
|
82
|
+
path: layer.path,
|
|
83
|
+
duration: layer.cutTo
|
|
84
|
+
? layer.cutTo - (layer.cutFrom ?? 0)
|
|
85
|
+
: undefined,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
filterComplex: `[${inputLabel}]${filters.join(",")}[${outputLabel}]`,
|
|
89
|
+
outputLabel,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (layer.resizeMode === "contain-blur") {
|
|
94
|
+
const baseFilters = filters.join(",");
|
|
95
|
+
const blurLabel = `vblur${index}`;
|
|
96
|
+
const fgLabel = `vfg${index}`;
|
|
97
|
+
const filterComplex = [
|
|
98
|
+
`[${inputLabel}]${baseFilters},split[${blurLabel}][${fgLabel}]`,
|
|
99
|
+
`[${blurLabel}]scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height},boxblur=20:5,setsar=1[${blurLabel}bg]`,
|
|
100
|
+
`[${fgLabel}]scale=${width}:${height}:force_original_aspect_ratio=decrease,setsar=1[${fgLabel}fg]`,
|
|
101
|
+
`[${blurLabel}bg][${fgLabel}fg]overlay=(W-w)/2:(H-h)/2,fps=30,settb=1/30[${outputLabel}]`,
|
|
102
|
+
].join(";");
|
|
103
|
+
return {
|
|
104
|
+
inputs: [
|
|
105
|
+
{
|
|
106
|
+
label: inputLabel,
|
|
107
|
+
path: layer.path,
|
|
108
|
+
duration: layer.cutTo
|
|
109
|
+
? layer.cutTo - (layer.cutFrom ?? 0)
|
|
110
|
+
: undefined,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
filterComplex,
|
|
114
|
+
outputLabel,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let scaleFilter = `scale=${width}:${height}:force_original_aspect_ratio=decrease`;
|
|
119
|
+
if (layer.resizeMode === "cover") {
|
|
120
|
+
scaleFilter = `scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}`;
|
|
121
|
+
} else if (layer.resizeMode === "stretch") {
|
|
122
|
+
scaleFilter = `scale=${width}:${height}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
filters.push(scaleFilter);
|
|
126
|
+
filters.push(`pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`);
|
|
127
|
+
filters.push("setsar=1");
|
|
128
|
+
filters.push("fps=30");
|
|
129
|
+
filters.push("settb=1/30");
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
inputs: [
|
|
133
|
+
{
|
|
134
|
+
label: inputLabel,
|
|
135
|
+
path: layer.path,
|
|
136
|
+
duration: layer.cutTo ? layer.cutTo - (layer.cutFrom ?? 0) : undefined,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
filterComplex: `[${inputLabel}]${filters.join(",")}[${outputLabel}]`,
|
|
140
|
+
outputLabel,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getVideoFilterWithTrim(
|
|
145
|
+
layer: VideoLayer,
|
|
146
|
+
inputIndex: number,
|
|
147
|
+
width: number,
|
|
148
|
+
height: number,
|
|
149
|
+
trimStart: number,
|
|
150
|
+
trimEnd: number,
|
|
151
|
+
outputLabel: string,
|
|
152
|
+
isOverlay = false,
|
|
153
|
+
): LayerFilter {
|
|
154
|
+
const inputLabel = `${inputIndex}:v`;
|
|
155
|
+
const filters: string[] = [];
|
|
156
|
+
|
|
157
|
+
filters.push(`trim=start=${trimStart}:end=${trimEnd}`);
|
|
158
|
+
filters.push("setpts=PTS-STARTPTS");
|
|
159
|
+
|
|
160
|
+
const layerWidth = parseSize(layer.width, width);
|
|
161
|
+
const layerHeight = parseSize(layer.height, height);
|
|
162
|
+
|
|
163
|
+
if (isOverlay) {
|
|
164
|
+
filters.push(
|
|
165
|
+
`scale=${layerWidth}:${layerHeight}:force_original_aspect_ratio=decrease`,
|
|
166
|
+
);
|
|
167
|
+
filters.push("setsar=1");
|
|
168
|
+
filters.push("fps=30");
|
|
169
|
+
filters.push("settb=1/30");
|
|
170
|
+
} else {
|
|
171
|
+
let scaleFilter = `scale=${width}:${height}:force_original_aspect_ratio=decrease`;
|
|
172
|
+
if (layer.resizeMode === "cover") {
|
|
173
|
+
scaleFilter = `scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}`;
|
|
174
|
+
} else if (layer.resizeMode === "stretch") {
|
|
175
|
+
scaleFilter = `scale=${width}:${height}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
filters.push(scaleFilter);
|
|
179
|
+
filters.push(`pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`);
|
|
180
|
+
filters.push("setsar=1");
|
|
181
|
+
filters.push("fps=30");
|
|
182
|
+
filters.push("settb=1/30");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
inputs: [],
|
|
187
|
+
filterComplex: `[${inputLabel}]${filters.join(",")}[${outputLabel}]`,
|
|
188
|
+
outputLabel,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getOverlayFilter(
|
|
193
|
+
baseLabel: string,
|
|
194
|
+
overlayLabel: string,
|
|
195
|
+
layer: VideoLayer,
|
|
196
|
+
width: number,
|
|
197
|
+
height: number,
|
|
198
|
+
outputLabel: string,
|
|
199
|
+
): string {
|
|
200
|
+
const baseX = layer.left !== undefined ? parseSize(layer.left, width) : 0;
|
|
201
|
+
const baseY = layer.top !== undefined ? parseSize(layer.top, height) : 0;
|
|
202
|
+
|
|
203
|
+
let xExpr = String(baseX);
|
|
204
|
+
let yExpr = String(baseY);
|
|
205
|
+
|
|
206
|
+
if (layer.originX === "center") {
|
|
207
|
+
xExpr = `${baseX}-overlay_w/2`;
|
|
208
|
+
} else if (layer.originX === "right") {
|
|
209
|
+
xExpr = `${baseX}-overlay_w`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (layer.originY === "center") {
|
|
213
|
+
yExpr = `${baseY}-overlay_h/2`;
|
|
214
|
+
} else if (layer.originY === "bottom") {
|
|
215
|
+
yExpr = `${baseY}-overlay_h`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return `[${baseLabel}][${overlayLabel}]overlay=${xExpr}:${yExpr}:shortest=1[${outputLabel}]`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function getImageFilter(
|
|
222
|
+
layer: ImageLayer,
|
|
223
|
+
index: number,
|
|
224
|
+
width: number,
|
|
225
|
+
height: number,
|
|
226
|
+
duration: number,
|
|
227
|
+
): LayerFilter {
|
|
228
|
+
const inputLabel = `${index}:v`;
|
|
229
|
+
const outputLabel = `imgout${index}`;
|
|
230
|
+
const filters: string[] = [];
|
|
231
|
+
|
|
232
|
+
const zoomDir =
|
|
233
|
+
layer.zoomDirection === null ? null : (layer.zoomDirection ?? "in");
|
|
234
|
+
const zoomAmt = layer.zoomAmount ?? 0.1;
|
|
235
|
+
const totalFrames = Math.ceil(duration * 30);
|
|
236
|
+
|
|
237
|
+
if (zoomDir !== null) {
|
|
238
|
+
let zoomExpr: string;
|
|
239
|
+
let xExpr: string;
|
|
240
|
+
let yExpr: string;
|
|
241
|
+
|
|
242
|
+
if (zoomDir === "left" || zoomDir === "right") {
|
|
243
|
+
// Pan horizontally while zoomed in slightly - creates cinematic pan effect
|
|
244
|
+
// Zoom is constant, x position animates from one side to other
|
|
245
|
+
const zoom = 1 + zoomAmt;
|
|
246
|
+
zoomExpr = String(zoom);
|
|
247
|
+
yExpr = "trunc((ih-ih/zoom)/2)";
|
|
248
|
+
if (zoomDir === "left") {
|
|
249
|
+
// Start right, pan left (x decreases)
|
|
250
|
+
xExpr = `trunc((iw-iw/zoom)*(1-on/${totalFrames}))`;
|
|
251
|
+
} else {
|
|
252
|
+
// Start left, pan right (x increases)
|
|
253
|
+
xExpr = `trunc((iw-iw/zoom)*on/${totalFrames})`;
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
// in/out: zoom animation, centered
|
|
257
|
+
const startZoom = zoomDir === "in" ? 1 : 1 + zoomAmt;
|
|
258
|
+
const endZoom = zoomDir === "in" ? 1 + zoomAmt : 1;
|
|
259
|
+
zoomExpr = `${startZoom}+(${endZoom}-${startZoom})*on/${totalFrames}`;
|
|
260
|
+
xExpr = "trunc((iw-iw/zoom)/2)";
|
|
261
|
+
yExpr = "trunc((ih-ih/zoom)/2)";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// DO NOT REMOVE: zoompan needs high resolution to avoid shaking at zoom edges.
|
|
265
|
+
// cover mode: use output aspect ratio (4x) - faster, ~40s for 3 clips at 1080x1920
|
|
266
|
+
// contain mode: use square (maxDim * 4) - slower but handles any input aspect ratio
|
|
267
|
+
// we tried animated crop instead of zoompan but ffmpeg's crop filter doesn't
|
|
268
|
+
// support frame-based expressions properly with looped static images.
|
|
269
|
+
const zoomWidth = width * 4;
|
|
270
|
+
const zoomHeight = height * 4;
|
|
271
|
+
const maxDim = Math.max(width, height);
|
|
272
|
+
const zoomSize = maxDim * 4;
|
|
273
|
+
|
|
274
|
+
if (layer.resizeMode === "cover") {
|
|
275
|
+
filters.push(
|
|
276
|
+
`scale=${zoomWidth}:${zoomHeight}:force_original_aspect_ratio=increase`,
|
|
277
|
+
);
|
|
278
|
+
filters.push(`crop=${zoomWidth}:${zoomHeight}`);
|
|
279
|
+
filters.push(
|
|
280
|
+
`zoompan=z='${zoomExpr}':x='${xExpr}':y='${yExpr}':d=${totalFrames}:s=${zoomWidth}x${zoomHeight}:fps=30`,
|
|
281
|
+
);
|
|
282
|
+
filters.push(
|
|
283
|
+
`scale=${width}:${height}:force_original_aspect_ratio=increase`,
|
|
284
|
+
);
|
|
285
|
+
filters.push(`crop=${width}:${height}`);
|
|
286
|
+
} else if (layer.resizeMode === "stretch") {
|
|
287
|
+
filters.push(`scale=${zoomWidth}:${zoomHeight}`);
|
|
288
|
+
filters.push(
|
|
289
|
+
`zoompan=z='${zoomExpr}':x='${xExpr}':y='${yExpr}':d=${totalFrames}:s=${width}x${height}:fps=30`,
|
|
290
|
+
);
|
|
291
|
+
} else if (layer.resizeMode === "contain") {
|
|
292
|
+
filters.push(
|
|
293
|
+
`scale=${zoomSize}:${zoomSize}:force_original_aspect_ratio=increase`,
|
|
294
|
+
);
|
|
295
|
+
filters.push(
|
|
296
|
+
`zoompan=z='${zoomExpr}':x='${xExpr}':y='${yExpr}':d=${totalFrames}:s=${zoomSize}x${zoomSize}:fps=30`,
|
|
297
|
+
);
|
|
298
|
+
filters.push(
|
|
299
|
+
`scale=${width}:${height}:force_original_aspect_ratio=decrease`,
|
|
300
|
+
);
|
|
301
|
+
filters.push(`pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:black`);
|
|
302
|
+
} else {
|
|
303
|
+
// Default: fast path - zoompan at target resolution directly
|
|
304
|
+
filters.push(
|
|
305
|
+
`scale=${zoomWidth}:${zoomHeight}:force_original_aspect_ratio=increase`,
|
|
306
|
+
);
|
|
307
|
+
filters.push(
|
|
308
|
+
`zoompan=z='${zoomExpr}':x='${xExpr}':y='${yExpr}':d=${totalFrames}:s=${width}x${height}:fps=30`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
filters.push(`loop=loop=-1:size=1:start=0`);
|
|
313
|
+
filters.push(`fps=30`);
|
|
314
|
+
filters.push(`trim=duration=${duration}`);
|
|
315
|
+
|
|
316
|
+
if (layer.resizeMode === "contain-blur") {
|
|
317
|
+
const blurLabel = `imgblur${index}`;
|
|
318
|
+
const fgLabel = `imgfg${index}`;
|
|
319
|
+
const baseFilters = filters.join(",");
|
|
320
|
+
const filterComplex = [
|
|
321
|
+
`[${inputLabel}]${baseFilters},split[${blurLabel}][${fgLabel}]`,
|
|
322
|
+
`[${blurLabel}]scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height},boxblur=20:5,setsar=1[${blurLabel}bg]`,
|
|
323
|
+
`[${fgLabel}]scale=${width}:${height}:force_original_aspect_ratio=decrease,setsar=1[${fgLabel}fg]`,
|
|
324
|
+
`[${blurLabel}bg][${fgLabel}fg]overlay=(W-w)/2:(H-h)/2,settb=1/30[${outputLabel}]`,
|
|
325
|
+
].join(";");
|
|
326
|
+
return {
|
|
327
|
+
inputs: [{ label: inputLabel, path: layer.path }],
|
|
328
|
+
filterComplex,
|
|
329
|
+
outputLabel,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let scaleFilter = `scale=${width}:${height}:force_original_aspect_ratio=decrease`;
|
|
334
|
+
if (layer.resizeMode === "cover") {
|
|
335
|
+
scaleFilter = `scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}`;
|
|
336
|
+
} else if (layer.resizeMode === "stretch") {
|
|
337
|
+
scaleFilter = `scale=${width}:${height}`;
|
|
338
|
+
}
|
|
339
|
+
filters.push(scaleFilter);
|
|
340
|
+
filters.push(`pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:black`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
filters.push("setsar=1");
|
|
344
|
+
filters.push("settb=1/30");
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
inputs: [{ label: inputLabel, path: layer.path }],
|
|
348
|
+
filterComplex: `[${inputLabel}]${filters.join(",")}[${outputLabel}]`,
|
|
349
|
+
outputLabel,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function getFillColorFilter(
|
|
354
|
+
layer: FillColorLayer,
|
|
355
|
+
index: number,
|
|
356
|
+
width: number,
|
|
357
|
+
height: number,
|
|
358
|
+
duration: number,
|
|
359
|
+
): LayerFilter {
|
|
360
|
+
const color = layer.color ?? "#000000";
|
|
361
|
+
const outputLabel = `color${index}`;
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
inputs: [],
|
|
365
|
+
filterComplex: `color=c=${color}:s=${width}x${height}:d=${duration}:r=30[${outputLabel}]`,
|
|
366
|
+
outputLabel,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function hexToRgb(hex: string): string {
|
|
371
|
+
const h = hex.replace("#", "");
|
|
372
|
+
const r = parseInt(h.substring(0, 2), 16);
|
|
373
|
+
const g = parseInt(h.substring(2, 4), 16);
|
|
374
|
+
const b = parseInt(h.substring(4, 6), 16);
|
|
375
|
+
return `0x${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function getGradientFilter(
|
|
379
|
+
layer: RadialGradientLayer | LinearGradientLayer,
|
|
380
|
+
index: number,
|
|
381
|
+
width: number,
|
|
382
|
+
height: number,
|
|
383
|
+
duration: number,
|
|
384
|
+
): LayerFilter {
|
|
385
|
+
const colors = layer.colors ?? ["#ff6b6b", "#4ecdc4"];
|
|
386
|
+
const outputLabel = `grad${index}`;
|
|
387
|
+
|
|
388
|
+
const c0 = hexToRgb(colors[0]);
|
|
389
|
+
const c1 = hexToRgb(colors[1]);
|
|
390
|
+
|
|
391
|
+
if (layer.type === "radial-gradient") {
|
|
392
|
+
return {
|
|
393
|
+
inputs: [],
|
|
394
|
+
filterComplex: `gradients=s=${width}x${height}:c0=${c0}:c1=${c1}:type=radial:d=${duration}:r=30[${outputLabel}]`,
|
|
395
|
+
outputLabel,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
inputs: [],
|
|
401
|
+
filterComplex: `gradients=s=${width}x${height}:c0=${c0}:c1=${c1}:d=${duration}:r=30[${outputLabel}]`,
|
|
402
|
+
outputLabel,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// IMAGE-OVERLAY IMPLEMENTATION NOTES:
|
|
407
|
+
// Unlike full-screen image layer, image-overlay:
|
|
408
|
+
// 1. Has position/width/height like video overlay
|
|
409
|
+
// 2. Supports Ken Burns (zoom/pan) effects
|
|
410
|
+
// 3. Gets composited on top of base layers (not as base layer)
|
|
411
|
+
// 4. Uses overlay filter for positioning instead of pad filter
|
|
412
|
+
|
|
413
|
+
function resolvePositionForOverlay(
|
|
414
|
+
position: Position | undefined,
|
|
415
|
+
width: number,
|
|
416
|
+
height: number,
|
|
417
|
+
): { x: string; y: string } {
|
|
418
|
+
if (!position) {
|
|
419
|
+
return { x: "(W-w)/2", y: "(H-h)/2" };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (typeof position === "object") {
|
|
423
|
+
const baseX = parseSize(position.x, width);
|
|
424
|
+
const baseY = parseSize(position.y, height);
|
|
425
|
+
|
|
426
|
+
let xExpr = String(baseX);
|
|
427
|
+
let yExpr = String(baseY);
|
|
428
|
+
|
|
429
|
+
if (position.originX === "center") {
|
|
430
|
+
xExpr = `${baseX}-overlay_w/2`;
|
|
431
|
+
} else if (position.originX === "right") {
|
|
432
|
+
xExpr = `${baseX}-overlay_w`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (position.originY === "center") {
|
|
436
|
+
yExpr = `${baseY}-overlay_h/2`;
|
|
437
|
+
} else if (position.originY === "bottom") {
|
|
438
|
+
yExpr = `${baseY}-overlay_h`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return { x: xExpr, y: yExpr };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const posMap: Record<string, { x: string; y: string }> = {
|
|
445
|
+
"top-left": { x: "W*0.1", y: "H*0.1" },
|
|
446
|
+
top: { x: "(W-w)/2", y: "H*0.1" },
|
|
447
|
+
"top-right": { x: "W*0.9-w", y: "H*0.1" },
|
|
448
|
+
"center-left": { x: "W*0.1", y: "(H-h)/2" },
|
|
449
|
+
center: { x: "(W-w)/2", y: "(H-h)/2" },
|
|
450
|
+
"center-right": { x: "W*0.9-w", y: "(H-h)/2" },
|
|
451
|
+
"bottom-left": { x: "W*0.1", y: "H*0.9-h" },
|
|
452
|
+
bottom: { x: "(W-w)/2", y: "H*0.9-h" },
|
|
453
|
+
"bottom-right": { x: "W*0.9-w", y: "H*0.9-h" },
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
return posMap[position] ?? { x: "(W-w)/2", y: "(H-h)/2" };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function getImageOverlayFilter(
|
|
460
|
+
layer: ImageOverlayLayer,
|
|
461
|
+
index: number,
|
|
462
|
+
width: number,
|
|
463
|
+
height: number,
|
|
464
|
+
duration: number,
|
|
465
|
+
): LayerFilter {
|
|
466
|
+
const inputLabel = `${index}:v`;
|
|
467
|
+
const outputLabel = `imgovout${index}`;
|
|
468
|
+
const filters: string[] = [];
|
|
469
|
+
|
|
470
|
+
const targetWidth = layer.width
|
|
471
|
+
? parseSize(layer.width, width)
|
|
472
|
+
: Math.round(width * 0.3);
|
|
473
|
+
const scaleExpr = layer.height
|
|
474
|
+
? `scale=${targetWidth}:${parseSize(layer.height, height)}`
|
|
475
|
+
: `scale=${targetWidth}:-2`;
|
|
476
|
+
|
|
477
|
+
const zoomDir = layer.zoomDirection ?? null;
|
|
478
|
+
const zoomAmt = layer.zoomAmount ?? 0.1;
|
|
479
|
+
const totalFrames = Math.ceil(duration * 30);
|
|
480
|
+
|
|
481
|
+
if (zoomDir) {
|
|
482
|
+
let zoomExpr: string;
|
|
483
|
+
let xExpr: string;
|
|
484
|
+
let yExpr: string;
|
|
485
|
+
|
|
486
|
+
if (zoomDir === "left" || zoomDir === "right") {
|
|
487
|
+
const zoom = 1 + zoomAmt;
|
|
488
|
+
zoomExpr = String(zoom);
|
|
489
|
+
yExpr = "trunc((ih-ih/zoom)/2)";
|
|
490
|
+
if (zoomDir === "left") {
|
|
491
|
+
xExpr = `trunc((iw-iw/zoom)*(1-on/${totalFrames}))`;
|
|
492
|
+
} else {
|
|
493
|
+
xExpr = `trunc((iw-iw/zoom)*on/${totalFrames})`;
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
const startZoom = zoomDir === "in" ? 1 : 1 + zoomAmt;
|
|
497
|
+
const endZoom = zoomDir === "in" ? 1 + zoomAmt : 1;
|
|
498
|
+
zoomExpr = `${startZoom}+(${endZoom}-${startZoom})*on/${totalFrames}`;
|
|
499
|
+
xExpr = "trunc((iw-iw/zoom)/2)";
|
|
500
|
+
yExpr = "trunc((ih-ih/zoom)/2)";
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Upscale, zoompan at high res, then scale to target preserving aspect ratio
|
|
504
|
+
filters.push("scale=4000:-2");
|
|
505
|
+
filters.push(
|
|
506
|
+
`zoompan=z='${zoomExpr}':x='${xExpr}':y='${yExpr}':d=${totalFrames}:s=4000x4000:fps=30`,
|
|
507
|
+
);
|
|
508
|
+
filters.push(scaleExpr);
|
|
509
|
+
} else {
|
|
510
|
+
filters.push(scaleExpr);
|
|
511
|
+
filters.push("loop=loop=-1:size=1:start=0");
|
|
512
|
+
filters.push("fps=30");
|
|
513
|
+
filters.push(`trim=duration=${duration}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
filters.push("setsar=1");
|
|
517
|
+
filters.push("settb=1/30");
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
inputs: [{ label: inputLabel, path: layer.path }],
|
|
521
|
+
filterComplex: `[${inputLabel}]${filters.join(",")}[${outputLabel}]`,
|
|
522
|
+
outputLabel,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function getImageOverlayPositionFilter(
|
|
527
|
+
baseLabel: string,
|
|
528
|
+
overlayLabel: string,
|
|
529
|
+
layer: ImageOverlayLayer,
|
|
530
|
+
width: number,
|
|
531
|
+
height: number,
|
|
532
|
+
outputLabel: string,
|
|
533
|
+
): string {
|
|
534
|
+
const { x, y } = resolvePositionForOverlay(layer.position, width, height);
|
|
535
|
+
return `[${baseLabel}][${overlayLabel}]overlay=${x}:${y}:shortest=1[${outputLabel}]`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function getEnableExpr(
|
|
539
|
+
start: number | undefined,
|
|
540
|
+
stop: number | undefined,
|
|
541
|
+
clipDuration: number,
|
|
542
|
+
): string {
|
|
543
|
+
if (start === undefined && stop === undefined) return "";
|
|
544
|
+
const s = start ?? 0;
|
|
545
|
+
const e = stop ?? clipDuration;
|
|
546
|
+
return `:enable='between(t,${s},${e})'`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function getTitleFilter(
|
|
550
|
+
layer: TitleLayer,
|
|
551
|
+
baseLabel: string,
|
|
552
|
+
width: number,
|
|
553
|
+
height: number,
|
|
554
|
+
clipDuration?: number,
|
|
555
|
+
): string {
|
|
556
|
+
const text = escapeDrawText(layer.text);
|
|
557
|
+
const color = layer.textColor ?? "white";
|
|
558
|
+
const fontSize = Math.round(Math.min(width, height) * 0.08);
|
|
559
|
+
|
|
560
|
+
let x = "(w-text_w)/2";
|
|
561
|
+
let y = "(h-text_h)/2";
|
|
562
|
+
|
|
563
|
+
const pos = layer.position ?? "center";
|
|
564
|
+
if (typeof pos === "string") {
|
|
565
|
+
if (pos.includes("left")) x = "w*0.1";
|
|
566
|
+
if (pos.includes("right")) x = "w*0.9-text_w";
|
|
567
|
+
if (pos.includes("top")) y = "h*0.1";
|
|
568
|
+
if (pos.includes("bottom")) y = "h*0.9-text_h";
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const fontFile = layer.fontPath
|
|
572
|
+
? `:fontfile='${escapeDrawText(layer.fontPath)}'`
|
|
573
|
+
: "";
|
|
574
|
+
const fontFamily = layer.fontFamily ? `:font='${layer.fontFamily}'` : "";
|
|
575
|
+
const enable = getEnableExpr(layer.start, layer.stop, clipDuration ?? 9999);
|
|
576
|
+
|
|
577
|
+
return `[${baseLabel}]drawtext=text='${text}':fontsize=${fontSize}:fontcolor=${color}:x=${x}:y=${y}${fontFile}${fontFamily}${enable}`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function getSubtitleFilter(
|
|
581
|
+
layer: SubtitleLayer,
|
|
582
|
+
baseLabel: string,
|
|
583
|
+
width: number,
|
|
584
|
+
height: number,
|
|
585
|
+
clipDuration?: number,
|
|
586
|
+
): string {
|
|
587
|
+
const text = escapeDrawText(layer.text);
|
|
588
|
+
const textColor = layer.textColor ?? "white";
|
|
589
|
+
const bgColor = layer.backgroundColor ?? "black@0.7";
|
|
590
|
+
const fontSize = Math.round(Math.min(width, height) * 0.05);
|
|
591
|
+
const boxPadding = Math.round(fontSize * 0.4);
|
|
592
|
+
|
|
593
|
+
const fontFile = layer.fontPath
|
|
594
|
+
? `:fontfile='${escapeDrawText(layer.fontPath)}'`
|
|
595
|
+
: "";
|
|
596
|
+
const fontFamily = layer.fontFamily ? `:font='${layer.fontFamily}'` : "";
|
|
597
|
+
const enable = getEnableExpr(layer.start, layer.stop, clipDuration ?? 9999);
|
|
598
|
+
|
|
599
|
+
return `[${baseLabel}]drawtext=text='${text}':fontsize=${fontSize}:fontcolor=${textColor}:x=(w-text_w)/2:y=h*0.85-text_h/2:box=1:boxcolor=${bgColor}:boxborderw=${boxPadding}${fontFile}${fontFamily}${enable}`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function getTitleBackgroundFilter(
|
|
603
|
+
layer: TitleBackgroundLayer,
|
|
604
|
+
index: number,
|
|
605
|
+
width: number,
|
|
606
|
+
height: number,
|
|
607
|
+
duration: number,
|
|
608
|
+
): LayerFilter {
|
|
609
|
+
const bg = layer.background ?? {
|
|
610
|
+
type: "fill-color" as const,
|
|
611
|
+
color: "#000000",
|
|
612
|
+
};
|
|
613
|
+
let bgFilter: LayerFilter;
|
|
614
|
+
|
|
615
|
+
if (bg.type === "radial-gradient" || bg.type === "linear-gradient") {
|
|
616
|
+
bgFilter = getGradientFilter(bg, index, width, height, duration);
|
|
617
|
+
} else {
|
|
618
|
+
bgFilter = getFillColorFilter(
|
|
619
|
+
bg as FillColorLayer,
|
|
620
|
+
index,
|
|
621
|
+
width,
|
|
622
|
+
height,
|
|
623
|
+
duration,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const text = escapeDrawText(layer.text);
|
|
628
|
+
const textColor = layer.textColor ?? "white";
|
|
629
|
+
const fontSize = Math.round(Math.min(width, height) * 0.1);
|
|
630
|
+
|
|
631
|
+
const fontFile = layer.fontPath
|
|
632
|
+
? `:fontfile='${escapeDrawText(layer.fontPath)}'`
|
|
633
|
+
: "";
|
|
634
|
+
const fontFamily = layer.fontFamily ? `:font='${layer.fontFamily}'` : "";
|
|
635
|
+
|
|
636
|
+
const outputLabel = `titlebg${index}`;
|
|
637
|
+
const drawText = `drawtext=text='${text}':fontsize=${fontSize}:fontcolor=${textColor}:x=(w-text_w)/2:y=(h-text_h)/2${fontFile}${fontFamily}`;
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
inputs: bgFilter.inputs,
|
|
641
|
+
filterComplex: `${bgFilter.filterComplex};[${bgFilter.outputLabel}]${drawText}[${outputLabel}]`,
|
|
642
|
+
outputLabel,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function getRainbowColorsFilter(
|
|
647
|
+
_layer: RainbowColorsLayer,
|
|
648
|
+
index: number,
|
|
649
|
+
width: number,
|
|
650
|
+
height: number,
|
|
651
|
+
duration: number,
|
|
652
|
+
): LayerFilter {
|
|
653
|
+
const outputLabel = `rainbow${index}`;
|
|
654
|
+
const fps = 30;
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
inputs: [],
|
|
658
|
+
filterComplex: `color=c=red:s=${width}x${height}:d=${duration}:r=${fps},hue=h=t*60[${outputLabel}]`,
|
|
659
|
+
outputLabel,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function getNewsTitleFilter(
|
|
664
|
+
layer: NewsTitleLayer,
|
|
665
|
+
baseLabel: string,
|
|
666
|
+
width: number,
|
|
667
|
+
height: number,
|
|
668
|
+
clipDuration?: number,
|
|
669
|
+
): string {
|
|
670
|
+
const text = escapeDrawText(layer.text);
|
|
671
|
+
const textColor = layer.textColor ?? "white";
|
|
672
|
+
const bgColor = layer.backgroundColor ?? "red";
|
|
673
|
+
const fontSize = Math.round(Math.min(width, height) * 0.05);
|
|
674
|
+
const barHeight = Math.round(fontSize * 2.5);
|
|
675
|
+
const padding = Math.round(fontSize * 0.5);
|
|
676
|
+
|
|
677
|
+
const fontFile = layer.fontPath
|
|
678
|
+
? `:fontfile='${escapeDrawText(layer.fontPath)}'`
|
|
679
|
+
: "";
|
|
680
|
+
const fontFamily = layer.fontFamily ? `:font='${layer.fontFamily}'` : "";
|
|
681
|
+
const enable = getEnableExpr(layer.start, layer.stop, clipDuration ?? 9999);
|
|
682
|
+
|
|
683
|
+
const pos = layer.position ?? "bottom";
|
|
684
|
+
const yBar = pos === "top" ? 0 : height - barHeight;
|
|
685
|
+
const yText = pos === "top" ? padding : height - barHeight + padding;
|
|
686
|
+
|
|
687
|
+
return `[${baseLabel}]drawbox=x=0:y=${yBar}:w=iw:h=${barHeight}:color=${bgColor}:t=fill${enable},drawtext=text='${text}':fontsize=${fontSize}:fontcolor=${textColor}:x=${padding}:y=${yText}${fontFile}${fontFamily}${enable}`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function getSlideInTextFilter(
|
|
691
|
+
layer: SlideInTextLayer,
|
|
692
|
+
baseLabel: string,
|
|
693
|
+
width: number,
|
|
694
|
+
height: number,
|
|
695
|
+
duration: number,
|
|
696
|
+
): string {
|
|
697
|
+
const text = escapeDrawText(layer.text);
|
|
698
|
+
const textColor = layer.color ?? layer.textColor ?? "white";
|
|
699
|
+
const fontSize = layer.fontSize ?? Math.round(Math.min(width, height) * 0.08);
|
|
700
|
+
|
|
701
|
+
const fontFile = layer.fontPath
|
|
702
|
+
? `:fontfile='${escapeDrawText(layer.fontPath)}'`
|
|
703
|
+
: "";
|
|
704
|
+
const fontFamily = layer.fontFamily ? `:font='${layer.fontFamily}'` : "";
|
|
705
|
+
const enable = getEnableExpr(layer.start, layer.stop, duration);
|
|
706
|
+
|
|
707
|
+
const pos = layer.position ?? "center";
|
|
708
|
+
let yExpr = "(h-text_h)/2";
|
|
709
|
+
if (typeof pos === "string") {
|
|
710
|
+
if (pos.includes("top")) yExpr = "h*0.2";
|
|
711
|
+
if (pos.includes("bottom")) yExpr = "h*0.8-text_h";
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const slideInFrames = Math.round(duration * 30 * 0.3);
|
|
715
|
+
const xExpr = `if(lt(t\\,${slideInFrames}/30)\\,-text_w+(w/2+text_w/2)*t/(${slideInFrames}/30)\\,(w-text_w)/2)`;
|
|
716
|
+
|
|
717
|
+
return `[${baseLabel}]drawtext=text='${text}':fontsize=${fontSize}:fontcolor=${textColor}:x='${xExpr}':y=${yExpr}${fontFile}${fontFamily}${enable}`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function processLayer(
|
|
721
|
+
layer: Layer,
|
|
722
|
+
index: number,
|
|
723
|
+
width: number,
|
|
724
|
+
height: number,
|
|
725
|
+
duration: number,
|
|
726
|
+
isOverlay = false,
|
|
727
|
+
): LayerFilter | null {
|
|
728
|
+
switch (layer.type) {
|
|
729
|
+
case "video":
|
|
730
|
+
return getVideoFilter(layer, index, width, height, duration, isOverlay);
|
|
731
|
+
case "image":
|
|
732
|
+
return getImageFilter(layer, index, width, height, duration);
|
|
733
|
+
case "fill-color":
|
|
734
|
+
case "pause":
|
|
735
|
+
return getFillColorFilter(
|
|
736
|
+
layer as FillColorLayer,
|
|
737
|
+
index,
|
|
738
|
+
width,
|
|
739
|
+
height,
|
|
740
|
+
duration,
|
|
741
|
+
);
|
|
742
|
+
case "radial-gradient":
|
|
743
|
+
case "linear-gradient":
|
|
744
|
+
return getGradientFilter(
|
|
745
|
+
layer as RadialGradientLayer | LinearGradientLayer,
|
|
746
|
+
index,
|
|
747
|
+
width,
|
|
748
|
+
height,
|
|
749
|
+
duration,
|
|
750
|
+
);
|
|
751
|
+
case "title-background":
|
|
752
|
+
return getTitleBackgroundFilter(
|
|
753
|
+
layer as TitleBackgroundLayer,
|
|
754
|
+
index,
|
|
755
|
+
width,
|
|
756
|
+
height,
|
|
757
|
+
duration,
|
|
758
|
+
);
|
|
759
|
+
case "rainbow-colors":
|
|
760
|
+
return getRainbowColorsFilter(
|
|
761
|
+
layer as RainbowColorsLayer,
|
|
762
|
+
index,
|
|
763
|
+
width,
|
|
764
|
+
height,
|
|
765
|
+
duration,
|
|
766
|
+
);
|
|
767
|
+
case "audio":
|
|
768
|
+
return null;
|
|
769
|
+
default:
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
}
|