videodraft 0.4.1 → 0.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.
package/README.md CHANGED
@@ -37,6 +37,8 @@ Standalone images, clips and audio are complete deliverables. They do not need a
37
37
  ```bash
38
38
  videodraft generate image "isometric workspace, warm light" --num 4 --download "./out/{job_id}_{index}.{ext}"
39
39
  videodraft generate video "slow dolly over a misty lake" --model google-veo3.1 --duration 6 --estimate
40
+ videodraft generate audio "Read this in a calm documentary voice" --voice vivi_mixed_en_zh_ja_es_id --download narration.mp3
41
+ videodraft generate audio "Extend @Audio1 with soft rain" --ref-audio ./opening.wav --download extended.wav --format wav
40
42
  videodraft generate voiceover "Welcome to VideoDraft" --download welcome.mp3
41
43
  videodraft generate music "minimal ambient, 60 BPM" --download bgm.mp3
42
44
  videodraft generate sound-effect "cinematic whoosh, sub hit" --duration 3 --download sfx.mp3
@@ -50,6 +52,17 @@ videodraft edit motion ./character.png "Apply the reference dance" --motion-vide
50
52
  videodraft generate video "Match this performance" --model kling-o3-video-ref-edit --ref-video ./performance.mp4 --ref ./wardrobe.png --download ./guided.mp4
51
53
  ```
52
54
 
55
+ `generate audio` automatically retries transient and lost responses with one
56
+ stable operation key. To recover after stopping or losing the CLI process, set
57
+ your own UUID up front and reuse it if needed:
58
+
59
+ ```bash
60
+ videodraft generate audio "..." --idempotency-key "$(uuidgen)"
61
+ ```
62
+
63
+ The server then returns the existing result or in-progress operation without
64
+ generating or charging twice.
65
+
53
66
  Discover the full asset lane:
54
67
 
