@koda-sl/baker-cli 0.276.0-dev.1f1c09c80 → 0.279.0-dev.1f1c09c80

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/cli.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-4IJX4R4J.js";
61
+ } from "./chunk-AUOSDGWY.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -3699,14 +3699,22 @@ var avatarSummarySchema = z7.object({
3699
3699
  subjectDescription: z7.string(),
3700
3700
  /** The identity sheet to wire into `--reference`. Absent until the build settles. */
3701
3701
  sheetUrl: z7.string().optional(),
3702
+ /**
3703
+ * The avatar's pinned voice.
3704
+ *
3705
+ * A casting essential, not a profile detail: an avatar is a face AND a voice, and
3706
+ * casting the face without it is what makes a presenter speak in somebody else's.
3707
+ * It lived only behind `--full`, so an ad that grounded every frame on the identity
3708
+ * sheet still had its lines read by a voice cast from a prose description.
3709
+ */
3710
+ voiceId: z7.string().optional(),
3711
+ voiceDescription: z7.string().optional(),
3702
3712
  coverUrl: z7.string().optional(),
3703
3713
  errorMessage: z7.string().optional()
3704
3714
  });
3705
3715
  var avatarDetailSchema = avatarSummarySchema.extend({
3706
3716
  profile: avatarProfileSchema,
3707
3717
  sourceUrls: z7.array(z7.string()),
3708
- voiceId: z7.string().optional(),
3709
- voiceDescription: z7.string().optional(),
3710
3718
  createdAt: z7.number()
3711
3719
  });
3712
3720
  var avatarsListRequestSchema = z7.object({
@@ -28705,7 +28713,7 @@ function buildPhrases(blueprint, canonical2, compositeScenes, presenterPresent,
28705
28713
  flush();
28706
28714
  return phrases;
28707
28715
  }
28708
- function makeVoiceFactory(blueprint, canonical2, nodes, voiceLanguage) {
28716
+ function makeVoiceFactory(blueprint, canonical2, nodes, voiceLanguage, pinnedVoiceId) {
28709
28717
  const bySpeaker = /* @__PURE__ */ new Map();
28710
28718
  const describe = (speaker) => {
28711
28719
  for (const scene of blueprint.scenes)
@@ -28721,7 +28729,9 @@ function makeVoiceFactory(blueprint, canonical2, nodes, voiceLanguage) {
28721
28729
  const description = describe(speaker);
28722
28730
  const traits = parseVoiceTraits(description);
28723
28731
  if (voiceLanguage) traits.language = voiceLanguage;
28724
- nodes.push({ id, type: "voice_select", params: { description, ...traits } });
28732
+ nodes.push(
28733
+ pinnedVoiceId ? { id, type: "voice_select", params: { description, voice_id: pinnedVoiceId, ...traits } } : { id, type: "voice_select", params: { description, ...traits } }
28734
+ );
28725
28735
  bySpeaker.set(speaker, id);
28726
28736
  return id;
28727
28737
  };
@@ -29122,7 +29132,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
29122
29132
  });
29123
29133
  }
29124
29134
  const canonical2 = collapseVoiceover(blueprint);
29125
- const ensureVoiceNode = makeVoiceFactory(blueprint, canonical2, nodes, opts.voiceLanguage);
29135
+ const ensureVoiceNode = makeVoiceFactory(blueprint, canonical2, nodes, opts.voiceLanguage, opts.voiceId);
29126
29136
  const aspect = resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel));
29127
29137
  const env = {
29128
29138
  blueprint,
@@ -30357,7 +30367,11 @@ function buildRunRecord(result, meta, plan, finalLabels) {
30357
30367
  },
30358
30368
  nodes,
30359
30369
  finalOutputs: finalOutputs.length > 0 ? finalOutputs : void 0,
30360
- ...meta.review && meta.review.length > 0 ? { review: meta.review.slice(0, 40) } : {}
30370
+ // Sent whenever the review RAN, empty list included. Sending it only when there
30371
+ // were findings makes a clean render and an unreviewed one look identical in the
30372
+ // record — which is the exact ambiguity this field was added to remove: proving a
30373
+ // review had happened once meant re-running a cached canvas and reading raw JSON.
30374
+ ...meta.review ? { review: meta.review.slice(0, 40) } : {}
30361
30375
  };
30362
30376
  }
