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 +25 -12
- package/dist/client.d.ts +6 -0
- package/dist/client.js +141 -4
- package/dist/index.js +371 -22
- package/package.json +1 -1
- package/skills/index.json +9 -9
- package/skills/videodraft/SKILL.md +6 -5
- package/skills/videodraft/references/examples.md +1 -0
- package/skills/videodraft/references/models.md +3 -0
- package/skills/videodraft/references/pipeline.md +1 -0
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|