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.
Files changed (3) hide show
  1. package/README.md +21 -3
  2. package/dist/cli.js +88 -22
  3. 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
- | `upload <files or folders…>` | Upload up to 50 videos, print their ids |
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 folders…> --style <name> [--out dir] [--hooks file] [--ctas file] [--yes]` | All of the above in one go |
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 local videos (up to 50)",
644
- usage: "batchedits upload <files or folders\u2026>",
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 files = expandFiles(positionals);
713
+ const inputs = expandInputs(positionals);
648
714
  const uploaded = [];
649
- for (let i = 0; i < files.length; i++) {
650
- info(`Uploading ${i + 1}/${files.length}: ${import_node_path3.default.basename(files[i])}`);
651
- uploaded.push({ ...await api.upload(files[i]), file: files[i] });
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 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',
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 files = expandFiles(positionals);
796
+ const inputs = expandInputs(positionals);
731
797
  const style = await resolveStyle(api, flags.style);
732
- const hooks = readLinesFile(flags.hooks, style, files.length, "hook");
733
- const ctas = readLinesFile(flags.ctas, style, files.length, "cta");
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 ${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" : ""}.`);
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 < files.length; 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.5.0" : "dev";
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.5.0",
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",