30363
30377
  function buildInitialRunRecord(runId, meta) {
@@ -31030,6 +31044,32 @@ function videoPathFromOutput(output) {
31030
31044
  }
31031
31045
  return found;
31032
31046
  }
31047
+ function classifyAudio(volumedetect, hasAudioStream) {
31048
+ if (!hasAudioStream) {
31049
+ return [
31050
+ {
31051
+ severity: "blocking",
31052
+ what: "The finished video has no audio track at all \u2014 no voice, no music, nothing. An ad that plays silent in a feed is not an ad."
31053
+ }
31054
+ ];
31055
+ }
31056
+ const mean = Number.parseFloat(volumedetect.match(/mean_volume:\s*(-?[\d.]+) dB/)?.[1] ?? "");
31057
+ const max = Number.parseFloat(volumedetect.match(/max_volume:\s*(-?[\d.]+) dB/)?.[1] ?? "");
31058
+ const findings = [];
31059
+ if (Number.isFinite(mean) && mean < -30) {
31060
+ findings.push({
31061
+ severity: "warning",
31062
+ what: `The mix is very quiet (${mean} dB average). On a phone at half volume this is close to silence \u2014 check the music bed is not ducked into nothing under the voice.`
31063
+ });
31064
+ }
31065
+ if (Number.isFinite(max) && max > -0.5) {
31066
+ findings.push({
31067
+ severity: "warning",
31068
+ what: `The mix peaks at ${max} dB, which is at or over the ceiling \u2014 the voice will distort on a phone speaker. Bring the loudest track down a few dB.`
31069
+ });
31070
+ }
31071
+ return findings;
31072
+ }
31033
31073
  var RENDER_REVIEW_UNAVAILABLE = "The render was NOT reviewed \u2014 no frame-vision key in this environment. Watch it yourself before sending it on.";
31034
31074
  var REVIEW_CONCURRENCY = 3;
