batchedits 1.5.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 +21 -3
- package/dist/cli.js +88 -22
- 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>] [--broll] [--cta [text]] [--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
|
|
|
@@ -57,6 +58,12 @@ batchedits run ./raw --style "Hooks" --hooks hooks.txt --out ./edited --yes
|
|
|
57
58
|
batchedits style create "Talking head" --silence --broll --captions
|
|
58
59
|
```
|
|
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
|
+
|
|
60
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:
|
|
61
68
|
|
|
62
69
|
```bash
|
|
@@ -106,3 +113,14 @@ openModal({ videoUrl: "https://example.com/video.mp4", mode: "popup", onComplete
|
|
|
106
113
|
```
|
|
107
114
|
|
|
108
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 });
|
|
@@ -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", "broll", "add_captions", "punch_zoom", "ai_clips", "hook", "cta"];
|
|
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",
|
|
@@ -365,7 +392,8 @@ var ACTION_ALIASES = {
|
|
|
365
392
|
clips: "ai_clips",
|
|
366
393
|
hook: "hook",
|
|
367
394
|
cta: "cta",
|
|
368
|
-
broll: "broll"
|
|
395
|
+
broll: "broll",
|
|
396
|
+
sfx: "sound_effects"
|
|
369
397
|
};
|
|
370
398
|
var SET_HELP = [
|
|
371
399
|
" --set <action.setting=value> Any setting from `batchedits actions` (repeatable). Adds the edit if missing.",
|
|
@@ -373,7 +401,8 @@ var SET_HELP = [
|
|
|
373
401
|
" captions.fontSize=64 silence.silentThreshold=0.06 zoom.zoomPercent=125 clips.maxSeconds=60",
|
|
374
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",
|
|
375
403
|
' cta.text="Link in bio" cta.duration=auto|5 cta.position=center|top|bottom cta.animation=pop|fade|none',
|
|
376
|
-
" broll.seconds=auto|3 broll.source=mine_first|mine|stock"
|
|
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"
|
|
377
406
|
].join("\n");
|
|
378
407
|
var actionName = (raw) => ACTION_ALIASES[raw.trim().toLowerCase()] ?? raw.trim().toLowerCase();
|
|
379
408
|
var list = (v) => Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
|
|
@@ -399,6 +428,7 @@ function applyFlagActions(actions, flags) {
|
|
|
399
428
|
out = upsert(out, "hook");
|
|
400
429
|
}
|
|
401
430
|
if (flags.broll) out = upsert(out, "broll");
|
|
431
|
+
if (flags.sfx) out = upsert(out, "sound_effects");
|
|
402
432
|
if (flags.cta) out = upsert(out, "cta", typeof flags.cta === "string" && flags.cta.trim() ? { text: flags.cta.trim() } : void 0);
|
|
403
433
|
return out;
|
|
404
434
|
}
|
|
@@ -566,7 +596,7 @@ var commands = {
|
|
|
566
596
|
},
|
|
567
597
|
"style create": {
|
|
568
598
|
summary: "Create a style from simple flags",
|
|
569
|
-
usage: "batchedits style create <name> [--captions] [--emojis] [--silence] [--crop 9:16] [--zoom] [--clips [n]] [--repeats] [--hook] [--broll] [--cta [text]] [--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]",
|
|
570
600
|
help: [
|
|
571
601
|
"Creates a reusable style. Order of edits: crop, silence, repeats, captions, zoom, AI clips.",
|
|
572
602
|
" --captions Auto captions (word-by-word highlight)",
|
|
@@ -582,6 +612,7 @@ var commands = {
|
|
|
582
612
|
" With --hook too, the text hook is drawn on the clip.",
|
|
583
613
|
" --broll Cut away to your B-roll while you talk about what it shows (name clips in the dashboard's Library)",
|
|
584
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",
|
|
585
616
|
SET_HELP,
|
|
586
617
|
'Example: batchedits style create "Shorts" --crop 9:16 --silence --captions --zoom --set captions.position=center',
|
|
587
618
|
'Example: batchedits style create "Hooks" --captions --hook --set hook.position=top --set hook.duration=3',
|
|
@@ -639,16 +670,51 @@ Use it with: --style "${name}"`);
|
|
|
639
670
|
return EXIT.OK;
|
|
640
671
|
}
|
|
641
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
|
+
},
|
|
642
708
|
upload: {
|
|
643
|
-
summary: "Upload
|
|
644
|
-
usage: "batchedits upload <files or
|
|
645
|
-
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",
|
|
646
712
|
async run({ api, positionals }) {
|
|
647
|
-
const
|
|
713
|
+
const inputs = expandInputs(positionals);
|
|
648
714
|
const uploaded = [];
|
|
649
|
-
for (let i = 0; i <
|
|
650
|
-
|
|
651
|
-
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 });
|
|
652
718
|
}
|
|
653
719
|
if (isJson()) printJson({ videos: uploaded });
|
|
654
720
|
else {
|
|
@@ -723,21 +789,18 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
723
789
|
},
|
|
724
790
|
run: {
|
|
725
791
|
summary: "Upload, edit, wait and download in one go",
|
|
726
|
-
usage: "batchedits run <files or
|
|
727
|
-
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\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',
|
|
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',
|
|
728
794
|
async run(ctx) {
|
|
729
795
|
const { api, positionals, flags } = ctx;
|
|
730
|
-
const
|
|
796
|
+
const inputs = expandInputs(positionals);
|
|
731
797
|
const style = await resolveStyle(api, flags.style);
|
|
732
|
-
const hooks = readLinesFile(flags.hooks, style,
|
|
733
|
-
const ctas = readLinesFile(flags.ctas, style,
|
|
798
|
+
const hooks = readLinesFile(flags.hooks, style, inputs.length, "hook");
|
|
799
|
+
const ctas = readLinesFile(flags.ctas, style, inputs.length, "cta");
|
|
734
800
|
const clips = (style.actions ?? []).some((a) => a.name === "ai_clips");
|
|
735
|
-
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" : ""}.`);
|
|
736
802
|
const ids = [];
|
|
737
|
-
for (let i = 0; i <
|
|
738
|
-
info(`Uploading ${i + 1}/${files.length}: ${import_node_path3.default.basename(files[i])}`);
|
|
739
|
-
ids.push((await api.upload(files[i])).id);
|
|
740
|
-
}
|
|
803
|
+
for (let i = 0; i < inputs.length; i++) ids.push((await uploadInput(api, inputs[i], i, inputs.length)).id);
|
|
741
804
|
for (const [i, videoId] of ids.entries()) await api.call("process_video", { videoId, styleId: style.id, ...perVideoText(hooks, ctas, i) });
|
|
742
805
|
info(`Editing ${ids.length} video${ids.length === 1 ? "" : "s"} with "${style.name}"\u2026`);
|
|
743
806
|
const group = await watch(api, ids, Number(flags.interval) || 10);
|
|
@@ -754,7 +817,7 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
754
817
|
};
|
|
755
818
|
|
|
756
819
|
// src/cli.ts
|
|
757
|
-
var VERSION = true ? "1.
|
|
820
|
+
var VERSION = true ? "1.6.0" : "dev";
|
|
758
821
|
var FLAG_OPTIONS = {
|
|
759
822
|
help: { type: "boolean", short: "h" },
|
|
760
823
|
version: { type: "boolean", short: "v" },
|
|
@@ -777,6 +840,7 @@ var FLAG_OPTIONS = {
|
|
|
777
840
|
hooks: { type: "string" },
|
|
778
841
|
cta: { type: "string" },
|
|
779
842
|
broll: { type: "boolean" },
|
|
843
|
+
sfx: { type: "boolean" },
|
|
780
844
|
ctas: { type: "string" },
|
|
781
845
|
set: { type: "string", multiple: true },
|
|
782
846
|
remove: { type: "string", multiple: true },
|
|
@@ -800,7 +864,9 @@ function mainHelp() {
|
|
|
800
864
|
" batchedits login",
|
|
801
865
|
' batchedits style create "Shorts" --crop 9:16 --silence --captions --zoom',
|
|
802
866
|
' batchedits style edit "Shorts" --set captions.position=center',
|
|
867
|
+
' batchedits style copy https://www.tiktok.com/@creator/video/123 --name "Like this"',
|
|
803
868
|
' batchedits run ./raw --style "Shorts" --out ./edited',
|
|
869
|
+
' batchedits run https://youtu.be/dQw4w9WgXcQ ./raw --style "Shorts" --yes',
|
|
804
870
|
"",
|
|
805
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"
|
|
806
872
|
].join("\n");
|
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",
|