@koda-sl/baker-cli 0.277.0-dev.1f1c09c80 → 0.280.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/README.md +9 -0
- package/dist/cli.js +133 -14
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3648,6 +3648,15 @@ because a video model garbles a wordmark every time:
|
|
|
3648
3648
|
the ad needs something other than the client's default.
|
|
3649
3649
|
- **The spoken language is read off the script** when the spec does not declare it, so a
|
|
3650
3650
|
Spanish ad is cast from Spanish voices without anyone having to say so.
|
|
3651
|
+
- **`sound`** (per beat) — one sound effect for that shot, rendered by ElevenLabs
|
|
3652
|
+
alongside the music bed. Most beats should not have one: it belongs where the picture
|
|
3653
|
+
shows a specific physical event (a drill, a door, rain on glass), not on a talking head
|
|
3654
|
+
or a calm lifestyle shot. A bed plus two well-placed effects beats eight.
|
|
3655
|
+
- **A real brand mark is required.** `scaffold-ad` refuses to build without one in
|
|
3656
|
+
`src/brand/logos/` (SVG or PNG): the mark is drawn on every frame and on the closing
|
|
3657
|
+
card, so a stand-in ships an ad wearing the wrong brand.
|
|
3658
|
+
- **An avatar with no pinned voice cannot speak on camera.** Their lines would be read by
|
|
3659
|
+
a separately cast voice, which is dubbing and looks like it.
|
|
3651
3660
|
- **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
|
|
3652
3661
|
become on-screen text and are captioned straight from the script, so the words are
|
|
3653
3662
|
exact. Give it `music` too.
|
package/dist/cli.js
CHANGED
|
@@ -30367,7 +30367,11 @@ function buildRunRecord(result, meta, plan, finalLabels) {
|
|
|
30367
30367
|
},
|
|
30368
30368
|
nodes,
|
|
30369
30369
|
finalOutputs: finalOutputs.length > 0 ? finalOutputs : void 0,
|
|
30370
|
-
|
|
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) } : {}
|
|
30371
30375
|
};
|
|
30372
30376
|
}
|
|
30373
30377
|
function buildInitialRunRecord(runId, meta) {
|
|
@@ -31040,6 +31044,32 @@ function videoPathFromOutput(output) {
|
|
|
31040
31044
|
}
|
|
31041
31045
|
return found;
|
|
31042
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
|
+
}
|
|
31043
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.";
|
|
31044
31074
|
var REVIEW_CONCURRENCY = 3;
|
|
31045
31075
|
async function mapWithLimit(items, limit, fn) {
|
|
@@ -31055,6 +31085,21 @@ async function mapWithLimit(items, limit, fn) {
|
|
|
31055
31085
|
return out;
|
|
31056
31086
|
}
|
|
31057
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
|
+
}
|
|
31058
31103
|
async function videoDuration(video) {
|
|
31059
31104
|
try {
|
|
31060
31105
|
const { stdout } = await execFileAsync2(
|
|
@@ -31118,7 +31163,12 @@ async function reviewRenderedVideo(opts) {
|
|
|
31118
31163
|
}));
|
|
31119
31164
|
const unread = perFrame.filter((f) => f.json === null).length;
|
|
31120
31165
|
if (unread === perFrame.length && reel === null) return null;
|
|
31121
|
-
const findings = [
|
|
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
|
+
];
|
|
31122
31172
|
if (unread > 0) {
|
|
31123
31173
|
findings.push({
|
|
31124
31174
|
severity: "warning",
|
|
@@ -31444,6 +31494,9 @@ ${describeRewrites(healed.rewrites)}
|
|
|
31444
31494
|
] : [];
|
|
31445
31495
|
if (poster) {
|
|
31446
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.
|
|
31447
31500
|
buildRunRecord(result, { ...recordMeta, ...review ? { review } : {} }, progress?.planInfo(), finalLabels)
|
|
31448
31501
|
);
|
|
31449
31502
|
}
|
|
@@ -32521,7 +32574,7 @@ var scaffoldStaticAdCommand = defineCommand103({
|
|
|
32521
32574
|
});
|
|
32522
32575
|
|
|
32523
32576
|
// src/commands/canvas/scaffold-ad.ts
|
|
32524
|
-
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";
|
|
32525
32578
|
import path24 from "path";
|
|
32526
32579
|
import { defineCommand as defineCommand104 } from "citty";
|
|
32527
32580
|
|
|
@@ -32540,6 +32593,23 @@ var Beat = z30.object({
|
|
|
32540
32593
|
).refine(oneClause, "a beat is one clause: split a second sentence into its own beat"),
|
|
32541
32594
|
/** What is on screen while it is said, as a shot brief for the video model. */
|
|
32542
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(),
|
|
32543
32613
|
/**
|
|
32544
32614
|
* Set only when this beat's subject is talking to camera. Off by default, and
|
|
32545
32615
|
* the default is load-bearing: a person visibly speaking under a voice that is
|
|
@@ -32834,6 +32904,10 @@ function adSpecToBlueprint(spec) {
|
|
|
32834
32904
|
// No dialogue at all when nobody speaks: an empty array is what stops the engine
|
|
32835
32905
|
// wiring a voice track, and no voice track is what makes this a music-led ad
|
|
32836
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) }] } : {},
|
|
32837
32911
|
dialogue: spec.voiceover === false ? [] : [
|
|
32838
32912
|
{
|
|
32839
32913
|
line: beat.say,
|
|
@@ -33052,6 +33126,9 @@ async function readBrandFromWorkspace(root = ".") {
|
|
|
33052
33126
|
}
|
|
33053
33127
|
return found;
|
|
33054
33128
|
}
|
|
33129
|
+
function svgIsLiveText(svg) {
|
|
33130
|
+
return /<text[\s>]/i.test(svg);
|
|
33131
|
+
}
|
|
33055
33132
|
|
|
33056
33133
|
// src/commands/canvas/composition-path.ts
|
|
33057
33134
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -33080,7 +33157,7 @@ registerSchema({
|
|
|
33080
33157
|
spec: {
|
|
33081
33158
|
type: "string",
|
|
33082
33159
|
required: true,
|
|
33083
|
-
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.'
|
|
33160
|
+
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.'
|
|
33084
33161
|
},
|
|
33085
33162
|
avatar: {
|
|
33086
33163
|
type: "string",
|
|
@@ -33191,6 +33268,48 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
33191
33268
|
...filledPalette ? { palette: filledPalette } : {}
|
|
33192
33269
|
};
|
|
33193
33270
|
}
|
|
33271
|
+
const speaksOnCamera = spec.data.beats.some((beat) => beat.on_camera === true);
|
|
33272
|
+
if (avatar && !avatar.voiceId && speaksOnCamera) {
|
|
33273
|
+
writeJson({
|
|
33274
|
+
ok: false,
|
|
33275
|
+
error: {
|
|
33276
|
+
code: "VALIDATION_ERROR",
|
|
33277
|
+
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.`,
|
|
33278
|
+
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.`
|
|
33279
|
+
}
|
|
33280
|
+
});
|
|
33281
|
+
process.exit(1);
|
|
33282
|
+
return;
|
|
33283
|
+
}
|
|
33284
|
+
const resolvedLogo = spec.data.brand?.logo?.trim();
|
|
33285
|
+
const logoExists = resolvedLogo ? await stat4(resolvedLogo).then(() => true, () => false) : false;
|
|
33286
|
+
if (!logoExists) {
|
|
33287
|
+
writeJson({
|
|
33288
|
+
ok: false,
|
|
33289
|
+
error: {
|
|
33290
|
+
code: "VALIDATION_ERROR",
|
|
33291
|
+
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.`,
|
|
33292
|
+
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.`
|
|
33293
|
+
}
|
|
33294
|
+
});
|
|
33295
|
+
process.exit(1);
|
|
33296
|
+
return;
|
|
33297
|
+
}
|
|
33298
|
+
if (resolvedLogo?.toLowerCase().endsWith(".svg")) {
|
|
33299
|
+
const svg = await readFile16(resolvedLogo, "utf-8").catch(() => "");
|
|
33300
|
+
if (svg && svgIsLiveText(svg)) {
|
|
33301
|
+
writeJson({
|
|
33302
|
+
ok: false,
|
|
33303
|
+
error: {
|
|
33304
|
+
code: "VALIDATION_ERROR",
|
|
33305
|
+
message: `The mark at \`${resolvedLogo}\` draws its wordmark with live \`<text>\`, not outlines. That is a typed stand-in, not a logo: it renders in whatever font the machine happens to have, and the one that shipped this way came out reading "greenle\xE1" in every frame.`,
|
|
33306
|
+
fix: `Ask the client for their real logo file and put it in \`${BRAND_DIR}/logos/\` \u2014 an outlined SVG, or a PNG at 1000px or wider. Do not redraw it.`
|
|
33307
|
+
}
|
|
33308
|
+
});
|
|
33309
|
+
process.exit(1);
|
|
33310
|
+
return;
|
|
33311
|
+
}
|
|
33312
|
+
}
|
|
33194
33313
|
const blueprint = adSpecToBlueprint(spec.data);
|
|
33195
33314
|
const slug = args.slug ?? path24.basename(specPath).replace(/\.[^.]+$/, "");
|
|
33196
33315
|
const outPath = args.out ?? (args.slug ? path24.join("src/creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(specPath), `${slug}.canvas.json`));
|
|
@@ -39061,7 +39180,7 @@ function cropSprite(input, region) {
|
|
|
39061
39180
|
|
|
39062
39181
|
// src/lib/image/io.ts
|
|
39063
39182
|
import { randomBytes } from "crypto";
|
|
39064
|
-
import { glob as fsGlob, readFile as readFile23, rename, stat as
|
|
39183
|
+
import { glob as fsGlob, readFile as readFile23, rename, stat as stat5, writeFile as writeFile12 } from "fs/promises";
|
|
39065
39184
|
import { dirname as dirname2, extname as extname2, join as join5, resolve as resolve4 } from "path";
|
|
39066
39185
|
var REMOTE_RE = /^https?:\/\//i;
|
|
39067
39186
|
var GLOB_RE = /[*?[\]{}]/;
|
|
@@ -39098,7 +39217,7 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
39098
39217
|
}
|
|
39099
39218
|
async function isDirectory(path41) {
|
|
39100
39219
|
try {
|
|
39101
|
-
const s = await
|
|
39220
|
+
const s = await stat5(path41);
|
|
39102
39221
|
return s.isDirectory();
|
|
39103
39222
|
} catch {
|
|
39104
39223
|
return false;
|
|
@@ -42495,7 +42614,7 @@ Full guide: __tooling__/docs/tools/baker/images.md`
|
|
|
42495
42614
|
import { defineCommand as defineCommand167 } from "citty";
|
|
42496
42615
|
|
|
42497
42616
|
// src/commands/landing/critique.ts
|
|
42498
|
-
import { readdir as readdir11, stat as
|
|
42617
|
+
import { readdir as readdir11, stat as stat7 } from "fs/promises";
|
|
42499
42618
|
import path31 from "path";
|
|
42500
42619
|
import { defineCommand as defineCommand157 } from "citty";
|
|
42501
42620
|
|
|
@@ -43482,7 +43601,7 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
|
|
|
43482
43601
|
}
|
|
43483
43602
|
|
|
43484
43603
|
// src/commands/landing/source-version.ts
|
|
43485
|
-
import { readdir as readdir10, readFile as readFile24, stat as
|
|
43604
|
+
import { readdir as readdir10, readFile as readFile24, stat as stat6 } from "fs/promises";
|
|
43486
43605
|
import path30 from "path";
|
|
43487
43606
|
async function landingSourceRelPaths(landingDir) {
|
|
43488
43607
|
const rel = [];
|
|
@@ -43515,7 +43634,7 @@ async function computeLandingSourceSha(landingDir) {
|
|
|
43515
43634
|
}
|
|
43516
43635
|
async function isFile(p) {
|
|
43517
43636
|
try {
|
|
43518
|
-
return (await
|
|
43637
|
+
return (await stat6(p)).isFile();
|
|
43519
43638
|
} catch {
|
|
43520
43639
|
return false;
|
|
43521
43640
|
}
|
|
@@ -43668,7 +43787,7 @@ function present(f) {
|
|
|
43668
43787
|
}
|
|
43669
43788
|
async function isDir(p) {
|
|
43670
43789
|
try {
|
|
43671
|
-
return (await
|
|
43790
|
+
return (await stat7(p)).isDirectory();
|
|
43672
43791
|
} catch {
|
|
43673
43792
|
return false;
|
|
43674
43793
|
}
|
|
@@ -51363,7 +51482,7 @@ var groupCommand2 = defineCommand210({
|
|
|
51363
51482
|
});
|
|
51364
51483
|
|
|
51365
51484
|
// src/commands/videos/ingest.ts
|
|
51366
|
-
import { mkdtemp as mkdtemp3, rm as rm8, stat as
|
|
51485
|
+
import { mkdtemp as mkdtemp3, rm as rm8, stat as stat8 } from "fs/promises";
|
|
51367
51486
|
import { tmpdir as tmpdir4 } from "os";
|
|
51368
51487
|
import path40 from "path";
|
|
51369
51488
|
import { defineCommand as defineCommand211 } from "citty";
|
|
@@ -51747,7 +51866,7 @@ async function downloadThenIngest(args, country) {
|
|
|
51747
51866
|
// we allow to fetch it cannot drift apart.
|
|
51748
51867
|
timeoutMs: downloadTimeoutMs(durationSeconds(probe.info))
|
|
51749
51868
|
});
|
|
51750
|
-
const stats = await
|
|
51869
|
+
const stats = await stat8(filePath);
|
|
51751
51870
|
if (stats.size > MAX_VIDEO_INGEST_BYTES) {
|
|
51752
51871
|
throw new ApiError(
|
|
51753
51872
|
"VALIDATION_ERROR",
|
|
@@ -51852,7 +51971,7 @@ var searchCommand4 = defineCommand212({
|
|
|
51852
51971
|
var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
51853
51972
|
|
|
51854
51973
|
// src/commands/videos/upload.ts
|
|
51855
|
-
import { readFile as readFile26, stat as
|
|
51974
|
+
import { readFile as readFile26, stat as stat9 } from "fs/promises";
|
|
51856
51975
|
import { basename as basename3, extname as extname4 } from "path";
|
|
51857
51976
|
import { defineCommand as defineCommand213 } from "citty";
|
|
51858
51977
|
var MIME_MAP = {
|
|
@@ -51936,7 +52055,7 @@ var uploadCommand2 = defineCommand213({
|
|
|
51936
52055
|
const originalFilename = basename3(filePath);
|
|
51937
52056
|
const descriptionContext = args.context;
|
|
51938
52057
|
if (args["dry-run"]) {
|
|
51939
|
-
const fileStats = await
|
|
52058
|
+
const fileStats = await stat9(filePath);
|
|
51940
52059
|
writeJson({
|
|
51941
52060
|
ok: true,
|
|
51942
52061
|
dryRun: true,
|