31035
31075
  async function mapWithLimit(items, limit, fn) {
@@ -31045,6 +31085,21 @@ async function mapWithLimit(items, limit, fn) {
31045
31085
  return out;
31046
31086
  }
31047
31087
  var execFileAsync2 = promisify2(execFile2);
31088
+ async function measureAudio(video) {
31089
+ const streams = await execFileAsync2(
31090
+ "ffprobe",
31091
+ ["-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_type", "-of", "csv=p=0", video],
31092
+ { timeout: 2e4 }
31093
+ ).catch(() => null);
31094
+ if (!streams) return [];
31095
+ const hasAudio = streams.stdout.trim().length > 0;
31096
+ if (!hasAudio) return classifyAudio("", false);
31097
+ const vol = await execFileAsync2("ffmpeg", ["-i", video, "-map", "0:a:0", "-af", "volumedetect", "-f", "null", "-"], {
31098
+ timeout: 12e4,
31099
+ maxBuffer: 8 * 1024 * 1024
31100
+ }).catch((e) => ({ stdout: "", stderr: e.stderr ?? "" }));
31101
+ return classifyAudio(vol.stderr ?? "", true);
31102
+ }
31048
31103
  async function videoDuration(video) {
31049
31104
  try {
31050
31105
  const { stdout } = await execFileAsync2(
@@ -31108,7 +31163,12 @@ async function reviewRenderedVideo(opts) {
31108
31163
  }));
31109
31164
  const unread = perFrame.filter((f) => f.json === null).length;
31110
31165
  if (unread === perFrame.length && reel === null) return null;
31111
- const findings = [...classifyFrameFindings(perFrame, opts.windows ?? []), ...classifyReelFindings(reel)];
31166
+ const findings = [
31167
+ ...classifyFrameFindings(perFrame, opts.windows ?? []),
31168
+ ...classifyReelFindings(reel),
31169
+ // Measured, not judged: a model looking at frames cannot hear the mix.
31170
+ ...await measureAudio(opts.video)
31171
+ ];
31112
31172
  if (unread > 0) {
31113
31173
  findings.push({
31114
31174
  severity: "warning",
@@ -31434,6 +31494,9 @@ ${describeRewrites(healed.rewrites)}
31434
31494
  ] : [];
31435
31495
  if (poster) {
31436
31496
  await poster.flush(
31497
+ // `review` is null only when nobody looked (no key, no ffmpeg, unreadable video);
31498
+ // an empty array is a render that was reviewed and came back clean. The record
31499
+ // has to keep those apart.
31437
31500
  buildRunRecord(result, { ...recordMeta, ...review ? { review } : {} }, progress?.planInfo(), finalLabels)
31438
31501
  );
31439
31502
  }
@@ -32511,7 +32574,7 @@ var scaffoldStaticAdCommand = defineCommand103({
32511
32574
  });
32512
32575
 
32513
32576
  // src/commands/canvas/scaffold-ad.ts
32514
- import { copyFile, cp, mkdir as mkdir7, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
32577
+ import { copyFile, cp, mkdir as mkdir7, readFile as readFile16, stat as stat4, writeFile as writeFile9 } from "fs/promises";
32515
32578
  import path24 from "path";
32516
32579
  import { defineCommand as defineCommand104 } from "citty";
32517
32580
 
@@ -32530,6 +32593,23 @@ var Beat = z30.object({
32530
32593
  ).refine(oneClause, "a beat is one clause: split a second sentence into its own beat"),
32531
32594
  /** What is on screen while it is said, as a shot brief for the video model. */
32532
32595
  show: z30.string().min(1),
32596
+ /**
32597
+ * What this shot SOUNDS like — one effect, in words. Usually absent.
32598
+ *
32599
+ * An effect earns its place when the picture shows a SPECIFIC PHYSICAL EVENT the
32600
+ * viewer expects to hear: a drill tightening a bolt, a door closing, rain on glass, a
32601
+ * card tapping a reader, a graphic snapping into place. It grounds the shot.
32602
+ *
32603
+ * It does NOT belong on a talking head, on a lifestyle shot with no event in it, or
32604
+ * on a beat that is simply someone sitting and reacting. Sound laid over those reads
32605
+ * as a stock-library sting, and a bed of them turns a calm ad into a noisy one. Most
32606
+ * ads are a music bed and a voice; a couple of effects at the right moments, or none
32607
+ * at all, is a normal and often better answer than one per beat.
32608
+ *
32609
+ * Describe the SOUND, not the picture: "a cordless drill tightening a bolt", not "the
32610
+ * fitter works on the rail".
32611
+ */
32612
+ sound: z30.string().min(1).max(200).optional(),
32533
32613
  /**
32534
32614
  * Set only when this beat's subject is talking to camera. Off by default, and
32535
32615
  * the default is load-bearing: a person visibly speaking under a voice that is
@@ -32775,7 +32855,8 @@ function adSpecCastElements(spec, avatar) {
32775
32855
  function motionPrompt(beat, isClosingCard, place, physics) {
32776
32856
  if (isClosingCard) return BRAND_PLATE;
32777
32857
  const shot = `${beat.show}${place}${physics}`;
32778
- return beat.on_camera ? shot : `${shot} Nobody in frame is speaking \u2014 mouths closed, no dialogue.`;
32858
+ if (beat.on_camera) return shot;
32859
+ return `${shot} Nobody in frame is speaking: mouths CLOSED and STILL throughout, no talking, no lip movement. Whoever is in shot is looking, reacting or working \u2014 never addressing the camera.`;
32779
32860
  }
32780
32861
  function staticTranscript(spec) {
32781
32862
  const out = [];
@@ -32823,6 +32904,10 @@ function adSpecToBlueprint(spec) {
32823
32904
  // No dialogue at all when nobody speaks: an empty array is what stops the engine
32824
32905
  // wiring a voice track, and no voice track is what makes this a music-led ad
32825
32906
  // rather than one with a silent narrator.
32907
+ // One effect per beat, placed on this beat's window. The engine clamps the
32908
+ // length to what the provider accepts. An ad emitted none of these until now,
32909
+ // so every one shipped with a music bed and no sound design at all.
32910
+ ...beat.sound?.trim() ? { sfx: [{ sound_effect_prompt: beat.sound.trim(), duration_s: Math.min(duration, 30) }] } : {},
32826
32911
  dialogue: spec.voiceover === false ? [] : [
32827
32912
  {
32828
32913
  line: beat.say,
@@ -33069,7 +33154,7 @@ registerSchema({
33069
33154
  spec: {
33070
33155
  type: "string",
33071
33156
  required: true,
33072
- description: 'Path to the ad spec JSON. Shape: { format?, market?, brand?, cast?, end_card?, voice?, music?, beats: [{ say, show, on_camera?, cast? }] }. Fill `brand` from src/brand/BRAND.md \u2014 `palette` (its hex tokens, most important first) and `logo` (the repo path to the mark) are what make the ad look like the client rather than like stock. `market` is where the ad is SET; omitted, it is inferred from the voice language, and getting it wrong is what fills a Spanish ad with British houses. `say` is ONE clause ending in its own punctuation \u2014 it becomes a caption card verbatim, so two sentences in one beat produce a card holding both. `show` is the shot brief for that line. Set `on_camera` ONLY when that beat\'s subject talks to camera. `voiceover: false` makes it a MUSIC-LED ad: nobody speaks, the `say` lines become on-screen text captioned straight from the script, and it needs `music`. `cast` is WHO the ad is about, and an ad with people in it needs one: `{ "avatar": "marta" }` names a cast avatar (`baker avatars list`) and every beat is grounded on that avatar\'s identity sheet with its subject description copied verbatim \u2014 the same face here as in the rest of the company\'s work. Without it each beat invents its own stranger, which is how one 28-second ad came back with five different men playing one customer. `{ "description": "..." }` is the fallback when there is no avatar, and it scaffolds a canvas that asks you to drop a photo before it can run \u2014 so prefer the avatar. A beat sets `cast: false` for a shot they are not in. `end_card` is on by default whenever `brand` is set: the last beat becomes a flat brand plate with the mark and a call to action drawn over it. `{ "cta": "..." }` sets the button copy, `false` keeps the footage.'
33157
+ description: 'Path to the ad spec JSON. Shape: { format?, market?, brand?, cast?, end_card?, voice?, music?, beats: [{ say, show, on_camera?, cast? }] }. Fill `brand` from src/brand/BRAND.md \u2014 `palette` (its hex tokens, most important first) and `logo` (the repo path to the mark) are what make the ad look like the client rather than like stock. `market` is where the ad is SET; omitted, it is inferred from the voice language, and getting it wrong is what fills a Spanish ad with British houses. `say` is ONE clause ending in its own punctuation \u2014 it becomes a caption card verbatim, so two sentences in one beat produce a card holding both. `show` is the shot brief for that line. Set `on_camera` ONLY when that beat\'s subject talks to camera. A beat\'s `sound` is one sound EFFECT for that shot \u2014 and MOST BEATS SHOULD NOT HAVE ONE. Add it only where the picture shows a specific physical event the viewer expects to hear (a drill tightening a bolt, a door closing, rain on glass, a graphic snapping in). On a talking head or a calm lifestyle shot it reads as a stock sting, and one per beat turns a quiet ad into a noisy one. A music bed plus two well-placed effects beats eight. `voiceover: false` makes it a MUSIC-LED ad: nobody speaks, the `say` lines become on-screen text captioned straight from the script, and it needs `music`. `cast` is WHO the ad is about, and an ad with people in it needs one: `{ "avatar": "marta" }` names a cast avatar (`baker avatars list`) and every beat is grounded on that avatar\'s identity sheet with its subject description copied verbatim \u2014 the same face here as in the rest of the company\'s work. Without it each beat invents its own stranger, which is how one 28-second ad came back with five different men playing one customer. `{ "description": "..." }` is the fallback when there is no avatar, and it scaffolds a canvas that asks you to drop a photo before it can run \u2014 so prefer the avatar. A beat sets `cast: false` for a shot they are not in. `end_card` is on by default whenever `brand` is set: the last beat becomes a flat brand plate with the mark and a call to action drawn over it. `{ "cta": "..." }` sets the button copy, `false` keeps the footage.'
33073
33158
  },
33074
33159
  avatar: {
33075
33160
  type: "string",
@@ -33180,6 +33265,33 @@ var scaffoldAdCommand = defineCommand104({
33180
33265
  ...filledPalette ? { palette: filledPalette } : {}
33181
33266
  };
33182
33267
  }
33268
+ const speaksOnCamera = spec.data.beats.some((beat) => beat.on_camera === true);
33269
+ if (avatar && !avatar.voiceId && speaksOnCamera) {
33270
+ writeJson({
33271
+ ok: false,
33272
+ error: {
33273
+ code: "VALIDATION_ERROR",
33274
+ message: `Avatar \`${handle}\` has no pinned voice, and this ad has them speaking on camera. Their lines would be read by a voice cast from a description \u2014 a mouth moving under somebody else's voice, which is exactly what dubbing looks like.`,
33275
+ fix: `Give \`${handle}\` a voice first, or drop \`on_camera\` from every beat so they appear without speaking and the narration carries the read. An avatar is a face AND a voice; casting one without the other is what puts the wrong voice in their mouth.`
33276
+ }
33277
+ });
33278
+ process.exit(1);
33279
+ return;
33280
+ }
33281
+ const resolvedLogo = spec.data.brand?.logo?.trim();
33282
+ const logoExists = resolvedLogo ? await stat4(resolvedLogo).then(() => true, () => false) : false;
33283
+ if (!logoExists) {
33284
+ writeJson({
33285
+ ok: false,
33286
+ error: {
33287
+ code: "VALIDATION_ERROR",
33288
+ message: resolvedLogo ? `The brand mark at \`${resolvedLogo}\` does not exist, so this ad has no logo to put on screen.` : `This company has no brand mark in \`${BRAND_DIR}/logos/\`, so an ad cannot carry its logo.`,
33289
+ fix: `Put the client's real mark in \`${BRAND_DIR}/logos/\` \u2014 an SVG is best, a PNG is fine \u2014 and run this again. Ask them for it if you have to; do NOT draw one, and do not pass a placeholder. The mark is drawn on every frame and on the closing card, so a stand-in ships an ad wearing the wrong brand.`
33290
+ }
33291
+ });
33292
+ process.exit(1);
33293
+ return;
33294
+ }
33183
33295
  const blueprint = adSpecToBlueprint(spec.data);
33184
33296
  const slug = args.slug ?? path24.basename(specPath).replace(/\.[^.]+$/, "");
33185
33297
  const outPath = args.out ?? (args.slug ? path24.join("src/creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(specPath), `${slug}.canvas.json`));
@@ -33219,6 +33331,10 @@ var scaffoldAdCommand = defineCommand104({
33219
33331
  // Told, not guessed: the voice description is written in the ad's own language
33220
33332
  // and the engine's trait parser reads English only.
33221
33333
  voiceLanguage: voiceLanguageFor(spec.data),
33334
+ // The cast avatar's own voice. An avatar is a face AND a voice; grounding every
33335
+ // frame on their identity sheet while a separately cast voice reads the lines is
33336
+ // how a presenter ends up speaking in somebody else's.
33337
+ ...avatar?.voiceId ? { voiceId: avatar.voiceId } : {},
33222
33338
  ...spec.data.voiceover === false ? { staticTranscriptPath: path24.relative(outDir, transcriptPath) } : {}
33223
33339
  };
33224
33340
  const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data, avatar), opts);
@@ -33272,6 +33388,9 @@ var scaffoldAdCommand = defineCommand104({
33272
33388
  ] : [],
33273
33389
  ...avatarError ? [avatarError] : [],
33274
33390
  ...avatar ? [
33391
+ ...avatar?.voiceId ? [] : [
33392
+ `Avatar \`${handle}\` has no pinned voice, so the ad's lines are read by a voice cast from a description \u2014 it will not sound like them. Give them one (\`baker avatars\` voice) before this ships.`
33393
+ ],
33275
33394
  `Every beat the customer appears in is grounded on \`${handle}\`'s identity sheet, and their subject description was copied into the frames verbatim \u2014 so it is the same face throughout and the same face as the rest of this company's work.`
33276
33395
  ] : [],
33277
33396
  ...presenterIsInEveryShot(spec.data) ? [
@@ -39043,7 +39162,7 @@ function cropSprite(input, region) {
39043
39162
 
39044
39163
  // src/lib/image/io.ts
39045
39164
  import { randomBytes } from "crypto";
39046
- import { glob as fsGlob, readFile as readFile23, rename, stat as stat4, writeFile as writeFile12 } from "fs/promises";
39165
+ import { glob as fsGlob, readFile as readFile23, rename, stat as stat5, writeFile as writeFile12 } from "fs/promises";
39047
39166
  import { dirname as dirname2, extname as extname2, join as join5, resolve as resolve4 } from "path";
39048
39167
  var REMOTE_RE = /^https?:\/\//i;
39049
39168
  var GLOB_RE = /[*?[\]{}]/;
@@ -39080,7 +39199,7 @@ async function readImageBuffer(pathOrUrl) {
39080
39199
  }
39081
39200
  async function isDirectory(path41) {
39082
39201
  try {
39083
- const s = await stat4(path41);
39202
+ const s = await stat5(path41);
39084
39203
  return s.isDirectory();
39085
39204
  } catch {
39086
39205
  return false;
@@ -42477,7 +42596,7 @@ Full guide: __tooling__/docs/tools/baker/images.md`
42477
42596
  import { defineCommand as defineCommand167 } from "citty";
42478
42597
 
42479
42598
  // src/commands/landing/critique.ts
42480
- import { readdir as readdir11, stat as stat6 } from "fs/promises";
42599
+ import { readdir as readdir11, stat as stat7 } from "fs/promises";
42481
42600
  import path31 from "path";
42482
42601
  import { defineCommand as defineCommand157 } from "citty";
42483
42602
 
@@ -43464,7 +43583,7 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
43464
43583
  }
43465
43584
 
43466
43585
  // src/commands/landing/source-version.ts
43467
- import { readdir as readdir10, readFile as readFile24, stat as stat5 } from "fs/promises";
43586
+ import { readdir as readdir10, readFile as readFile24, stat as stat6 } from "fs/promises";
43468
43587
  import path30 from "path";
43469
43588
  async function landingSourceRelPaths(landingDir) {
43470
43589
  const rel = [];
@@ -43497,7 +43616,7 @@ async function computeLandingSourceSha(landingDir) {
43497
43616
  }
43498
43617
  async function isFile(p) {
43499
43618
  try {
43500
- return (await stat5(p)).isFile();
43619
+ return (await stat6(p)).isFile();
43501
43620
  } catch {
43502
43621
  return false;
43503
43622
  }
@@ -43650,7 +43769,7 @@ function present(f) {
43650
43769
  }
43651
43770
  async function isDir(p) {
43652
43771
  try {
43653
- return (await stat6(p)).isDirectory();
43772
+ return (await stat7(p)).isDirectory();
43654
43773
  } catch {
43655
43774
  return false;
43656
43775
  }
@@ -51345,7 +51464,7 @@ var groupCommand2 = defineCommand210({
51345
51464
  });
51346
51465
 
51347
51466
  // src/commands/videos/ingest.ts
51348
- import { mkdtemp as mkdtemp3, rm as rm8, stat as stat7 } from "fs/promises";
51467
+ import { mkdtemp as mkdtemp3, rm as rm8, stat as stat8 } from "fs/promises";
51349
51468
  import { tmpdir as tmpdir4 } from "os";
51350
51469
  import path40 from "path";
51351
51470
  import { defineCommand as defineCommand211 } from "citty";
@@ -51729,7 +51848,7 @@ async function downloadThenIngest(args, country) {
51729
51848
  // we allow to fetch it cannot drift apart.
51730
51849
  timeoutMs: downloadTimeoutMs(durationSeconds(probe.info))
51731
51850
  });
51732
- const stats = await stat7(filePath);
51851
+ const stats = await stat8(filePath);
51733
51852
  if (stats.size > MAX_VIDEO_INGEST_BYTES) {
51734
51853
  throw new ApiError(
51735
51854
  "VALIDATION_ERROR",
@@ -51834,7 +51953,7 @@ var searchCommand4 = defineCommand212({
51834
51953
  var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
51835
51954
 
51836
51955
  // src/commands/videos/upload.ts
51837
- import { readFile as readFile26, stat as stat8 } from "fs/promises";
51956
+ import { readFile as readFile26, stat as stat9 } from "fs/promises";
51838
51957
  import { basename as basename3, extname as extname4 } from "path";
51839
51958
  import { defineCommand as defineCommand213 } from "citty";
51840
51959
  var MIME_MAP = {
@@ -51918,7 +52037,7 @@ var uploadCommand2 = defineCommand213({
51918
52037
  const originalFilename = basename3(filePath);
51919
52038
  const descriptionContext = args.context;
51920
52039
  if (args["dry-run"]) {
51921
- const fileStats = await stat8(filePath);
52040
+ const fileStats = await stat9(filePath);
51922
52041
  writeJson({
51923
52042
  ok: true,
51924
52043
  dryRun: true,