videodraft 0.1.1 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # videodraft
2
2
 
3
- The official [VideoDraft](https://videodraft.ai) CLI — create AI videos, images, voiceovers and music from your terminal. Built for humans **and** coding agents: every command supports `--json`, exit codes are stable, async jobs are first-class.
3
+ The official [VideoDraft](https://videodraft.ai) CLI — create AI videos, images and audio from your terminal. Built for humans **and** coding agents: every command supports `--json`, exit codes are stable, async jobs are first-class.
4
4
 
5
5
  ```bash
6
6
  npx videodraft login
@@ -11,10 +11,14 @@ npx videodraft create "30s launch video for our espresso machine" --ar 9:16
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- npm install -g videodraft # or: npx videodraft <command>
14
+ npm install -g videodraft # global: adds the `videodraft` command to your PATH
15
+ videodraft --version # → confirms it's installed
16
+
17
+ # …or run without installing — npx fetches and runs the latest each time:
18
+ npx videodraft <command>
15
19
  ```
16
20
 
17
- Requires Node ≥ 20.18.1.
21
+ Requires Node ≥ 20.18.1. (Publishing to npm doesn't put `videodraft` on your PATH — `npm i -g` or `npx` does. A Homebrew tap and a `curl | sh` installer are on the way.)
18
22
 
19
23
  ## Authenticate
20
24
 
@@ -44,6 +48,10 @@ videodraft generate image "isometric workspace, warm light" --num 4 --download "
44
48
  videodraft generate video "slow dolly over a misty lake" --model google-veo3.1 --duration 6 --estimate
45
49
  videodraft generate voiceover "Welcome to VideoDraft" --download welcome.mp3
46
50
  videodraft generate music "minimal ambient, 60 BPM" --download bgm.mp3
51
+ videodraft generate sound-effect "cinematic whoosh, sub hit" --duration 3 --download sfx.mp3
52
+ videodraft generate dialogue --line "elevenlabs-kPzsL2i3teMYv0FxEYQ6:Ready?" --line "elevenlabs-s3TPKV1kjDlVtZbl4Ksh:Let's go." --download dialogue.mp3
53
+ videodraft generate voice-changer ./speech.wav --voice elevenlabs-kPzsL2i3teMYv0FxEYQ6 --duration 12 --download changed.mp3
54
+ videodraft generate dub ./clip.mp4 --to es --duration 30 --download dubbed.mp4
47
55
  videodraft upscale image ./photo.png --scale 4x --download ./photo-4x.png
48
56
  videodraft avatar create ./founder.jpg --script "$(videodraft avatar script 'our launch' --json | jq -r .script)"
49
57
  ```
@@ -53,10 +61,10 @@ videodraft avatar create ./founder.jpg --script "$(videodraft avatar script 'our
53
61
  | Group | Commands |
54
62
  |---|---|
55
63
  | Auth | `login` `logout` `whoami` |
56
- | Account | `credits` `costs [model]` `models [image\|video\|voices\|styles]` `workspaces` `sessions list/create` |
64
+ | Account | `credits` `costs [model]` `models [image\|video\|audio\|voices\|styles]` `workspaces` `sessions list/create` |
57
65
  | Projects | `projects list/get/delete/favorite/open` `checkpoint create/list/restore` |
58
66
  | Pipeline | `create` `shots` `produce` (`--mode full_video`) `attach` `finalize` `export` `export-status` `video-prompts` |
59
- | Generate | `generate image/video/voiceover/music` `upscale image/video` `avatar script/create/render/get/list` |
67
+ | Generate | `generate image/video/voiceover/music/sound-effect/dialogue/voice-changer/dub` `upscale image/video` `avatar script/create/render/get/list` |
60
68
  | Jobs | `status <job>` `wait <job>` `generations` |
61
69
  | Media | `upload <file>` `media list` `describe <url\|file>` `download <url>` |
62
70
  | Everything else | `tools list` `tools schema <name>` `call <tool> --args '<json>'` |
@@ -81,6 +89,7 @@ Install the VideoDraft skill so your agent knows the workflow:
81
89
  npx videodraft skills install # zero-install: npx fetches the CLI and installs the skill
82
90
  videodraft skills install # if the CLI is on PATH — auto-detects your installed agents
83
91
  videodraft skills install --agent claude,codex # target specific agents (repeatable/comma; --all for every agent)
92
+ videodraft skills install --project # into ./.claude/skills for just this repo (else global)
84
93
  npx skills add videodraft-ai/cli # via the vercel-labs skills tool (discovery / 69-agent multiselect)
85
94
  ```
86
95
 
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ __export(version_exports, {
15
15
  VERSION: () => VERSION
16
16
  });
17
17
  import fs from "fs";
18
- function readVersion() {
18
+ function readVersionFromDisk() {
19
19
  try {
20
20
  const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
21
21
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
@@ -23,11 +23,17 @@ function readVersion() {
23
23
  return "0.0.0";
24
24
  }
25
25
  }
26
+ function resolveVersion() {
27
+ if ("0.2.0") {
28
+ return "0.2.0";
29
+ }
30
+ return readVersionFromDisk();
31
+ }
26
32
  var VERSION;
27
33
  var init_version = __esm({
28
34
  "src/version.ts"() {
29
35
  "use strict";
30
- VERSION = readVersion();
36
+ VERSION = resolveVersion();
31
37
  }
32
38
  });
33
39
 
@@ -1068,7 +1074,12 @@ function registerAccountCommands(program) {
1068
1074
  ]);
1069
1075
  });
1070
1076
  });
