batchedits 1.4.0 → 1.5.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 +17 -3
- package/dist/cli.js +42 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,14 +19,14 @@ 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]] [--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
|
| `upload <files or folders…>` | Upload up to 50 videos, print their ids |
|
|
26
26
|
| `process <ids…> --style <name> [--yes]` | Edit videos (1 credit each, 1 per AI clip) |
|
|
27
27
|
| `status [ids…] [--watch]` | Show status, or wait until done |
|
|
28
28
|
| `download <ids…> [--out dir]` | Save finished videos (and their AI clips) |
|
|
29
|
-
| `run <files or folders…> --style <name> [--out dir] [--hooks file] [--yes]` | All of the above in one go |
|
|
29
|
+
| `run <files or folders…> --style <name> [--out dir] [--hooks file] [--ctas file] [--yes]` | All of the above in one go |
|
|
30
30
|
|
|
31
31
|
Every command has `--help` with an example, and `--json` for machine-readable output.
|
|
32
32
|
|
|
@@ -36,10 +36,11 @@ Every command has `--help` with an example, and `--json` for machine-readable ou
|
|
|
36
36
|
|
|
37
37
|
### Hooks
|
|
38
38
|
|
|
39
|
-
`--hook-clip` plays a short intro clip (a video hook) before every video.
|
|
39
|
+
`--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
40
|
|
|
41
41
|
```bash
|
|
42
42
|
batchedits hooks surprise reaction
|
|
43
|
+
batchedits style create "Intro" --captions --hook --hook-clip ai
|
|
43
44
|
batchedits style create "Intro" --captions --hook --hook-clip 727
|
|
44
45
|
```
|
|
45
46
|
|
|
@@ -50,6 +51,19 @@ batchedits style create "Hooks" --captions --silence --hook --set hook.position=
|
|
|
50
51
|
batchedits run ./raw --style "Hooks" --hooks hooks.txt --out ./edited --yes
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
`--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.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
batchedits style create "Talking head" --silence --broll --captions
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`--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
|
+
|
|
62
|
+
```bash
|
|
63
|
+
batchedits style create "Ends" --captions --cta "Comment GUIDE and I'll send it"
|
|
64
|
+
batchedits run ./raw --style "Ends" --ctas ctas.txt --out ./edited --yes
|
|
65
|
+
```
|
|
66
|
+
|
|
53
67
|
```bash
|
|
54
68
|
batchedits style create "Shorts" --crop 9:16 --silence --captions \
|
|
55
69
|
--set captions.position=center --set captions.highlightColor=#FF6B00 --set zoom.zoomPercent=125
|
package/dist/cli.js
CHANGED
|
@@ -312,7 +312,7 @@ var BatchEdits = class {
|
|
|
312
312
|
}
|
|
313
313
|
};
|
|
314
314
|
function creditsHint(message, host) {
|
|
315
|
-
if (/credit/i.test(message)) return `Start your free trial or upgrade: ${host}/onboarding/pricing`;
|
|
315
|
+
if (/credit/i.test(message) && !/https?:\/\//.test(message)) return `Start your free trial or upgrade: ${host}/onboarding/pricing?from=cli`;
|
|
316
316
|
return void 0;
|
|
317
317
|
}
|
|
318
318
|
|
|
@@ -355,7 +355,7 @@ async function confirm(flags, question) {
|
|
|
355
355
|
rl.close();
|
|
356
356
|
if (answer !== "y" && answer !== "yes") throw new CliError("Cancelled. Nothing was processed.", EXIT.ERROR);
|
|
357
357
|
}
|
|
358
|
-
var ACTION_ORDER = ["crop_video", "remove_silence", "remove_repeated_sentences", "add_captions", "punch_zoom", "ai_clips", "hook"];
|
|
358
|
+
var ACTION_ORDER = ["crop_video", "remove_silence", "remove_repeated_sentences", "broll", "add_captions", "punch_zoom", "ai_clips", "hook", "cta"];
|
|
359
359
|
var ACTION_ALIASES = {
|
|
360
360
|
crop: "crop_video",
|
|
361
361
|
silence: "remove_silence",
|
|
@@ -363,13 +363,17 @@ var ACTION_ALIASES = {
|
|
|
363
363
|
captions: "add_captions",
|
|
364
364
|
zoom: "punch_zoom",
|
|
365
365
|
clips: "ai_clips",
|
|
366
|
-
hook: "hook"
|
|
366
|
+
hook: "hook",
|
|
367
|
+
cta: "cta",
|
|
368
|
+
broll: "broll"
|
|
367
369
|
};
|
|
368
370
|
var SET_HELP = [
|
|
369
371
|
" --set <action.setting=value> Any setting from `batchedits actions` (repeatable). Adds the edit if missing.",
|
|
370
372
|
" captions.position=top|center|bottom captions.fontColor=#FFFFFF captions.highlightColor=#FF6B00",
|
|
371
373
|
" 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"
|
|
374
|
+
" 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
|
+
' 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"
|
|
373
377
|
].join("\n");
|
|
374
378
|
var actionName = (raw) => ACTION_ALIASES[raw.trim().toLowerCase()] ?? raw.trim().toLowerCase();
|
|
375
379
|
var list = (v) => Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
|
|
@@ -394,6 +398,8 @@ function applyFlagActions(actions, flags) {
|
|
|
394
398
|
} else if (flags.hook) {
|
|
395
399
|
out = upsert(out, "hook");
|
|
396
400
|
}
|
|
401
|
+
if (flags.broll) out = upsert(out, "broll");
|
|
402
|
+
if (flags.cta) out = upsert(out, "cta", typeof flags.cta === "string" && flags.cta.trim() ? { text: flags.cta.trim() } : void 0);
|
|
397
403
|
return out;
|
|
398
404
|
}
|
|
399
405
|
function parseValue(raw) {
|
|
@@ -425,17 +431,20 @@ function describeActions(actions) {
|
|
|
425
431
|
return p.length ? `${a.name} (${p.map(([k, v]) => `${k} ${v}`).join(", ")})` : a.name;
|
|
426
432
|
}).join(", ");
|
|
427
433
|
}
|
|
428
|
-
function
|
|
434
|
+
function readLinesFile(file, style, count, kind) {
|
|
429
435
|
if (typeof file !== "string" || !file) return [];
|
|
430
|
-
|
|
436
|
+
const noun = kind === "hook" ? "hook" : "call to action";
|
|
437
|
+
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
438
|
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
|
|
439
|
+
if (!(style.actions ?? []).some((a) => a.name === kind)) {
|
|
440
|
+
info(`Note: "${style.name}" has no ${noun}, so --${kind}s won't show. Add one with: batchedits style edit "${style.name}" --${kind}`);
|
|
434
441
|
}
|
|
435
|
-
|
|
436
|
-
|
|
442
|
+
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";
|
|
443
|
+
if (lines.length < count) info(`Note: ${lines.length} line${lines.length === 1 ? "" : "s"} in ${file} for ${count} videos; ${rest}.`);
|
|
444
|
+
else if (lines.length > count) info(`Note: ${lines.length - count} extra line${lines.length - count === 1 ? "" : "s"} in ${file} ignored.`);
|
|
437
445
|
return lines.slice(0, count).map((l) => l.slice(0, 120));
|
|
438
446
|
}
|
|
447
|
+
var perVideoText = (hooks, ctas, i) => ({ ...hooks[i] ? { hookText: hooks[i] } : {}, ...ctas[i] ? { ctaText: ctas[i] } : {} });
|
|
439
448
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
440
449
|
function withClips(all, ids) {
|
|
441
450
|
const wanted = new Set(ids);
|
|
@@ -557,7 +566,7 @@ var commands = {
|
|
|
557
566
|
},
|
|
558
567
|
"style create": {
|
|
559
568
|
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]",
|
|
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]",
|
|
561
570
|
help: [
|
|
562
571
|
"Creates a reusable style. Order of edits: crop, silence, repeats, captions, zoom, AI clips.",
|
|
563
572
|
" --captions Auto captions (word-by-word highlight)",
|
|
@@ -568,12 +577,16 @@ var commands = {
|
|
|
568
577
|
" --zoom Zoom in on key moments",
|
|
569
578
|
" --clips [n] Cut long videos into n short clips (default 10; 1 credit per clip)",
|
|
570
579
|
" --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
|
|
580
|
+
" --hook-clip <ai|id|words> An intro clip before each video: ai (AI picks a matching clip for each video),",
|
|
581
|
+
" or one clip from `batchedits hooks` (an id, or words for the best match).",
|
|
572
582
|
" With --hook too, the text hook is drawn on the clip.",
|
|
583
|
+
" --broll Cut away to your B-roll while you talk about what it shows (name clips in the dashboard's Library)",
|
|
584
|
+
` --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)`,
|
|
573
585
|
SET_HELP,
|
|
574
586
|
'Example: batchedits style create "Shorts" --crop 9:16 --silence --captions --zoom --set captions.position=center',
|
|
575
587
|
'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"'
|
|
588
|
+
'Example: batchedits style create "Intro" --captions --hook --hook-clip "surprise reaction"',
|
|
589
|
+
`Example: batchedits style create "Ends" --captions --cta "Comment GUIDE and I'll send it"`
|
|
577
590
|
].join("\n"),
|
|
578
591
|
async run({ api, positionals, flags }) {
|
|
579
592
|
const name = positionals.join(" ").trim();
|
|
@@ -648,18 +661,19 @@ Next: batchedits process ${uploaded.map((v) => v.id).join(" ")} --style "<name>"
|
|
|
648
661
|
},
|
|
649
662
|
process: {
|
|
650
663
|
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',
|
|
664
|
+
usage: "batchedits process <video ids\u2026> --style <name|id> [--hooks <file>] [--ctas <file>] [--yes]",
|
|
665
|
+
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
666
|
async run({ api, positionals, flags }) {
|
|
654
667
|
if (positionals.length === 0) throw new CliError("Give the video ids to process.", EXIT.USAGE, "Find them with: batchedits status");
|
|
655
668
|
const style = await resolveStyle(api, flags.style);
|
|
656
|
-
const hooks =
|
|
669
|
+
const hooks = readLinesFile(flags.hooks, style, positionals.length, "hook");
|
|
670
|
+
const ctas = readLinesFile(flags.ctas, style, positionals.length, "cta");
|
|
657
671
|
const clips = (style.actions ?? []).some((a) => a.name === "ai_clips");
|
|
658
672
|
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
673
|
const results = [];
|
|
660
674
|
for (const [i, videoId] of positionals.entries()) {
|
|
661
675
|
try {
|
|
662
|
-
const r = await api.call("process_video", { videoId, styleId: style.id, ...hooks
|
|
676
|
+
const r = await api.call("process_video", { videoId, styleId: style.id, ...perVideoText(hooks, ctas, i) });
|
|
663
677
|
results.push({ videoId, status: r.status ?? "queued" });
|
|
664
678
|
} catch (err) {
|
|
665
679
|
if (err instanceof CliError && err.hint && results.length === 0) throw err;
|
|
@@ -709,13 +723,14 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
709
723
|
},
|
|
710
724
|
run: {
|
|
711
725
|
summary: "Upload, edit, wait and download in one go",
|
|
712
|
-
usage: "batchedits run <files or folders\u2026> --style <name|id> [--out ./edited] [--hooks <file>] [--yes]",
|
|
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',
|
|
726
|
+
usage: "batchedits run <files or folders\u2026> --style <name|id> [--out ./edited] [--hooks <file>] [--ctas <file>] [--yes]",
|
|
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',
|
|
714
728
|
async run(ctx) {
|
|
715
729
|
const { api, positionals, flags } = ctx;
|
|
716
730
|
const files = expandFiles(positionals);
|
|
717
731
|
const style = await resolveStyle(api, flags.style);
|
|
718
|
-
const hooks =
|
|
732
|
+
const hooks = readLinesFile(flags.hooks, style, files.length, "hook");
|
|
733
|
+
const ctas = readLinesFile(flags.ctas, style, files.length, "cta");
|
|
719
734
|
const clips = (style.actions ?? []).some((a) => a.name === "ai_clips");
|
|
720
735
|
await confirm(flags, `Edit ${files.length} video${files.length === 1 ? "" : "s"} with "${style.name}"? This uses ${files.length} credit${files.length === 1 ? "" : "s"}${clips ? ", plus 1 per AI clip" : ""}.`);
|
|
721
736
|
const ids = [];
|
|
@@ -723,7 +738,7 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
723
738
|
info(`Uploading ${i + 1}/${files.length}: ${import_node_path3.default.basename(files[i])}`);
|
|
724
739
|
ids.push((await api.upload(files[i])).id);
|
|
725
740
|
}
|
|
726
|
-
for (const [i, videoId] of ids.entries()) await api.call("process_video", { videoId, styleId: style.id, ...hooks
|
|
741
|
+
for (const [i, videoId] of ids.entries()) await api.call("process_video", { videoId, styleId: style.id, ...perVideoText(hooks, ctas, i) });
|
|
727
742
|
info(`Editing ${ids.length} video${ids.length === 1 ? "" : "s"} with "${style.name}"\u2026`);
|
|
728
743
|
const group = await watch(api, ids, Number(flags.interval) || 10);
|
|
729
744
|
const { saved, notReady } = await downloadAll(api, group, typeof flags.out === "string" ? flags.out : "edited");
|
|
@@ -739,7 +754,7 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
739
754
|
};
|
|
740
755
|
|
|
741
756
|
// src/cli.ts
|
|
742
|
-
var VERSION = true ? "1.
|
|
757
|
+
var VERSION = true ? "1.5.0" : "dev";
|
|
743
758
|
var FLAG_OPTIONS = {
|
|
744
759
|
help: { type: "boolean", short: "h" },
|
|
745
760
|
version: { type: "boolean", short: "v" },
|
|
@@ -760,6 +775,9 @@ var FLAG_OPTIONS = {
|
|
|
760
775
|
hook: { type: "boolean" },
|
|
761
776
|
"hook-clip": { type: "string" },
|
|
762
777
|
hooks: { type: "string" },
|
|
778
|
+
cta: { type: "string" },
|
|
779
|
+
broll: { type: "boolean" },
|
|
780
|
+
ctas: { type: "string" },
|
|
763
781
|
set: { type: "string", multiple: true },
|
|
764
782
|
remove: { type: "string", multiple: true },
|
|
765
783
|
name: { type: "string" }
|
|
@@ -788,12 +806,13 @@ function mainHelp() {
|
|
|
788
806
|
].join("\n");
|
|
789
807
|
}
|
|
790
808
|
function normalizeArgv(argv) {
|
|
791
|
-
return argv.flatMap((a, i) => a === "--clips" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? [
|
|
809
|
+
return argv.flatMap((a, i) => (a === "--clips" || a === "--cta") && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? [`${a}=`] : [a]);
|
|
792
810
|
}
|
|
793
811
|
async function main() {
|
|
794
812
|
const { values, positionals } = (0, import_node_util.parseArgs)({ args: normalizeArgv(process.argv.slice(2)), options: FLAG_OPTIONS, allowPositionals: true, strict: true });
|
|
795
813
|
const flags = values;
|
|
796
814
|
if (flags.clips === "") flags.clips = true;
|
|
815
|
+
if (flags.cta === "") flags.cta = true;
|
|
797
816
|
setJsonMode(Boolean(flags.json));
|
|
798
817
|
if (flags.version) {
|
|
799
818
|
console.log(VERSION);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "batchedits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|