vargai 0.4.0-alpha4 → 0.4.0-alpha40
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/.env.example +6 -0
- package/README.md +483 -61
- package/assets/fonts/TikTokSans-Bold.ttf +0 -0
- package/examples/grok-imagine-test.tsx +155 -0
- package/launch-videos/06-kawaii-fruits.tsx +93 -0
- package/launch-videos/07-ugc-weight-loss.tsx +132 -0
- package/launch-videos/08-talking-head-varg.tsx +107 -0
- package/launch-videos/09-girl.tsx +160 -0
- package/launch-videos/README.md +42 -0
- package/package.json +10 -4
- package/pipeline/cookbooks/round-video-character.md +1 -1
- package/skills/varg-video-generation/SKILL.md +224 -0
- package/skills/varg-video-generation/references/templates.md +380 -0
- package/skills/varg-video-generation/scripts/setup.ts +265 -0
- package/src/ai-sdk/cache.ts +1 -3
- package/src/ai-sdk/examples/google-image.ts +62 -0
- package/src/ai-sdk/index.ts +10 -0
- package/src/ai-sdk/middleware/wrap-image-model.ts +4 -21
- package/src/ai-sdk/middleware/wrap-music-model.ts +4 -16
- package/src/ai-sdk/middleware/wrap-video-model.ts +5 -17
- package/src/ai-sdk/providers/CONTRIBUTING.md +457 -0
- package/src/ai-sdk/providers/editly/backends/index.ts +8 -0
- package/src/ai-sdk/providers/editly/backends/local.ts +94 -0
- package/src/ai-sdk/providers/editly/backends/types.ts +74 -0
- package/src/ai-sdk/providers/editly/editly.test.ts +49 -1
- package/src/ai-sdk/providers/editly/index.ts +164 -80
- package/src/ai-sdk/providers/editly/layers.ts +58 -6
- package/src/ai-sdk/providers/editly/rendi/editly-with-rendi-backend.test.ts +335 -0
- package/src/ai-sdk/providers/editly/rendi/index.ts +289 -0
- package/src/ai-sdk/providers/editly/rendi/rendi.test.ts +35 -0
- package/src/ai-sdk/providers/editly/types.ts +30 -0
- package/src/ai-sdk/providers/elevenlabs.ts +10 -2
- package/src/ai-sdk/providers/fal.test.ts +214 -0
- package/src/ai-sdk/providers/fal.ts +435 -40
- package/src/ai-sdk/providers/google.ts +423 -0
- package/src/ai-sdk/providers/together.ts +191 -0
- package/src/cli/commands/find.tsx +1 -0
- package/src/cli/commands/frame.tsx +616 -0
- package/src/cli/commands/hello.ts +85 -0
- package/src/cli/commands/help.tsx +18 -30
- package/src/cli/commands/index.ts +11 -2
- package/src/cli/commands/init.tsx +570 -0
- package/src/cli/commands/list.tsx +1 -0
- package/src/cli/commands/render.tsx +322 -76
- package/src/cli/commands/run.tsx +1 -0
- package/src/cli/commands/storyboard.tsx +1714 -0
- package/src/cli/commands/which.tsx +1 -0
- package/src/cli/index.ts +23 -4
- package/src/cli/ui/components/Badge.tsx +1 -0
- package/src/cli/ui/components/DataTable.tsx +1 -0
- package/src/cli/ui/components/Header.tsx +1 -0
- package/src/cli/ui/components/HelpBlock.tsx +1 -0
- package/src/cli/ui/components/KeyValue.tsx +1 -0
- package/src/cli/ui/components/OptionRow.tsx +1 -0
- package/src/cli/ui/components/Separator.tsx +1 -0
- package/src/cli/ui/components/StatusBox.tsx +1 -0
- package/src/cli/ui/components/VargBox.tsx +1 -0
- package/src/cli/ui/components/VargProgress.tsx +1 -0
- package/src/cli/ui/components/VargSpinner.tsx +1 -0
- package/src/cli/ui/components/VargText.tsx +1 -0
- package/src/definitions/actions/grok-edit.ts +133 -0
- package/src/definitions/actions/index.ts +16 -0
- package/src/definitions/actions/qwen-angles.ts +218 -0
- package/src/index.ts +1 -0
- package/src/providers/fal.ts +196 -0
- package/src/react/assets.ts +9 -0
- package/src/react/elements.ts +0 -5
- package/src/react/examples/branching.tsx +6 -4
- package/src/react/examples/character-video.tsx +13 -10
- package/src/react/examples/local-files-test.tsx +19 -0
- package/src/react/examples/ltx2-test.tsx +25 -0
- package/src/react/examples/madi.tsx +13 -10
- package/src/react/examples/mcmeows.tsx +40 -0
- package/src/react/examples/music-defaults.tsx +24 -0
- package/src/react/examples/quickstart-test.tsx +101 -0
- package/src/react/examples/qwen-angles-test.tsx +72 -0
- package/src/react/index.ts +3 -3
- package/src/react/layouts/grid.tsx +1 -1
- package/src/react/layouts/index.ts +2 -1
- package/src/react/layouts/slot.tsx +85 -0
- package/src/react/layouts/split.tsx +18 -0
- package/src/react/react.test.ts +60 -11
- package/src/react/renderers/burn-captions.ts +95 -0
- package/src/react/renderers/cache.test.ts +182 -0
- package/src/react/renderers/captions.ts +25 -6
- package/src/react/renderers/clip.ts +56 -25
- package/src/react/renderers/context.ts +5 -2
- package/src/react/renderers/image.ts +5 -2
- package/src/react/renderers/index.ts +0 -1
- package/src/react/renderers/music.ts +8 -3
- package/src/react/renderers/packshot/blinking-button.ts +413 -0
- package/src/react/renderers/packshot.ts +170 -8
- package/src/react/renderers/progress.ts +4 -3
- package/src/react/renderers/render.ts +127 -71
- package/src/react/renderers/speech.ts +2 -2
- package/src/react/renderers/split.ts +34 -13
- package/src/react/renderers/utils.test.ts +80 -0
- package/src/react/renderers/utils.ts +37 -1
- package/src/react/renderers/video.ts +47 -9
- package/src/react/types.ts +70 -17
- package/src/studio/stages.ts +40 -39
- package/src/studio/step-renderer.ts +14 -24
- package/src/studio/ui/index.html +2 -2
- package/src/tests/all.test.ts +4 -4
- package/src/tests/index.ts +1 -1
- package/test-slot-grid.tsx +19 -0
- package/test-slot-userland.tsx +30 -0
- package/test-sync-v2.ts +30 -0
- package/test-sync-v2.tsx +29 -0
- package/tsconfig.json +1 -1
- package/video.tsx +7 -0
- package/src/ai-sdk/providers/editly/ffmpeg.ts +0 -60
- package/src/react/renderers/animate.ts +0 -59
- /package/src/cli/commands/{studio.tsx → studio.ts} +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ffprobe, multipleOf2 } from "./ffmpeg";
|
|
1
|
+
import { type FFmpegBackend, localBackend } from "./backends";
|
|
3
2
|
import {
|
|
4
3
|
getImageOverlayFilter,
|
|
5
4
|
getImageOverlayPositionFilter,
|
|
@@ -18,6 +17,7 @@ import type {
|
|
|
18
17
|
Clip,
|
|
19
18
|
DetachedAudioLayer,
|
|
20
19
|
EditlyConfig,
|
|
20
|
+
EditlyResult,
|
|
21
21
|
ImageOverlayLayer,
|
|
22
22
|
Layer,
|
|
23
23
|
NewsTitleLayer,
|
|
@@ -28,6 +28,8 @@ import type {
|
|
|
28
28
|
VideoLayer,
|
|
29
29
|
} from "./types";
|
|
30
30
|
|
|
31
|
+
export * from "./backends";
|
|
32
|
+
export * from "./rendi";
|
|
31
33
|
export * from "./types";
|
|
32
34
|
|
|
33
35
|
const DEFAULT_DURATION = 4;
|
|
@@ -36,12 +38,22 @@ const DEFAULT_FPS = 30;
|
|
|
36
38
|
const DEFAULT_WIDTH = 1280;
|
|
37
39
|
const DEFAULT_HEIGHT = 720;
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
function multipleOf2(n: number): number {
|
|
42
|
+
return Math.round(n / 2) * 2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getVideoDuration(
|
|
46
|
+
path: string,
|
|
47
|
+
backend: FFmpegBackend,
|
|
48
|
+
): Promise<number> {
|
|
49
|
+
const info = await backend.ffprobe(path);
|
|
41
50
|
return info.duration;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
async function getFirstVideoInfo(
|
|
53
|
+
async function getFirstVideoInfo(
|
|
54
|
+
clips: Clip[],
|
|
55
|
+
backend: FFmpegBackend,
|
|
56
|
+
): Promise<{
|
|
45
57
|
width?: number;
|
|
46
58
|
height?: number;
|
|
47
59
|
fps?: number;
|
|
@@ -49,7 +61,7 @@ async function getFirstVideoInfo(clips: Clip[]): Promise<{
|
|
|
49
61
|
for (const clip of clips) {
|
|
50
62
|
for (const layer of clip.layers) {
|
|
51
63
|
if (layer.type === "video") {
|
|
52
|
-
const info = await ffprobe((layer as VideoLayer).path);
|
|
64
|
+
const info = await backend.ffprobe((layer as VideoLayer).path);
|
|
53
65
|
return { width: info.width, height: info.height, fps: info.fps };
|
|
54
66
|
}
|
|
55
67
|
}
|
|
@@ -72,6 +84,7 @@ function applyLayerDefaults(
|
|
|
72
84
|
async function processClips(
|
|
73
85
|
clips: Clip[],
|
|
74
86
|
defaults: EditlyConfig["defaults"],
|
|
87
|
+
backend: FFmpegBackend,
|
|
75
88
|
): Promise<ProcessedClip[]> {
|
|
76
89
|
const processed: ProcessedClip[] = [];
|
|
77
90
|
const defaultDuration = defaults?.duration ?? DEFAULT_DURATION;
|
|
@@ -86,7 +99,7 @@ async function processClips(
|
|
|
86
99
|
for (const layer of layers) {
|
|
87
100
|
if (layer.type === "video" && !clip.duration) {
|
|
88
101
|
const videoLayer = layer as VideoLayer;
|
|
89
|
-
const videoDuration = await getVideoDuration(videoLayer.path);
|
|
102
|
+
const videoDuration = await getVideoDuration(videoLayer.path, backend);
|
|
90
103
|
const cutFrom = videoLayer.cutFrom ?? 0;
|
|
91
104
|
const cutTo = videoLayer.cutTo ?? videoDuration;
|
|
92
105
|
duration = cutTo - cutFrom;
|
|
@@ -137,6 +150,15 @@ function isOverlayLayer(layer: Layer): boolean {
|
|
|
137
150
|
return isVideoOverlayLayer(layer) || isImageOverlayLayer(layer);
|
|
138
151
|
}
|
|
139
152
|
|
|
153
|
+
function isTextOverlayLayer(layer: Layer): boolean {
|
|
154
|
+
return (
|
|
155
|
+
layer.type === "title" ||
|
|
156
|
+
layer.type === "subtitle" ||
|
|
157
|
+
layer.type === "news-title" ||
|
|
158
|
+
layer.type === "slide-in-text"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
140
162
|
function buildBaseClipFilter(
|
|
141
163
|
clip: ProcessedClip,
|
|
142
164
|
clipIndex: number,
|
|
@@ -164,7 +186,10 @@ function buildBaseClipFilter(
|
|
|
164
186
|
let baseLabel = "";
|
|
165
187
|
let inputIdx = inputOffset;
|
|
166
188
|
|
|
167
|
-
|
|
189
|
+
// Filter out overlay layers AND text overlay layers (text will be applied after image overlays)
|
|
190
|
+
const baseLayers = clip.layers.filter(
|
|
191
|
+
(l) => l && !isOverlayLayer(l) && !isTextOverlayLayer(l),
|
|
192
|
+
);
|
|
168
193
|
|
|
169
194
|
for (let i = 0; i < baseLayers.length; i++) {
|
|
170
195
|
const layer = baseLayers[i];
|
|
@@ -201,58 +226,6 @@ function buildBaseClipFilter(
|
|
|
201
226
|
inputIdx++;
|
|
202
227
|
}
|
|
203
228
|
}
|
|
204
|
-
|
|
205
|
-
if (layer.type === "title") {
|
|
206
|
-
const titleFilter = getTitleFilter(
|
|
207
|
-
layer as TitleLayer,
|
|
208
|
-
baseLabel,
|
|
209
|
-
width,
|
|
210
|
-
height,
|
|
211
|
-
clip.duration,
|
|
212
|
-
);
|
|
213
|
-
const newLabel = `title${clipIndex}_${i}`;
|
|
214
|
-
filters.push(`${titleFilter}[${newLabel}]`);
|
|
215
|
-
baseLabel = newLabel;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (layer.type === "subtitle") {
|
|
219
|
-
const subtitleFilter = getSubtitleFilter(
|
|
220
|
-
layer as SubtitleLayer,
|
|
221
|
-
baseLabel,
|
|
222
|
-
width,
|
|
223
|
-
height,
|
|
224
|
-
clip.duration,
|
|
225
|
-
);
|
|
226
|
-
const newLabel = `sub${clipIndex}_${i}`;
|
|
227
|
-
filters.push(`${subtitleFilter}[${newLabel}]`);
|
|
228
|
-
baseLabel = newLabel;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (layer.type === "news-title") {
|
|
232
|
-
const newsFilter = getNewsTitleFilter(
|
|
233
|
-
layer as NewsTitleLayer,
|
|
234
|
-
baseLabel,
|
|
235
|
-
width,
|
|
236
|
-
height,
|
|
237
|
-
clip.duration,
|
|
238
|
-
);
|
|
239
|
-
const newLabel = `news${clipIndex}_${i}`;
|
|
240
|
-
filters.push(`${newsFilter}[${newLabel}]`);
|
|
241
|
-
baseLabel = newLabel;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (layer.type === "slide-in-text") {
|
|
245
|
-
const slideFilter = getSlideInTextFilter(
|
|
246
|
-
layer as SlideInTextLayer,
|
|
247
|
-
baseLabel,
|
|
248
|
-
width,
|
|
249
|
-
height,
|
|
250
|
-
clip.duration,
|
|
251
|
-
);
|
|
252
|
-
const newLabel = `slide${clipIndex}_${i}`;
|
|
253
|
-
filters.push(`${slideFilter}[${newLabel}]`);
|
|
254
|
-
baseLabel = newLabel;
|
|
255
|
-
}
|
|
256
229
|
}
|
|
257
230
|
|
|
258
231
|
return {
|
|
@@ -358,6 +331,41 @@ function collectAudioLayers(
|
|
|
358
331
|
return audioLayers;
|
|
359
332
|
}
|
|
360
333
|
|
|
334
|
+
type TextLayer = TitleLayer | SubtitleLayer | NewsTitleLayer | SlideInTextLayer;
|
|
335
|
+
|
|
336
|
+
interface TimedTextLayer {
|
|
337
|
+
layer: TextLayer;
|
|
338
|
+
startTime: number;
|
|
339
|
+
duration: number;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function collectTextLayers(clips: ProcessedClip[]): TimedTextLayer[] {
|
|
343
|
+
const textLayers: TimedTextLayer[] = [];
|
|
344
|
+
let currentTime = 0;
|
|
345
|
+
|
|
346
|
+
for (let i = 0; i < clips.length; i++) {
|
|
347
|
+
const clip = clips[i];
|
|
348
|
+
if (!clip) continue;
|
|
349
|
+
|
|
350
|
+
for (const layer of clip.layers) {
|
|
351
|
+
if (layer && isTextOverlayLayer(layer)) {
|
|
352
|
+
textLayers.push({
|
|
353
|
+
layer: layer as TextLayer,
|
|
354
|
+
startTime: currentTime,
|
|
355
|
+
duration: clip.duration,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
currentTime += clip.duration;
|
|
361
|
+
if (i < clips.length - 1) {
|
|
362
|
+
currentTime -= clip.transition.duration;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return textLayers;
|
|
367
|
+
}
|
|
368
|
+
|
|
361
369
|
function buildTransitionFilter(
|
|
362
370
|
fromLabel: string,
|
|
363
371
|
toLabel: string,
|
|
@@ -527,7 +535,7 @@ function buildAudioFilter(
|
|
|
527
535
|
};
|
|
528
536
|
}
|
|
529
537
|
|
|
530
|
-
export async function editly(config: EditlyConfig): Promise<
|
|
538
|
+
export async function editly(config: EditlyConfig): Promise<EditlyResult> {
|
|
531
539
|
const {
|
|
532
540
|
outPath,
|
|
533
541
|
clips: clipsIn,
|
|
@@ -544,11 +552,17 @@ export async function editly(config: EditlyConfig): Promise<void> {
|
|
|
544
552
|
fast,
|
|
545
553
|
} = config;
|
|
546
554
|
|
|
555
|
+
const backend: FFmpegBackend = config.backend ?? localBackend;
|
|
556
|
+
|
|
557
|
+
if (verbose) {
|
|
558
|
+
console.log(`[editly] using backend: ${backend.name}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
547
561
|
if (!clipsIn || clipsIn.length === 0) {
|
|
548
562
|
throw new Error("At least one clip is required");
|
|
549
563
|
}
|
|
550
564
|
|
|
551
|
-
const firstVideoInfo = await getFirstVideoInfo(clipsIn);
|
|
565
|
+
const firstVideoInfo = await getFirstVideoInfo(clipsIn, backend);
|
|
552
566
|
let width = config.width ?? firstVideoInfo.width ?? DEFAULT_WIDTH;
|
|
553
567
|
let height = config.height ?? firstVideoInfo.height ?? DEFAULT_HEIGHT;
|
|
554
568
|
const fps = config.fps ?? firstVideoInfo.fps ?? DEFAULT_FPS;
|
|
@@ -566,7 +580,7 @@ export async function editly(config: EditlyConfig): Promise<void> {
|
|
|
566
580
|
console.log(`Output: ${width}x${height} @ ${fps}fps`);
|
|
567
581
|
}
|
|
568
582
|
|
|
569
|
-
const clips = await processClips(clipsIn, defaults);
|
|
583
|
+
const clips = await processClips(clipsIn, defaults, backend);
|
|
570
584
|
|
|
571
585
|
const continuousVideoOverlays = collectContinuousVideoOverlays(clips);
|
|
572
586
|
const imageOverlays = collectImageOverlays(clips);
|
|
@@ -744,6 +758,67 @@ export async function editly(config: EditlyConfig): Promise<void> {
|
|
|
744
758
|
finalVideoLabel = currentBase;
|
|
745
759
|
}
|
|
746
760
|
|
|
761
|
+
const textLayers = collectTextLayers(clips);
|
|
762
|
+
if (textLayers.length > 0) {
|
|
763
|
+
let currentBase = finalVideoLabel;
|
|
764
|
+
|
|
765
|
+
for (let i = 0; i < textLayers.length; i++) {
|
|
766
|
+
const timedLayer = textLayers[i];
|
|
767
|
+
if (!timedLayer) continue;
|
|
768
|
+
|
|
769
|
+
const { layer, startTime, duration } = timedLayer;
|
|
770
|
+
const outputLabel = `vwithtext${i}`;
|
|
771
|
+
|
|
772
|
+
const timedLayerWithEnable = {
|
|
773
|
+
...layer,
|
|
774
|
+
start: layer.start ?? startTime,
|
|
775
|
+
stop: layer.stop ?? startTime + duration,
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
if (layer.type === "title") {
|
|
779
|
+
const titleFilter = getTitleFilter(
|
|
780
|
+
timedLayerWithEnable as TitleLayer,
|
|
781
|
+
currentBase,
|
|
782
|
+
width,
|
|
783
|
+
height,
|
|
784
|
+
totalDuration,
|
|
785
|
+
);
|
|
786
|
+
allFilters.push(`${titleFilter}[${outputLabel}]`);
|
|
787
|
+
} else if (layer.type === "subtitle") {
|
|
788
|
+
const subtitleFilter = getSubtitleFilter(
|
|
789
|
+
timedLayerWithEnable as SubtitleLayer,
|
|
790
|
+
currentBase,
|
|
791
|
+
width,
|
|
792
|
+
height,
|
|
793
|
+
totalDuration,
|
|
794
|
+
);
|
|
795
|
+
allFilters.push(`${subtitleFilter}[${outputLabel}]`);
|
|
796
|
+
} else if (layer.type === "news-title") {
|
|
797
|
+
const newsFilter = getNewsTitleFilter(
|
|
798
|
+
timedLayerWithEnable as NewsTitleLayer,
|
|
799
|
+
currentBase,
|
|
800
|
+
width,
|
|
801
|
+
height,
|
|
802
|
+
totalDuration,
|
|
803
|
+
);
|
|
804
|
+
allFilters.push(`${newsFilter}[${outputLabel}]`);
|
|
805
|
+
} else if (layer.type === "slide-in-text") {
|
|
806
|
+
const slideFilter = getSlideInTextFilter(
|
|
807
|
+
timedLayerWithEnable as SlideInTextLayer,
|
|
808
|
+
currentBase,
|
|
809
|
+
width,
|
|
810
|
+
height,
|
|
811
|
+
totalDuration,
|
|
812
|
+
);
|
|
813
|
+
allFilters.push(`${slideFilter}[${outputLabel}]`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
currentBase = outputLabel;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
finalVideoLabel = currentBase;
|
|
820
|
+
}
|
|
821
|
+
|
|
747
822
|
const clipAudioLayers = collectAudioLayers(clips);
|
|
748
823
|
const videoInputCount = allInputs.length;
|
|
749
824
|
const audioFilter = buildAudioFilter(
|
|
@@ -765,10 +840,9 @@ export async function editly(config: EditlyConfig): Promise<void> {
|
|
|
765
840
|
allFilters.push(audioFilter.filter);
|
|
766
841
|
}
|
|
767
842
|
|
|
768
|
-
const inputArgs = allInputs.flatMap((input) => ["-i", input]);
|
|
769
843
|
const filterComplex = allFilters.join(";");
|
|
770
844
|
|
|
771
|
-
const
|
|
845
|
+
const codecArgs = customOutputArgs ?? [
|
|
772
846
|
"-c:v",
|
|
773
847
|
"libx264",
|
|
774
848
|
"-preset",
|
|
@@ -785,33 +859,43 @@ export async function editly(config: EditlyConfig): Promise<void> {
|
|
|
785
859
|
? ["-map", `[${finalVideoLabel}]`, "-map", `[${audioFilter.outputLabel}]`]
|
|
786
860
|
: ["-map", `[${finalVideoLabel}]`];
|
|
787
861
|
|
|
788
|
-
const
|
|
789
|
-
"-hide_banner",
|
|
790
|
-
"-loglevel",
|
|
791
|
-
verbose ? "info" : "error",
|
|
792
|
-
...inputArgs,
|
|
793
|
-
"-filter_complex",
|
|
794
|
-
filterComplex,
|
|
862
|
+
const outputArgs = [
|
|
795
863
|
...mapArgs,
|
|
796
864
|
"-r",
|
|
797
865
|
String(fps),
|
|
798
|
-
...
|
|
799
|
-
"-
|
|
800
|
-
outPath,
|
|
866
|
+
...codecArgs,
|
|
867
|
+
...(config.shortest ? ["-shortest"] : []),
|
|
801
868
|
];
|
|
802
869
|
|
|
803
870
|
if (verbose) {
|
|
804
|
-
|
|
871
|
+
const inputArgs = allInputs.flatMap((input) => ["-i", input]);
|
|
872
|
+
console.log(
|
|
873
|
+
"ffmpeg",
|
|
874
|
+
[
|
|
875
|
+
...inputArgs,
|
|
876
|
+
"-filter_complex",
|
|
877
|
+
filterComplex,
|
|
878
|
+
...outputArgs,
|
|
879
|
+
"-y",
|
|
880
|
+
outPath,
|
|
881
|
+
].join(" "),
|
|
882
|
+
);
|
|
805
883
|
console.log("\nFilter complex:\n", filterComplex.split(";").join(";\n"));
|
|
806
884
|
}
|
|
807
885
|
|
|
808
|
-
const result = await
|
|
886
|
+
const result = await backend.run({
|
|
887
|
+
inputs: allInputs,
|
|
888
|
+
filterComplex,
|
|
889
|
+
outputArgs,
|
|
890
|
+
outputPath: outPath,
|
|
891
|
+
verbose,
|
|
892
|
+
});
|
|
809
893
|
|
|
810
|
-
if (result.
|
|
811
|
-
|
|
894
|
+
if (result.output.type === "file" && verbose) {
|
|
895
|
+
console.log(`Output: ${result.output.path}`);
|
|
812
896
|
}
|
|
813
897
|
|
|
814
|
-
|
|
898
|
+
return { output: result.output };
|
|
815
899
|
}
|
|
816
900
|
|
|
817
901
|
export default editly;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
CropPosition,
|
|
2
3
|
FillColorLayer,
|
|
3
4
|
ImageLayer,
|
|
4
5
|
ImageOverlayLayer,
|
|
@@ -15,6 +16,33 @@ import type {
|
|
|
15
16
|
VideoLayer,
|
|
16
17
|
} from "./types";
|
|
17
18
|
|
|
19
|
+
function getCropPositionExpr(position: CropPosition | undefined): {
|
|
20
|
+
x: string;
|
|
21
|
+
y: string;
|
|
22
|
+
} {
|
|
23
|
+
switch (position) {
|
|
24
|
+
case "top-left":
|
|
25
|
+
return { x: "0", y: "0" };
|
|
26
|
+
case "top":
|
|
27
|
+
return { x: "(iw-ow)/2", y: "0" };
|
|
28
|
+
case "top-right":
|
|
29
|
+
return { x: "iw-ow", y: "0" };
|
|
30
|
+
case "left":
|
|
31
|
+
return { x: "0", y: "(ih-oh)/2" };
|
|
32
|
+
case "right":
|
|
33
|
+
return { x: "iw-ow", y: "(ih-oh)/2" };
|
|
34
|
+
case "bottom-left":
|
|
35
|
+
return { x: "0", y: "ih-oh" };
|
|
36
|
+
case "bottom":
|
|
37
|
+
return { x: "(iw-ow)/2", y: "ih-oh" };
|
|
38
|
+
case "bottom-right":
|
|
39
|
+
return { x: "iw-ow", y: "ih-oh" };
|
|
40
|
+
case "center":
|
|
41
|
+
default:
|
|
42
|
+
return { x: "(iw-ow)/2", y: "(ih-oh)/2" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
function escapeDrawText(text: string): string {
|
|
19
47
|
return text
|
|
20
48
|
.replace(/\\/g, "\\\\")
|
|
@@ -161,9 +189,14 @@ export function getVideoFilterWithTrim(
|
|
|
161
189
|
const layerHeight = parseSize(layer.height, height);
|
|
162
190
|
|
|
163
191
|
if (isOverlay) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
192
|
+
let scaleFilter = `scale=${layerWidth}:${layerHeight}:force_original_aspect_ratio=decrease`;
|
|
193
|
+
if (layer.resizeMode === "cover") {
|
|
194
|
+
const { x, y } = getCropPositionExpr(layer.cropPosition);
|
|
195
|
+
scaleFilter = `scale=${layerWidth}:${layerHeight}:force_original_aspect_ratio=increase,crop=${layerWidth}:${layerHeight}:${x}:${y}`;
|
|
196
|
+
} else if (layer.resizeMode === "stretch") {
|
|
197
|
+
scaleFilter = `scale=${layerWidth}:${layerHeight}`;
|
|
198
|
+
}
|
|
199
|
+
filters.push(scaleFilter);
|
|
167
200
|
filters.push("setsar=1");
|
|
168
201
|
filters.push("fps=30");
|
|
169
202
|
filters.push("settb=1/30");
|
|
@@ -555,7 +588,13 @@ export function getTitleFilter(
|
|
|
555
588
|
): string {
|
|
556
589
|
const text = escapeDrawText(layer.text);
|
|
557
590
|
const color = layer.textColor ?? "white";
|
|
558
|
-
|
|
591
|
+
|
|
592
|
+
// Auto-size font to fit within 90% of frame width (same approach as subtitle)
|
|
593
|
+
const maxFontSize = Math.round(Math.min(width, height) * 0.08);
|
|
594
|
+
const maxTextWidth = width * 0.9;
|
|
595
|
+
// Average char width ≈ fontSize * 0.55 for sans-serif fonts
|
|
596
|
+
const fittedFontSize = Math.floor(maxTextWidth / (layer.text.length * 0.55));
|
|
597
|
+
const fontSize = Math.max(16, Math.min(maxFontSize, fittedFontSize));
|
|
559
598
|
|
|
560
599
|
let x = "(w-text_w)/2";
|
|
561
600
|
let y = "(h-text_h)/2";
|
|
@@ -587,7 +626,13 @@ export function getSubtitleFilter(
|
|
|
587
626
|
const text = escapeDrawText(layer.text);
|
|
588
627
|
const textColor = layer.textColor ?? "white";
|
|
589
628
|
const bgColor = layer.backgroundColor ?? "black@0.7";
|
|
590
|
-
|
|
629
|
+
|
|
630
|
+
// Auto-size font to fit within 90% of frame width
|
|
631
|
+
const maxFontSize = Math.round(Math.min(width, height) * 0.05);
|
|
632
|
+
const maxTextWidth = width * 0.9;
|
|
633
|
+
// Average char width ≈ fontSize * 0.55 for sans-serif fonts
|
|
634
|
+
const fittedFontSize = Math.floor(maxTextWidth / (layer.text.length * 0.55));
|
|
635
|
+
const fontSize = Math.max(16, Math.min(maxFontSize, fittedFontSize));
|
|
591
636
|
const boxPadding = Math.round(fontSize * 0.4);
|
|
592
637
|
|
|
593
638
|
const fontFile = layer.fontPath
|
|
@@ -626,7 +671,14 @@ export function getTitleBackgroundFilter(
|
|
|
626
671
|
|
|
627
672
|
const text = escapeDrawText(layer.text);
|
|
628
673
|
const textColor = layer.textColor ?? "white";
|
|
629
|
-
|
|
674
|
+
|
|
675
|
+
// Auto-size font to fit within 90% of frame width
|
|
676
|
+
const maxFontSizeBg = Math.round(Math.min(width, height) * 0.1);
|
|
677
|
+
const maxTextWidthBg = width * 0.9;
|
|
678
|
+
const fittedFontSizeBg = Math.floor(
|
|
679
|
+
maxTextWidthBg / (layer.text.length * 0.55),
|
|
680
|
+
);
|
|
681
|
+
const fontSize = Math.max(16, Math.min(maxFontSizeBg, fittedFontSizeBg));
|
|
630
682
|
|
|
631
683
|
const fontFile = layer.fontPath
|
|
632
684
|
? `:fontfile='${escapeDrawText(layer.fontPath)}'`
|