videodraft 0.3.0 → 0.3.2

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/dist/client.d.ts CHANGED
@@ -12,7 +12,7 @@ interface TokenProvider {
12
12
  * Called once after a 401. Return a fresh token to retry with, or null to
13
13
  * give up (the 401 then surfaces as AuthError).
14
14
  */
15
- onUnauthorized?(): Promise<string | null>;
15
+ onUnauthorized?(failedToken?: string): Promise<string | null>;
16
16
  }
17
17
  interface VideoDraftClientOptions {
18
18
  tokenProvider: TokenProvider;
package/dist/client.js CHANGED
@@ -221,7 +221,7 @@ var VideoDraftClient = class {
221
221
  const token = await this.tokenProvider.getAccessToken();
222
222
  let response = await this.post(method, params, token);
223
223
  if (response.status === 401 && this.tokenProvider.onUnauthorized) {
224
- const fresh = await this.tokenProvider.onUnauthorized();
224
+ const fresh = await this.tokenProvider.onUnauthorized(token);
225
225
  if (fresh) response = await this.post(method, params, fresh);
226
226
  }
227
227
  if (response.status === 401) {
@@ -284,7 +284,7 @@ var VideoDraftClient = class {
284
284
  const token = await this.tokenProvider.getAccessToken();
285
285
  let response = await doFetch(token);
286
286
  if (response.status === 401 && this.tokenProvider.onUnauthorized) {
287
- const fresh = await this.tokenProvider.onUnauthorized();
287
+ const fresh = await this.tokenProvider.onUnauthorized(token);
288
288
  if (fresh) response = await doFetch(fresh);
289
289
  }
290
290
  if (response.status === 401) {
@@ -330,7 +330,7 @@ var VideoDraftClient = class {
330
330
  });
331
331
  let response = await post(token);
332
332
  if (response.status === 401 && this.tokenProvider.onUnauthorized) {
333
- const fresh = await this.tokenProvider.onUnauthorized();
333
+ const fresh = await this.tokenProvider.onUnauthorized(token);
334
334
  if (fresh) response = await post(fresh);
335
335
  }
336
336
  if (response.status === 401) throw new AuthError("Token is invalid, expired, or revoked.");
@@ -622,6 +622,7 @@ async function refreshUnderLock(profileName, env) {
622
622
  );
623
623
  }
624
624
  function profileTokenProvider(profileName, env = process.env) {
625
+ let lastAccessToken = null;
625
626
  return {
626
627
  async getAccessToken() {
627
628
  const { profile } = getProfile(profileName, env);
@@ -629,18 +630,30 @@ function profileTokenProvider(profileName, env = process.env) {
629
630
  if (profile.auth_kind === "oauth" && isExpiring(profile)) {
630
631
  const fresh = await refreshUnderLock(profileName, env);
631
632
  if (!fresh) throw new AuthError("Your session expired and could not be refreshed.");
633
+ lastAccessToken = fresh;
632
634
  return fresh;
633
635
  }
636
+ lastAccessToken = profile.access_token;
634
637
  return profile.access_token;
635
638
  },
636
- async onUnauthorized() {
639
+ async onUnauthorized(failedToken) {
637
640
  const { profile } = getProfile(profileName, env);
638
- if (profile?.auth_kind !== "oauth") return null;
641
+ if (!profile?.access_token) return null;
642
+ if (profile.auth_kind !== "oauth") {
643
+ const staleToken = failedToken ?? lastAccessToken;
644
+ if (profile.access_token !== staleToken) {
645
+ lastAccessToken = profile.access_token;
646
+ return profile.access_token;
647
+ }
648
+ return null;
649
+ }
639
650
  updateConfig((config) => {
640
651
  const p = config.profiles[profileName];
641
- if (p) p.expires_at = (/* @__PURE__ */ new Date(0)).toISOString();
652
+ if (p?.auth_kind === "oauth") p.expires_at = (/* @__PURE__ */ new Date(0)).toISOString();
642
653
  }, env);
643
- return refreshUnderLock(profileName, env);
654
+ const fresh = await refreshUnderLock(profileName, env);
655
+ if (fresh) lastAccessToken = fresh;
656
+ return fresh;
644
657
  }
645
658
  };
646
659
  }
package/dist/index.js CHANGED
@@ -24,8 +24,8 @@ function readVersionFromDisk() {
24
24
  }
25
25
  }
26
26
  function resolveVersion() {
27
- if ("0.3.0") {
28
- return "0.3.0";
27
+ if ("0.3.2") {
28
+ return "0.3.2";
29
29
  }
30
30
  return readVersionFromDisk();
31
31
  }
@@ -648,7 +648,7 @@ var VideoDraftClient = class {
648
648
  const token = await this.tokenProvider.getAccessToken();
649
649
  let response = await this.post(method, params, token);
650
650
  if (response.status === 401 && this.tokenProvider.onUnauthorized) {
651
- const fresh = await this.tokenProvider.onUnauthorized();
651
+ const fresh = await this.tokenProvider.onUnauthorized(token);
652
652
  if (fresh) response = await this.post(method, params, fresh);
653
653
  }
654
654
  if (response.status === 401) {
@@ -711,7 +711,7 @@ var VideoDraftClient = class {
711
711
  const token = await this.tokenProvider.getAccessToken();
712
712
  let response = await doFetch(token);
713
713
  if (response.status === 401 && this.tokenProvider.onUnauthorized) {
714
- const fresh = await this.tokenProvider.onUnauthorized();
714
+ const fresh = await this.tokenProvider.onUnauthorized(token);
715
715
  if (fresh) response = await doFetch(fresh);
716
716
  }
717
717
  if (response.status === 401) {
@@ -757,7 +757,7 @@ var VideoDraftClient = class {
757
757
  });
758
758
  let response = await post(token);
759
759
  if (response.status === 401 && this.tokenProvider.onUnauthorized) {
760
- const fresh = await this.tokenProvider.onUnauthorized();
760
+ const fresh = await this.tokenProvider.onUnauthorized(token);
761
761
  if (fresh) response = await post(fresh);
762
762
  }
763
763
  if (response.status === 401) throw new AuthError("Token is invalid, expired, or revoked.");
@@ -869,6 +869,7 @@ async function refreshUnderLock(profileName, env) {
869
869
  );
870
870
  }
871
871
  function profileTokenProvider(profileName, env = process.env) {
872
+ let lastAccessToken = null;
872
873
  return {
873
874
  async getAccessToken() {
874
875
  const { profile } = getProfile(profileName, env);
@@ -876,18 +877,30 @@ function profileTokenProvider(profileName, env = process.env) {
876
877
  if (profile.auth_kind === "oauth" && isExpiring(profile)) {
877
878
  const fresh = await refreshUnderLock(profileName, env);
878
879
  if (!fresh) throw new AuthError("Your session expired and could not be refreshed.");
880
+ lastAccessToken = fresh;
879
881
  return fresh;
880
882
  }
883
+ lastAccessToken = profile.access_token;
881
884
  return profile.access_token;
882
885
  },
883
- async onUnauthorized() {
886
+ async onUnauthorized(failedToken) {
884
887
  const { profile } = getProfile(profileName, env);
885
- if (profile?.auth_kind !== "oauth") return null;
888
+ if (!profile?.access_token) return null;
889
+ if (profile.auth_kind !== "oauth") {
890
+ const staleToken = failedToken ?? lastAccessToken;
891
+ if (profile.access_token !== staleToken) {
892
+ lastAccessToken = profile.access_token;
893
+ return profile.access_token;
894
+ }
895
+ return null;
896
+ }
886
897
  updateConfig((config) => {
887
898
  const p = config.profiles[profileName];
888
- if (p) p.expires_at = (/* @__PURE__ */ new Date(0)).toISOString();
899
+ if (p?.auth_kind === "oauth") p.expires_at = (/* @__PURE__ */ new Date(0)).toISOString();
889
900
  }, env);
890
- return refreshUnderLock(profileName, env);
901
+ const fresh = await refreshUnderLock(profileName, env);
902
+ if (fresh) lastAccessToken = fresh;
903
+ return fresh;
891
904
  }
892
905
  };
893
906
  }
@@ -1907,7 +1920,7 @@ function registerGenerateCommands(program) {
1907
1920
  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(
1908
1921
  "--quality <tier>",
1909
1922
  'e.g. "mini", "fast", "standard", "quality", "pro"'
1910
- ).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 = []) {
1923
+ ).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; Gemini Omni Flash, Seedance 2, Kling O3, 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 = []) {
1911
1924
  const ctx = buildContext(this);
1912
1925
  const opts = this.opts();
1913
1926
  const prompt = promptWords.join(" ").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "videodraft",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
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",
@@ -12,8 +12,8 @@ videodraft models styles --json # visual style presets
12
12
 
13
13
  ## Defaults (safe starting points)
14
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.
16
- - **Video**: `google-veo3.1` at fast quality (6s / 720p) — the platform default.
15
+ - **Image**: `nano-banana-2` (the platform default, 1K, up to 14 reference images). Use `--num 1..4` for variations of one prompt in a single call — never loop for variations. `nano-banana-2-lite` is the fastest/cheapest Google direct image model (1K only, up to 14 reference images).
16
+ - **Video**: `google-veo3.1` at fast quality (6s / 720p) — the platform default. `gemini-omni-flash` is Google's any-to-any multimodal video model (text/image/video → video, auto or 3-10s, 720p, audio always on).
17
17
  - **Voiceover**: ElevenLabs Brittney (default voice).
18
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
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.
@@ -24,7 +24,7 @@ videodraft models styles --json # visual style presets
24
24
  - Most video models support only 16:9 / 9:16 / 1:1. A 3:4 request hard-fails on most.
25
25
  - `--seed` reproduces a specific output on models that support it (e.g. Flux, Ideogram V4); everything else ignores it. You do not need a seed for variation — `--num` already varies.
26
26
  - `--rendering-speed` applies to Ideogram (V3: `Default`/`Turbo`/`Quality`; V4: `Turbo`/`Balanced`/`Quality`) and affects image cost — pass it to `videodraft costs ... --rendering-speed <tier>` for an accurate estimate. Always trust `videodraft models image --json` over this list; new models and tiers appear there the moment the platform ships them, with no CLI update.
27
- - Reference inputs: `--ref <img>` (images), `--ref-video <v>` (Seedance 2, Wan 2.7), `--ref-audio <a>` (Seedance 2). The CLI uploads local files for all of these, so you can pass a path or a URL. `--segment "<prompt>:<seconds>"` (repeatable) drives multi-prompt models (Kling 3.0 / 3.0 Turbo / O3); total 3-15s. `generate image --video-ref` is the nano-banana-2 video reference.
27
+ - Reference inputs: `--ref <img>` (images), `--ref-video <v>` (Gemini Omni Flash, Seedance 2, Kling O3, Wan 2.7), `--ref-audio <a>` (Seedance 2). The CLI uploads local files for all of these, so you can pass a path or a URL. `--segment "<prompt>:<seconds>"` (repeatable) drives multi-prompt models (Kling 3.0 / 3.0 Turbo / O3); total 3-15s. `generate image --video-ref` is the nano-banana-2 video reference.
28
28
  - The top-level prompt is OPTIONAL for `generate video` with multi-prompt models and for Kling 3.0 Turbo (`--model kling-v3-turbo`) image-to-video — a `--segment`-only or `--start-image`-only call is valid. Every other model still needs a prompt; the server enforces per-model rules.
29
29
  - AI Production: `videodraft produce <project> --mode full_video` generates one Seedance 2 video per scene; poll with `videodraft generations`, then `videodraft finalize <project>` swaps them into the timeline before `export`.
30
30