55
68
  ```bash
@@ -79,18 +92,18 @@ videodraft export <project> --download final.mp4
79
92
 
80
93
  ## Commands
81
94
 
82
- | Group | Commands |
83
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
84
- | Auth | `login` `logout` `whoami` |
85
- | Account | `credits` `costs [model]` `models [image\|video\|audio\|voices\|styles]` `workspaces` `sessions list/create` |
86
- | Projects | `projects list/get/delete/favorite/open` `checkpoint create/list/restore` |
87
- | Pipeline | `create` `shots` `produce` (`--mode full_video`) `attach` `finalize` `export` `export-status` `video-prompts` |
88
- | Generate | `generate image/video/voiceover/music/sound-effect/dialogue/voice-changer/dub` `edit video/motion` `upscale image/video` `avatar script/create/render/get/list/fabric/lipsync` |
89
- | Jobs | `status <job>` `wait <job>` `generations` |
90
- | Media | `upload <file>` `media list` `describe <url\|file>` `download <url>` |
91
- | Everything else | `tools list [--lane assets\|asset_io\|project_data\|production]` `tools schema <name>` `call <tool> --args '<json>'` |
92
- | Agents | `skills install [--agent claude\|codex\|cursor]` `skills path` |
93
- | Utility | `config get/set/path` `completion bash\|zsh` `docs` `--version` |
95
+ | Group | Commands |
96
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
97
+ | Auth | `login` `logout` `whoami` |
98
+ | Account | `credits` `costs [model]` `models [image\|video\|audio\|voices\|styles]` `workspaces` `sessions list/create` |
99
+ | Projects | `projects list/get/delete/favorite/open` `checkpoint create/list/restore` |
100
+ | Pipeline | `create` `shots` `produce` (`--mode full_video`) `attach` `finalize` `export` `export-status` `video-prompts` |
101
+ | Generate | `generate image/video/audio/voiceover/music/sound-effect/dialogue/voice-changer/dub` `edit video/motion` `upscale image/video` `avatar script/create/render/get/list/fabric/lipsync` |
102
+ | Jobs | `status <job>` `wait <job>` `generations` |
103
+ | Media | `upload <file>` `media list` `describe <url\|file>` `download <url>` |
104
+ | Everything else | `tools list [--lane assets\|asset_io\|project_data\|production]` `tools schema <name>` `call <tool> --args '<json>'` |
105
+ | Agents | `skills install [--agent claude\|codex\|cursor]` `skills path` |
106
+ | Utility | `config get/set/path` `completion bash\|zsh` `docs` `--version` |
94
107
 
95
108
  `call` reaches **every** VideoDraft API tool (the full MCP catalog), including ones without a curated command — new platform features work in the CLI the day they ship.
96
109
 
package/dist/client.d.ts CHANGED
@@ -217,6 +217,12 @@ interface DownloadedFile {
217
217
  url: string;
218
218
  path: string;
219
219
  bytes: number;
220
+ /** Downscaled view copy in <dir>/previews/ (images over the size threshold
221
+ * only). Agents should inspect this instead of the full-res original —
222
+ * every full-res image an agent views is re-sent with each later message
223
+ * of its chat. Best-effort: absent when preview generation failed or was
224
+ * skipped. The original at `path` is always the deliverable. */
225
+ preview?: string;
220
226
  }
221
227
  declare function extFromUrl(url: string, fallback?: string): string;
222
228
  declare function renderTemplate(template: string, vars: {
package/dist/client.js CHANGED
@@ -818,10 +818,136 @@ async function pollGenerations(client, jobIds, options = {}) {
818
818
  }
819
819
 
820
820
  // src/core/download.ts
821
+ import { execFile } from "child_process";
821
822
  import fs2 from "fs";
822
823
  import path2 from "path";
823
824
  import { pipeline } from "stream/promises";
825
+ import { promisify } from "util";
824
826
  import { Readable } from "stream";
827
+ var execFileAsync = promisify(execFile);
828
+ var PREVIEW_SOURCE_RE = /\.(png|jpe?g|webp|tiff?)$/i;
829
+ var PREVIEW_SKIP_BYTES = 500 * 1024;
830
+ var PREVIEW_MAX_DIM = 1536;
831
+ var PREVIEW_JPEG_QUALITY = 85;
832
+ var PREVIEW_CDN_WIDTH = 1080;
833
+ var PREVIEW_EXTS = ["jpg", "webp", "png"];
834
+ function previewPathFor(originalPath, ext = "jpg") {
835
+ return path2.join(
836
+ path2.dirname(originalPath),
837
+ "previews",
838
+ `${path2.basename(originalPath)}.${ext}`
839
+ );
840
+ }
841
+ function invalidatePreviews(originalPath) {
842
+ for (const ext of PREVIEW_EXTS) {
843
+ try {
844
+ fs2.rmSync(previewPathFor(originalPath, ext), { force: true });
845
+ } catch {
846
+ }
847
+ }
848
+ }
849
+ function shouldWritePreview(originalPath, bytes) {
850
+ return PREVIEW_SOURCE_RE.test(originalPath) && bytes >= PREVIEW_SKIP_BYTES;
851
+ }
852
+ function tempPreviewPath(previewPath) {
853
+ return `${previewPath}.tmp-${process.pid}`;
854
+ }
855
+ function commitPreview(tmpPath, previewPath) {
856
+ try {
857
+ if (fs2.existsSync(tmpPath) && fs2.statSync(tmpPath).size > 0) {
858
+ fs2.renameSync(tmpPath, previewPath);
859
+ return true;
860
+ }
861
+ } catch {
862
+ }
863
+ fs2.rmSync(tmpPath, { force: true });
864
+ return false;
865
+ }
866
+ async function writePreviewWithSips(originalPath, previewPath) {
867
+ if (process.platform !== "darwin") return false;
868
+ const tmp = tempPreviewPath(previewPath);
869
+ try {
870
+ await execFileAsync(
871
+ "sips",
872
+ [
873
+ "-Z",
874
+ String(PREVIEW_MAX_DIM),
875
+ "-s",
876
+ "format",
877
+ "jpeg",
878
+ "-s",
879
+ "formatOptions",
880
+ String(PREVIEW_JPEG_QUALITY),
881
+ originalPath,
882
+ "--out",
883
+ tmp
884
+ ],
885
+ { timeout: 1e4 }
886
+ );
887
+ } catch {
888
+ fs2.rmSync(tmp, { force: true });
889
+ return false;
890
+ }
891
+ return commitPreview(tmp, previewPath);
892
+ }
893
+ var cdnPreviewConsecutiveFailures = 0;
894
+ var PREVIEW_CDN_MAX_CONSECUTIVE_FAILURES = 2;
895
+ var PREVIEW_CDN_TIMEOUT_MS = 1e4;
896
+ async function writePreviewFromCdn(sourceUrl, originalPath, fetchImpl) {
897
+ let host;
898
+ try {
899
+ host = new URL(sourceUrl).hostname;
900
+ } catch {
901
+ return void 0;
902
+ }
903
+ if (host !== "cdn.videodraft.ai") return void 0;
904
+ if (cdnPreviewConsecutiveFailures >= PREVIEW_CDN_MAX_CONSECUTIVE_FAILURES) return void 0;
905
+ const variant = `${DEFAULT_BASE_URL}/_next/image?url=${encodeURIComponent(sourceUrl)}&w=${PREVIEW_CDN_WIDTH}&q=75`;
906
+ let res;
907
+ try {
908
+ res = await fetchImpl(variant, {
909
+ headers: { Accept: "image/webp,image/jpeg,image/*" },
910
+ signal: AbortSignal.timeout(PREVIEW_CDN_TIMEOUT_MS)
911
+ });
912
+ } catch (err) {
913
+ cdnPreviewConsecutiveFailures++;
914
+ throw err;
915
+ }
916
+ if (!res.ok || !res.body) {
917
+ cdnPreviewConsecutiveFailures++;
918
+ return void 0;
919
+ }
920
+ cdnPreviewConsecutiveFailures = 0;
921
+ const contentType = res.headers.get("content-type") ?? "";
922
+ const ext = contentType.includes("webp") ? "webp" : contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("png") ? "png" : "webp";
923
+ const previewPath = previewPathFor(originalPath, ext);
924
+ const tmp = tempPreviewPath(previewPath);
925
+ try {
926
+ await pipeline(Readable.fromWeb(res.body), fs2.createWriteStream(tmp));
927
+ } catch {
928
+ fs2.rmSync(tmp, { force: true });
929
+ return void 0;
930
+ }
931
+ return commitPreview(tmp, previewPath) ? previewPath : void 0;
932
+ }
933
+ async function writePreview(originalPath, sourceUrl, bytes, fetchImpl = fetch) {
934
+ if (!PREVIEW_SOURCE_RE.test(originalPath)) return void 0;
935
+ invalidatePreviews(originalPath);
936
+ if (!shouldWritePreview(originalPath, bytes)) return void 0;
937
+ try {
938
+ const jpgPath = previewPathFor(originalPath, "jpg");
939
+ fs2.mkdirSync(path2.dirname(jpgPath), { recursive: true });
940
+ if (await writePreviewWithSips(originalPath, jpgPath).catch(() => false)) {
941
+ return jpgPath;
942
+ }
943
+ const cdnPath = await writePreviewFromCdn(sourceUrl, originalPath, fetchImpl).catch(
944
+ () => void 0
945
+ );
946
+ if (cdnPath) return cdnPath;
947
+ } catch {
948
+ }
949
+ return void 0;
950
+ }
825
951
  function extFromUrl(url, fallback = "bin") {
826
952
  try {
827
953
  const pathname = new URL(url).pathname;
@@ -866,7 +992,10 @@ async function downloadOutputs(urls, template, vars, fetchImpl = fetch) {
866
992
  index: i,
867
993
  ext: extFromUrl(url)
868
994
  });
869
- files.push(await downloadUrl(url, dest, fetchImpl));
995
+ const file = await downloadUrl(url, dest, fetchImpl);
996
+ const preview = await writePreview(file.path, url, file.bytes, fetchImpl);
997
+ if (preview) file.preview = preview;
998
+ files.push(file);
870
999
  }
871
1000
  return files;
872
1001
  }
@@ -888,6 +1017,8 @@ var MIME_BY_EXT = {
888
1017
  m4v: "video/x-m4v",
889
1018
  mp3: "audio/mpeg",
890
1019
  wav: "audio/wav",
1020
+ pcm: "audio/L16",
1021
+ opus: "audio/opus",
891
1022
  m4a: "audio/mp4",
892
1023
  aac: "audio/aac",
893
1024
  ogg: "audio/ogg",
@@ -917,19 +1048,25 @@ async function uploadFile(client, localPath, options = {}) {
917
1048
  const uploadUrl = created?.upload_url;
918
1049
  const filePath = created?.file_path;
919
1050
  if (!uploadUrl || !filePath) {
920
- throw new CliError("create_media_upload did not return upload_url/file_path.");
1051
+ throw new CliError(
1052
+ "create_media_upload did not return upload_url/file_path."
1053
+ );
921
1054
  }
922
1055
  const { size } = fs3.statSync(resolved);
923
1056
  const putRes = await fetchImpl(uploadUrl, {
924
1057
  method: "PUT",
925
1058
  headers: { "content-type": contentType, "content-length": String(size) },
926
- body: Readable2.toWeb(fs3.createReadStream(resolved)),
1059
+ body: Readable2.toWeb(
1060
+ fs3.createReadStream(resolved)
1061
+ ),
927
1062
  // Node/undici requires duplex:"half" when the body is a stream.
928
1063
  duplex: "half",
929
1064
  signal: AbortSignal.timeout(6e5)
930
1065
  });
931
1066
  if (!putRes.ok) {
932
- throw new CliError(`Upload PUT failed (HTTP ${putRes.status}). The presigned URL may have expired \u2014 retry.`);
1067
+ throw new CliError(
1068
+ `Upload PUT failed (HTTP ${putRes.status}). The presigned URL may have expired \u2014 retry.`
1069
+ );
933
1070
  }
934
1071
  const finalized = await client.callTool("finalize_media_upload", {
935
1072
  file_path: filePath,