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.
Files changed (3) hide show
  1. package/README.md +17 -3
  2. package/dist/cli.js +42 -23
  3. 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. Find one 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:
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 readHooksFile(file, style, count) {
434
+ function readLinesFile(file, style, count, kind) {
429
435
  if (typeof file !== "string" || !file) return [];
430
- if (!import_node_fs3.default.existsSync(file)) throw new CliError(`No such file: ${file}`, EXIT.USAGE, "Pass a text file with one hook per line.");
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 === "hook")) {
433
- info(`Note: "${style.name}" has no hook, so --hooks won't show. Add one with: batchedits style edit "${style.name}" --hook`);
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
- if (lines.length < count) info(`Note: ${lines.length} hook${lines.length === 1 ? "" : "s"} for ${count} videos; the rest get an AI hook (or none, if the style's hook source is manual).`);
436
- else if (lines.length > count) info(`Note: ${lines.length - count} extra hook line${lines.length - count === 1 ? "" : "s"} ignored.`);
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, from `batchedits hooks` (an id, or words for the best match).",
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 = readHooksFile(flags.hooks, style, positionals.length);
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[i] ? { hookText: hooks[i] } : {} });
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 = readHooksFile(flags.hooks, style, files.length);
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[i] ? { hookText: hooks[i] } : {} });
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.4.0" : "dev";
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("-")) ? ["--clips="] : [a]);
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.4.0",
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",