batchedits 1.3.1 → 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 +24 -2
- package/dist/cli.js +65 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,13 +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] [--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
|
+
| `hooks [words]` | Find intro clips for a video hook |
|
|
23
24
|
| `style edit <name> [same flags] [--set …] [--remove <action>…] [--name <new>]` | Change a saved style's edits and settings |
|
|
24
25
|
| `upload <files or folders…>` | Upload up to 50 videos, print their ids |
|
|
25
26
|
| `process <ids…> --style <name> [--yes]` | Edit videos (1 credit each, 1 per AI clip) |
|
|
26
27
|
| `status [ids…] [--watch]` | Show status, or wait until done |
|
|
27
28
|
| `download <ids…> [--out dir]` | Save finished videos (and their AI clips) |
|
|
28
|
-
| `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 |
|
|
29
30
|
|
|
30
31
|
Every command has `--help` with an example, and `--json` for machine-readable output.
|
|
31
32
|
|
|
@@ -35,6 +36,14 @@ Every command has `--help` with an example, and `--json` for machine-readable ou
|
|
|
35
36
|
|
|
36
37
|
### Hooks
|
|
37
38
|
|
|
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
|
+
|
|
41
|
+
```bash
|
|
42
|
+
batchedits hooks surprise reaction
|
|
43
|
+
batchedits style create "Intro" --captions --hook --hook-clip ai
|
|
44
|
+
batchedits style create "Intro" --captions --hook --hook-clip 727
|
|
45
|
+
```
|
|
46
|
+
|
|
38
47
|
`--hook` adds a text hook at the start of every video; AI writes each line. To use your own, put one hook per line in a file and pass it with `--hooks` (line 1 → first video, in upload order; a folder uploads its files A-Z):
|
|
39
48
|
|
|
40
49
|
```bash
|
|
@@ -42,6 +51,19 @@ batchedits style create "Hooks" --captions --silence --hook --set hook.position=
|
|
|
42
51
|
batchedits run ./raw --style "Hooks" --hooks hooks.txt --out ./edited --yes
|
|
43
52
|
```
|
|
44
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
|
+
|
|
45
67
|
```bash
|
|
46
68
|
batchedits style create "Shorts" --crop 9:16 --silence --captions \
|
|
47
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] : [];
|
|
@@ -387,7 +391,15 @@ function applyFlagActions(actions, flags) {
|
|
|
387
391
|
if (flags.captions || flags.emojis) out = upsert(out, "add_captions", flags.emojis ? { emojis: true } : void 0);
|
|
388
392
|
if (flags.zoom) out = upsert(out, "punch_zoom");
|
|
389
393
|
if (flags.clips) out = upsert(out, "ai_clips", typeof flags.clips === "string" ? { maxClips: Number(flags.clips) } : void 0);
|
|
390
|
-
|
|
394
|
+
const clip = typeof flags["hook-clip"] === "string" ? flags["hook-clip"].trim() : "";
|
|
395
|
+
if (clip) {
|
|
396
|
+
const pick = /^\d+$/.test(clip) ? { video: { libraryId: Number(clip) } } : { videoQuery: clip };
|
|
397
|
+
out = upsert(out, "hook", { kinds: flags.hook ? ["text", "video"] : ["video"], ...pick });
|
|
398
|
+
} else if (flags.hook) {
|
|
399
|
+
out = upsert(out, "hook");
|
|
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);
|
|
391
403
|
return out;
|
|
392
404
|
}
|
|
393
405
|
function parseValue(raw) {
|
|
@@ -419,17 +431,20 @@ function describeActions(actions) {
|
|
|
419
431
|
return p.length ? `${a.name} (${p.map(([k, v]) => `${k} ${v}`).join(", ")})` : a.name;
|
|
420
432
|
}).join(", ");
|
|
421
433
|
}
|
|
422
|
-
function
|
|
434
|
+
function readLinesFile(file, style, count, kind) {
|
|
423
435
|
if (typeof file !== "string" || !file) return [];
|
|
424
|
-
|
|
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.`);
|
|
425
438
|
const lines = import_node_fs3.default.readFileSync(file, "utf8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
426
|
-
if (!(style.actions ?? []).some((a) => a.name ===
|
|
427
|
-
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}`);
|
|
428
441
|
}
|
|
429
|
-
|
|
430
|
-
|
|
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.`);
|
|
431
445
|
return lines.slice(0, count).map((l) => l.slice(0, 120));
|
|
432
446
|
}
|
|
447
|
+
var perVideoText = (hooks, ctas, i) => ({ ...hooks[i] ? { hookText: hooks[i] } : {}, ...ctas[i] ? { ctaText: ctas[i] } : {} });
|
|
433
448
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
434
449
|
function withClips(all, ids) {
|
|
435
450
|
const wanted = new Set(ids);
|
|
@@ -536,9 +551,22 @@ var commands = {
|
|
|
536
551
|
return EXIT.OK;
|
|
537
552
|
}
|
|
538
553
|
},
|
|
554
|
+
hooks: {
|
|
555
|
+
summary: "Find intro clips for a video hook",
|
|
556
|
+
usage: "batchedits hooks [words]",
|
|
557
|
+
help: 'Searches the transitional hook library (short vertical intro clips). Without words, lists the newest.\nUse an id with: batchedits style create "Intro" --hook-clip <id>\nExample: batchedits hooks surprise reaction',
|
|
558
|
+
async run({ api, positionals }) {
|
|
559
|
+
const query = positionals.join(" ").trim();
|
|
560
|
+
const res = await api.call("search_hook_clips", query ? { query } : {});
|
|
561
|
+
if (isJson()) printJson({ clips: res.clips });
|
|
562
|
+
else if (res.clips.length === 0) say(res.message);
|
|
563
|
+
else table(res.clips.map((c) => ({ id: String(c.id), seconds: `${c.seconds}s`, title: c.title })), ["id", "seconds", "title"]);
|
|
564
|
+
return EXIT.OK;
|
|
565
|
+
}
|
|
566
|
+
},
|
|
539
567
|
"style create": {
|
|
540
568
|
summary: "Create a style from simple flags",
|
|
541
|
-
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]",
|
|
542
570
|
help: [
|
|
543
571
|
"Creates a reusable style. Order of edits: crop, silence, repeats, captions, zoom, AI clips.",
|
|
544
572
|
" --captions Auto captions (word-by-word highlight)",
|
|
@@ -549,9 +577,16 @@ var commands = {
|
|
|
549
577
|
" --zoom Zoom in on key moments",
|
|
550
578
|
" --clips [n] Cut long videos into n short clips (default 10; 1 credit per clip)",
|
|
551
579
|
" --hook A text hook at the start of each video (AI writes it, or pass --hooks to run/process)",
|
|
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).",
|
|
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)`,
|
|
552
585
|
SET_HELP,
|
|
553
586
|
'Example: batchedits style create "Shorts" --crop 9:16 --silence --captions --zoom --set captions.position=center',
|
|
554
|
-
'Example: batchedits style create "Hooks" --captions --hook --set hook.position=top --set hook.duration=3'
|
|
587
|
+
'Example: batchedits style create "Hooks" --captions --hook --set hook.position=top --set hook.duration=3',
|
|
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"`
|
|
555
590
|
].join("\n"),
|
|
556
591
|
async run({ api, positionals, flags }) {
|
|
557
592
|
const name = positionals.join(" ").trim();
|
|
@@ -626,18 +661,19 @@ Next: batchedits process ${uploaded.map((v) => v.id).join(" ")} --style "<name>"
|
|
|
626
661
|
},
|
|
627
662
|
process: {
|
|
628
663
|
summary: "Edit videos with a style (uses credits)",
|
|
629
|
-
usage: "batchedits process <video ids\u2026> --style <name|id> [--hooks <file>] [--yes]",
|
|
630
|
-
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',
|
|
631
666
|
async run({ api, positionals, flags }) {
|
|
632
667
|
if (positionals.length === 0) throw new CliError("Give the video ids to process.", EXIT.USAGE, "Find them with: batchedits status");
|
|
633
668
|
const style = await resolveStyle(api, flags.style);
|
|
634
|
-
const hooks =
|
|
669
|
+
const hooks = readLinesFile(flags.hooks, style, positionals.length, "hook");
|
|
670
|
+
const ctas = readLinesFile(flags.ctas, style, positionals.length, "cta");
|
|
635
671
|
const clips = (style.actions ?? []).some((a) => a.name === "ai_clips");
|
|
636
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" : ""}.`);
|
|
637
673
|
const results = [];
|
|
638
674
|
for (const [i, videoId] of positionals.entries()) {
|
|
639
675
|
try {
|
|
640
|
-
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) });
|
|
641
677
|
results.push({ videoId, status: r.status ?? "queued" });
|
|
642
678
|
} catch (err) {
|
|
643
679
|
if (err instanceof CliError && err.hint && results.length === 0) throw err;
|
|
@@ -687,13 +723,14 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
687
723
|
},
|
|
688
724
|
run: {
|
|
689
725
|
summary: "Upload, edit, wait and download in one go",
|
|
690
|
-
usage: "batchedits run <files or folders\u2026> --style <name|id> [--out ./edited] [--hooks <file>] [--yes]",
|
|
691
|
-
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',
|
|
692
728
|
async run(ctx) {
|
|
693
729
|
const { api, positionals, flags } = ctx;
|
|
694
730
|
const files = expandFiles(positionals);
|
|
695
731
|
const style = await resolveStyle(api, flags.style);
|
|
696
|
-
const hooks =
|
|
732
|
+
const hooks = readLinesFile(flags.hooks, style, files.length, "hook");
|
|
733
|
+
const ctas = readLinesFile(flags.ctas, style, files.length, "cta");
|
|
697
734
|
const clips = (style.actions ?? []).some((a) => a.name === "ai_clips");
|
|
698
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" : ""}.`);
|
|
699
736
|
const ids = [];
|
|
@@ -701,7 +738,7 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
701
738
|
info(`Uploading ${i + 1}/${files.length}: ${import_node_path3.default.basename(files[i])}`);
|
|
702
739
|
ids.push((await api.upload(files[i])).id);
|
|
703
740
|
}
|
|
704
|
-
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) });
|
|
705
742
|
info(`Editing ${ids.length} video${ids.length === 1 ? "" : "s"} with "${style.name}"\u2026`);
|
|
706
743
|
const group = await watch(api, ids, Number(flags.interval) || 10);
|
|
707
744
|
const { saved, notReady } = await downloadAll(api, group, typeof flags.out === "string" ? flags.out : "edited");
|
|
@@ -717,7 +754,7 @@ Next: batchedits status ${positionals.join(" ")} --watch`);
|
|
|
717
754
|
};
|
|
718
755
|
|
|
719
756
|
// src/cli.ts
|
|
720
|
-
var VERSION = true ? "1.
|
|
757
|
+
var VERSION = true ? "1.5.0" : "dev";
|
|
721
758
|
var FLAG_OPTIONS = {
|
|
722
759
|
help: { type: "boolean", short: "h" },
|
|
723
760
|
version: { type: "boolean", short: "v" },
|
|
@@ -736,7 +773,11 @@ var FLAG_OPTIONS = {
|
|
|
736
773
|
zoom: { type: "boolean" },
|
|
737
774
|
clips: { type: "string" },
|
|
738
775
|
hook: { type: "boolean" },
|
|
776
|
+
"hook-clip": { type: "string" },
|
|
739
777
|
hooks: { type: "string" },
|
|
778
|
+
cta: { type: "string" },
|
|
779
|
+
broll: { type: "boolean" },
|
|
780
|
+
ctas: { type: "string" },
|
|
740
781
|
set: { type: "string", multiple: true },
|
|
741
782
|
remove: { type: "string", multiple: true },
|
|
742
783
|
name: { type: "string" }
|
|
@@ -765,12 +806,13 @@ function mainHelp() {
|
|
|
765
806
|
].join("\n");
|
|
766
807
|
}
|
|
767
808
|
function normalizeArgv(argv) {
|
|
768
|
-
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]);
|
|
769
810
|
}
|
|
770
811
|
async function main() {
|
|
771
812
|
const { values, positionals } = (0, import_node_util.parseArgs)({ args: normalizeArgv(process.argv.slice(2)), options: FLAG_OPTIONS, allowPositionals: true, strict: true });
|
|
772
813
|
const flags = values;
|
|
773
814
|
if (flags.clips === "") flags.clips = true;
|
|
815
|
+
if (flags.cta === "") flags.cta = true;
|
|
774
816
|
setJsonMode(Boolean(flags.json));
|
|
775
817
|
if (flags.version) {
|
|
776
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",
|