batchedits 1.4.0 → 1.6.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/README.md +36 -4
- package/dist/cli.js +121 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,14 +19,15 @@ Needs Node.js 18.17+ and a [BatchEdits](https://batchedits.com) account (7-day f
|
|
|
19
19
|
| `login` / `logout` / `whoami` | Sign in through your browser; check the connection |
|
|
20
20
|
| `styles` | List your styles |
|
|
21
21
|
| `actions` | List editing actions and their settings |
|
|
22
|
-
| `style create <name> [--captions] [--emojis] [--silence] [--repeats] [--crop 9:16] [--zoom] [--clips [n]] [--hook] [--hook-clip <id or words>] [--set action.setting=value…]` | Create a style from flags |
|
|
22
|
+
| `style create <name> [--captions] [--emojis] [--silence] [--repeats] [--crop 9:16] [--zoom] [--clips [n]] [--hook] [--hook-clip <id or words>] [--broll] [--cta [text]] [--sfx] [--set action.setting=value…]` | Create a style from flags |
|
|
23
23
|
| `hooks [words]` | Find intro clips for a video hook |
|
|
24
24
|
| `style edit <name> [same flags] [--set …] [--remove <action>…] [--name <new>]` | Change a saved style's edits and settings |
|
|
25
|
-
| `
|
|
25
|
+
| `style copy <link\|file\|video id> [--name <name>]` | Create a style copying a reference video's captions, zooms and crop |
|
|
26
|
+
| `upload <files, folders or links…>` | Upload up to 50 videos (YouTube or video-file links are downloaded by BatchEdits), print their ids |
|
|
26
27
|
| `process <ids…> --style <name> [--yes]` | Edit videos (1 credit each, 1 per AI clip) |
|
|
27
28
|
| `status [ids…] [--watch]` | Show status, or wait until done |
|
|
28
29
|
| `download <ids…> [--out dir]` | Save finished videos (and their AI clips) |
|
|
29
|
-
| `run <files or
|
|
30
|
+
| `run <files, folders or links…> --style <name> [--out dir] [--hooks file] [--ctas file] [--yes]` | All of the above in one go |
|
|
30
31
|
|
|
31
32
|
Every command has `--help` with an example, and `--json` for machine-readable output.
|
|
32
33
|
|
|
@@ -36,10 +37,11 @@ Every command has `--help` with an example, and `--json` for machine-readable ou
|
|
|
36
37
|
|
|
37
38
|
### Hooks
|
|
38
39
|
|
|
39
|
-
`--hook-clip` plays a short intro clip (a video hook) before every video.
|
|
40
|
+
`--hook-clip` plays a short intro clip (a video hook) before every video. `--hook-clip ai` lets AI pick a different matching clip for each video. To use one clip for all of them, find it with `batchedits hooks <words>`, then pass its id, or just the words for the best match. With `--hook` too, the text hook is drawn on the clip:
|
|
40
41
|
|
|
41
42
|
```bash
|
|
42
43
|
batchedits hooks surprise reaction
|
|
44
|
+
batchedits style create "Intro" --captions --hook --hook-clip ai
|
|
43
45
|
batchedits style create "Intro" --captions --hook --hook-clip 727
|
|
44
46
|
```
|
|
45
47
|
|
|
@@ -50,6 +52,25 @@ batchedits style create "Hooks" --captions --silence --hook --set hook.position=
|
|
|
50
52
|
batchedits run ./raw --style "Hooks" --hooks hooks.txt --out ./edited --yes
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
`--broll` cuts away to B-roll while you talk about what it shows. Upload clips in the dashboard's Library and name what each shows ("Me typing on laptop"): AI matches the names to what you say, and stock video from Pexels fills the rest (`--set broll.source=mine` for only yours). AI decides how many cutaways each video needs.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
batchedits style create "Talking head" --silence --broll --captions
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`--sfx` adds sound effects to the other edits' moments: a whoosh as B-roll cuts in and on each zoom, a pop for each caption emoji, a riser into the text hook and a ding on the call to action. They sit under your voice; change it with `--set sfx.volume=low|medium|high`, or `--set sfx.zoom=impact` for a hit instead of a whoosh on zooms.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
batchedits style create "Punchy" --silence --broll --captions --emojis --zoom --sfx
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`--cta` adds a call to action over the last 3 seconds of every video ("Follow for more" unless you give your own line). For a different line per video, pass a file with `--ctas`, one per line in upload order. An outro clip played after each video is uploaded once in the dashboard's Call to action settings:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
batchedits style create "Ends" --captions --cta "Comment GUIDE and I'll send it"
|
|
71
|
+
batchedits run ./raw --style "Ends" --ctas ctas.txt --out ./edited --yes
|
|
72
|
+
```
|
|
73
|
+
|
|
53
74
|
```bash
|
|
54
75
|
batchedits style create "Shorts" --crop 9:16 --silence --captions \
|
|
55
76
|
--set captions.position=center --set captions.highlightColor=#FF6B00 --set zoom.zoomPercent=125
|
|
@@ -92,3 +113,14 @@ openModal({ videoUrl: "https://example.com/video.mp4", mode: "popup", onComplete
|
|
|
92
113
|
```
|
|
93
114
|
|
|
94
115
|
MIT licensed.
|
|
116
|
+
|
|
117
|
+
## Copy a style, or edit videos from links
|
|
118
|
+
|
|
119
|
+
`style copy` reads a reference video (a TikTok, Reel, Short or YouTube link, a direct video link, a local file or one of your video ids) and saves its look as a style: caption font, colours, word-by-word or letter-by-letter highlight, position, zooms and crop. Only the first 90 seconds are read; colours and font are a close match.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
batchedits style copy https://www.tiktok.com/@creator/video/123 --name "Like this"
|
|
123
|
+
batchedits run https://youtu.be/dQw4w9WgXcQ ./raw --style "Like this" --out ./edited --yes
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`upload` and `run` take links next to files and folders: YouTube links, or public links to a video file (direct .mp4, Dropbox, Google Drive). BatchEdits downloads them, which can take a minute for long YouTube videos.
|
package/dist/cli.js
CHANGED
|
@@ -298,6 +298,17 @@ var BatchEdits = class {
|
|
|
298
298
|
}
|
|
299
299
|
return body.videos[0];
|
|
300
300
|
}
|
|
301
|
+
/** Uploads a video from a link (YouTube, or a public link to a video file); BatchEdits downloads it. */
|
|
302
|
+
async uploadLink(url) {
|
|
303
|
+
const res = await this.call("upload_videos", { video_url: url });
|
|
304
|
+
const video = res.videos?.[0];
|
|
305
|
+
if (!video) throw new CliError("The server didn't upload the link.", EXIT.ERROR, "It may not support links yet. Download the video and upload the file instead.");
|
|
306
|
+
return video;
|
|
307
|
+
}
|
|
308
|
+
/** Saves a new style copying a reference video's look; returns its id, name and what was copied. */
|
|
309
|
+
async copyStyle(ref, name) {
|
|
310
|
+
return this.call("copy_style", { ...ref, ...name ? { name } : {} });
|
|
311
|
+
}
|
|
301
312
|
/** Streams a download link to `dir`, never overwriting an existing file. */
|
|
302
313
|
async download(url, name, dir) {
|
|
303
314
|
import_node_fs2.default.mkdirSync(dir, { recursive: true });
|
|
@@ -312,7 +323,7 @@ var BatchEdits = class {
|
|
|
312
323
|
}
|
|
313
324
|
};
|
|
314
325
|
function creditsHint(message, host) {
|
|
315
|
-
if (/credit/i.test(message)) return `Start your free trial or upgrade: ${host}/onboarding/pricing`;
|
|
326
|
+
if (/credit/i.test(message) && !/https?:\/\//.test(message)) return `Start your free trial or upgrade: ${host}/onboarding/pricing?from=cli`;
|
|
316
327
|
return void 0;
|
|
317
328
|
}
|
|
318
329
|
|
|
@@ -340,6 +351,22 @@ function expandFiles(inputs) {
|
|
|
340
351
|
if (files.length > MAX_BATCH) throw new CliError(`${files.length} files: upload up to ${MAX_BATCH} at a time.`, EXIT.USAGE, "Split the folder and run once per part.");
|
|
341
352
|
return files;
|
|
342
353
|
}
|
|
354
|
+
var isLink = (input) => /^https?:\/\//i.test(input);
|
|
355
|
+
function expandInputs(inputs) {
|
|
356
|
+
const result = [];
|
|
357
|
+
for (const input of inputs) {
|
|
358
|
+
if (isLink(input)) result.push({ kind: "link", url: input });
|
|
359
|
+
else result.push(...expandFiles([input]).map((p) => ({ kind: "file", path: p })));
|
|
360
|
+
}
|
|
361
|
+
if (result.length === 0) throw new CliError("Give files, folders or video links.", EXIT.USAGE, "Example: batchedits upload ./raw https://youtu.be/\u2026");
|
|
362
|
+
if (result.length > MAX_BATCH) throw new CliError(`${result.length} videos: upload up to ${MAX_BATCH} at a time.`, EXIT.USAGE, "Split them and run once per part.");
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
var inputLabel = (input) => input.kind === "link" ? input.url : import_node_path3.default.basename(input.path);
|
|
366
|
+
async function uploadInput(api, input, i, total) {
|
|
367
|
+
info(`${input.kind === "link" ? "Importing" : "Uploading"} ${i + 1}/${total}: ${inputLabel(input)}${input.kind === "link" ? " (links can take a minute)" : ""}`);
|
|
368
|
+
return input.kind === "link" ? api.uploadLink(input.url) : api.upload(input.path);
|
|
369
|
+
}
|
|
343
370
|
async function resolveStyle(api, ref) {
|
|
344
371
|
if (typeof ref !== "string" || !ref) throw new CliError("--style is required.", EXIT.USAGE, "List them with: batchedits styles");
|
|
345
372
|
const styles = await api.styles();
|
|
@@ -355,7 +382,7 @@ async function confirm(flags, question) {
|
|
|
355
382
|
rl.close();
|
|
356
383
|
if (answer !== "y" && answer !== "yes") throw new CliError("Cancelled. Nothing was processed.", EXIT.ERROR);
|
|
357
384
|
}
|
|
358
|
-
var ACTION_ORDER = ["crop_video", "remove_silence", "remove_repeated_sentences", "add_captions", "punch_zoom", "ai_clips", "hook"];
|
|
385
|
+
var ACTION_ORDER = ["crop_video", "remove_silence", "remove_repeated_sentences", "broll", "add_captions", "punch_zoom", "ai_clips", "hook", "cta", "sound_effects"];
|
|
359
386
|
var ACTION_ALIASES = {
|
|
360
387
|
crop: "crop_video",
|
|
361
388
|
silence: "remove_silence",
|
|
@@ -363,13 +390,19 @@ var ACTION_ALIASES = {
|
|
|
363
390
|
captions: "add_captions",
|
|
364
391
|
zoom: "punch_zoom",
|
|
365
392
|
clips: "ai_clips",
|
|
366
|
-
hook: "hook"
|
|
393
|
+
hook: "hook",
|
|
394
|
+
cta: "cta",
|
|
395
|
+
broll: "broll",
|
|
396
|
+
sfx: "sound_effects"
|
|
367
397
|
};
|
|
368
398
|
var SET_HELP = [
|
|
369
399
|
" --set <action.setting=value> Any setting from `batchedits actions` (repeatable). Adds the edit if missing.",
|
|
370
400
|
" captions.position=top|center|bottom captions.fontColor=#FFFFFF captions.highlightColor=#FF6B00",
|
|
371
401
|
" captions.fontSize=64 silence.silentThreshold=0.06 zoom.zoomPercent=125 clips.maxSeconds=60",
|
|
372
|
-
" hook.position=top|center|bottom hook.y=0.3 hook.duration=auto|3 hook.source=ai|manual hook.animation=fade|pop|none hook.backgroundColor=#FF6B00"
|
|
402
|
+
" hook.position=top|center|bottom hook.y=0.3 hook.duration=auto|3 hook.source=ai|manual hook.animation=fade|pop|none hook.backgroundColor=#FF6B00",
|
|
403
|
+
' cta.text="Link in bio" cta.duration=auto|5 cta.position=center|top|bottom cta.animation=pop|fade|none',
|
|
404
|
+
" broll.seconds=auto|3 broll.source=mine_first|mine|stock",
|
|
405
|
+
" sfx.volume=low|medium|high sfx.zoom=whoosh|impact|off sfx.emojis=false sfx.hook=false sfx.cta=false sfx.broll=false"
|
|
373
406
|
].join("\n");
|
|
374
407
|
var actionName = (raw) => ACTION_ALIASES[raw.trim().toLowerCase()] ?? raw.trim().toLowerCase();
|
|
375
408
|
var list = (v) => Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
|
|
@@ -394,6 +427,9 @@ function applyFlagActions(actions, flags) {
|
|
|
394
427
|
} else if (flags.hook) {
|
|
395
428
|
out = upsert(out, "hook");
|
|
396
429
|
}
|
|
430
|
+
if (flags.broll) out = upsert(out, "broll");
|
|
431
|
+
if (flags.sfx) out = upsert(out, "sound_effects");
|
|
432
|
+
if (flags.cta) out = upsert(out, "cta", typeof flags.cta === "string" && flags.cta.trim() ? { text: flags.cta.trim() } : void 0);
|
|
397
433
|
return out;
|
|
398
434
|
}
|
|
399
435
|
function parseValue(raw) {
|
|
@@ -425,17 +461,20 @@ function describeActions(actions) {
|
|
|
425
461
|
return p.length ? `${a.name} (${p.map(([k, v]) => `${k} ${v}`).join(", ")})` : a.name;
|
|
426
462
|
}).join(", ");
|
|
427
463
|
}
|
|
428
|
-
function
|
|
464
|
+
function readLinesFile(file, style, count, kind) {
|
|
429
465
|
if (typeof file !== "string" || !file) return [];
|
|
430
|
-
|
|
466
|
+
const noun = kind === "hook" ? "hook" : "call to action";
|
|
467
|
+
if (!import_node_fs3.default.existsSync(file)) throw new CliError(`No such file: ${file}`, EXIT.USAGE, `Pass a text file with one ${noun} per line.`);
|
|
431
468
|
const lines = import_node_fs3.default.readFileSync(file, "utf8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
432
|
-
if (!(style.actions ?? []).some((a) => a.name ===
|
|
433
|
-
info(`Note: "${style.name}" has no
|
|
469
|
+
if (!(style.actions ?? []).some((a) => a.name === kind)) {
|
|
470
|
+
info(`Note: "${style.name}" has no ${noun}, so --${kind}s won't show. Add one with: batchedits style edit "${style.name}" --${kind}`);
|
|
434
471
|
}
|
|
435
|
-
|
|
436
|
-
|
|
472
|
+
const rest = kind === "hook" ? "the rest get an AI hook (or none, if the style's hook source is manual)" : "the rest get the style's line";
|
|
473
|
+
if (lines.length < count) info(`Note: ${lines.length} line${lines.length === 1 ? "" : "s"} in ${file} for ${count} videos; ${rest}.`);
|
|
474
|
+
else if (lines.length > count) info(`Note: ${lines.length - count} extra line${lines.length - count === 1 ? "" : "s"} in ${file} ignored.`);
|
|
437
475
|
return lines.slice(0, count).map((l) => l.slice(0, 120));
|
|
438
476
|
}
|
|
477
|
+
var perVideoText = (hooks, ctas, i) => ({ ...hooks[i] ? { hookText: hooks[i] } : {}, ...ctas[i] ? { ctaText: ctas[i] } : {} });
|
|
439
478
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
440
479
|
function withClips(all, ids) {
|
|
441
480
|
const wanted = new Set(ids);
|
|
@@ -557,7 +596,7 @@ var commands = {
|
|
|
557
596
|
},
|
|
558
597
|
"style create": {
|
|
559
598
|
summary: "Create a style from simple flags",
|
|
560
|
-
usage: "batchedits style create <name> [--captions] [--emojis] [--silence] [--crop 9:16] [--zoom] [--clips [n]] [--repeats] [--hook] [--set action.setting=value\u2026]",
|
|
599
|
+
usage: "batchedits style create <name> [--captions] [--emojis] [--silence] [--crop 9:16] [--zoom] [--clips [n]] [--repeats] [--hook] [--broll] [--cta [text]] [--sfx] [--set action.setting=value\u2026]",
|
|
561
600
|
help: [
|
|
562
601
|
"Creates a reusable style. Order of edits: crop, silence, repeats, captions, zoom, AI clips.",
|
|
563
602
|
" --captions Auto captions (word-by-word highlight)",
|
|
@@ -568,12 +607,17 @@ var commands = {
|
|
|
568
607
|
" --zoom Zoom in on key moments",
|
|
569
608
|
" --clips [n] Cut long videos into n short clips (default 10; 1 credit per clip)",
|
|
570
609
|
" --hook A text hook at the start of each video (AI writes it, or pass --hooks to run/process)",
|
|
571
|
-
" --hook-clip <id|words> An intro clip before each video
|
|
610
|
+
" --hook-clip <ai|id|words> An intro clip before each video: ai (AI picks a matching clip for each video),",
|
|
611
|
+
" or one clip from `batchedits hooks` (an id, or words for the best match).",
|
|
572
612
|
" With --hook too, the text hook is drawn on the clip.",
|
|
613
|
+
" --broll Cut away to your B-roll while you talk about what it shows (name clips in the dashboard's Library)",
|
|
614
|
+
` --cta [text] A call to action in each video's last seconds (default "Follow for more"; pass --ctas to run/process for one per video)`,
|
|
615
|
+
" --sfx Sound effects on the other edits: whoosh on B-roll and zooms, pop on emojis, riser into the hook, ding on the CTA",
|
|
573
616
|
SET_HELP,
|
|
574
617
|
'Example: batchedits style create "Shorts" --crop 9:16 --silence --captions --zoom --set captions.position=center',
|
|
575
618
|
'Example: batchedits style create "Hooks" --captions --hook --set hook.position=top --set hook.duration=3',
|
|
576
|
-
'Example: batchedits style create "Intro" --captions --hook --hook-clip "surprise reaction"'
|
|
619
|
+
'Example: batchedits style create "Intro" --captions --hook --hook-clip "surprise reaction"',
|
|
620
|
+
`Example: batchedits style create "Ends" --captions --cta "Comment GUIDE and I'll send it"`
|
|
577
621
|
].join("\n"),
|
|
578
622
|
async run({ api, positionals, flags }) {
|
|
579
623
|
const name = positionals.join(" ").trim();
|
|
@@ -626,16 +670,51 @@ Use it with: --style "${name}"`);
|
|
|
626
670
|
return EXIT.OK;
|
|
627
671
|
}
|
|
628
672
|
},
|
|
673
|
+
"style copy": {
|
|
674
|
+
summary: "Create a style that copies a video's look",
|
|
675
|
+
usage: "batchedits style copy <link|file|video id> [--name <name>]",
|
|
676
|
+
help: [
|
|
677
|
+
"Reads a reference video's captions (font, colours, word or letter highlight, position), zooms and crop, and saves them as a new style.",
|
|
678
|
+
"The reference is a YouTube, TikTok, Instagram, X or Vimeo link, a direct link to a video file, a local file (uploaded first), or the id of one of your videos.",
|
|
679
|
+
"Only the first 90 seconds are read. Colours and font are a close match. Uses no credits; up to 10 copies a day.",
|
|
680
|
+
" --name <name> Name for the new style (default: one based on the look)",
|
|
681
|
+
'Example: batchedits style copy https://www.tiktok.com/@creator/video/123 --name "Like this"',
|
|
682
|
+
'Example: batchedits style copy ./reference.mp4 && batchedits run ./raw --style "Like this" --yes'
|
|
683
|
+
].join("\n"),
|
|
684
|
+
async run({ api, positionals, flags }) {
|
|
685
|
+
const ref = positionals.join(" ").trim();
|
|
686
|
+
if (!ref) throw new CliError("Give a reference video: a link, a file or a video id.", EXIT.USAGE, "Example: batchedits style copy https://youtu.be/\u2026");
|
|
687
|
+
const name = typeof flags.name === "string" && flags.name.trim() ? flags.name.trim() : void 0;
|
|
688
|
+
let source;
|
|
689
|
+
if (isLink(ref)) source = { video_url: ref };
|
|
690
|
+
else if (import_node_fs3.default.existsSync(ref) && import_node_fs3.default.statSync(ref).isFile()) {
|
|
691
|
+
info(`Uploading ${import_node_path3.default.basename(ref)}\u2026`);
|
|
692
|
+
source = { video_id: (await api.upload(ref)).id };
|
|
693
|
+
} else if (/^[0-9a-f-]{8,64}$/i.test(ref)) source = { video_id: ref };
|
|
694
|
+
else throw new CliError(`No such file, link or video id: ${ref}`, EXIT.USAGE, "Give a link (https://\u2026), a local video file, or an id from: batchedits status");
|
|
695
|
+
info("Reading the reference video's style (about 20-60 seconds)\u2026");
|
|
696
|
+
const res = await api.copyStyle(source, name);
|
|
697
|
+
if (!res.styleId) throw new CliError("The server didn't create the style.", EXIT.ERROR, "It may not support copying styles yet. Use: batchedits style create");
|
|
698
|
+
if (isJson()) printJson({ styleId: res.styleId, name: res.name, summary: res.summary ?? [] });
|
|
699
|
+
else {
|
|
700
|
+
say(`Created style "${res.name}" (${res.styleId}).`);
|
|
701
|
+
for (const line of res.summary ?? []) say(` \u2022 ${line}`);
|
|
702
|
+
say(`Colours and font are a close match.
|
|
703
|
+
Use it with: --style "${res.name}"`);
|
|
704
|
+
}
|
|
705
|
+
return EXIT.OK;
|
|
706
|
+
}
|
|
707
|
+
},
|
|
629
708
|
upload: {
|
|
630
|
-
summary: "Upload
|
|
631
|
-
usage: "batchedits upload <files or
|
|
632
|
-
help: "Uploads videos or audio and prints their ids. A folder uploads the media files inside it.\nExample: batchedits upload ./raw",
|
|
709
|
+
summary: "Upload videos from files, folders or links (up to 50)",
|
|
710
|
+
usage: "batchedits upload <files, folders or links\u2026>",
|
|
711
|
+
help: "Uploads videos or audio and prints their ids. A folder uploads the media files inside it. A link (YouTube, or a public link to a video file such as Dropbox or Google Drive) is downloaded by BatchEdits.\nExample: batchedits upload ./raw https://youtu.be/dQw4w9WgXcQ",
|
|
633
712
|
async run({ api, positionals }) {
|
|
634
|
-
const
|
|
713
|
+
const inputs = expandInputs(positionals);
|
|
635
714
|
const uploaded = [];
|
|
636
|
-
for (let i = 0; i <
|
|
637
|
-
|
|
638
|
-
uploaded.push({ ...await api.
|
|
715
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
716
|
+
const input = inputs[i];
|
|
717
|
+
uploaded.push({ ...await uploadInput(api, input, i, inputs.length), file: input.kind === "link" ? input.url : input.path });
|
|
639
718
|
}
|
|
640
719
|
if (isJson()) printJson({ videos: uploaded });
|
|
641
720
|
else {
|
|
@@ -648,18 +727,19 @@ Next: batchedits process ${uploaded.map((v) => v.id).join(" ")} --style "<name>"
|
|
|
648
727
|
},
|
|
649
728
|
process: {
|
|
650
729
|
summary: "Edit videos with a style (uses credits)",
|
|
651
|
-
usage: "batchedits process <video ids\u2026> --style <name|id> [--hooks <file>] [--yes]",
|
|
652
|
-
help: 'Queues each video with the style. Each video uses 1 credit; with AI clips each clip uses 1.\nAsks before spending credits; agents and scripts pass --yes.\n --hooks <file> One hook per line, in the same order as the ids (for styles with a hook)\nExample: batchedits process 3f2a\u2026 9c1d\u2026 --style "Shorts" --yes',
|
|
730
|
+
usage: "batchedits process <video ids\u2026> --style <name|id> [--hooks <file>] [--ctas <file>] [--yes]",
|
|
731
|
+
help: 'Queues each video with the style. Each video uses 1 credit; with AI clips each clip uses 1.\nAsks before spending credits; agents and scripts pass --yes.\n --hooks <file> One hook per line, in the same order as the ids (for styles with a hook)\n --ctas <file> One call to action per line, same order (for styles with a cta)\nExample: batchedits process 3f2a\u2026 9c1d\u2026 --style "Shorts" --yes',
|
|
653
732
|
async run({ api, positionals, flags }) {
|
|
654
733
|
if (positionals.length === 0) throw new CliError("Give the video ids to process.", EXIT.USAGE, "Find them with: batchedits status");
|
|
655
734
|
const style = await resolveStyle(api, flags.style);
|
|
656
|
-
const hooks =
|
|
735
|
+
const hooks = readLinesFile(flags.hooks, style, positionals.length, "hook");
|
|
736
|
+
const ctas = readLinesFile(flags.ctas, style, positionals.length, "cta");
|
|
657
737
|
const clips = (style.actions ?? []).some((a) => a.name === "ai_clips");
|
|
658
738
|
await confirm(flags, `Edit ${positionals.length} video${positionals.length === 1 ? "" : "s"} with "${style.name}"? This uses ${positionals.length} credit${positionals.length === 1 ? "" : "s"}${clips ? ", plus 1 per AI clip" : ""}.`);
|
|
659
739
|
const results = [];
|
|
660
740
|
for (const [i, videoId] of positionals.entries()) {
|
|
661
741
|
try {
|
|
662
|
-
const r = await api.call("process_video", { videoId, styleId: style.id, ...hooks
|
|
742
|
+
const r = await api.call("process_video", { videoId, styleId: style.id, ...perVideoText(hooks, ctas, i) });
|
|
663
743
|
results.push({ videoId, status: r.status ?? "queued" });
|
|
664
744
|
} catch (err) {
|
|
665
745
|
if (err instanceof CliError && err.hint && results.length === 0) throw err;
|
|
@@ -709,21 +789,19 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
709
789
|
},
|
|
710
790
|
run: {
|
|
711
791
|
summary: "Upload, edit, wait and download in one go",
|
|
712
|
-
usage: "batchedits run <files or
|
|
713
|
-
help: 'Uploads up to 50 videos, edits them with the style, waits, and downloads the results.\n --hooks <file> One hook per line, in upload order (a folder uploads its files A-Z), for styles with a hook\nExample: batchedits run ./raw --style "Shorts" --out ./edited --yes',
|
|
792
|
+
usage: "batchedits run <files, folders or links\u2026> --style <name|id> [--out ./edited] [--hooks <file>] [--ctas <file>] [--yes]",
|
|
793
|
+
help: 'Uploads up to 50 videos (files, folders, or YouTube and video-file links), edits them with the style, waits, and downloads the results.\n --hooks <file> One hook per line, in upload order (a folder uploads its files A-Z), for styles with a hook\n --ctas <file> One call to action per line, same order, for styles with a cta\nExample: batchedits run ./raw --style "Shorts" --out ./edited --yes',
|
|
714
794
|
async run(ctx) {
|
|
715
795
|
const { api, positionals, flags } = ctx;
|
|
716
|
-
const
|
|
796
|
+
const inputs = expandInputs(positionals);
|
|
717
797
|
const style = await resolveStyle(api, flags.style);
|
|
718
|
-
const hooks =
|
|
798
|
+
const hooks = readLinesFile(flags.hooks, style, inputs.length, "hook");
|
|
799
|
+
const ctas = readLinesFile(flags.ctas, style, inputs.length, "cta");
|
|
719
800
|
const clips = (style.actions ?? []).some((a) => a.name === "ai_clips");
|
|
720
|
-
await confirm(flags, `Edit ${
|
|
801
|
+
await confirm(flags, `Edit ${inputs.length} video${inputs.length === 1 ? "" : "s"} with "${style.name}"? This uses ${inputs.length} credit${inputs.length === 1 ? "" : "s"}${clips ? ", plus 1 per AI clip" : ""}.`);
|
|
721
802
|
const ids = [];
|
|
722
|
-
for (let i = 0; i <
|
|
723
|
-
|
|
724
|
-
ids.push((await api.upload(files[i])).id);
|
|
725
|
-
}
|
|
726
|
-
for (const [i, videoId] of ids.entries()) await api.call("process_video", { videoId, styleId: style.id, ...hooks[i] ? { hookText: hooks[i] } : {} });
|
|
803
|
+
for (let i = 0; i < inputs.length; i++) ids.push((await uploadInput(api, inputs[i], i, inputs.length)).id);
|
|
804
|
+
for (const [i, videoId] of ids.entries()) await api.call("process_video", { videoId, styleId: style.id, ...perVideoText(hooks, ctas, i) });
|
|
727
805
|
info(`Editing ${ids.length} video${ids.length === 1 ? "" : "s"} with "${style.name}"\u2026`);
|
|
728
806
|
const group = await watch(api, ids, Number(flags.interval) || 10);
|
|
729
807
|
const { saved, notReady } = await downloadAll(api, group, typeof flags.out === "string" ? flags.out : "edited");
|
|
@@ -739,7 +817,7 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
739
817
|
};
|
|
740
818
|
|
|
741
819
|
// src/cli.ts
|
|
742
|
-
var VERSION = true ? "1.
|
|
820
|
+
var VERSION = true ? "1.6.0" : "dev";
|
|
743
821
|
var FLAG_OPTIONS = {
|
|
744
822
|
help: { type: "boolean", short: "h" },
|
|
745
823
|
version: { type: "boolean", short: "v" },
|
|
@@ -760,6 +838,10 @@ var FLAG_OPTIONS = {
|
|
|
760
838
|
hook: { type: "boolean" },
|
|
761
839
|
"hook-clip": { type: "string" },
|
|
762
840
|
hooks: { type: "string" },
|
|
841
|
+
cta: { type: "string" },
|
|
842
|
+
broll: { type: "boolean" },
|
|
843
|
+
sfx: { type: "boolean" },
|
|
844
|
+
ctas: { type: "string" },
|
|
763
845
|
set: { type: "string", multiple: true },
|
|
764
846
|
remove: { type: "string", multiple: true },
|
|
765
847
|
name: { type: "string" }
|
|
@@ -782,18 +864,21 @@ function mainHelp() {
|
|
|
782
864
|
" batchedits login",
|
|
783
865
|
' batchedits style create "Shorts" --crop 9:16 --silence --captions --zoom',
|
|
784
866
|
' batchedits style edit "Shorts" --set captions.position=center',
|
|
867
|
+
' batchedits style copy https://www.tiktok.com/@creator/video/123 --name "Like this"',
|
|
785
868
|
' batchedits run ./raw --style "Shorts" --out ./edited',
|
|
869
|
+
' batchedits run https://youtu.be/dQw4w9WgXcQ ./raw --style "Shorts" --yes',
|
|
786
870
|
"",
|
|
787
871
|
"Exit codes: 0 ok \xB7 1 error \xB7 2 usage \xB7 3 sign in needed \xB7 4 some videos failed \xB7 5 needs --yes"
|
|
788
872
|
].join("\n");
|
|
789
873
|
}
|
|
790
874
|
function normalizeArgv(argv) {
|
|
791
|
-
return argv.flatMap((a, i) => a === "--clips" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? [
|
|
875
|
+
return argv.flatMap((a, i) => (a === "--clips" || a === "--cta") && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? [`${a}=`] : [a]);
|
|
792
876
|
}
|
|
793
877
|
async function main() {
|
|
794
878
|
const { values, positionals } = (0, import_node_util.parseArgs)({ args: normalizeArgv(process.argv.slice(2)), options: FLAG_OPTIONS, allowPositionals: true, strict: true });
|
|
795
879
|
const flags = values;
|
|
796
880
|
if (flags.clips === "") flags.clips = true;
|
|
881
|
+
if (flags.cta === "") flags.cta = true;
|
|
797
882
|
setJsonMode(Boolean(flags.json));
|
|
798
883
|
if (flags.version) {
|
|
799
884
|
console.log(VERSION);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "batchedits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "BatchEdits CLI, SDK and React widgets: edit up to 50 videos at once from your terminal or AI agent (Claude Code, OpenClaw, Hermes)",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|