1071
- program.command("costs [model]").description("Show credit costs \u2014 pass a model id plus video settings for an exact estimate").option("--type <type>", "image | video").option("--duration <seconds>", "video duration in seconds").option("--resolution <res>", 'e.g. "720p", "1080p", "1K", "2K"').option("--quality <tier>", 'e.g. "standard", "pro", "fast"').option("--rendering-speed <tier>", 'image speed/cost tier, e.g. Ideogram V4 "Turbo"/"Balanced"/"Quality"').option("--audio", "include native model audio in the estimate").option("--no-audio", "exclude native model audio").option("--num <n>", "image batch size").action(async function(model) {
1077
+ program.command("costs [model]").description(
1078
+ "Show credit costs \u2014 pass a model id plus settings for an exact estimate"
1079
+ ).option("--type <type>", "image | video | audio").option("--duration <seconds>", "video/audio duration in seconds").option("--length <seconds>", "ElevenLabs Music output length in seconds").option("--chars <n>", "ElevenLabs Dialogue character count").option("--resolution <res>", 'e.g. "720p", "1080p", "1K", "2K"').option("--quality <tier>", 'e.g. "standard", "pro", "fast"').option(
1080
+ "--rendering-speed <tier>",
1081
+ 'image speed/cost tier, e.g. Ideogram V4 "Turbo"/"Balanced"/"Quality"'
1082
+ ).option("--audio", "include native model audio in the estimate").option("--no-audio", "exclude native model audio").option("--num <n>", "image batch size").action(async function(model) {
1072
1083
  const ctx = buildContext(this);
1073
1084
  const opts = this.opts();
1074
1085
  const result = await ctx.client.callTool(
@@ -1077,6 +1088,8 @@ function registerAccountCommands(program) {
1077
1088
  model_id: model,
1078
1089
  type: opts.type,
1079
1090
  duration_seconds: opts.duration ? Number(opts.duration) : void 0,
1091
+ length_seconds: opts.length ? Number(opts.length) : void 0,
1092
+ characters: opts.chars ? Number(opts.chars) : void 0,
1080
1093
  resolution: opts.resolution,
1081
1094
  quality: opts.quality,
1082
1095
  rendering_speed: opts.renderingSpeed,
@@ -1086,7 +1099,9 @@ function registerAccountCommands(program) {
1086
1099
  );
1087
1100
  emit(ctx.out, result);
1088
1101
  });
1089
- program.command("models [kind]").description("List available models: image | video | voices | styles (default: image + video)").action(async function(kind) {
1102
+ program.command("models [kind]").description(
1103
+ "List available models: image | video | audio | voices | styles (default: image + video + audio)"
1104
+ ).action(async function(kind) {
1090
1105
  const ctx = buildContext(this);
1091
1106
  const wanted = kind ?? "all";
1092
1107
  const result = {};
@@ -1096,8 +1111,15 @@ function registerAccountCommands(program) {
1096
1111
  if (wanted === "video" || wanted === "all") {
1097
1112
  result.video = await ctx.client.callTool("list_available_video_models");
1098
1113
  }
1099
- if (wanted === "voices") result.voices = await ctx.client.callTool("list_available_voices");
1100
- if (wanted === "styles") result.styles = await ctx.client.callTool("list_available_styles");
1114
+ if (wanted === "audio" || wanted === "all") {
1115
+ result.audio = await ctx.client.callTool("list_available_audio_models");
1116
+ }
1117
+ if (wanted === "voices") {
1118
+ result.voices = await ctx.client.callTool("list_available_voices");
1119
+ }
1120
+ if (wanted === "styles") {
1121
+ result.styles = await ctx.client.callTool("list_available_styles");
1122
+ }
1101
1123
  emit(ctx.out, result, (o) => {
1102
1124
  for (const [section, payload] of Object.entries(result)) {
1103
1125
  const models = Array.isArray(payload) ? payload : payload?.models ?? payload?.voices ?? payload?.styles ?? [];
@@ -1413,6 +1435,43 @@ async function pollExport(client, ref, options = {}) {
1413
1435
  }
1414
1436
  }
1415
1437
 
1438
+ // src/core/media.ts
1439
+ var IMAGE_EXTS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp", "gif", "bmp", "heic", "heif", "avif", "svg"]);
1440
+ var VIDEO_EXTS = /* @__PURE__ */ new Set(["mp4", "webm", "mov", "m4v"]);
1441
+ var AUDIO_EXTS = /* @__PURE__ */ new Set(["mp3", "wav", "m4a", "ogg", "flac", "aac"]);
1442
+ function kindOf(url, typeHint) {
1443
+ const t = (typeHint ?? "").toLowerCase();
1444
+ if (t.includes("image")) return "image";
1445
+ if (t.includes("video")) return "video";
1446
+ if (t.includes("audio") || t.includes("music") || t.includes("sound") || t.includes("voice") || t.includes("speech") || t.includes("tts")) {
1447
+ return "audio";
1448
+ }
1449
+ const path5 = url.split(/[?#]/)[0]?.toLowerCase() ?? "";
1450
+ if (path5.includes("/img/")) return "image";
1451
+ if (path5.includes("/vid/")) return "video";
1452
+ if (path5.includes("/aud/")) return "audio";
1453
+ const ext = path5.match(/\.([a-z0-9]+)$/)?.[1];
1454
+ if (ext) {
1455
+ if (IMAGE_EXTS.has(ext)) return "image";
1456
+ if (VIDEO_EXTS.has(ext)) return "video";
1457
+ if (AUDIO_EXTS.has(ext)) return "audio";
1458
+ }
1459
+ return null;
1460
+ }
1461
+ function buildMediaDescriptors(urls, typeHint) {
1462
+ if (!Array.isArray(urls)) return [];
1463
+ const seen = /* @__PURE__ */ new Set();
1464
+ const out = [];
1465
+ for (const url of urls) {
1466
+ if (typeof url !== "string" || !/^https?:\/\//i.test(url) || seen.has(url)) continue;
1467
+ const kind = kindOf(url, typeHint);
1468
+ if (!kind) continue;
1469
+ seen.add(url);
1470
+ out.push({ kind, url });
1471
+ }
1472
+ return out;
1473
+ }
1474
+
1416
1475
  // src/core/download.ts
1417
1476
  import fs3 from "fs";
1418
1477
  import path2 from "path";
@@ -1540,6 +1599,14 @@ async function uploadFile(client, localPath, options = {}) {
1540
1599
 
1541
1600
  // src/commands/generate.ts
1542
1601
  var URI_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
1602
+ var VIDEO_SOURCE_RE = /\.(mp4|mov|webm|m4v|gif)(?:[?#].*)?$/i;
1603
+ function inferDubMediaType(source, explicit) {
1604
+ if (explicit) {
1605
+ if (explicit === "audio" || explicit === "video") return explicit;
1606
+ throw new Error('--type must be "audio" or "video"');
1607
+ }
1608
+ return VIDEO_SOURCE_RE.test(source) ? "video" : "audio";
1609
+ }
1543
1610
  async function resolveRefs(ctx, refs) {
1544
1611
  const resolved = [];
1545
1612
  for (const ref of refs) {
@@ -1615,9 +1682,10 @@ async function handleAsyncJob(ctx, submitted, options) {
1615
1682
  if (options.download && result.outputUrls.length > 0) {
1616
1683
  downloaded = await downloadOutputs(result.outputUrls, options.download, { job_id: jobId });
1617
1684
  }
1685
+ const media = buildMediaDescriptors(result.outputUrls, result.payload?.type);
1618
1686
  emit(
1619
1687
  ctx.out,
1620
- { job_id: jobId, status: result.status, outputs: result.outputUrls, downloaded_files: downloaded },
1688
+ { job_id: jobId, status: result.status, outputs: result.outputUrls, downloaded_files: downloaded, output_media: media },
1621
1689
  (o) => {
1622
1690
  note(o, fmt.green(o, `Completed \u2014 job ${jobId}`));
1623
1691
  for (const url of result.outputUrls) process.stdout.write(`${url}
@@ -1631,7 +1699,7 @@ async function handleAsyncJob(ctx, submitted, options) {
1631
1699
  }
1632
1700
  }
1633
1701
  function registerGenerateCommands(program) {
1634
- const generate = program.command("generate").description("Generate images, video, voiceovers and music");
1702
+ const generate = program.command("generate").description("Generate images, video and audio");
1635
1703
  generate.command("image <prompt...>").description("Generate an image (async; waits by default)").option("--model <id>", "image model id (default nano-banana-2)").option("--ar <ratio>", 'aspect ratio, e.g. "16:9"').option("--resolution <res>", 'e.g. "1K", "2K", "4K"').option("--quality <tier>", "model-specific quality tier").option("--rendering-speed <tier>", 'Ideogram speed/cost tier, e.g. V4 "Turbo"/"Balanced"/"Quality"').option("--num <n>", "variations of this prompt in one call (1-4)").option("--seed <n>", "seed (supported models only, e.g. Flux, Ideogram V4)").option("--ref <url|file>", "reference image (repeatable; local files are uploaded)", collect, []).option("--video-ref <url|file>", "video reference \u2014 nano-banana-2 only (http(s)/gs:///YouTube, or local file)").option("--style <id>", "style preset id").option("--project <id>", "attach to a project").option("--session <id>", "AI Studio session id").option("--scene <n>", "0-based scene index (with --project: writes onto that shot)").option("--shot <n>", "0-based shot index").option("--download <path>", "download outputs (template: {job_id} {index} {ext})").option("--no-wait", "submit and return the job id immediately").option("--estimate", "print the cost estimate and exit (spends nothing)").action(async function(promptWords) {
1636
1704
  const ctx = buildContext(this);
1637
1705
  const opts = this.opts();
@@ -1678,7 +1746,10 @@ function registerGenerateCommands(program) {
1678
1746
  label: "Generating image"
1679
1747
  });
1680
1748
  });
1681
- generate.command("video [prompt...]").description("Generate a video clip (async; per-second pricing \u2014 see --estimate)").option("--model <id>", "video model id (default google-veo3.1 fast)").option("--ar <ratio>", 'aspect ratio, e.g. "16:9", "9:16"').option("--duration <seconds>", "clip duration in seconds").option("--resolution <res>", 'e.g. "720p", "1080p"').option("--quality <tier>", 'e.g. "fast", "quality", "standard", "pro"').option("--audio", "generate native model audio").option("--no-audio", "disable native model audio").option("--start-image <url|file>", "start frame (image-to-video)").option("--end-image <url|file>", "end frame (supported models only)").option("--ref <url|file>", "reference image (repeatable)", collect, []).option("--ref-video <url|file>", "reference video (repeatable; Seedance 2, Wan 2.7; local files uploaded)", collect, []).option("--ref-audio <url|file>", "reference audio (repeatable; Seedance 2; local files uploaded)", collect, []).option("--segment <prompt:seconds>", "multi-prompt segment (repeatable; Kling 3.0 / 3.0 Turbo / O3)", collect, []).option("--negative <text>", "negative prompt (Kling/Wan/Luma)").option("--seed <n>", "seed").option("--project <id>", "attach to a project").option("--session <id>", "AI Studio session id").option("--scene <n>", "0-based scene index").option("--shot <n>", "0-based shot index").option("--download <path>", "download outputs (template: {job_id} {index} {ext})").option("--no-wait", "submit and return the job id immediately").option("--estimate", "print the cost estimate and exit (spends nothing)").action(async function(promptWords = []) {
1749
+ generate.command("video [prompt...]").description("Generate a video clip (async; per-second pricing \u2014 see --estimate)").option("--model <id>", "video model id (default google-veo3.1 fast)").option("--ar <ratio>", 'aspect ratio, e.g. "16:9", "9:16"').option("--duration <seconds>", "clip duration in seconds").option("--resolution <res>", 'e.g. "480p", "720p", "1080p", "4k"').option(
1750
+ "--quality <tier>",
1751
+ 'e.g. "mini", "fast", "standard", "quality", "pro"'
1752
+ ).option("--audio", "generate native model audio").option("--no-audio", "disable native model audio").option("--start-image <url|file>", "start frame (image-to-video)").option("--end-image <url|file>", "end frame (supported models only)").option("--ref <url|file>", "reference image (repeatable)", collect, []).option("--ref-video <url|file>", "reference video (repeatable; Seedance 2, Wan 2.7; local files uploaded)", collect, []).option("--ref-audio <url|file>", "reference audio (repeatable; Seedance 2; local files uploaded)", collect, []).option("--segment <prompt:seconds>", "multi-prompt segment (repeatable; Kling 3.0 / 3.0 Turbo / O3)", collect, []).option("--negative <text>", "negative prompt (Kling/Wan/Luma)").option("--seed <n>", "seed").option("--project <id>", "attach to a project").option("--session <id>", "AI Studio session id").option("--scene <n>", "0-based scene index").option("--shot <n>", "0-based shot index").option("--download <path>", "download outputs (template: {job_id} {index} {ext})").option("--no-wait", "submit and return the job id immediately").option("--estimate", "print the cost estimate and exit (spends nothing)").action(async function(promptWords = []) {
1682
1753
  const ctx = buildContext(this);
1683
1754
  const opts = this.opts();
1684
1755
  const prompt = promptWords.join(" ").trim();
@@ -1759,22 +1830,26 @@ function registerGenerateCommands(program) {
1759
1830
  if (opts.download && urls.length > 0) {
1760
1831
  downloaded = await downloadOutputs(urls, opts.download, { name: "voiceover" });
1761
1832
  }
1762
- emit(ctx.out, { ...result, downloaded_files: downloaded }, (o) => {
1833
+ const media = buildMediaDescriptors(urls, "audio");
1834
+ emit(ctx.out, { ...result, downloaded_files: downloaded, output_media: media }, (o) => {
1763
1835
  for (const url of urls) process.stdout.write(`${url}
1764
1836
  `);
1765
1837
  for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
1766
1838
  });
1767
1839
  });
1768
- generate.command("music <prompt...>").description("Generate background music (Lyria 3)").option("--model <id>", "lyria-3-clip-preview (30s, default) | lyria-3-pro-preview (180s)").option("--ref <url|file>", "reference image to inspire the music (repeatable)", collect, []).option("--project <id>", "link the generation to a project's AI Studio session").option("--attach <project_id>", "also set the track as that project's background music").option("--volume <n>", "0-100 BGM volume when attaching (default 30)").option("--bgm-disabled", "when attaching, store the BGM as disabled (enabled:false)").option("--session <id>", "AI Studio session id").option("--download <path>", "download the audio file").action(async function(promptWords) {
1840
+ generate.command("music <prompt...>").description("Generate background music").option("--model <id>", "lyria-3-clip-preview (default) | lyria-3-pro-preview | elevenlabs-music").option("--length <seconds>", "for --model elevenlabs-music: length 10\u2013120s (default 30)").option("--instrumental", "for --model elevenlabs-music: force instrumental (no vocals)").option("--ref <url|file>", "reference image to inspire the music (Lyria only, repeatable)", collect, []).option("--project <id>", "link the generation to a project's AI Studio session").option("--attach <project_id>", "also set the track as that project's background music").option("--volume <n>", "0-100 BGM volume when attaching (default 30)").option("--bgm-disabled", "when attaching, store the BGM as disabled (enabled:false)").option("--session <id>", "AI Studio session id").option("--download <path>", "download the audio file").action(async function(promptWords) {
1769
1841
  const ctx = buildContext(this);
1770
1842
  const opts = this.opts();
1771
- const refs = await resolveRefs(ctx, opts.ref ?? []);
1772
- capture("cli_generate", { kind: "music", model: opts.model ?? "default" });
1843
+ const musicModel = opts.model ?? "lyria-3-clip-preview";
1844
+ const refs = musicModel === "elevenlabs-music" ? [] : await resolveRefs(ctx, opts.ref ?? []);
1845
+ capture("cli_generate", { kind: "music", model: musicModel });
1773
1846
  const result = await ctx.client.callTool(
1774
1847
  "generate_music",
1775
1848
  compact({
1776
1849
  prompt: promptWords.join(" "),
1777
- model: opts.model,
1850
+ model: musicModel,
1851
+ length_seconds: opts.length ? Number(opts.length) : void 0,
1852
+ force_instrumental: opts.instrumental ? true : void 0,
1778
1853
  image_urls: refs.length > 0 ? refs : void 0,
1779
1854
  project_id: opts.project,
1780
1855
  attach_to_project_id: opts.attach,
@@ -1788,12 +1863,180 @@ function registerGenerateCommands(program) {
1788
1863
  if (opts.download && urls.length > 0) {
1789
1864
  downloaded = await downloadOutputs(urls, opts.download, { name: "music" });
1790
1865
  }
1791
- emit(ctx.out, { ...result, downloaded_files: downloaded }, (o) => {
1866
+ const media = buildMediaDescriptors(urls, "music");
1867
+ emit(ctx.out, { ...result, downloaded_files: downloaded, output_media: media }, (o) => {
1792
1868
  for (const url of urls) process.stdout.write(`${url}
1793
1869
  `);
1794
1870
  for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
1795
1871
  });
1796
1872
  });
1873
+ generate.command("sound-effect <prompt...>").description("Generate a sound effect (ElevenLabs Sound Effects)").option("--duration <seconds>", "length 0.5\u201322s (default 5)").option("--influence <0-1>", "prompt influence (default 0.3)").option("--project <id>", "link to a project's AI Studio session").option("--session <id>", "AI Studio session id").option("--download <path>", "download the audio file").action(async function(promptWords) {
1874
+ const ctx = buildContext(this);
1875
+ const opts = this.opts();
1876
+ capture("cli_generate", { kind: "sound_effect" });
1877
+ const result = await ctx.client.callTool(
1878
+ "generate_sound_effect",
1879
+ compact({
1880
+ prompt: promptWords.join(" "),
1881
+ duration_seconds: opts.duration ? Number(opts.duration) : void 0,
1882
+ prompt_influence: opts.influence ? Number(opts.influence) : void 0,
1883
+ project_id: opts.project,
1884
+ session_id: opts.session
1885
+ })
1886
+ );
1887
+ const urls = extractOutputUrls(result);
1888
+ let downloaded;
1889
+ if (opts.download && urls.length > 0) {
1890
+ downloaded = await downloadOutputs(urls, opts.download, {
1891
+ name: "sound-effect"
1892
+ });
1893
+ }
1894
+ const media = buildMediaDescriptors(urls, "audio");
1895
+ emit(
1896
+ ctx.out,
1897
+ { ...result, downloaded_files: downloaded, output_media: media },
1898
+ (o) => {
1899
+ for (const url of urls) process.stdout.write(`${url}
1900
+ `);
1901
+ for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
1902
+ }
1903
+ );
1904
+ });
1905
+ generate.command("dialogue").description(
1906
+ "Generate multi-speaker dialogue (ElevenLabs Text-to-Dialogue). Repeat --line."
1907
+ ).option(
1908
+ "--line <voiceId:text>",
1909
+ 'a dialogue line as "voiceId:text" (repeatable)',
1910
+ collect,
1911
+ []
1912
+ ).option("--stability <0|0.5|1>", "voice stability").option("--language <iso>", "ISO 639-1 language code").option("--project <id>", "link to a project's AI Studio session").option("--session <id>", "AI Studio session id").option("--download <path>", "download the audio file").action(async function() {
1913
+ const ctx = buildContext(this);
1914
+ const opts = this.opts();
1915
+ const lines = opts.line.map((raw) => {
1916
+ const i = raw.indexOf(":");
1917
+ if (i < 0) {
1918
+ throw new Error(`--line must be "voiceId:text" (got "${raw}")`);
1919
+ }
1920
+ return { voice_id: raw.slice(0, i).trim(), text: raw.slice(i + 1).trim() };
1921
+ });
1922
+ if (lines.length === 0) throw new Error("at least one --line is required");
1923
+ capture("cli_generate", { kind: "dialogue" });
1924
+ const result = await ctx.client.callTool(
1925
+ "generate_dialogue",
1926
+ compact({
1927
+ lines,
1928
+ stability: opts.stability !== void 0 ? Number(opts.stability) : void 0,
1929
+ language_code: opts.language,
1930
+ project_id: opts.project,
1931
+ session_id: opts.session
1932
+ })
1933
+ );
1934
+ const urls = extractOutputUrls(result);
1935
+ let downloaded;
1936
+ if (opts.download && urls.length > 0) {
1937
+ downloaded = await downloadOutputs(urls, opts.download, {
1938
+ name: "dialogue"
1939
+ });
1940
+ }
1941
+ const media = buildMediaDescriptors(urls, "audio");
1942
+ emit(
1943
+ ctx.out,
1944
+ { ...result, downloaded_files: downloaded, output_media: media },
1945
+ (o) => {
1946
+ for (const url of urls) process.stdout.write(`${url}
1947
+ `);
1948
+ for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
1949
+ }
1950
+ );
1951
+ });
1952
+ generate.command("voice-changer <audio>").description("Restyle speech into another ElevenLabs voice (Voice Changer)").option("--voice <id>", "target ElevenLabs voice id (default Brittney)").option(
1953
+ "--duration <seconds>",
1954
+ "length of the source audio in seconds (required, max 300)"
1955
+ ).option("--remove-noise", "remove background noise from the input").option("--project <id>", "link to a project's AI Studio session").option("--session <id>", "AI Studio session id").option("--download <path>", "download the audio file").action(async function(source) {
1956
+ const ctx = buildContext(this);
1957
+ const opts = this.opts();
1958
+ if (!opts.duration) {
1959
+ throw new Error(
1960
+ "--duration <seconds> is required (length of the source audio)"
1961
+ );
1962
+ }
1963
+ const [audioUrl] = await resolveRefs(ctx, [source]);
1964
+ capture("cli_generate", { kind: "voice_changer" });
1965
+ const result = await ctx.client.callTool(
1966
+ "change_voice",
1967
+ compact({
1968
+ audio_url: audioUrl,
1969
+ voice_id: opts.voice,
1970
+ duration_seconds: Number(opts.duration),
1971
+ remove_background_noise: opts.removeNoise ? true : void 0,
1972
+ project_id: opts.project,
1973
+ session_id: opts.session
1974
+ })
1975
+ );
1976
+ const urls = extractOutputUrls(result);
1977
+ let downloaded;
1978
+ if (opts.download && urls.length > 0) {
1979
+ downloaded = await downloadOutputs(urls, opts.download, {
1980
+ name: "voice-changed"
1981
+ });
1982
+ }
1983
+ const media = buildMediaDescriptors(urls, "audio");
1984
+ emit(
1985
+ ctx.out,
1986
+ { ...result, downloaded_files: downloaded, output_media: media },
1987
+ (o) => {
1988
+ for (const url of urls) process.stdout.write(`${url}
1989
+ `);
1990
+ for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
1991
+ }
1992
+ );
1993
+ });
1994
+ generate.command("dub <media>").description("Dub a video/audio file into another language (ElevenLabs Dubbing)").option("--to <iso>", "target language ISO 639-1 code, e.g. es or te (required)").option("--from <iso>", "source language ISO 639-1 code (auto-detected if omitted)").option("--type <audio|video>", "source media type override").option(
1995
+ "--duration <seconds>",
1996
+ "length of the source media in seconds (required, max 300)"
1997
+ ).option("--speakers <n>", "number of speakers (auto-detected if omitted)").option("--project <id>", "link to a project's AI Studio session").option("--session <id>", "AI Studio session id").option("--download <path>", "download the dubbed file").action(async function(source) {
1998
+ const ctx = buildContext(this);
1999
+ const opts = this.opts();
2000
+ if (!opts.to) throw new Error("--to <iso> (target language) is required");
2001
+ if (!opts.duration) {
2002
+ throw new Error(
2003
+ "--duration <seconds> is required (length of the source media)"
2004
+ );
2005
+ }
2006
+ const mediaType = inferDubMediaType(source, opts.type);
2007
+ const [mediaUrl] = await resolveRefs(ctx, [source]);
2008
+ capture("cli_generate", { kind: "dub" });
2009
+ const result = await ctx.client.callTool(
2010
+ "dub_media",
2011
+ compact({
2012
+ video_url: mediaType === "video" ? mediaUrl : void 0,
2013
+ audio_url: mediaType === "audio" ? mediaUrl : void 0,
2014
+ target_lang: opts.to,
2015
+ source_lang: opts.from,
2016
+ num_speakers: opts.speakers ? Number(opts.speakers) : void 0,
2017
+ duration_seconds: Number(opts.duration),
2018
+ project_id: opts.project,
2019
+ session_id: opts.session
2020
+ })
2021
+ );
2022
+ const urls = extractOutputUrls(result);
2023
+ let downloaded;
2024
+ if (opts.download && urls.length > 0) {
2025
+ downloaded = await downloadOutputs(urls, opts.download, {
2026
+ name: "dubbed"
2027
+ });
2028
+ }
2029
+ const media = buildMediaDescriptors(urls, mediaType);
2030
+ emit(
2031
+ ctx.out,
2032
+ { ...result, downloaded_files: downloaded, output_media: media },
2033
+ (o) => {
2034
+ for (const url of urls) process.stdout.write(`${url}
2035
+ `);
2036
+ for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
2037
+ }
2038
+ );
2039
+ });
1797
2040
  const upscale = program.command("upscale").description("Upscale images and videos (Topaz)");
1798
2041
  upscale.command("image <url|file>").description("Upscale an image (synchronous)").option("--scale <factor>", '"1x" | "2x" | "4x" (default 2x)').option("--session <id>", "AI Studio session id").option("--download <path>", "download the result").action(async function(source) {
1799
2042
  const ctx = buildContext(this);
@@ -1809,7 +2052,8 @@ function registerGenerateCommands(program) {
1809
2052
  if (opts.download && urls.length > 0) {
1810
2053
  downloaded = await downloadOutputs(urls, opts.download, { name: "upscaled" });
1811
2054
  }
1812
- emit(ctx.out, { ...result, downloaded_files: downloaded }, (o) => {
2055
+ const media = buildMediaDescriptors(urls, "image");
2056
+ emit(ctx.out, { ...result, downloaded_files: downloaded, output_media: media }, (o) => {
1813
2057
  for (const u of urls) process.stdout.write(`${u}
1814
2058
  `);
1815
2059
  for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
@@ -1948,7 +2192,18 @@ function registerPipelineCommands(program) {
1948
2192
  const failed = results.filter((r) => r.status === "failed").length;
1949
2193
  emit(
1950
2194
  ctx.out,
1951
- { job_ids: jobIds, results: results.map((r) => ({ status: r.status, outputs: r.outputUrls })) },
2195
+ {
2196
+ job_ids: jobIds,
2197
+ results: jobIds.map((jobId) => {
2198
+ const result = resultMap.get(jobId);
2199
+ return {
2200
+ job_id: jobId,
2201
+ status: result.status,
2202
+ outputs: result.outputUrls,
2203
+ output_media: buildMediaDescriptors(result.outputUrls, result.payload?.type)
2204
+ };
2205
+ })
2206
+ },
1952
2207
  (o) => {
1953
2208
  note(
1954
2209
  o,
@@ -2084,12 +2339,17 @@ function registerPipelineCommands(program) {
2084
2339
  name: "export"
2085
2340
  });
2086
2341
  }
2087
- emit(ctx.out, { export_id: exportId, video_url: result.videoUrl, downloaded_files: downloaded }, (o) => {
2088
- note(o, fmt.green(o, "Export finished."));
2089
- process.stdout.write(`${result.videoUrl}
2342
+ const media = buildMediaDescriptors([result.videoUrl], "video");
2343
+ emit(
2344
+ ctx.out,
2345
+ { export_id: exportId, video_url: result.videoUrl, downloaded_files: downloaded, output_media: media },
2346
+ (o) => {
2347
+ note(o, fmt.green(o, "Export finished."));
2348
+ process.stdout.write(`${result.videoUrl}
2090
2349
  `);
2091
- for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
2092
- });
2350
+ for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
2351
+ }
2352
+ );
2093
2353
  } catch (err) {
2094
2354
  spin.stop();
2095
2355
  throw err;
@@ -2106,7 +2366,8 @@ function registerPipelineCommands(program) {
2106
2366
  wait_seconds: opts.wait ? Number(opts.wait) : void 0
2107
2367
  })
2108
2368
  );
2109
- emit(ctx.out, result);
2369
+ const media = buildMediaDescriptors(extractOutputUrls(result), "video");
2370
+ emit(ctx.out, { ...result, output_media: media });
2110
2371
  });
2111
2372
  program.command("video-prompts <project_id>").description("Generate advisory per-shot motion/video prompts for a project").option("--ar <ratio>", "aspect ratio").option("--instructions <text>", "authoring instructions").option("--has-voiceover", "tell the generator the project has a voiceover track").option("--video-audio", "tell the generator that generated videos may include native audio").option("--has-bgm", "tell the generator the project has background music").action(async function(projectId) {
2112
2373
  const ctx = buildContext(this);
@@ -2135,7 +2396,8 @@ function registerJobCommands(program) {
2135
2396
  "check_generation_status",
2136
2397
  compact({ job_id: jobId, project_id: opts.project, scene_id: opts.sceneId })
2137
2398
  );
2138
- emit(ctx.out, result, (o) => {
2399
+ const media = buildMediaDescriptors(extractOutputUrls(result), result?.type);
2400
+ emit(ctx.out, { ...result, output_media: media }, (o) => {
2139
2401
  note(o, `${jobId}: ${result?.status ?? "unknown"}`);
2140
2402
  for (const url of extractOutputUrls(result)) process.stdout.write(`${url}
2141
2403
  `);
@@ -2177,6 +2439,7 @@ function registerJobCommands(program) {
2177
2439
  status: result.status,
2178
2440
  outputs: result.outputUrls,
2179
2441
  downloaded_files: downloaded,
2442
+ output_media: buildMediaDescriptors(result.outputUrls, result.payload?.type),
2180
2443
  ...result.status === "failed" ? { error: result.payload?.error } : {}
2181
2444
  });
2182
2445
  }
@@ -2197,7 +2460,7 @@ function registerJobCommands(program) {
2197
2460
  throw err;
2198
2461
  }
2199
2462
  });
2200
- program.command("generations").description("List recent + in-flight generations").option("--type <type>", "image | video").option("--status <status>", "pending | processing | completed | failed").option("--limit <n>", "max rows (default 30)").action(async function() {
2463
+ program.command("generations").description("List recent + in-flight generations").option("--type <type>", "image | video | sounds").option("--status <status>", "pending | processing | completed | failed").option("--limit <n>", "max rows (default 30)").action(async function() {
2201
2464
  const ctx = buildContext(this);
2202
2465
  const opts = this.opts();
2203
2466
  const result = await ctx.client.callTool(
@@ -2403,7 +2666,8 @@ function registerAvatarCommands(program) {
2403
2666
  target_language: opts.language
2404
2667
  })
2405
2668
  );
2406
- emit(ctx.out, result, (o) => {
2669
+ const media = buildMediaDescriptors(extractOutputUrls(result), "audio");
2670
+ emit(ctx.out, { ...result, output_media: media }, (o) => {
2407
2671
  note(o, fmt.green(o, `Avatar video ${result?.avatar_video_id ?? "created"}.`));
2408
2672
  note(o, fmt.dim(o, `Render (paid): videodraft avatar render ${result?.avatar_video_id}`));
2409
2673
  });
@@ -2435,7 +2699,8 @@ function registerAvatarCommands(program) {
2435
2699
  spin.update(`Rendering avatar video \u2014 ${exportStatus}`);
2436
2700
  if (exportStatus === "completed") {
2437
2701
  spin.stop();
2438
- emit(ctx.out, status, (o) => {
2702
+ const media = buildMediaDescriptors(extractOutputUrls(status), "video");
2703
+ emit(ctx.out, { ...status, output_media: media }, (o) => {
2439
2704
  note(o, fmt.green(o, "Avatar render completed."));
2440
2705
  if (status?.video_url) process.stdout.write(`${status.video_url}
2441
2706
  `);
@@ -2463,7 +2728,8 @@ function registerAvatarCommands(program) {
2463
2728
  avatar.command("get <avatar_video_id>").description("Fetch one avatar video (status + video_url when rendered)").action(async function(avatarVideoId) {
2464
2729
  const ctx = buildContext(this);
2465
2730
  const result = await ctx.client.callTool("get_avatar_video", { avatar_video_id: avatarVideoId });
2466
- emit(ctx.out, result);
2731
+ const media = buildMediaDescriptors(extractOutputUrls(result), "video");
2732
+ emit(ctx.out, { ...result, output_media: media });
2467
2733
  });
2468
2734
  avatar.command("list").description("List your avatar videos").action(async function() {
2469
2735
  const ctx = buildContext(this);
@@ -2724,7 +2990,7 @@ async function configureProxy() {
2724
2990
  function buildProgram() {
2725
2991
  const program = new Command();
2726
2992
  program.name("videodraft").description(
2727
- "Create AI videos, images, voiceovers and music from your terminal.\nAgent-friendly: every command supports --json; exit codes are stable\n(0 ok, 1 error, 2 usage, 3 auth, 4 insufficient credits)."
2993
+ "Create AI videos, images and audio from your terminal.\nAgent-friendly: every command supports --json; exit codes are stable\n(0 ok, 1 error, 2 usage, 3 auth, 4 insufficient credits)."
2728
2994
  ).version(VERSION, "-v, --version", "print the CLI version").option("--json", "machine-readable JSON output").option("--no-color", "disable colored output (NO_COLOR is also respected)").option("--base-url <url>", "VideoDraft server (default https://app.videodraft.ai; env VIDEODRAFT_BASE_URL)").option("--token <vd_mcp_token>", "bearer token for this invocation (env VIDEODRAFT_API_KEY)").option("--profile <name>", "config profile (default: default)").option("--wait-interval <duration>", "poll interval for --wait, e.g. 3s (default)").option("--wait-timeout <duration>", "max wait for --wait, e.g. 10m (default)").showSuggestionAfterError(true).exitOverride();
2729
2995
  registerAuthCommands(program);
2730
2996
  registerAccountCommands(program);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "videodraft",
3
- "version": "0.1.1",
4
- "description": "Official VideoDraft CLI — create AI videos, images, voiceovers and music from your terminal. Agent-friendly: --json everywhere, stable exit codes, async job polling.",
3
+ "version": "0.2.0",
4
+ "description": "Official VideoDraft CLI — create AI videos, images and audio from your terminal. Agent-friendly: --json everywhere, stable exit codes, async job polling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "homepage": "https://videodraft.ai/cli",
@@ -1,10 +1,6 @@
1
1
  ---
2
2
  name: videodraft
3
- description: Create AI videos, images, voiceovers, music, storyboards, avatar videos and product/ad videos with VideoDraft. Use when the user mentions VideoDraft, or asks to generate/make a video, video ad, explainer, storyboard, talking-head/avatar video, AI image, voiceover/TTS, or background music — including batch/programmatic video generation in scripts or CI. Works via the `videodraft` CLI (preferred in terminals) or the VideoDraft MCP connector.
4
- license: MIT
5
- metadata:
6
- author: VideoDraft (videodraft.ai)
7
- homepage: https://videodraft.ai/cli
3
+ description: Create AI videos, images, voiceovers, music, sound effects, dialogue, dubbing, storyboards, avatar videos and product/ad videos with VideoDraft. Use when the user mentions VideoDraft, or asks to generate/make a video, video ad, explainer, storyboard, talking-head/avatar video, AI image, voiceover/TTS, background music, sound effects, dialogue audio, voice changing, or dubbing — including batch/programmatic video generation in scripts or CI. Works via the `videodraft` CLI (preferred in terminals) or the VideoDraft MCP connector.
8
4
  ---
9
5
 
10
6
  # VideoDraft
@@ -26,7 +22,7 @@ Two equivalent surfaces (same backend, same credits, same projects):
26
22
 
27
23
  ## First decision: asset or video?
28
24
 
29
- - **One standalone asset** (a single image, clip, voiceover, or music track, no story): generate it directly. Do NOT create a project.
25
+ - **One standalone asset** (a single image, clip, voiceover, music track, sound effect, dialogue track, voice-changed file, or dubbed media file, no story): generate it directly. Do NOT create a project.
30
26
  - `videodraft generate image "a red fox in snow, cinematic" --ar 16:9 --download ./out/`
31
27
  - `videodraft generate video "slow dolly over a misty lake" --model google-veo3.1 --duration 6 --download ./out/`
32
28
  - **A video / ad / explainer / anything multi-scene**: create a project so the work stays organized, editable in the web app, and exportable.
@@ -39,10 +35,10 @@ Two equivalent surfaces (same backend, same credits, same projects):
39
35
  Generation costs credits (video is per-second; shot-image batches are the largest single spend). Before anything expensive:
40
36
 
41
37
  1. `videodraft credits` — check the balance.
42
- 2. `videodraft generate video "..." --estimate` or `videodraft costs <model> --duration 8 --resolution 1080p` — get the quote.
38
+ 2. `videodraft generate video "..." --estimate` or `videodraft costs <model> --duration 8 --resolution 1080p` — get the quote. For ElevenLabs audio, use `--type audio` plus `--duration`, `--length`, or `--chars`.
43
39
  3. Tell the user the model + settings + rough cost and get a go-ahead. Ask rather than assume aspect ratio, duration, and model when they matter.
44
40
 
45
- `videodraft models image|video` lists every model with its supported inputs (aspect ratios, resolutions, reference limits) — consult it instead of guessing capabilities.
41
+ `videodraft models image|video|audio` lists every model with its supported inputs (aspect ratios, resolutions, reference limits, audio billing inputs) — consult it instead of guessing capabilities.
46
42
 
47
43
  ## Async jobs
48
44
 
@@ -80,7 +76,7 @@ videodraft produce <project_id> # voiceovers + captions + produc
80
76
  videodraft export <project_id> --download final.mp4
81
77
  ```
82
78
 
83
- Optional between produce and export: per-shot motion clips (`videodraft generate video ... --project <id>` then place it with `videodraft attach <project> --scene N --shot M --media <url|file> --type video --duration <s>`), and music (`videodraft generate music "..." --attach <project_id>`). Details, per-step tools and editing rules: [references/pipeline.md](references/pipeline.md).
79
+ Optional between produce and export: per-shot motion clips (`videodraft generate video ... --project <id>` then place it with `videodraft attach <project> --scene N --shot M --media <url|file> --type video --duration <s>`), music (`videodraft generate music "..." --attach <project_id>`), and standalone audio assets (`generate sound-effect`, `generate dialogue`, `generate voice-changer`, `generate dub`). Details, per-step tools and editing rules: [references/pipeline.md](references/pipeline.md).
84
80
 
85
81
  Avatar/talking-head videos are their own short flow: `videodraft avatar script` → `avatar create` → `avatar render` (paid step).
86
82
 
@@ -5,6 +5,7 @@ Always consult the live catalog instead of memorizing this page — models chang
5
5
  ```bash
6
6
  videodraft models image --json # every image model + inputs (aspect ratios, resolutions, max refs)
7
7
  videodraft models video --json # every video model + inputs + per-second pricing metadata
8
+ videodraft models audio --json # standalone audio/media models + pricing inputs
8
9
  videodraft models voices --json # TTS voices
9
10
  videodraft models styles --json # visual style presets
10
11
  ```
@@ -14,7 +15,8 @@ videodraft models styles --json # visual style presets
14
15
  - **Image**: `nano-banana-2` (the platform default, 1K). Use `--num 1..4` for variations of one prompt in a single call — never loop for variations.
15
16
  - **Video**: `google-veo3.1` at fast quality (6s / 720p) — the platform default.
16
17
  - **Voiceover**: ElevenLabs Brittney (default voice).
17
- - **Music**: `lyria-3-clip-preview` (30s, cheap); `lyria-3-pro-preview` for 180s/quality.
18
+ - **Music**: `lyria-3-clip-preview` (30s, cheap); `lyria-3-pro-preview` for 180s/quality; `elevenlabs-music` for music that can include vocals/lyrics.
19
+ - **ElevenLabs audio**: `generate sound-effect`, `generate dialogue`, `generate voice-changer`, and `generate dub` are synchronous audio/media calls. Voice changer and dubbing require the source media duration in seconds for billing and currently accept source media up to 300s.
18
20
 
19
21
  ## Capability gotchas
20
22
 
@@ -32,12 +34,15 @@ videodraft models styles --json # visual style presets
32
34
  - Video: usually credits/second × duration; rate depends on model + resolution + quality + native audio on/off.
33
35
  - Shot-image batches: one image per shot (+1 grid image per scene in `--grid` mode) — the largest single spend in the pipeline.
34
36
  - Avatar renders: ~10 credits/sec at 480p, ~20/sec at 720p.
37
+ - ElevenLabs audio: sound effects are per second, dialogue is per character, music/voice-changer/dubbing are per started minute. Voice changer and dubbing reject source media above 300s in the current synchronous flow.
35
38
  - Upscales: priced by scale and source size.
36
39
 
37
40
  Quote before spending:
38
41
 
39
42
  ```bash
40
43
  videodraft costs google-veo3.1 --type video --duration 8 --resolution 1080p --audio
44
+ videodraft costs elevenlabs-dubbing --type audio --duration 60
45
+ videodraft costs elevenlabs-dialogue --type audio --chars 350
41
46
  videodraft generate video "..." --estimate # same quote, inline
42
47
  videodraft credits # current balance
43
48
  ```
@@ -16,6 +16,10 @@ Everything here works through the CLI (`videodraft <command>` / `videodraft call
16
16
  | Motion clip for a shot | `videodraft generate video --project <id>` | `generate_video` |
17
17
  | Attach a finished clip to the timeline | `videodraft attach <project> --scene N --shot M --media <url> --type video` | `attach_media_to_shot` |
18
18
  | Background music | `videodraft generate music --attach <project>` | `generate_music` / `set_background_music` |
19
+ | Sound effect | `videodraft generate sound-effect "..."` | `generate_sound_effect` |
20
+ | Dialogue audio | `videodraft generate dialogue --line "voice:text"` | `generate_dialogue` |
21
+ | Voice changer | `videodraft generate voice-changer <audio>` | `change_voice` |
22
+ | Dubbing | `videodraft generate dub <audio_or_video>` | `dub_media` |
19
23
  | Scene voiceover | `videodraft generate voiceover --project <id> --scene N` | `generate_voiceover` |
20
24
  | Final MP4 | `videodraft export <project>` | `export_video` + `check_export_status` |
21
25