vargai 0.4.0-alpha32 → 0.4.0-alpha33
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/package.json
CHANGED
|
@@ -126,7 +126,13 @@ export class RendiBackend implements FFmpegBackend {
|
|
|
126
126
|
if (placeholder) {
|
|
127
127
|
return placeholder;
|
|
128
128
|
}
|
|
129
|
-
|
|
129
|
+
let result = arg;
|
|
130
|
+
for (const [url, ph] of pathToPlaceholder) {
|
|
131
|
+
if (result.includes(url)) {
|
|
132
|
+
result = result.replaceAll(url, ph);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
130
136
|
});
|
|
131
137
|
|
|
132
138
|
const filteredArgs = this.stripInternalFlags(commandArgs);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { localBackend } from "@/ai-sdk/providers/editly";
|
|
2
|
+
import type {
|
|
3
|
+
FFmpegBackend,
|
|
4
|
+
FFmpegOutput,
|
|
5
|
+
} from "../../ai-sdk/providers/editly/backends/types";
|
|
6
|
+
import { uploadBuffer } from "../../providers/storage";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves an FFmpegOutput to a string path/URL, uploading local files if needed.
|
|
10
|
+
*
|
|
11
|
+
* - URL input → returns URL as-is
|
|
12
|
+
* - File input + shouldUpload=false → returns local path
|
|
13
|
+
* - File input + shouldUpload=true → uploads to storage, returns URL
|
|
14
|
+
*/
|
|
15
|
+
async function resolveInputPathMaybeUpload(
|
|
16
|
+
input: FFmpegOutput,
|
|
17
|
+
options: { shouldUpload: boolean },
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
if (input.type === "url") return input.url;
|
|
20
|
+
if (!options.shouldUpload) return input.path;
|
|
21
|
+
|
|
22
|
+
const buffer = await Bun.file(input.path).arrayBuffer();
|
|
23
|
+
return uploadBuffer(
|
|
24
|
+
buffer,
|
|
25
|
+
`tmp/${Date.now()}-${input.path.split("/").pop()}`,
|
|
26
|
+
"application/octet-stream",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CaptionOverlayOptions {
|
|
31
|
+
video: FFmpegOutput;
|
|
32
|
+
assPath: string;
|
|
33
|
+
outputPath: string;
|
|
34
|
+
backend?: FFmpegBackend;
|
|
35
|
+
verbose?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Burns ASS subtitle captions onto a video using FFmpeg.
|
|
40
|
+
*
|
|
41
|
+
* {@link burnCaptions} composites the subtitle file directly into the video frames,
|
|
42
|
+
* producing a new video with hardcoded captions. Supports both local and cloud
|
|
43
|
+
* FFmpeg backends - when using a cloud backend, input files are automatically
|
|
44
|
+
* uploaded to storage.
|
|
45
|
+
*
|
|
46
|
+
* @param options - Configuration for the caption burn operation
|
|
47
|
+
* @param options.video - Source video as {@link FFmpegOutput} (file path or URL)
|
|
48
|
+
* @param options.assPath - Path to the ASS subtitle file to burn
|
|
49
|
+
* @param options.outputPath - Destination path for the output video (defaults to "output.mp4")
|
|
50
|
+
* @param options.backend - Optional {@link FFmpegBackend} for cloud processing; uses local FFmpeg if omitted
|
|
51
|
+
* @param options.verbose - Enable verbose FFmpeg logging
|
|
52
|
+
*
|
|
53
|
+
* @returns Promise resolving to {@link FFmpegOutput} containing the path or URL of the captioned video
|
|
54
|
+
*
|
|
55
|
+
* @throws May throw if FFmpeg execution fails, input files are missing, or upload fails for cloud backends
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const result = await burnCaptions({
|
|
60
|
+
* video: { type: "file", path: "input.mp4" },
|
|
61
|
+
* assPath: "captions.ass",
|
|
62
|
+
* outputPath: "output-with-captions.mp4",
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export async function burnCaptions(
|
|
67
|
+
options: CaptionOverlayOptions,
|
|
68
|
+
): Promise<FFmpegOutput> {
|
|
69
|
+
const { video, assPath, outputPath = "output.mp4", verbose } = options;
|
|
70
|
+
const captions: FFmpegOutput = { type: "file", path: assPath };
|
|
71
|
+
|
|
72
|
+
const isCloud = options.backend !== undefined;
|
|
73
|
+
|
|
74
|
+
const videoInput = await resolveInputPathMaybeUpload(video, {
|
|
75
|
+
shouldUpload: isCloud,
|
|
76
|
+
});
|
|
77
|
+
const assInput = await resolveInputPathMaybeUpload(captions, {
|
|
78
|
+
shouldUpload: isCloud,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const backend = options.backend ?? localBackend;
|
|
82
|
+
|
|
83
|
+
// FFmpeg filter syntax requires escaping backslashes and colons
|
|
84
|
+
const escapedAssPath = assInput.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
|
|
85
|
+
|
|
86
|
+
const result = await backend.run({
|
|
87
|
+
args: [
|
|
88
|
+
"-i",
|
|
89
|
+
videoInput,
|
|
90
|
+
"-vf",
|
|
91
|
+
`subtitles=${escapedAssPath}`,
|
|
92
|
+
"-crf",
|
|
93
|
+
"18",
|
|
94
|
+
"-preset",
|
|
95
|
+
"fast",
|
|
96
|
+
"-c:a",
|
|
97
|
+
"copy",
|
|
98
|
+
"-y",
|
|
99
|
+
outputPath,
|
|
100
|
+
],
|
|
101
|
+
inputs: [videoInput, assInput],
|
|
102
|
+
outputPath,
|
|
103
|
+
verbose,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return result.output;
|
|
107
|
+
}
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
Layer,
|
|
15
15
|
VideoLayer,
|
|
16
16
|
} from "../../ai-sdk/providers/editly/types";
|
|
17
|
+
|
|
17
18
|
import type {
|
|
18
19
|
CaptionsProps,
|
|
19
20
|
ClipProps,
|
|
@@ -25,6 +26,7 @@ import type {
|
|
|
25
26
|
SpeechProps,
|
|
26
27
|
VargElement,
|
|
27
28
|
} from "../types";
|
|
29
|
+
import { burnCaptions } from "./burn-captions";
|
|
28
30
|
import { renderCaptions } from "./captions";
|
|
29
31
|
import { renderClip } from "./clip";
|
|
30
32
|
import type { RenderContext } from "./context";
|
|
@@ -271,7 +273,7 @@ export async function renderRoot(
|
|
|
271
273
|
const editlyTaskId = addTask(progress, "editly", "ffmpeg");
|
|
272
274
|
startTask(progress, editlyTaskId);
|
|
273
275
|
|
|
274
|
-
await editly({
|
|
276
|
+
const editlyResult = await editly({
|
|
275
277
|
outPath: tempOutPath,
|
|
276
278
|
width: ctx.width,
|
|
277
279
|
height: ctx.height,
|
|
@@ -280,27 +282,50 @@ export async function renderRoot(
|
|
|
280
282
|
audioTracks: audioTracks.length > 0 ? audioTracks : undefined,
|
|
281
283
|
shortest: props.shortest,
|
|
282
284
|
verbose: options.verbose,
|
|
285
|
+
backend: options.backend,
|
|
283
286
|
});
|
|
284
287
|
|
|
285
288
|
completeTask(progress, editlyTaskId);
|
|
286
289
|
|
|
290
|
+
let output = editlyResult.output;
|
|
291
|
+
|
|
287
292
|
if (hasCaptions && captionsResult) {
|
|
288
293
|
const captionsTaskId = addTask(progress, "captions", "ffmpeg");
|
|
289
294
|
startTask(progress, captionsTaskId);
|
|
290
295
|
|
|
291
|
-
|
|
292
|
-
|
|
296
|
+
output = await burnCaptions({
|
|
297
|
+
video: output,
|
|
298
|
+
assPath: captionsResult.assPath,
|
|
299
|
+
outputPath: finalOutPath,
|
|
300
|
+
backend: options.backend,
|
|
301
|
+
verbose: options.verbose,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!options.backend) {
|
|
305
|
+
ctx.tempFiles.push(tempOutPath);
|
|
306
|
+
}
|
|
293
307
|
|
|
294
|
-
ctx.tempFiles.push(tempOutPath);
|
|
295
308
|
completeTask(progress, captionsTaskId);
|
|
296
309
|
}
|
|
297
310
|
|
|
311
|
+
let finalBuffer: ArrayBuffer;
|
|
312
|
+
if (output.type === "url") {
|
|
313
|
+
const res = await fetch(output.url);
|
|
314
|
+
if (!res.ok)
|
|
315
|
+
throw new Error(`Failed to download final render: ${res.status}`);
|
|
316
|
+
finalBuffer = await res.arrayBuffer();
|
|
317
|
+
if (options.output) {
|
|
318
|
+
await Bun.write(options.output, finalBuffer);
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
finalBuffer = await Bun.file(output.path).arrayBuffer();
|
|
322
|
+
}
|
|
323
|
+
|
|
298
324
|
if (!options.quiet && mode === "preview" && placeholderCount.total > 0) {
|
|
299
325
|
console.log(
|
|
300
326
|
`\x1b[36mℹ preview mode: ${placeholderCount.total} placeholders used (${placeholderCount.images} images, ${placeholderCount.videos} videos)\x1b[0m`,
|
|
301
327
|
);
|
|
302
328
|
}
|
|
303
329
|
|
|
304
|
-
|
|
305
|
-
return new Uint8Array(result);
|
|
330
|
+
return new Uint8Array(finalBuffer);
|
|
306
331
|
}
|
package/src/react/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageModelV3, SpeechModelV3 } from "@ai-sdk/provider";
|
|
2
|
+
import type { FFmpegBackend } from "@/ai-sdk/providers/editly/backends";
|
|
2
3
|
import type { MusicModelV3 } from "../ai-sdk/music-model";
|
|
3
4
|
import type {
|
|
4
5
|
CropPosition,
|
|
@@ -260,6 +261,7 @@ export interface RenderOptions {
|
|
|
260
261
|
verbose?: boolean;
|
|
261
262
|
mode?: RenderMode;
|
|
262
263
|
defaults?: DefaultModels;
|
|
264
|
+
backend?: FFmpegBackend;
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
export interface ElementPropsMap {
|