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.
Files changed (3) hide show
  1. package/README.md +24 -2
  2. package/dist/cli.js +65 -23
  3. 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
- if (flags.hook) out = upsert(out, "hook");
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 readHooksFile(file, style, count) {
434
+ function readLinesFile(file, style, count, kind) {
423
435
  if (typeof file !== "string" || !file) return [];
424
- 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.`);
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 === "hook")) {
427
- 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}`);
428
441
  }
429
- 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).`);
430
- 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.`);
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 = 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");
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[i] ? { hookText: hooks[i] } : {} });
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 = 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");
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[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) });
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.3.1" : "dev";
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("-")) ? ["--clips="] : [a]);
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.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",