@koda-sl/baker-cli 0.98.0-dev.62be5b016 → 0.98.1
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 +12 -0
- package/canvas/tiktok-captions-composition/index.html +7 -2
- package/canvas/video-overlay-composition/index.html +18 -25
- package/dist/{chunk-IKMDQQ4M.js → chunk-3JVYU72O.js} +133 -29
- package/dist/chunk-3JVYU72O.js.map +1 -0
- package/dist/cli.js +413 -314
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IKMDQQ4M.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
AssetRef,
|
|
4
3
|
ELEVENLABS_MAX_MUSIC_LENGTH_MS,
|
|
5
4
|
IMAGE_GENERATE_MODELS,
|
|
6
5
|
MODEL_REGISTRY,
|
|
@@ -8,10 +7,9 @@ import {
|
|
|
8
7
|
ValidationError,
|
|
9
8
|
createEngineFromEnv,
|
|
10
9
|
defaultRegistry,
|
|
11
|
-
extForMime,
|
|
12
10
|
generateCatalog,
|
|
13
11
|
validateCanvasDeep
|
|
14
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-3JVYU72O.js";
|
|
15
13
|
|
|
16
14
|
// src/cli.ts
|
|
17
15
|
import { defineCommand as defineCommand149, runMain } from "citty";
|
|
@@ -149,9 +147,9 @@ async function handleResponse(response) {
|
|
|
149
147
|
throw new ApiError("INTERNAL_ERROR", "Failed to parse API response as JSON");
|
|
150
148
|
}
|
|
151
149
|
}
|
|
152
|
-
async function apiGet(
|
|
150
|
+
async function apiGet(path11, params) {
|
|
153
151
|
const env = getEnv();
|
|
154
|
-
const url = new URL(
|
|
152
|
+
const url = new URL(path11, env.BAKER_API_URL);
|
|
155
153
|
if (params) {
|
|
156
154
|
const clean = sanitizeParams(params);
|
|
157
155
|
for (const [key, value] of Object.entries(clean)) {
|
|
@@ -176,12 +174,12 @@ async function apiGet(path8, params) {
|
|
|
176
174
|
}
|
|
177
175
|
return handleResponse(response);
|
|
178
176
|
}
|
|
179
|
-
async function apiPost(
|
|
177
|
+
async function apiPost(path11, body, opts) {
|
|
180
178
|
const env = getEnv();
|
|
181
179
|
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
182
180
|
let response;
|
|
183
181
|
try {
|
|
184
|
-
response = await fetchWithRateLimitRetry(new URL(
|
|
182
|
+
response = await fetchWithRateLimitRetry(new URL(path11, env.BAKER_API_URL).toString(), {
|
|
185
183
|
method: "POST",
|
|
186
184
|
headers: {
|
|
187
185
|
Authorization: `Bearer ${env.BAKER_API_KEY}`,
|
|
@@ -1329,31 +1327,31 @@ function cachePath(category, key) {
|
|
|
1329
1327
|
return join(dir, `${hashKey(key)}.json`);
|
|
1330
1328
|
}
|
|
1331
1329
|
function cacheGet(category, key) {
|
|
1332
|
-
const
|
|
1333
|
-
if (!existsSync(
|
|
1330
|
+
const path11 = cachePath(category, key);
|
|
1331
|
+
if (!existsSync(path11)) {
|
|
1334
1332
|
return null;
|
|
1335
1333
|
}
|
|
1336
1334
|
try {
|
|
1337
|
-
const raw = readFileSync(
|
|
1335
|
+
const raw = readFileSync(path11, "utf-8");
|
|
1338
1336
|
const entry = JSON.parse(raw);
|
|
1339
1337
|
if (entry.expiresAt < Date.now()) {
|
|
1340
|
-
rmSync(
|
|
1338
|
+
rmSync(path11, { force: true });
|
|
1341
1339
|
return null;
|
|
1342
1340
|
}
|
|
1343
1341
|
return entry;
|
|
1344
1342
|
} catch {
|
|
1345
|
-
rmSync(
|
|
1343
|
+
rmSync(path11, { force: true });
|
|
1346
1344
|
return null;
|
|
1347
1345
|
}
|
|
1348
1346
|
}
|
|
1349
1347
|
function cacheSet(category, key, data, ttlMs, fields) {
|
|
1350
|
-
const
|
|
1348
|
+
const path11 = cachePath(category, key);
|
|
1351
1349
|
const entry = {
|
|
1352
1350
|
expiresAt: Date.now() + ttlMs,
|
|
1353
1351
|
data,
|
|
1354
1352
|
fields
|
|
1355
1353
|
};
|
|
1356
|
-
writeFileSync(
|
|
1354
|
+
writeFileSync(path11, JSON.stringify(entry), "utf-8");
|
|
1357
1355
|
}
|
|
1358
1356
|
var HOUR = 60 * 60 * 1e3;
|
|
1359
1357
|
var MINUTE = 60 * 1e3;
|
|
@@ -8063,232 +8061,14 @@ var catalogCommand = defineCommand78({
|
|
|
8063
8061
|
}
|
|
8064
8062
|
});
|
|
8065
8063
|
|
|
8066
|
-
// src/commands/canvas/gallery.ts
|
|
8067
|
-
import { readdir, readFile } from "fs/promises";
|
|
8068
|
-
import path from "path";
|
|
8069
|
-
import { defineCommand as defineCommand79 } from "citty";
|
|
8070
|
-
|
|
8071
|
-
// src/engine/gallery/descriptor.ts
|
|
8072
|
-
var KNOWN_RATIOS = [
|
|
8073
|
-
["9:16", 9 / 16],
|
|
8074
|
-
["4:5", 4 / 5],
|
|
8075
|
-
["1:1", 1],
|
|
8076
|
-
["1.91:1", 1.91],
|
|
8077
|
-
["16:9", 16 / 9],
|
|
8078
|
-
["4:1", 4]
|
|
8079
|
-
];
|
|
8080
|
-
var RATIO_TOLERANCE = 0.06;
|
|
8081
|
-
function aspectLabel(width, height) {
|
|
8082
|
-
if (!width || !height) {
|
|
8083
|
-
return "other";
|
|
8084
|
-
}
|
|
8085
|
-
const ratio = width / height;
|
|
8086
|
-
let best = "other";
|
|
8087
|
-
let bestErr = Number.POSITIVE_INFINITY;
|
|
8088
|
-
for (const [label, value] of KNOWN_RATIOS) {
|
|
8089
|
-
const err = Math.abs(ratio - value) / value;
|
|
8090
|
-
if (err < bestErr) {
|
|
8091
|
-
bestErr = err;
|
|
8092
|
-
best = label;
|
|
8093
|
-
}
|
|
8094
|
-
}
|
|
8095
|
-
return bestErr <= RATIO_TOLERANCE ? best : `${width}x${height}`;
|
|
8096
|
-
}
|
|
8097
|
-
function visualRef(value) {
|
|
8098
|
-
const parsed = AssetRef.safeParse(value);
|
|
8099
|
-
if (!parsed.success) {
|
|
8100
|
-
return null;
|
|
8101
|
-
}
|
|
8102
|
-
if (parsed.data.kind !== "image" && parsed.data.kind !== "video") {
|
|
8103
|
-
return null;
|
|
8104
|
-
}
|
|
8105
|
-
return parsed.data;
|
|
8106
|
-
}
|
|
8107
|
-
function deliverableFor(ref, stem, resolveLocal) {
|
|
8108
|
-
const width = "width" in ref ? ref.width : void 0;
|
|
8109
|
-
const height = "height" in ref ? ref.height : void 0;
|
|
8110
|
-
return {
|
|
8111
|
-
kind: ref.kind,
|
|
8112
|
-
format: aspectLabel(width, height),
|
|
8113
|
-
// Remote-node outputs already carry a public R2 url; local composites are
|
|
8114
|
-
// resolved against the mounted run dir's public base.
|
|
8115
|
-
url: ref.url ?? resolveLocal(`${stem}.${extForMime(ref.mime)}`),
|
|
8116
|
-
width,
|
|
8117
|
-
height,
|
|
8118
|
-
label: stem
|
|
8119
|
-
};
|
|
8120
|
-
}
|
|
8121
|
-
function deliverablesFromOutput(output, resolveLocal) {
|
|
8122
|
-
if (Array.isArray(output)) {
|
|
8123
|
-
const out = [];
|
|
8124
|
-
output.forEach((entry, i) => {
|
|
8125
|
-
const ref2 = visualRef(entry);
|
|
8126
|
-
if (ref2) {
|
|
8127
|
-
out.push(deliverableFor(ref2, `_final__${i}`, resolveLocal));
|
|
8128
|
-
}
|
|
8129
|
-
});
|
|
8130
|
-
return out;
|
|
8131
|
-
}
|
|
8132
|
-
const ref = visualRef(output);
|
|
8133
|
-
return ref ? [deliverableFor(ref, "_final", resolveLocal)] : [];
|
|
8134
|
-
}
|
|
8135
|
-
function buildGeneration(runId, manifest, resolveLocal) {
|
|
8136
|
-
const m = manifest ?? {};
|
|
8137
|
-
const credits = typeof m.stats?.total_credits === "number" ? m.stats.total_credits : 0;
|
|
8138
|
-
return {
|
|
8139
|
-
runId,
|
|
8140
|
-
createdAt: typeof m.completed_at === "number" ? m.completed_at : 0,
|
|
8141
|
-
credits,
|
|
8142
|
-
deliverables: deliverablesFromOutput(m.output, resolveLocal)
|
|
8143
|
-
};
|
|
8144
|
-
}
|
|
8145
|
-
function buildGalleryDescriptor(input) {
|
|
8146
|
-
const generations = [...input.generations].sort((a, b) => b.createdAt - a.createdAt);
|
|
8147
|
-
return {
|
|
8148
|
-
slug: input.slug,
|
|
8149
|
-
title: input.definition.title,
|
|
8150
|
-
platform: input.definition.platform,
|
|
8151
|
-
status: input.definition.status,
|
|
8152
|
-
reference: input.definition.reference,
|
|
8153
|
-
selectedRun: input.definition.selectedRun,
|
|
8154
|
-
generations
|
|
8155
|
-
};
|
|
8156
|
-
}
|
|
8157
|
-
function titleFromSlug(slug) {
|
|
8158
|
-
return slug.split(/[-_/]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
8159
|
-
}
|
|
8160
|
-
function stripQuotes(raw) {
|
|
8161
|
-
const trimmed = raw.trim();
|
|
8162
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
8163
|
-
return trimmed.slice(1, -1);
|
|
8164
|
-
}
|
|
8165
|
-
return trimmed;
|
|
8166
|
-
}
|
|
8167
|
-
function parseInlineList(raw) {
|
|
8168
|
-
return raw.slice(1, -1).split(",").map((item) => stripQuotes(item)).filter(Boolean);
|
|
8169
|
-
}
|
|
8170
|
-
function parseFrontmatter(markdown) {
|
|
8171
|
-
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
8172
|
-
const block = match?.[1];
|
|
8173
|
-
if (!block) {
|
|
8174
|
-
return {};
|
|
8175
|
-
}
|
|
8176
|
-
const out = {};
|
|
8177
|
-
let listKey = null;
|
|
8178
|
-
for (const line of block.split(/\r?\n/)) {
|
|
8179
|
-
const item = line.match(/^\s+-\s+(.*)$/)?.[1];
|
|
8180
|
-
if (listKey && item !== void 0) {
|
|
8181
|
-
out[listKey].push(stripQuotes(item));
|
|
8182
|
-
continue;
|
|
8183
|
-
}
|
|
8184
|
-
const kv = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
8185
|
-
if (!kv?.[1]) {
|
|
8186
|
-
continue;
|
|
8187
|
-
}
|
|
8188
|
-
listKey = null;
|
|
8189
|
-
const key = kv[1];
|
|
8190
|
-
const value = (kv[2] ?? "").trim();
|
|
8191
|
-
if (value === "") {
|
|
8192
|
-
out[key] = [];
|
|
8193
|
-
listKey = key;
|
|
8194
|
-
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
8195
|
-
out[key] = parseInlineList(value);
|
|
8196
|
-
} else {
|
|
8197
|
-
out[key] = stripQuotes(value);
|
|
8198
|
-
}
|
|
8199
|
-
}
|
|
8200
|
-
return out;
|
|
8201
|
-
}
|
|
8202
|
-
function asString(value) {
|
|
8203
|
-
if (typeof value === "string" && value.length > 0) {
|
|
8204
|
-
return value;
|
|
8205
|
-
}
|
|
8206
|
-
return void 0;
|
|
8207
|
-
}
|
|
8208
|
-
function asList(value) {
|
|
8209
|
-
if (Array.isArray(value)) {
|
|
8210
|
-
return value;
|
|
8211
|
-
}
|
|
8212
|
-
return typeof value === "string" && value.length > 0 ? [value] : [];
|
|
8213
|
-
}
|
|
8214
|
-
function parseCreativeDefinition(markdown, slug) {
|
|
8215
|
-
const fm = parseFrontmatter(markdown);
|
|
8216
|
-
return {
|
|
8217
|
-
title: asString(fm.title) ?? titleFromSlug(slug),
|
|
8218
|
-
platform: asList(fm.platform),
|
|
8219
|
-
formats: asList(fm.formats),
|
|
8220
|
-
status: asString(fm.status) ?? "draft",
|
|
8221
|
-
reference: asString(fm.reference),
|
|
8222
|
-
selectedRun: asString(fm.selected_run)
|
|
8223
|
-
};
|
|
8224
|
-
}
|
|
8225
|
-
|
|
8226
|
-
// src/commands/canvas/gallery.ts
|
|
8227
|
-
async function readJson(file) {
|
|
8228
|
-
try {
|
|
8229
|
-
return JSON.parse(await readFile(file, "utf8"));
|
|
8230
|
-
} catch {
|
|
8231
|
-
return null;
|
|
8232
|
-
}
|
|
8233
|
-
}
|
|
8234
|
-
async function listRunDirs(runsDir) {
|
|
8235
|
-
try {
|
|
8236
|
-
const entries = await readdir(runsDir, { withFileTypes: true });
|
|
8237
|
-
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
8238
|
-
} catch {
|
|
8239
|
-
return [];
|
|
8240
|
-
}
|
|
8241
|
-
}
|
|
8242
|
-
var galleryCommand = defineCommand79({
|
|
8243
|
-
meta: {
|
|
8244
|
-
name: "gallery",
|
|
8245
|
-
description: "Read a creative's _definition.md + every persisted run manifest and emit the gallery descriptor (JSON) the dashboard renders."
|
|
8246
|
-
},
|
|
8247
|
-
args: {
|
|
8248
|
-
dir: { type: "positional", required: true, description: "Creative folder, e.g. src/creatives/<slug>" },
|
|
8249
|
-
"workspace-dir": { type: "string", description: "R2-mounted workspace root (default ./.creatives-workspace)" },
|
|
8250
|
-
"public-url": { type: "string", description: "R2 public base (default $R2_PUBLIC_URL)" },
|
|
8251
|
-
"company-id": { type: "string", description: "Company id for the R2 prefix (default $BAKER_COMPANY_ID)" }
|
|
8252
|
-
},
|
|
8253
|
-
async run({ args }) {
|
|
8254
|
-
const creativeDir = path.resolve(String(args.dir));
|
|
8255
|
-
const slug = path.basename(creativeDir);
|
|
8256
|
-
const workspaceDir = path.resolve(String(args["workspace-dir"] ?? ".creatives-workspace"));
|
|
8257
|
-
const runsDir = path.join(workspaceDir, slug, "runs");
|
|
8258
|
-
const runtimeEnv = process.env;
|
|
8259
|
-
const publicUrl = (args["public-url"] ?? runtimeEnv.R2_PUBLIC_URL ?? "").replace(/\/+$/, "");
|
|
8260
|
-
const companyId = String(args["company-id"] ?? runtimeEnv.BAKER_COMPANY_ID ?? "");
|
|
8261
|
-
const definitionPath = path.join(creativeDir, "_definition.md");
|
|
8262
|
-
let definitionMd = "";
|
|
8263
|
-
try {
|
|
8264
|
-
definitionMd = await readFile(definitionPath, "utf8");
|
|
8265
|
-
} catch {
|
|
8266
|
-
}
|
|
8267
|
-
const definition = parseCreativeDefinition(definitionMd, slug);
|
|
8268
|
-
const generations = [];
|
|
8269
|
-
for (const runId of await listRunDirs(runsDir)) {
|
|
8270
|
-
const manifest = await readJson(path.join(runsDir, runId, "manifest.json"));
|
|
8271
|
-
if (!manifest) {
|
|
8272
|
-
continue;
|
|
8273
|
-
}
|
|
8274
|
-
const runDir = path.join(runsDir, runId);
|
|
8275
|
-
const resolveLocal = (filename) => publicUrl && companyId ? `${publicUrl}/creatives/${companyId}/${slug}/runs/${runId}/${filename}` : path.join(runDir, filename);
|
|
8276
|
-
generations.push(buildGeneration(runId, manifest, resolveLocal));
|
|
8277
|
-
}
|
|
8278
|
-
const descriptor = buildGalleryDescriptor({ slug, definition, generations });
|
|
8279
|
-
process.stdout.write(`${JSON.stringify({ ok: true, descriptor }, null, 2)}
|
|
8280
|
-
`);
|
|
8281
|
-
}
|
|
8282
|
-
});
|
|
8283
|
-
|
|
8284
8064
|
// src/commands/canvas/inspect.ts
|
|
8285
8065
|
import { execFile } from "child_process";
|
|
8286
|
-
import { readdir
|
|
8287
|
-
import
|
|
8066
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
8067
|
+
import path from "path";
|
|
8288
8068
|
import { promisify } from "util";
|
|
8289
|
-
import { defineCommand as
|
|
8069
|
+
import { defineCommand as defineCommand79 } from "citty";
|
|
8290
8070
|
var execFileAsync = promisify(execFile);
|
|
8291
|
-
var inspectCommand =
|
|
8071
|
+
var inspectCommand = defineCommand79({
|
|
8292
8072
|
meta: {
|
|
8293
8073
|
name: "inspect",
|
|
8294
8074
|
description: "Dump a one-page summary of a canvas run: per-node duration + cache status, list of output files in the run dir, and optionally three thumbnail frames per video output. Pass either a run_id (resolved against --outputs-dir) or an absolute run directory."
|
|
@@ -8302,7 +8082,7 @@ var inspectCommand = defineCommand80({
|
|
|
8302
8082
|
}
|
|
8303
8083
|
},
|
|
8304
8084
|
async run({ args }) {
|
|
8305
|
-
const outputsDir =
|
|
8085
|
+
const outputsDir = path.resolve(String(args["outputs-dir"] ?? "canvas"));
|
|
8306
8086
|
const runArg = String(args.run);
|
|
8307
8087
|
const runDir = await resolveRunDir(runArg, outputsDir);
|
|
8308
8088
|
const manifest = await loadManifest(runDir);
|
|
@@ -8314,7 +8094,7 @@ var inspectCommand = defineCommand80({
|
|
|
8314
8094
|
}
|
|
8315
8095
|
const summary = {
|
|
8316
8096
|
ok: true,
|
|
8317
|
-
run_id: manifest.run_id ??
|
|
8097
|
+
run_id: manifest.run_id ?? path.basename(runDir),
|
|
8318
8098
|
run_dir: runDir,
|
|
8319
8099
|
stats: manifest.stats ?? null,
|
|
8320
8100
|
output: manifest.output ?? null,
|
|
@@ -8327,20 +8107,20 @@ var inspectCommand = defineCommand80({
|
|
|
8327
8107
|
}
|
|
8328
8108
|
});
|
|
8329
8109
|
async function resolveRunDir(run, outputsDir) {
|
|
8330
|
-
if (
|
|
8110
|
+
if (path.isAbsolute(run)) {
|
|
8331
8111
|
const s2 = await stat(run).catch(() => null);
|
|
8332
8112
|
if (s2?.isDirectory()) return run;
|
|
8333
8113
|
throw new Error(`inspect: ${run} is not a directory`);
|
|
8334
8114
|
}
|
|
8335
|
-
const candidate =
|
|
8115
|
+
const candidate = path.join(outputsDir, run);
|
|
8336
8116
|
const s = await stat(candidate).catch(() => null);
|
|
8337
8117
|
if (s?.isDirectory()) return candidate;
|
|
8338
8118
|
throw new Error(`inspect: no run directory at ${candidate}`);
|
|
8339
8119
|
}
|
|
8340
8120
|
async function loadManifest(runDir) {
|
|
8341
|
-
const manifestPath =
|
|
8121
|
+
const manifestPath = path.join(runDir, "manifest.json");
|
|
8342
8122
|
try {
|
|
8343
|
-
const raw = await
|
|
8123
|
+
const raw = await readFile(manifestPath, "utf-8");
|
|
8344
8124
|
return JSON.parse(raw);
|
|
8345
8125
|
} catch {
|
|
8346
8126
|
return {};
|
|
@@ -8348,9 +8128,9 @@ async function loadManifest(runDir) {
|
|
|
8348
8128
|
}
|
|
8349
8129
|
async function listRunFiles(runDir) {
|
|
8350
8130
|
const out = [];
|
|
8351
|
-
const names = await
|
|
8131
|
+
const names = await readdir(runDir);
|
|
8352
8132
|
for (const name of names) {
|
|
8353
|
-
const abs =
|
|
8133
|
+
const abs = path.join(runDir, name);
|
|
8354
8134
|
const s = await stat(abs).catch(() => null);
|
|
8355
8135
|
if (!s?.isFile()) continue;
|
|
8356
8136
|
out.push({ name, path: abs, size: s.size });
|
|
@@ -8395,9 +8175,9 @@ async function probeDuration(filePath) {
|
|
|
8395
8175
|
}
|
|
8396
8176
|
|
|
8397
8177
|
// src/commands/canvas/run.ts
|
|
8398
|
-
import { readFile as
|
|
8399
|
-
import
|
|
8400
|
-
import { defineCommand as
|
|
8178
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
8179
|
+
import path4 from "path";
|
|
8180
|
+
import { defineCommand as defineCommand80 } from "citty";
|
|
8401
8181
|
|
|
8402
8182
|
// src/commands/canvas/placeholders.ts
|
|
8403
8183
|
function unsuppliedPlaceholderAssets(canvas) {
|
|
@@ -8415,19 +8195,74 @@ function unsuppliedPlaceholderAssets(canvas) {
|
|
|
8415
8195
|
return out;
|
|
8416
8196
|
}
|
|
8417
8197
|
|
|
8198
|
+
// src/commands/canvas/resolve-paths.ts
|
|
8199
|
+
import path2 from "path";
|
|
8200
|
+
function resolveRelativeCanvasPaths(canvas, baseDir) {
|
|
8201
|
+
if (!canvas || typeof canvas !== "object") return canvas;
|
|
8202
|
+
const c = canvas;
|
|
8203
|
+
if (!Array.isArray(c.nodes)) return canvas;
|
|
8204
|
+
return { ...canvas, nodes: c.nodes.map((n) => resolveNode(n, baseDir)) };
|
|
8205
|
+
}
|
|
8206
|
+
function resolveNode(node, baseDir) {
|
|
8207
|
+
if (!node || typeof node !== "object") return node;
|
|
8208
|
+
const n = node;
|
|
8209
|
+
const params = n.params;
|
|
8210
|
+
if (!params || typeof params !== "object") return node;
|
|
8211
|
+
if (n.type === "ingest" && params.source === "path" && isResolvableRelative(params.path)) {
|
|
8212
|
+
return { ...node, params: { ...params, path: path2.resolve(baseDir, params.path) } };
|
|
8213
|
+
}
|
|
8214
|
+
if (n.type === "hyperframe_render" && isResolvableRelative(params.composition)) {
|
|
8215
|
+
return { ...node, params: { ...params, composition: path2.resolve(baseDir, params.composition) } };
|
|
8216
|
+
}
|
|
8217
|
+
return node;
|
|
8218
|
+
}
|
|
8219
|
+
function isResolvableRelative(value) {
|
|
8220
|
+
return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !path2.isAbsolute(value);
|
|
8221
|
+
}
|
|
8222
|
+
|
|
8223
|
+
// src/commands/canvas/run-retention.ts
|
|
8224
|
+
import { rm } from "fs/promises";
|
|
8225
|
+
import path3 from "path";
|
|
8226
|
+
function runDirsToPrune(entries, keep, currentRunId) {
|
|
8227
|
+
const runs = entries.filter((e) => /^r_[0-9A-Za-z]+$/.test(e) && e !== currentRunId).sort();
|
|
8228
|
+
if (keep <= 0) return runs;
|
|
8229
|
+
return runs.slice(0, Math.max(0, runs.length - keep));
|
|
8230
|
+
}
|
|
8231
|
+
async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
|
|
8232
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
8233
|
+
let entries;
|
|
8234
|
+
try {
|
|
8235
|
+
entries = await readdir3(outputsDir);
|
|
8236
|
+
} catch {
|
|
8237
|
+
return;
|
|
8238
|
+
}
|
|
8239
|
+
const toPrune = runDirsToPrune(entries, keep, currentRunId);
|
|
8240
|
+
if (toPrune.length === 0) return;
|
|
8241
|
+
for (const dir of toPrune) {
|
|
8242
|
+
await rm(path3.join(outputsDir, dir), { recursive: true, force: true }).catch(
|
|
8243
|
+
(e) => log(`[prune ] could not remove ${dir}: ${e.message}`)
|
|
8244
|
+
);
|
|
8245
|
+
}
|
|
8246
|
+
log(`[prune ] removed ${toPrune.length} old run dir(s), kept the ${keep} newest`);
|
|
8247
|
+
}
|
|
8248
|
+
|
|
8418
8249
|
// src/commands/canvas/run.ts
|
|
8419
|
-
var runCommand =
|
|
8250
|
+
var runCommand = defineCommand80({
|
|
8420
8251
|
meta: { name: "run", description: "Validate and execute a canvas JSON file." },
|
|
8421
8252
|
args: {
|
|
8422
8253
|
file: { type: "positional", required: true, description: "Path to canvas JSON" },
|
|
8423
8254
|
"cache-dir": { type: "string", description: "Cache root (default ./canvas/.cache)" },
|
|
8424
8255
|
"outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
|
|
8425
8256
|
"run-id": { type: "string", description: "Override run id" },
|
|
8426
|
-
"cache-policy": { type: "string", description: "read_write | bypass | read_only" }
|
|
8257
|
+
"cache-policy": { type: "string", description: "read_write | bypass | read_only" },
|
|
8258
|
+
"keep-runs": {
|
|
8259
|
+
type: "string",
|
|
8260
|
+
description: "After the run, prune old r_* run dirs, keeping the N newest (off by default)"
|
|
8261
|
+
}
|
|
8427
8262
|
},
|
|
8428
8263
|
async run({ args }) {
|
|
8429
|
-
const filePath =
|
|
8430
|
-
const raw = await
|
|
8264
|
+
const filePath = path4.resolve(String(args.file));
|
|
8265
|
+
const raw = await readFile2(filePath, "utf8");
|
|
8431
8266
|
let parsed;
|
|
8432
8267
|
try {
|
|
8433
8268
|
parsed = JSON.parse(raw);
|
|
@@ -8437,6 +8272,7 @@ var runCommand = defineCommand81({
|
|
|
8437
8272
|
`);
|
|
8438
8273
|
process.exit(2);
|
|
8439
8274
|
}
|
|
8275
|
+
parsed = resolveRelativeCanvasPaths(parsed, path4.dirname(filePath));
|
|
8440
8276
|
const pending = unsuppliedPlaceholderAssets(parsed);
|
|
8441
8277
|
if (pending.length > 0) {
|
|
8442
8278
|
process.stderr.write(
|
|
@@ -8468,6 +8304,12 @@ var runCommand = defineCommand81({
|
|
|
8468
8304
|
run_id: args["run-id"] ? String(args["run-id"]) : void 0,
|
|
8469
8305
|
cache_policy: policy
|
|
8470
8306
|
});
|
|
8307
|
+
const keepRuns = args["keep-runs"] !== void 0 ? Number(args["keep-runs"]) : void 0;
|
|
8308
|
+
if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
|
|
8309
|
+
const outputsDir = args["outputs-dir"] ? path4.resolve(String(args["outputs-dir"])) : path4.resolve("canvas");
|
|
8310
|
+
await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
|
|
8311
|
+
`));
|
|
8312
|
+
}
|
|
8471
8313
|
process.stdout.write(
|
|
8472
8314
|
`${JSON.stringify(
|
|
8473
8315
|
{
|
|
@@ -8499,9 +8341,9 @@ var runCommand = defineCommand81({
|
|
|
8499
8341
|
});
|
|
8500
8342
|
|
|
8501
8343
|
// src/commands/canvas/scaffold-static-ad.ts
|
|
8502
|
-
import { readFile as
|
|
8503
|
-
import
|
|
8504
|
-
import { defineCommand as
|
|
8344
|
+
import { readFile as readFile3, writeFile } from "fs/promises";
|
|
8345
|
+
import path5 from "path";
|
|
8346
|
+
import { defineCommand as defineCommand81 } from "citty";
|
|
8505
8347
|
|
|
8506
8348
|
// src/engine/scaffold/staticAd.ts
|
|
8507
8349
|
import { z as z2 } from "zod";
|
|
@@ -8714,7 +8556,7 @@ var SELECT_SYSTEM = 'You identify the MAIN, identity-critical visual elements of
|
|
|
8714
8556
|
var SELECT_PROMPT = 'AD BLUEPRINT (from image_describe):\n{{blueprint}}\n\nFrom this blueprint, list ONLY the elements that are prominent, important, and identity-bearing \u2014 the ones a reproduction must ground in a real asset:\n- the brand logo/wordmark (from brands_logos with function_in_image = advertiser_brand) -> type "logo"\n- trust/rating/certification/app-store/review badges (brands_logos with function_in_image = trust_badge | review_platform | certification_or_seal | app_store_badge | payment_method) -> type "badge"\n- a showcased/hero product or package (a foreground entry in subjects that the ad is selling) -> type "product"\n- a foreground person whose identity matters (from people) -> type "person"\n- a foreground animal/character with a specific expression (from subjects) -> type "animal"\n\nDROP background extras, decorative props, generic scenery, and anything small or incidental. Keep at most ~6. If there are none, return an empty list.\n\nFor each kept element return: { "type": one of logo|product|person|animal|badge, "label": a short UPPER_SNAKE_CASE name (e.g. LOGO, PRODUCT, HERO_DOG, TRUSTPILOT), "description": a concrete reusable description to source/shoot the real asset (include the exact expression for a living subject), "expression": the facial expression for a living subject or null, "reason": why it is identity-critical, "locator": the blueprint entry this element came from as { "collection": one of "subjects" | "people" | "brands_logos", "index": its 0-based position in that array } (people -> people; logos/badges -> brands_logos; products/animals/objects -> subjects). Output ONLY the JSON object.';
|
|
8715
8557
|
async function loadAssetText(ref, label) {
|
|
8716
8558
|
const r = ref;
|
|
8717
|
-
if (typeof r?.path === "string") return
|
|
8559
|
+
if (typeof r?.path === "string") return readFile3(r.path, "utf8");
|
|
8718
8560
|
if (typeof r?.url === "string") {
|
|
8719
8561
|
const res = await fetch(r.url);
|
|
8720
8562
|
if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
|
|
@@ -8820,7 +8662,7 @@ async function runVisionPasses(canvas) {
|
|
|
8820
8662
|
return fail("read_outputs", e instanceof Error ? e.message : String(e));
|
|
8821
8663
|
}
|
|
8822
8664
|
}
|
|
8823
|
-
var scaffoldStaticAdCommand =
|
|
8665
|
+
var scaffoldStaticAdCommand = defineCommand81({
|
|
8824
8666
|
meta: {
|
|
8825
8667
|
name: "scaffold-static-ad",
|
|
8826
8668
|
description: "Turn a source/inspiration image into a runnable static-ad canvas. Runs billed passes \u2014 image_describe (the blueprint, baked to prompt.json as the editable 'prompt'), an AI selection of the image's MAIN identity elements, and a structured global-layout pass (the column/row grid with per-region bounds and text sizes) \u2014 then scaffolds a canvas that wires one [TODO] ingest slot per element (logo/product/subject/badge + brand font) into image_generate. Edit prompt.json and drop the real assets, then `baker canvas run` it."
|
|
@@ -8837,10 +8679,10 @@ var scaffoldStaticAdCommand = defineCommand82({
|
|
|
8837
8679
|
"skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" }
|
|
8838
8680
|
},
|
|
8839
8681
|
async run({ args }) {
|
|
8840
|
-
const imagePath =
|
|
8841
|
-
const outPath = args.out ?
|
|
8842
|
-
const outDir =
|
|
8843
|
-
const blueprintPath =
|
|
8682
|
+
const imagePath = path5.resolve(String(args.file));
|
|
8683
|
+
const outPath = args.out ? path5.resolve(String(args.out)) : path5.join(path5.dirname(imagePath), "static-ad.canvas.json");
|
|
8684
|
+
const outDir = path5.dirname(outPath);
|
|
8685
|
+
const blueprintPath = path5.join(outDir, "prompt.json");
|
|
8844
8686
|
const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
|
|
8845
8687
|
const describeCanvas = buildDescribeCanvas(
|
|
8846
8688
|
imagePath,
|
|
@@ -8897,7 +8739,7 @@ var scaffoldStaticAdCommand = defineCommand82({
|
|
|
8897
8739
|
run_estimated_credits: validation.estimatedCredits
|
|
8898
8740
|
},
|
|
8899
8741
|
checklist: {
|
|
8900
|
-
edit_prompt: `Edit ${
|
|
8742
|
+
edit_prompt: `Edit ${path5.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
|
|
8901
8743
|
assets_to_supply: report.elements,
|
|
8902
8744
|
font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path, or delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
|
|
8903
8745
|
note: "Replace every [TODO] ingest path with a real file, then `baker canvas validate` and `baker canvas run`. Running generates a billed image \u2014 it is not free."
|
|
@@ -8913,12 +8755,12 @@ var scaffoldStaticAdCommand = defineCommand82({
|
|
|
8913
8755
|
|
|
8914
8756
|
// src/commands/canvas/scaffold-video.ts
|
|
8915
8757
|
import { cp, mkdir, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
8916
|
-
import
|
|
8917
|
-
import { defineCommand as
|
|
8758
|
+
import path8 from "path";
|
|
8759
|
+
import { defineCommand as defineCommand82 } from "citty";
|
|
8918
8760
|
|
|
8919
8761
|
// src/engine/nodes/local/lib/sceneDetect.ts
|
|
8920
8762
|
import { execFile as execFile2 } from "child_process";
|
|
8921
|
-
import { mkdtemp, readdir as
|
|
8763
|
+
import { mkdtemp, readdir as readdir2, readFile as readFile4, rm as rm2 } from "fs/promises";
|
|
8922
8764
|
import { tmpdir } from "os";
|
|
8923
8765
|
import { join as join2 } from "path";
|
|
8924
8766
|
import { promisify as promisify2 } from "util";
|
|
@@ -8982,11 +8824,11 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
|
|
|
8982
8824
|
],
|
|
8983
8825
|
{ encoding: "utf-8", maxBuffer: 32 * 1024 * 1024, timeout: timeoutMs }
|
|
8984
8826
|
);
|
|
8985
|
-
const csvName = (await
|
|
8827
|
+
const csvName = (await readdir2(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
|
|
8986
8828
|
if (!csvName) return [];
|
|
8987
|
-
return parsePySceneDetectCsvCuts(await
|
|
8829
|
+
return parsePySceneDetectCsvCuts(await readFile4(join2(outDir, csvName), "utf-8"));
|
|
8988
8830
|
} finally {
|
|
8989
|
-
await
|
|
8831
|
+
await rm2(outDir, { recursive: true, force: true });
|
|
8990
8832
|
}
|
|
8991
8833
|
}
|
|
8992
8834
|
async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
|
|
@@ -9242,6 +9084,13 @@ function stillHoldArgs(durationS, dims) {
|
|
|
9242
9084
|
`scale=${dims.w}:${dims.h}:force_original_aspect_ratio=increase,crop=${dims.w}:${dims.h},setsar=1,format=yuv420p`,
|
|
9243
9085
|
"-c:v",
|
|
9244
9086
|
"libx264",
|
|
9087
|
+
// Near-visually-lossless re-encode. libx264 DEFAULTS (crf 23, preset medium)
|
|
9088
|
+
// roughly halve the source bitrate and add banding on smooth surfaces; the
|
|
9089
|
+
// spine concats these by stream-copy, so any loss here ships to the final cut.
|
|
9090
|
+
"-crf",
|
|
9091
|
+
"18",
|
|
9092
|
+
"-preset",
|
|
9093
|
+
"slow",
|
|
9245
9094
|
"-pix_fmt",
|
|
9246
9095
|
"yuv420p",
|
|
9247
9096
|
"{{out.video}}"
|
|
@@ -9257,6 +9106,13 @@ function trimArgs(durationS, offsetS = 0) {
|
|
|
9257
9106
|
"-an",
|
|
9258
9107
|
"-c:v",
|
|
9259
9108
|
"libx264",
|
|
9109
|
+
// Preserve the seedance source quality through the trim. libx264 DEFAULTS
|
|
9110
|
+
// (crf 23) halve the bitrate (measured 9.57→4.40 Mbps) and band on motion;
|
|
9111
|
+
// the spine stream-copies the result, so the loss is permanent without this.
|
|
9112
|
+
"-crf",
|
|
9113
|
+
"18",
|
|
9114
|
+
"-preset",
|
|
9115
|
+
"slow",
|
|
9260
9116
|
"-pix_fmt",
|
|
9261
9117
|
"yuv420p",
|
|
9262
9118
|
"{{out.video}}"
|
|
@@ -9323,6 +9179,13 @@ var Scene = z3.object({
|
|
|
9323
9179
|
// The scene's role in the ad's persuasion arc (DECON-supplied); drives the
|
|
9324
9180
|
// script re-craft checklist. Inferred from position when absent.
|
|
9325
9181
|
narrative_role: z3.string().optional(),
|
|
9182
|
+
// DECON-supplied on the HOOK scene: the engineered physical/emotional state that
|
|
9183
|
+
// makes the first frame stop the scroll (sweaty/breathless/urgent …). Injected
|
|
9184
|
+
// into the hook's start-frame description so the generator renders that state,
|
|
9185
|
+
// not a calm influencer (CCA-11).
|
|
9186
|
+
hook_mechanic: z3.object({ mechanic: z3.string().optional(), why_it_stops_scroll: z3.string().optional() }).loose().optional(),
|
|
9187
|
+
// DECON-supplied per-scene location (so a gym hook isn't flattened to "home").
|
|
9188
|
+
scene_setting: z3.string().optional(),
|
|
9326
9189
|
// How this scene cuts to the next (DECON-supplied). A recognized non-cut type
|
|
9327
9190
|
// (fade/whip/zoom/dissolve/swipe) is reproduced as an ffmpeg xfade at the
|
|
9328
9191
|
// boundary; cut/match_cut/none/other stay hard cuts. The last scene's value is
|
|
@@ -9370,10 +9233,21 @@ var VideoBlueprint = z3.object({
|
|
|
9370
9233
|
mode: z3.string().optional(),
|
|
9371
9234
|
voice_description: z3.string().optional(),
|
|
9372
9235
|
persona: z3.string().optional()
|
|
9373
|
-
}).loose().optional()
|
|
9236
|
+
}).loose().optional(),
|
|
9237
|
+
// Visual palette — read only to colour a clean brand-card/CTA plate (the
|
|
9238
|
+
// first hex is the dominant brand colour); never to drive frame generation.
|
|
9239
|
+
style: z3.object({ palette: z3.array(z3.object({ hex: z3.string().optional() }).loose()).optional() }).loose().optional()
|
|
9374
9240
|
}).loose().optional(),
|
|
9375
9241
|
scenes: z3.array(Scene).min(1)
|
|
9376
9242
|
}).loose();
|
|
9243
|
+
function injectHookPhysicality(blueprint) {
|
|
9244
|
+
for (const scene of blueprint.scenes) {
|
|
9245
|
+
const why = scene.hook_mechanic?.why_it_stops_scroll?.trim();
|
|
9246
|
+
const prompt = scene.start_frame_prompt?.trim();
|
|
9247
|
+
if (!why || !prompt || prompt.includes(why)) continue;
|
|
9248
|
+
scene.start_frame_prompt = `${prompt} The subject's physical state IS the scroll-stopper \u2014 render it explicitly, not a calm pose: ${why}.`;
|
|
9249
|
+
}
|
|
9250
|
+
}
|
|
9377
9251
|
var AppearsItem = z3.union([z3.number(), z3.object({ scene: z3.number(), edge: z3.string().optional() }).loose()]);
|
|
9378
9252
|
var RecurringElement = z3.object({
|
|
9379
9253
|
// person | animal | product | logo | badge | other
|
|
@@ -9399,7 +9273,8 @@ function sanitizeId2(raw, fallback) {
|
|
|
9399
9273
|
return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
|
|
9400
9274
|
}
|
|
9401
9275
|
function labelFor2(el, used) {
|
|
9402
|
-
const
|
|
9276
|
+
const raw = el.type?.toLowerCase() === "logo" ? "BRAND_LOGO" : el.label ?? el.type ?? "ELEMENT";
|
|
9277
|
+
const base = raw.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "ELEMENT";
|
|
9403
9278
|
let label = base;
|
|
9404
9279
|
let n = 2;
|
|
9405
9280
|
while (used.has(label)) label = `${base}_${n++}`;
|
|
@@ -9566,6 +9441,8 @@ function buildElementSheets(slots, nodes) {
|
|
|
9566
9441
|
if (slot.sameAs) continue;
|
|
9567
9442
|
if (slot.presence.size < 1) continue;
|
|
9568
9443
|
const sheetId = `${slot.id}_sheet`;
|
|
9444
|
+
const slotType = slot.type.toLowerCase();
|
|
9445
|
+
const isCast = slotType === "person" || slotType === "animal";
|
|
9569
9446
|
nodes.push({
|
|
9570
9447
|
id: sheetId,
|
|
9571
9448
|
type: "image_reference_sheet",
|
|
@@ -9578,7 +9455,14 @@ function buildElementSheets(slots, nodes) {
|
|
|
9578
9455
|
// 4K: the sheet packs up to 8 cells (angles + tight face/detail close-ups), and
|
|
9579
9456
|
// it's the ONE reference every frame grounds on — per-cell sharpness here
|
|
9580
9457
|
// propagates to every clip, so it's worth the highest tier on this single asset.
|
|
9581
|
-
image_size: "4K"
|
|
9458
|
+
image_size: "4K",
|
|
9459
|
+
// The sheet is the look that propagates to EVERY grounded frame, so a glossy
|
|
9460
|
+
// studio turnaround makes the whole UGC ad read as "produced" (the #1 AI tell).
|
|
9461
|
+
// Force a flat, real, front-camera look on the cast sheet so the actor stays
|
|
9462
|
+
// authentic, not an airbrushed influencer (CCA-02).
|
|
9463
|
+
...isCast ? {
|
|
9464
|
+
style: "authentic UGC look: flat, even, natural front-camera lighting \u2014 no studio key/rim light, no seamless backdrop, no shallow depth of field; real skin texture and pores, no airbrushing or beauty retouch; true-to-life everyday styling"
|
|
9465
|
+
} : {}
|
|
9582
9466
|
}
|
|
9583
9467
|
});
|
|
9584
9468
|
slot.ref = `$ref:${sheetId}.sheet`;
|
|
@@ -9686,8 +9570,7 @@ function buildFrameRef(edge, url, framePrompt, present, ctx, nodes) {
|
|
|
9686
9570
|
const t = s.type.toLowerCase();
|
|
9687
9571
|
return t === "person" || t === "animal";
|
|
9688
9572
|
});
|
|
9689
|
-
const
|
|
9690
|
-
const useOriginalAnchor = Boolean(url) && (castSlots.length === 0 || castIdentityLocked);
|
|
9573
|
+
const useOriginalAnchor = Boolean(url) && castSlots.length === 0;
|
|
9691
9574
|
const hasOriginal = useOriginalAnchor;
|
|
9692
9575
|
const originalRef = useOriginalAnchor && url ? ingestFrameRef(url, edge, ctx, nodes) : void 0;
|
|
9693
9576
|
const reference = [...present.map((s) => s.ref), ...originalRef ? [originalRef] : []];
|
|
@@ -9898,6 +9781,39 @@ function isUiOnlyComposite(regions) {
|
|
|
9898
9781
|
const ui = regions.filter(regionIsUiSurface).length;
|
|
9899
9782
|
return ui >= 1 && regions.length - ui <= 1;
|
|
9900
9783
|
}
|
|
9784
|
+
function sceneIsFullScreenUi(scene, present) {
|
|
9785
|
+
if (scene.narrative_role?.trim() === "cta") return false;
|
|
9786
|
+
const hasCast = present.some((s) => {
|
|
9787
|
+
const t = s.type.toLowerCase();
|
|
9788
|
+
return t === "person" || t === "animal";
|
|
9789
|
+
});
|
|
9790
|
+
if (hasCast) return false;
|
|
9791
|
+
const hay = `${scene.summary ?? ""} ${scene.start_frame_prompt ?? ""} ${scene.end_frame_prompt ?? ""} ${scene.action_detail ?? ""}`;
|
|
9792
|
+
return UI_SURFACE_RE.test(hay);
|
|
9793
|
+
}
|
|
9794
|
+
function screenStillArgs(durationS, dims) {
|
|
9795
|
+
return [
|
|
9796
|
+
"-loop",
|
|
9797
|
+
"1",
|
|
9798
|
+
"-i",
|
|
9799
|
+
"{{in.frame}}",
|
|
9800
|
+
"-t",
|
|
9801
|
+
durationS.toFixed(3),
|
|
9802
|
+
"-r",
|
|
9803
|
+
"30",
|
|
9804
|
+
"-vf",
|
|
9805
|
+
`scale=${dims.w}:${dims.h}:force_original_aspect_ratio=decrease,pad=${dims.w}:${dims.h}:(ow-iw)/2:(oh-ih)/2:color=black,setsar=1,format=yuv420p`,
|
|
9806
|
+
"-c:v",
|
|
9807
|
+
"libx264",
|
|
9808
|
+
"-crf",
|
|
9809
|
+
"18",
|
|
9810
|
+
"-preset",
|
|
9811
|
+
"slow",
|
|
9812
|
+
"-pix_fmt",
|
|
9813
|
+
"yuv420p",
|
|
9814
|
+
"{{out.video}}"
|
|
9815
|
+
];
|
|
9816
|
+
}
|
|
9901
9817
|
function layeredComposition(scene) {
|
|
9902
9818
|
const comp = scene.composition;
|
|
9903
9819
|
const layout = (comp?.layout ?? "").toLowerCase();
|
|
@@ -10068,6 +9984,77 @@ function emitFlashHold(i, scene, slots, ctx, lengths, out, ar, nodes, clips) {
|
|
|
10068
9984
|
});
|
|
10069
9985
|
clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
|
|
10070
9986
|
}
|
|
9987
|
+
function emitScreenScene(i, scene, lengths, out, ar, nodes, clips) {
|
|
9988
|
+
const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
|
|
9989
|
+
const refId = `s${i}_screen_ref`;
|
|
9990
|
+
nodes.push({
|
|
9991
|
+
id: refId,
|
|
9992
|
+
type: "ingest",
|
|
9993
|
+
params: {
|
|
9994
|
+
source: "path",
|
|
9995
|
+
path: `[TODO: supply the REAL screen for "${label}" \u2014 NEVER AI-generate a UI. Capture a clean, text-free screenshot with \`baker images screenshot https://<brand-domain>/<path>\` (image-library skill); spoken/overlay text rides the overlay layer, not the screenshot]`,
|
|
9996
|
+
expect: "image"
|
|
9997
|
+
}
|
|
9998
|
+
});
|
|
9999
|
+
nodes.push({
|
|
10000
|
+
id: `s${i}_clip`,
|
|
10001
|
+
type: "ffmpeg",
|
|
10002
|
+
inputs: { frame: `$ref:${refId}.asset` },
|
|
10003
|
+
params: {
|
|
10004
|
+
args: screenStillArgs(lengths.trimTarget, canvasDims(ar)),
|
|
10005
|
+
outputs: { video: { kind: "video", ext: "mp4" } }
|
|
10006
|
+
}
|
|
10007
|
+
});
|
|
10008
|
+
clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
|
|
10009
|
+
}
|
|
10010
|
+
var BRAND_CARD_RE = /\b(?:solid|plain|flat|brand|logo|wordmark|end[- ]?card|cta card|title card|colou?r background|background colou?r)\b/i;
|
|
10011
|
+
function sceneIsBrandCard(scene, present, isCta) {
|
|
10012
|
+
if (!isCta) return false;
|
|
10013
|
+
const hasCast = present.some((s) => {
|
|
10014
|
+
const t = s.type.toLowerCase();
|
|
10015
|
+
return t === "person" || t === "animal";
|
|
10016
|
+
});
|
|
10017
|
+
if (hasCast) return false;
|
|
10018
|
+
const hay = `${scene.summary ?? ""} ${scene.start_frame_prompt ?? ""} ${scene.end_frame_prompt ?? ""}`;
|
|
10019
|
+
return BRAND_CARD_RE.test(hay);
|
|
10020
|
+
}
|
|
10021
|
+
var HEX6_RE = /^#?[0-9a-fA-F]{6}$/;
|
|
10022
|
+
function brandPlateColor(blueprint) {
|
|
10023
|
+
const palette = blueprint.global?.style?.palette;
|
|
10024
|
+
const hex = palette?.map((p) => p?.hex).find((h) => typeof h === "string" && HEX6_RE.test(h));
|
|
10025
|
+
return hex ? `0x${hex.replace(/^#/, "").toUpperCase()}` : "0x000000";
|
|
10026
|
+
}
|
|
10027
|
+
function colorPlateArgs(durationS, dims, color) {
|
|
10028
|
+
return [
|
|
10029
|
+
"-f",
|
|
10030
|
+
"lavfi",
|
|
10031
|
+
"-i",
|
|
10032
|
+
`color=c=${color}:s=${dims.w}x${dims.h}:r=30`,
|
|
10033
|
+
"-t",
|
|
10034
|
+
durationS.toFixed(3),
|
|
10035
|
+
"-c:v",
|
|
10036
|
+
"libx264",
|
|
10037
|
+
"-crf",
|
|
10038
|
+
"18",
|
|
10039
|
+
"-preset",
|
|
10040
|
+
"slow",
|
|
10041
|
+
"-pix_fmt",
|
|
10042
|
+
"yuv420p",
|
|
10043
|
+
"{{out.video}}"
|
|
10044
|
+
];
|
|
10045
|
+
}
|
|
10046
|
+
function emitBrandCardScene(i, lengths, out, ar, color, nodes, clips) {
|
|
10047
|
+
nodes.push({
|
|
10048
|
+
id: `s${i}_clip`,
|
|
10049
|
+
type: "ffmpeg",
|
|
10050
|
+
inputs: {},
|
|
10051
|
+
params: {
|
|
10052
|
+
args: colorPlateArgs(lengths.trimTarget, canvasDims(ar), color),
|
|
10053
|
+
outputs: { video: { kind: "video", ext: "mp4" } }
|
|
10054
|
+
}
|
|
10055
|
+
});
|
|
10056
|
+
clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
|
|
10057
|
+
}
|
|
10071
10058
|
function musicArcDigest(blueprint) {
|
|
10072
10059
|
const roles = blueprint.scenes.map((s) => s.narrative_role).filter((r) => Boolean(r));
|
|
10073
10060
|
const arc = roles.length > 0 ? roles.join(" \u2192 ") : "";
|
|
@@ -10187,7 +10174,7 @@ function makePresenterPresent(slots, canonical, opts = {}) {
|
|
|
10187
10174
|
const solePerson = !opts.strict && personSlots.length === 1 ? personSlots[0].presence : null;
|
|
10188
10175
|
return (speaker, sceneIndex) => {
|
|
10189
10176
|
const presence = bySpeaker.get(speaker) ?? solePerson;
|
|
10190
|
-
if (!presence) return opts.strict
|
|
10177
|
+
if (!presence) return !opts.strict;
|
|
10191
10178
|
return presence.has(sceneIndex);
|
|
10192
10179
|
};
|
|
10193
10180
|
}
|
|
@@ -10552,6 +10539,15 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
10552
10539
|
shootMode: mode,
|
|
10553
10540
|
ingestCache: env.ingestCache
|
|
10554
10541
|
};
|
|
10542
|
+
if (!env.reuse && sceneIsFullScreenUi(scene, present)) {
|
|
10543
|
+
emitScreenScene(i, scene, lengths, lengths.out, env.ar, nodes, out.clips);
|
|
10544
|
+
return void 0;
|
|
10545
|
+
}
|
|
10546
|
+
const isCta = scene.narrative_role?.trim() === "cta" || isLast;
|
|
10547
|
+
if (!env.reuse && sceneIsBrandCard(scene, present, isCta)) {
|
|
10548
|
+
emitBrandCardScene(i, lengths, lengths.out, env.ar, brandPlateColor(env.blueprint), nodes, out.clips);
|
|
10549
|
+
return void 0;
|
|
10550
|
+
}
|
|
10555
10551
|
if (!ambientBroll && lengths.dur <= FLASH_HOLD_MAX_S) {
|
|
10556
10552
|
emitFlashHold(i, scene, env.slots, ctx, lengths, lengths.out, env.ar, nodes, out.clips);
|
|
10557
10553
|
return void 0;
|
|
@@ -10817,7 +10813,7 @@ function overlayElement(ov, at, dur) {
|
|
|
10817
10813
|
const normAnim = normalizeAnim(ov.animation);
|
|
10818
10814
|
const anim = normAnim ? ` data-anim="${normAnim}"` : "";
|
|
10819
10815
|
const detail = ov.animation_detail ? ` data-anim-detail="${escapeHtml(ov.animation_detail)}"` : "";
|
|
10820
|
-
return `<div class="ov ${positionClass(ov.position)}" data-start="${at}" data-dur="${dur}"${role}${anim}${detail}>${escapeHtml(ov.text.trim())}</div>`;
|
|
10816
|
+
return `<div class="ov clip ${positionClass(ov.position)}" data-start="${at}" data-dur="${dur}"${role}${anim}${detail}>${escapeHtml(ov.text.trim())}</div>`;
|
|
10821
10817
|
}
|
|
10822
10818
|
var RICH_OVERLAY_RE = /notif|tweet|\bx post\b|post\b|comment|message|chat|bubble|card|review|rating|stat|counter|toast|popup/;
|
|
10823
10819
|
function sourceHint(fe) {
|
|
@@ -10847,7 +10843,7 @@ function floatingStub(fe, sceneStart) {
|
|
|
10847
10843
|
const slug = (fe.kind ?? "element").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "element";
|
|
10848
10844
|
return [
|
|
10849
10845
|
`<!-- ${kind}: ${label} @ ${at}s for ${dur}s (${positionClass(fe.position)}). Source a real asset: ${hint} \u2014 drop it in this dir and uncomment:`,
|
|
10850
|
-
`<img class="ov ${positionClass(fe.position)}" src="your-${slug}.png" data-start="${at}" data-dur="${dur}" alt="" /> -->`
|
|
10846
|
+
`<img class="ov clip ${positionClass(fe.position)}" src="your-${slug}.png" data-start="${at}" data-dur="${dur}" alt="" /> -->`
|
|
10851
10847
|
].join("\n");
|
|
10852
10848
|
}
|
|
10853
10849
|
function uiPipStub(scene) {
|
|
@@ -10867,7 +10863,7 @@ function uiPipStub(scene) {
|
|
|
10867
10863
|
" \u2014 OR hand-build a brand-accurate HTML screen; then frame it in a phone mockup:",
|
|
10868
10864
|
" npx hyperframes add phone-scroll (writes compositions/phone-scroll.html)",
|
|
10869
10865
|
" drop the screenshot as screenshot.png in this dir and nest it as a PIP clip:",
|
|
10870
|
-
` <div data-composition-src="compositions/phone-scroll.html" data-start="${at}" data-duration="${dur}" data-track-index="2" data-width="1080" data-height="1920"></div> -->`
|
|
10866
|
+
` <div class="clip" data-composition-src="compositions/phone-scroll.html" data-start="${at}" data-duration="${dur}" data-track-index="2" data-width="1080" data-height="1920"></div> -->`
|
|
10871
10867
|
].join("\n");
|
|
10872
10868
|
}
|
|
10873
10869
|
function buildOverlayHtml(input) {
|
|
@@ -10963,6 +10959,7 @@ function buildSpine(clips, nodes) {
|
|
|
10963
10959
|
}
|
|
10964
10960
|
function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
10965
10961
|
const blueprint = VideoBlueprint.parse(input);
|
|
10962
|
+
injectHookPhysicality(blueprint);
|
|
10966
10963
|
const elements = RecurringElements.parse(elementsInput);
|
|
10967
10964
|
const nodes = [];
|
|
10968
10965
|
nodes.push({
|
|
@@ -11374,18 +11371,44 @@ function videoReport(input, elementsInput) {
|
|
|
11374
11371
|
|
|
11375
11372
|
// src/commands/canvas/composition-path.ts
|
|
11376
11373
|
import { existsSync as existsSync3 } from "fs";
|
|
11377
|
-
import
|
|
11374
|
+
import path6 from "path";
|
|
11378
11375
|
function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
|
|
11379
|
-
const rel =
|
|
11376
|
+
const rel = path6.join("canvas", name);
|
|
11380
11377
|
let dir = startDir;
|
|
11381
11378
|
for (let i = 0; i < maxDepth; i++) {
|
|
11382
|
-
const candidate =
|
|
11383
|
-
if (exists(
|
|
11384
|
-
const parent =
|
|
11379
|
+
const candidate = path6.join(dir, rel);
|
|
11380
|
+
if (exists(path6.join(candidate, "meta.json"))) return candidate;
|
|
11381
|
+
const parent = path6.dirname(dir);
|
|
11385
11382
|
if (parent === dir) break;
|
|
11386
11383
|
dir = parent;
|
|
11387
11384
|
}
|
|
11388
|
-
return
|
|
11385
|
+
return path6.resolve(startDir, "../../../", rel);
|
|
11386
|
+
}
|
|
11387
|
+
|
|
11388
|
+
// src/commands/canvas/gitignore.ts
|
|
11389
|
+
import { appendFile, readFile as readFile5 } from "fs/promises";
|
|
11390
|
+
import path7 from "path";
|
|
11391
|
+
function missingGitignoreEntries(existing, entries) {
|
|
11392
|
+
const present = new Set(
|
|
11393
|
+
existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
|
|
11394
|
+
);
|
|
11395
|
+
return entries.filter((e) => !present.has(e.trim().replace(/\/+$/, "")));
|
|
11396
|
+
}
|
|
11397
|
+
async function ensureGitignore(dir, entries) {
|
|
11398
|
+
const file = path7.join(dir, ".gitignore");
|
|
11399
|
+
let existing;
|
|
11400
|
+
try {
|
|
11401
|
+
existing = await readFile5(file, "utf8");
|
|
11402
|
+
} catch {
|
|
11403
|
+
return;
|
|
11404
|
+
}
|
|
11405
|
+
const missing = missingGitignoreEntries(existing, entries);
|
|
11406
|
+
if (missing.length === 0) return;
|
|
11407
|
+
const prefix = existing.endsWith("\n") || existing.length === 0 ? "" : "\n";
|
|
11408
|
+
await appendFile(file, `${prefix}
|
|
11409
|
+
# Baker canvas (engine cache + scaffold working files)
|
|
11410
|
+
${missing.join("\n")}
|
|
11411
|
+
`);
|
|
11389
11412
|
}
|
|
11390
11413
|
|
|
11391
11414
|
// src/commands/canvas/scaffold-video.ts
|
|
@@ -11433,7 +11456,7 @@ async function loadTranscriptBestEffort(ref) {
|
|
|
11433
11456
|
async function stageCaptions(outDir, transcript) {
|
|
11434
11457
|
const text = transcript?.trim();
|
|
11435
11458
|
if (!text || text === "[]") return {};
|
|
11436
|
-
const compositionPath =
|
|
11459
|
+
const compositionPath = path8.join(outDir, "tiktok-captions-composition");
|
|
11437
11460
|
await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
|
|
11438
11461
|
return { compositionPath };
|
|
11439
11462
|
}
|
|
@@ -11565,7 +11588,7 @@ async function runAnalysisPasses(deconstructCanvas, selectModel) {
|
|
|
11565
11588
|
return fail2("deconstruct", e instanceof Error ? e.message : String(e));
|
|
11566
11589
|
}
|
|
11567
11590
|
}
|
|
11568
|
-
var scaffoldVideoCommand =
|
|
11591
|
+
var scaffoldVideoCommand = defineCommand82({
|
|
11569
11592
|
meta: {
|
|
11570
11593
|
name: "scaffold-video",
|
|
11571
11594
|
description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to prompt.json as the editable 'prompt') and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit prompt.json, drop the real source images, then `baker canvas run`."
|
|
@@ -11595,11 +11618,11 @@ var scaffoldVideoCommand = defineCommand83({
|
|
|
11595
11618
|
}
|
|
11596
11619
|
},
|
|
11597
11620
|
async run({ args }) {
|
|
11598
|
-
const videoPath =
|
|
11599
|
-
const base =
|
|
11600
|
-
const outPath = args.out ?
|
|
11601
|
-
const outDir =
|
|
11602
|
-
const blueprintPath =
|
|
11621
|
+
const videoPath = path8.resolve(String(args.file));
|
|
11622
|
+
const base = path8.basename(videoPath, path8.extname(videoPath));
|
|
11623
|
+
const outPath = args.out ? path8.resolve(String(args.out)) : path8.join(path8.dirname(videoPath), `${base}.video.canvas.json`);
|
|
11624
|
+
const outDir = path8.dirname(outPath);
|
|
11625
|
+
const blueprintPath = path8.join(outDir, "prompt.json");
|
|
11603
11626
|
const frames = args.frames === "reuse" ? "reuse" : "generate";
|
|
11604
11627
|
const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
|
|
11605
11628
|
if (Number.isFinite(maxScenes)) {
|
|
@@ -11622,9 +11645,9 @@ var scaffoldVideoCommand = defineCommand83({
|
|
|
11622
11645
|
const annotated = annotateBlueprintWithElements(blueprint, elements);
|
|
11623
11646
|
await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
|
|
11624
11647
|
`, "utf8");
|
|
11625
|
-
const compositionDest =
|
|
11648
|
+
const compositionDest = path8.join(outDir, "video-overlay-composition");
|
|
11626
11649
|
await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
|
|
11627
|
-
const indexPath =
|
|
11650
|
+
const indexPath = path8.join(compositionDest, "index.html");
|
|
11628
11651
|
const overlayHtml = buildOverlayHtml(blueprint);
|
|
11629
11652
|
const indexHtml = await readFile6(indexPath, "utf8");
|
|
11630
11653
|
const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
|
|
@@ -11639,9 +11662,9 @@ var scaffoldVideoCommand = defineCommand83({
|
|
|
11639
11662
|
const opts = {
|
|
11640
11663
|
imageModel,
|
|
11641
11664
|
videoModel,
|
|
11642
|
-
overlayCompositionPath: compositionDest,
|
|
11643
|
-
captionsCompositionPath: captions.compositionPath,
|
|
11644
|
-
blueprintPath,
|
|
11665
|
+
overlayCompositionPath: path8.relative(outDir, compositionDest),
|
|
11666
|
+
captionsCompositionPath: captions.compositionPath ? path8.relative(outDir, captions.compositionPath) : void 0,
|
|
11667
|
+
blueprintPath: path8.relative(outDir, blueprintPath),
|
|
11645
11668
|
frames,
|
|
11646
11669
|
ambient: Boolean(args.ambient),
|
|
11647
11670
|
...args.resolution ? { resolution: String(args.resolution) } : {}
|
|
@@ -11654,7 +11677,7 @@ var scaffoldVideoCommand = defineCommand83({
|
|
|
11654
11677
|
} catch (e) {
|
|
11655
11678
|
return fail2("scaffold", e instanceof Error ? e.message : String(e));
|
|
11656
11679
|
}
|
|
11657
|
-
const validation = await validateCanvasDeep(canvas, defaultRegistry());
|
|
11680
|
+
const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(canvas, outDir), defaultRegistry());
|
|
11658
11681
|
if (!validation.ok) {
|
|
11659
11682
|
process.stderr.write(
|
|
11660
11683
|
`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
|
|
@@ -11664,6 +11687,7 @@ var scaffoldVideoCommand = defineCommand83({
|
|
|
11664
11687
|
}
|
|
11665
11688
|
await writeFile2(outPath, `${JSON.stringify(canvas, null, 2)}
|
|
11666
11689
|
`, "utf8");
|
|
11690
|
+
await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
|
|
11667
11691
|
process.stdout.write(
|
|
11668
11692
|
`${JSON.stringify(
|
|
11669
11693
|
{
|
|
@@ -11681,7 +11705,7 @@ var scaffoldVideoCommand = defineCommand83({
|
|
|
11681
11705
|
run_estimated_credits: validation.estimatedCredits
|
|
11682
11706
|
},
|
|
11683
11707
|
checklist: {
|
|
11684
|
-
edit_prompt: `Edit ${
|
|
11708
|
+
edit_prompt: `Edit ${path8.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
|
|
11685
11709
|
recurring_elements_to_supply: report.elements,
|
|
11686
11710
|
voices_to_confirm: report.dialogue.map((d) => ({
|
|
11687
11711
|
scene: d.scene,
|
|
@@ -11706,9 +11730,84 @@ var scaffoldVideoCommand = defineCommand83({
|
|
|
11706
11730
|
}
|
|
11707
11731
|
});
|
|
11708
11732
|
|
|
11733
|
+
// src/commands/canvas/set-prompt.ts
|
|
11734
|
+
import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
11735
|
+
import path9 from "path";
|
|
11736
|
+
import { defineCommand as defineCommand83 } from "citty";
|
|
11737
|
+
function setNodePrompt(canvas, nodeId, text) {
|
|
11738
|
+
const nodes = canvas?.nodes;
|
|
11739
|
+
if (!Array.isArray(nodes)) throw new Error("canvas has no nodes array");
|
|
11740
|
+
const idx = nodes.findIndex((n) => n?.id === nodeId);
|
|
11741
|
+
if (idx < 0) {
|
|
11742
|
+
const ids = nodes.map((n) => n?.id).filter((id) => typeof id === "string");
|
|
11743
|
+
throw new Error(`node "${nodeId}" not found. Known nodes: ${ids.join(", ")}`);
|
|
11744
|
+
}
|
|
11745
|
+
const node = nodes[idx];
|
|
11746
|
+
const newNode = { ...node, params: { ...node.params ?? {}, prompt: text } };
|
|
11747
|
+
const newNodes = [...nodes];
|
|
11748
|
+
newNodes[idx] = newNode;
|
|
11749
|
+
return { ...canvas, nodes: newNodes };
|
|
11750
|
+
}
|
|
11751
|
+
var setPromptCommand = defineCommand83({
|
|
11752
|
+
meta: {
|
|
11753
|
+
name: "set-prompt",
|
|
11754
|
+
description: "Safely set a node's params.prompt (a frame description, motion prompt, etc.) without hand-editing the JSON. Prefer --text-file for multi-line/accented copy \u2014 it preserves UTF-8 exactly, unlike shell-quoted jq."
|
|
11755
|
+
},
|
|
11756
|
+
args: {
|
|
11757
|
+
file: { type: "positional", required: true, description: "Path to canvas JSON" },
|
|
11758
|
+
node: { type: "positional", required: true, description: "Node id to edit (e.g. s0_start)" },
|
|
11759
|
+
text: { type: "string", description: "New prompt text (inline)" },
|
|
11760
|
+
"text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
|
|
11761
|
+
},
|
|
11762
|
+
async run({ args }) {
|
|
11763
|
+
const filePath = path9.resolve(String(args.file));
|
|
11764
|
+
let canvas;
|
|
11765
|
+
try {
|
|
11766
|
+
canvas = JSON.parse(await readFile7(filePath, "utf8"));
|
|
11767
|
+
} catch (e) {
|
|
11768
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
|
|
11769
|
+
`);
|
|
11770
|
+
process.exit(2);
|
|
11771
|
+
}
|
|
11772
|
+
let text;
|
|
11773
|
+
if (args["text-file"]) text = await readFile7(path9.resolve(String(args["text-file"])), "utf8");
|
|
11774
|
+
else if (args.text !== void 0) text = String(args.text);
|
|
11775
|
+
else {
|
|
11776
|
+
process.stderr.write(
|
|
11777
|
+
`${JSON.stringify({ ok: false, error: { code: "no_text", message: "pass --text or --text-file" } }, null, 2)}
|
|
11778
|
+
`
|
|
11779
|
+
);
|
|
11780
|
+
process.exit(2);
|
|
11781
|
+
return;
|
|
11782
|
+
}
|
|
11783
|
+
let updated;
|
|
11784
|
+
try {
|
|
11785
|
+
updated = setNodePrompt(canvas, String(args.node), text);
|
|
11786
|
+
} catch (e) {
|
|
11787
|
+
process.stderr.write(
|
|
11788
|
+
`${JSON.stringify({ ok: false, error: { code: "node_not_found", message: String(e.message) } }, null, 2)}
|
|
11789
|
+
`
|
|
11790
|
+
);
|
|
11791
|
+
process.exit(2);
|
|
11792
|
+
return;
|
|
11793
|
+
}
|
|
11794
|
+
const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path9.dirname(filePath)), defaultRegistry());
|
|
11795
|
+
if (!validation.ok) {
|
|
11796
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
|
|
11797
|
+
`);
|
|
11798
|
+
process.exit(2);
|
|
11799
|
+
return;
|
|
11800
|
+
}
|
|
11801
|
+
await writeFile3(filePath, `${JSON.stringify(updated, null, 2)}
|
|
11802
|
+
`, "utf8");
|
|
11803
|
+
process.stdout.write(`${JSON.stringify({ ok: true, node: String(args.node), bytes: text.length }, null, 2)}
|
|
11804
|
+
`);
|
|
11805
|
+
}
|
|
11806
|
+
});
|
|
11807
|
+
|
|
11709
11808
|
// src/commands/canvas/validate.ts
|
|
11710
|
-
import { readFile as
|
|
11711
|
-
import
|
|
11809
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
11810
|
+
import path10 from "path";
|
|
11712
11811
|
import { defineCommand as defineCommand84 } from "citty";
|
|
11713
11812
|
var validateCommand = defineCommand84({
|
|
11714
11813
|
meta: {
|
|
@@ -11717,8 +11816,8 @@ var validateCommand = defineCommand84({
|
|
|
11717
11816
|
},
|
|
11718
11817
|
args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
|
|
11719
11818
|
async run({ args }) {
|
|
11720
|
-
const filePath =
|
|
11721
|
-
const raw = await
|
|
11819
|
+
const filePath = path10.resolve(String(args.file));
|
|
11820
|
+
const raw = await readFile8(filePath, "utf8");
|
|
11722
11821
|
let parsed;
|
|
11723
11822
|
try {
|
|
11724
11823
|
parsed = JSON.parse(raw);
|
|
@@ -11728,6 +11827,7 @@ var validateCommand = defineCommand84({
|
|
|
11728
11827
|
`);
|
|
11729
11828
|
process.exit(2);
|
|
11730
11829
|
}
|
|
11830
|
+
parsed = resolveRelativeCanvasPaths(parsed, path10.dirname(filePath));
|
|
11731
11831
|
const result = await validateCanvasDeep(parsed, defaultRegistry());
|
|
11732
11832
|
if (!result.ok) {
|
|
11733
11833
|
process.stderr.write(`${JSON.stringify({ ok: false, issues: result.issues }, null, 2)}
|
|
@@ -11764,7 +11864,6 @@ Subcommands:
|
|
|
11764
11864
|
baker canvas run <file.json> \u2014 execute the canvas, write outputs to ./canvas/<run_id>/
|
|
11765
11865
|
baker canvas catalog \u2014 print the agent-facing node + composition catalog (JSON Schema)
|
|
11766
11866
|
baker canvas inspect <run_id> \u2014 one-page summary of a completed run
|
|
11767
|
-
baker canvas gallery <dir> \u2014 read a creative folder's _definition.md + run manifests into the dashboard gallery descriptor (JSON)
|
|
11768
11867
|
baker canvas scaffold-video <video> \u2014 turn a reference video into a runnable reproduction canvas (deconstruct + recurring-element detection)
|
|
11769
11868
|
baker canvas scaffold-static-ad <image> \u2014 turn a source image into a runnable static-ad canvas (describe + element detection)`
|
|
11770
11869
|
},
|
|
@@ -11773,9 +11872,9 @@ Subcommands:
|
|
|
11773
11872
|
validate: validateCommand,
|
|
11774
11873
|
catalog: catalogCommand,
|
|
11775
11874
|
inspect: inspectCommand,
|
|
11776
|
-
gallery: galleryCommand,
|
|
11777
11875
|
"scaffold-video": scaffoldVideoCommand,
|
|
11778
|
-
"scaffold-static-ad": scaffoldStaticAdCommand
|
|
11876
|
+
"scaffold-static-ad": scaffoldStaticAdCommand,
|
|
11877
|
+
"set-prompt": setPromptCommand
|
|
11779
11878
|
}
|
|
11780
11879
|
});
|
|
11781
11880
|
|
|
@@ -12609,7 +12708,7 @@ function cropSprite(input, region) {
|
|
|
12609
12708
|
|
|
12610
12709
|
// src/lib/image/io.ts
|
|
12611
12710
|
import { randomBytes } from "crypto";
|
|
12612
|
-
import { glob as fsGlob, readFile as
|
|
12711
|
+
import { glob as fsGlob, readFile as readFile9, rename, stat as stat2, writeFile as writeFile4 } from "fs/promises";
|
|
12613
12712
|
import { dirname, extname, join as join3, resolve as resolve4 } from "path";
|
|
12614
12713
|
var REMOTE_RE = /^https?:\/\//i;
|
|
12615
12714
|
var GLOB_RE = /[*?[\]{}]/;
|
|
@@ -12645,11 +12744,11 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
12645
12744
|
}
|
|
12646
12745
|
return Buffer.from(await response.arrayBuffer());
|
|
12647
12746
|
}
|
|
12648
|
-
return
|
|
12747
|
+
return readFile9(pathOrUrl);
|
|
12649
12748
|
}
|
|
12650
|
-
async function isDirectory(
|
|
12749
|
+
async function isDirectory(path11) {
|
|
12651
12750
|
try {
|
|
12652
|
-
const s = await stat2(
|
|
12751
|
+
const s = await stat2(path11);
|
|
12653
12752
|
return s.isDirectory();
|
|
12654
12753
|
} catch {
|
|
12655
12754
|
return false;
|
|
@@ -12668,7 +12767,7 @@ async function atomicWrite(targetPath, data) {
|
|
|
12668
12767
|
const absolute = resolve4(targetPath);
|
|
12669
12768
|
const dir = dirname(absolute);
|
|
12670
12769
|
const tmp = join3(dir, `.baker-image-${randomBytes(8).toString("hex")}.tmp`);
|
|
12671
|
-
await
|
|
12770
|
+
await writeFile4(tmp, data);
|
|
12672
12771
|
await rename(tmp, absolute);
|
|
12673
12772
|
}
|
|
12674
12773
|
|
|
@@ -13009,7 +13108,7 @@ var findCommand = defineCommand98({
|
|
|
13009
13108
|
});
|
|
13010
13109
|
|
|
13011
13110
|
// src/commands/images/generate.ts
|
|
13012
|
-
import { readFile as
|
|
13111
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
13013
13112
|
import { defineCommand as defineCommand99 } from "citty";
|
|
13014
13113
|
import sharp2 from "sharp";
|
|
13015
13114
|
var GENERATE_TIMEOUT_MS = 18e4;
|
|
@@ -13092,7 +13191,7 @@ async function resolveReferences(spec) {
|
|
|
13092
13191
|
}
|
|
13093
13192
|
let raw;
|
|
13094
13193
|
try {
|
|
13095
|
-
raw = await
|
|
13194
|
+
raw = await readFile10(entry);
|
|
13096
13195
|
} catch {
|
|
13097
13196
|
throw new ApiError("VALIDATION_ERROR", `Reference file not found: ${entry}`);
|
|
13098
13197
|
}
|
|
@@ -14858,7 +14957,7 @@ function makeTagsCommand(command, label, endpoint) {
|
|
|
14858
14957
|
var tagsCommand2 = makeTagsCommand("images", "image", "/api/images/tags");
|
|
14859
14958
|
|
|
14860
14959
|
// src/commands/images/upload.ts
|
|
14861
|
-
import { readFile as
|
|
14960
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
14862
14961
|
import { extname as extname2 } from "path";
|
|
14863
14962
|
import { defineCommand as defineCommand114 } from "citty";
|
|
14864
14963
|
var MIME_MAP = {
|
|
@@ -14998,7 +15097,7 @@ async function uploadLocal(target, args) {
|
|
|
14998
15097
|
});
|
|
14999
15098
|
return;
|
|
15000
15099
|
}
|
|
15001
|
-
const fileBuffer = await
|
|
15100
|
+
const fileBuffer = await readFile11(target);
|
|
15002
15101
|
const base64 = fileBuffer.toString("base64");
|
|
15003
15102
|
const body = { base64, contentType };
|
|
15004
15103
|
if (args.source) body.source = args.source;
|
|
@@ -16975,7 +17074,7 @@ var searchCommand3 = defineCommand143({
|
|
|
16975
17074
|
var tagsCommand4 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
16976
17075
|
|
|
16977
17076
|
// src/commands/videos/upload.ts
|
|
16978
|
-
import { readFile as
|
|
17077
|
+
import { readFile as readFile12, stat as stat3 } from "fs/promises";
|
|
16979
17078
|
import { extname as extname3 } from "path";
|
|
16980
17079
|
import { defineCommand as defineCommand144 } from "citty";
|
|
16981
17080
|
var MIME_MAP2 = {
|
|
@@ -17040,7 +17139,7 @@ var uploadCommand2 = defineCommand144({
|
|
|
17040
17139
|
return;
|
|
17041
17140
|
}
|
|
17042
17141
|
const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
|
|
17043
|
-
const fileBuffer = await
|
|
17142
|
+
const fileBuffer = await readFile12(filePath);
|
|
17044
17143
|
const uploadResponse = await fetch(uploadUrl, {
|
|
17045
17144
|
method: "PUT",
|
|
17046
17145
|
headers: { "Content-Type": contentType },
|