@koda-sl/baker-cli 0.354.0-dev.9b7066502 → 0.354.0-dev.c60462bb7
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 +4 -3
- package/dist/{chunk-WJ5MYLLV.js → chunk-NA5CPMUA.js} +2 -14
- package/dist/{chunk-WJ5MYLLV.js.map → chunk-NA5CPMUA.js.map} +1 -1
- package/dist/cli.js +89 -26
- package/dist/cli.js.map +1 -1
- package/dist/{shared-4FS6FXCM.js → shared-RYAF7Y7H.js} +2 -4
- package/package.json +1 -1
- /package/dist/{shared-4FS6FXCM.js.map → shared-RYAF7Y7H.js.map} +0 -0
package/README.md
CHANGED
|
@@ -958,7 +958,7 @@ Status changes are `pause|resume|archive` subcommands on `campaigns`/`adsets`/`a
|
|
|
958
958
|
- Identity columns (`campaign_name`, `ad_name`, etc.) appended automatically per `--level` so rows are self-describing.
|
|
959
959
|
- Auto-async when the query is heavy (level=ad with breakdowns over an account, >2 breakdowns, >90-day range). Pass `--async` to force it or `--no-async` to refuse fallback.
|
|
960
960
|
|
|
961
|
-
**Listings (`campaigns`/`adsets`/`ads`)** default to `effective_status=ACTIVE` — pass `--all-statuses` to widen, or `--effective-status ACTIVE,PAUSED` for a custom set.
|
|
961
|
+
**Listings (`campaigns`/`adsets`/`ads`)** default to `effective_status=ACTIVE` — pass `--all-statuses` to widen, or `--effective-status ACTIVE,PAUSED` for a custom set.
|
|
962
962
|
|
|
963
963
|
**Pagination** is auto-drained (no manual cursor handling).
|
|
964
964
|
|
|
@@ -5392,7 +5392,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
|
|
|
5392
5392
|
|
|
5393
5393
|
A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
|
|
5394
5394
|
|
|
5395
|
-
It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose)
|
|
5395
|
+
It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast from company avatars** (`--avatar <handle>`): each recurring person grounds on that avatar's identity sheet, used as-is (no sheet is built from it); a person with no avatar gets a `[TODO]` asking for one, never for a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in.
|
|
5396
5396
|
|
|
5397
5397
|
**Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
|
|
5398
5398
|
|
|
@@ -5437,7 +5437,7 @@ The emitted canvas is validated (`validateCanvasDeep`) before it's written, so i
|
|
|
5437
5437
|
```bash
|
|
5438
5438
|
baker canvas scaffold-video ./reference-ad.mp4 --focus "competitor UGC ad for <brand>"
|
|
5439
5439
|
# → writes ./reference-ad.video.canvas.json + ./prompt.json (global) + ./scenes/sNN.json (per scene) + ./prompt.rebuild.json (+ video-overlay-composition/)
|
|
5440
|
-
#
|
|
5440
|
+
# cast the reference's people with --avatar <handle> (baker avatars list); edit scene files, fill every other el_* [TODO], confirm voices, then:
|
|
5441
5441
|
baker canvas validate ./reference-ad.video.canvas.json
|
|
5442
5442
|
baker canvas run ./reference-ad.video.canvas.json
|
|
5443
5443
|
```
|
|
@@ -5460,6 +5460,7 @@ baker canvas run ./reference-ad.video.canvas.json
|
|
|
5460
5460
|
| `--motion-transfer` | off | Motion driven from a reference / hyper-dynamic → the router picks Kling. |
|
|
5461
5461
|
| `--identity` | off | Same character/product across clips → the router picks the Seedance workhorse. |
|
|
5462
5462
|
| `--budget <tier>` | `standard` | Router cost posture: `economy` (cheap Veo-fast tier) / `standard` / `premium`. |
|
|
5463
|
+
| `--avatar <handles>` | — | Cast company avatars as the reference's people, comma-separated in the order they appear (`marta` or `marta,joan`). Each recurring person grounds on that avatar's identity sheet and takes its subject description; a `same_as` look of them uses the same sheet. Resolved before anything is billed — a handle that is not `ready` fails the command. |
|
|
5463
5464
|
| `--resolution <res>` | `1080p` | Output resolution for every generated clip (`480p`/`720p`/`1080p` for Seedance). The model defaults to a LOW tier when unset, which downscales the 2K keyframes — pinning the top tier keeps the clip as sharp as its frames. |
|
|
5464
5465
|
|
|
5465
5466
|
**Scored model router.** When `--video-model` is omitted, a small weighted scorer picks from the curated
|
|
@@ -97,17 +97,6 @@ function resolveEffectiveStatus(args) {
|
|
|
97
97
|
}
|
|
98
98
|
return "ACTIVE";
|
|
99
99
|
}
|
|
100
|
-
function statusFilterHints(rows, effectiveStatus, noun) {
|
|
101
|
-
if (rows.length > 0 || effectiveStatus === void 0) return [];
|
|
102
|
-
if (effectiveStatus === "ACTIVE") {
|
|
103
|
-
return [
|
|
104
|
-
`No ACTIVE ${noun}. The ACTIVE filter hides paused, archived and in-review ones (under a paused campaign or ad set that is all of them), so this does NOT mean none exist. Re-run with --all-statuses before concluding there are none.`
|
|
105
|
-
];
|
|
106
|
-
}
|
|
107
|
-
return [
|
|
108
|
-
`No ${noun} with effective_status ${effectiveStatus}. Re-run with --all-statuses to see every status before concluding there are none.`
|
|
109
|
-
];
|
|
110
|
-
}
|
|
111
100
|
|
|
112
101
|
export {
|
|
113
102
|
metaForbiddenFix,
|
|
@@ -117,7 +106,6 @@ export {
|
|
|
117
106
|
todayIso,
|
|
118
107
|
daysAgoIso,
|
|
119
108
|
csvOrJson,
|
|
120
|
-
resolveEffectiveStatus
|
|
121
|
-
statusFilterHints
|
|
109
|
+
resolveEffectiveStatus
|
|
122
110
|
};
|
|
123
|
-
//# sourceMappingURL=chunk-
|
|
111
|
+
//# sourceMappingURL=chunk-NA5CPMUA.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/ads/meta/shared.ts"],"sourcesContent":["import { ApiError } from \"../../../client.ts\";\nimport { getEnv } from \"../../../env.ts\";\nimport { handleConnectionError, needsConnectionFix } from \"../../../error-handler.ts\";\nimport { writeAdsJson } from \"../output.ts\";\nimport type { AdsErrorEnvelope, QueryFix } from \"../types.ts\";\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Meta answers a permission refusal (Graph codes 200/10/294) with a bare\n * \"Permissions error\" that covers two causes needing opposite remedies: a\n * consent grant the connection never got — nothing validates Meta's granted\n * scopes after connect, unlike HubSpot's `requireCapability` — or a role the\n * user lacks on one asset, since a Pixel/Dataset, Page or catalog lives in a\n * Business and is assigned separately from the ad account. Only the blast radius\n * separates them, so hand the agent that test rather than a verdict: guessing\n * \"reconnect\" wastes the user's time on an unassigned asset, and ruling it out\n * strands a connection that really is short a scope.\n */\nexport function metaForbiddenFix(message: string): QueryFix {\n return {\n action: \"reject\",\n explanation: [\n message,\n \"This is a permission refusal, not a dead connection, and Meta returns the same code for two causes with opposite remedies. Tell them apart by how much else works before you answer.\",\n \"If other reads on this account are ALSO refused (campaigns, ad sets, insights), the connection is short a permission it was never granted. Only the user can widen it, by reconnecting Meta from the dashboard.\",\n \"If those reads work and only this object is refused, the asset (pixel/dataset, Page, or catalog) belongs to a Business the connected user holds no role in. Ad-account access does not carry asset access and reconnecting cannot grant it — the client assigns that asset to the connected user or partner in Meta Business settings, under the asset's own section (Data sources → Datasets for a pixel).\",\n \"Either way do not retry the same call, and do not abort the wider job. Continue with what you can read, and state in your answer that this specific check could not run and why — never present the task as verified when this step was skipped.\",\n ].join(\"\\n\\n\"),\n };\n}\n\nexport function handleMetaError(err: unknown): never {\n if (err instanceof ApiError) {\n if (needsConnectionFix(err.code, err.message)) {\n handleConnectionError(\"meta_ads\", err.message);\n }\n const envelope: AdsErrorEnvelope = {\n ok: false,\n error: {\n code: err.code,\n message: err.message,\n fix: err.code === \"FORBIDDEN\" ? metaForbiddenFix(err.message) : { action: \"reject\", explanation: err.message },\n retryable: err.code === \"RATE_LIMITED\" || err.code === \"INTERNAL_ERROR\",\n },\n };\n writeAdsJson(envelope);\n process.exit(1);\n }\n const message = err instanceof Error ? err.message : \"Unexpected error\";\n writeAdsJson({ ok: false, error: { code: \"NETWORK_ERROR\", message } });\n process.exit(1);\n}\n\nconst ACCOUNT_ID_RE = /^(act_)?[0-9]+$/;\n\n/** Pull the default Meta ad account ID from `--account-id` arg or `BAKER_META_AD_ACCOUNT_ID` env. */\nexport function resolveAccountIdArg(args: Record<string, unknown>): string {\n const fromArgs = args[\"account-id\"] as string | undefined;\n const id = fromArgs || getEnv().BAKER_META_AD_ACCOUNT_ID;\n if (!id) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"MISSING_ACCOUNT_ID\",\n message: \"Pass --account-id or set BAKER_META_AD_ACCOUNT_ID. Run `baker ads meta accounts` to find IDs.\",\n },\n });\n process.exit(1);\n }\n if (!ACCOUNT_ID_RE.test(id)) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_ACCOUNT_ID\",\n message: `Invalid account ID \"${id}\". Expected numeric ID, optionally prefixed with \"act_\".`,\n },\n });\n process.exit(1);\n }\n return id.startsWith(\"act_\") ? id : `act_${id}`;\n}\n\nexport function getDateRange(args: Record<string, unknown>): { datePreset?: string; since?: string; until?: string } {\n const datePreset = args[\"date-preset\"] as string | undefined;\n const since = args.since as string | undefined;\n const until = args.until as string | undefined;\n if (since && until) return { since, until };\n return { datePreset: datePreset ?? \"last_7d\" };\n}\n\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nexport function daysAgoIso(days: number): string {\n return new Date(Date.now() - days * DAY_MS).toISOString().slice(0, 10);\n}\n\nexport function csvOrJson(args: Record<string, unknown>): string {\n return (args.output as string | undefined) ?? \"json\";\n}\n\n/**\n * Default to ACTIVE-only on listings — that's almost always what an AI agent or\n * a human glance wants. Pass `--effective-status ACTIVE,PAUSED` to override, or\n * `--all-statuses` to drop the filter entirely.\n */\nexport function resolveEffectiveStatus(args: Record<string, unknown>): string | undefined {\n if (args[\"all-statuses\"]) {\n return undefined;\n }\n const explicit = args[\"effective-status\"] as string | undefined;\n if (explicit) {\n return explicit;\n }\n return \"ACTIVE\";\n}\n
|
|
1
|
+
{"version":3,"sources":["../src/commands/ads/meta/shared.ts"],"sourcesContent":["import { ApiError } from \"../../../client.ts\";\nimport { getEnv } from \"../../../env.ts\";\nimport { handleConnectionError, needsConnectionFix } from \"../../../error-handler.ts\";\nimport { writeAdsJson } from \"../output.ts\";\nimport type { AdsErrorEnvelope, QueryFix } from \"../types.ts\";\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Meta answers a permission refusal (Graph codes 200/10/294) with a bare\n * \"Permissions error\" that covers two causes needing opposite remedies: a\n * consent grant the connection never got — nothing validates Meta's granted\n * scopes after connect, unlike HubSpot's `requireCapability` — or a role the\n * user lacks on one asset, since a Pixel/Dataset, Page or catalog lives in a\n * Business and is assigned separately from the ad account. Only the blast radius\n * separates them, so hand the agent that test rather than a verdict: guessing\n * \"reconnect\" wastes the user's time on an unassigned asset, and ruling it out\n * strands a connection that really is short a scope.\n */\nexport function metaForbiddenFix(message: string): QueryFix {\n return {\n action: \"reject\",\n explanation: [\n message,\n \"This is a permission refusal, not a dead connection, and Meta returns the same code for two causes with opposite remedies. Tell them apart by how much else works before you answer.\",\n \"If other reads on this account are ALSO refused (campaigns, ad sets, insights), the connection is short a permission it was never granted. Only the user can widen it, by reconnecting Meta from the dashboard.\",\n \"If those reads work and only this object is refused, the asset (pixel/dataset, Page, or catalog) belongs to a Business the connected user holds no role in. Ad-account access does not carry asset access and reconnecting cannot grant it — the client assigns that asset to the connected user or partner in Meta Business settings, under the asset's own section (Data sources → Datasets for a pixel).\",\n \"Either way do not retry the same call, and do not abort the wider job. Continue with what you can read, and state in your answer that this specific check could not run and why — never present the task as verified when this step was skipped.\",\n ].join(\"\\n\\n\"),\n };\n}\n\nexport function handleMetaError(err: unknown): never {\n if (err instanceof ApiError) {\n if (needsConnectionFix(err.code, err.message)) {\n handleConnectionError(\"meta_ads\", err.message);\n }\n const envelope: AdsErrorEnvelope = {\n ok: false,\n error: {\n code: err.code,\n message: err.message,\n fix: err.code === \"FORBIDDEN\" ? metaForbiddenFix(err.message) : { action: \"reject\", explanation: err.message },\n retryable: err.code === \"RATE_LIMITED\" || err.code === \"INTERNAL_ERROR\",\n },\n };\n writeAdsJson(envelope);\n process.exit(1);\n }\n const message = err instanceof Error ? err.message : \"Unexpected error\";\n writeAdsJson({ ok: false, error: { code: \"NETWORK_ERROR\", message } });\n process.exit(1);\n}\n\nconst ACCOUNT_ID_RE = /^(act_)?[0-9]+$/;\n\n/** Pull the default Meta ad account ID from `--account-id` arg or `BAKER_META_AD_ACCOUNT_ID` env. */\nexport function resolveAccountIdArg(args: Record<string, unknown>): string {\n const fromArgs = args[\"account-id\"] as string | undefined;\n const id = fromArgs || getEnv().BAKER_META_AD_ACCOUNT_ID;\n if (!id) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"MISSING_ACCOUNT_ID\",\n message: \"Pass --account-id or set BAKER_META_AD_ACCOUNT_ID. Run `baker ads meta accounts` to find IDs.\",\n },\n });\n process.exit(1);\n }\n if (!ACCOUNT_ID_RE.test(id)) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_ACCOUNT_ID\",\n message: `Invalid account ID \"${id}\". Expected numeric ID, optionally prefixed with \"act_\".`,\n },\n });\n process.exit(1);\n }\n return id.startsWith(\"act_\") ? id : `act_${id}`;\n}\n\nexport function getDateRange(args: Record<string, unknown>): { datePreset?: string; since?: string; until?: string } {\n const datePreset = args[\"date-preset\"] as string | undefined;\n const since = args.since as string | undefined;\n const until = args.until as string | undefined;\n if (since && until) return { since, until };\n return { datePreset: datePreset ?? \"last_7d\" };\n}\n\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nexport function daysAgoIso(days: number): string {\n return new Date(Date.now() - days * DAY_MS).toISOString().slice(0, 10);\n}\n\nexport function csvOrJson(args: Record<string, unknown>): string {\n return (args.output as string | undefined) ?? \"json\";\n}\n\n/**\n * Default to ACTIVE-only on listings — that's almost always what an AI agent or\n * a human glance wants. Pass `--effective-status ACTIVE,PAUSED` to override, or\n * `--all-statuses` to drop the filter entirely.\n */\nexport function resolveEffectiveStatus(args: Record<string, unknown>): string | undefined {\n if (args[\"all-statuses\"]) {\n return undefined;\n }\n const explicit = args[\"effective-status\"] as string | undefined;\n if (explicit) {\n return explicit;\n }\n return \"ACTIVE\";\n}\n"],"mappings":";;;;;;;;;;;;;AAMA,IAAM,SAAS;AAaR,SAAS,iBAAiB,SAA2B;AAC1D,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,aAAa;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,MAAM;AAAA,EACf;AACF;AAEO,SAAS,gBAAgB,KAAqB;AACnD,MAAI,eAAe,UAAU;AAC3B,QAAI,mBAAmB,IAAI,MAAM,IAAI,OAAO,GAAG;AAC7C,4BAAsB,YAAY,IAAI,OAAO;AAAA,IAC/C;AACA,UAAM,WAA6B;AAAA,MACjC,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM,IAAI;AAAA,QACV,SAAS,IAAI;AAAA,QACb,KAAK,IAAI,SAAS,cAAc,iBAAiB,IAAI,OAAO,IAAI,EAAE,QAAQ,UAAU,aAAa,IAAI,QAAQ;AAAA,QAC7G,WAAW,IAAI,SAAS,kBAAkB,IAAI,SAAS;AAAA,MACzD;AAAA,IACF;AACA,iBAAa,QAAQ;AACrB,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,eAAa,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,iBAAiB,QAAQ,EAAE,CAAC;AACrE,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,gBAAgB;AAGf,SAAS,oBAAoB,MAAuC;AACzE,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,KAAK,YAAY,OAAO,EAAE;AAChC,MAAI,CAAC,IAAI;AACP,iBAAa;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AAAA,IACF,CAAC;AACD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,CAAC,cAAc,KAAK,EAAE,GAAG;AAC3B,iBAAa;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,uBAAuB,EAAE;AAAA,MACpC;AAAA,IACF,CAAC;AACD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,SAAO,GAAG,WAAW,MAAM,IAAI,KAAK,OAAO,EAAE;AAC/C;AAEO,SAAS,aAAa,MAAwF;AACnH,QAAM,aAAa,KAAK,aAAa;AACrC,QAAM,QAAQ,KAAK;AACnB,QAAM,QAAQ,KAAK;AACnB,MAAI,SAAS,MAAO,QAAO,EAAE,OAAO,MAAM;AAC1C,SAAO,EAAE,YAAY,cAAc,UAAU;AAC/C;AAEO,SAAS,WAAmB;AACjC,UAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC7C;AAEO,SAAS,WAAW,MAAsB;AAC/C,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACvE;AAEO,SAAS,UAAU,MAAuC;AAC/D,SAAQ,KAAK,UAAiC;AAChD;AAOO,SAAS,uBAAuB,MAAmD;AACxF,MAAI,KAAK,cAAc,GAAG;AACxB,WAAO;AAAA,EACT;AACA,QAAM,WAAW,KAAK,kBAAkB;AACxC,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
|
package/dist/cli.js
CHANGED
|
@@ -76,9 +76,8 @@ import {
|
|
|
76
76
|
handleMetaError,
|
|
77
77
|
resolveAccountIdArg,
|
|
78
78
|
resolveEffectiveStatus,
|
|
79
|
-
statusFilterHints,
|
|
80
79
|
todayIso
|
|
81
|
-
} from "./chunk-
|
|
80
|
+
} from "./chunk-NA5CPMUA.js";
|
|
82
81
|
import {
|
|
83
82
|
buildQueryCacheKey,
|
|
84
83
|
cacheGet,
|
|
@@ -25467,7 +25466,7 @@ Example: baker ads meta ads retag 120252158038400247 --url-tags "utm_source={{si
|
|
|
25467
25466
|
const { apiPost: apiPost2 } = await import("./client-ODCNYCP4.js");
|
|
25468
25467
|
const { requireChatId: requireChatId2 } = await import("./env-BSG7TY5Y.js");
|
|
25469
25468
|
const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-2XBCYF7L.js");
|
|
25470
|
-
const { handleMetaError: handleMetaError2 } = await import("./shared-
|
|
25469
|
+
const { handleMetaError: handleMetaError2 } = await import("./shared-RYAF7Y7H.js");
|
|
25471
25470
|
try {
|
|
25472
25471
|
const accountId = bareAccountId2(args5);
|
|
25473
25472
|
const urlTags = typeof args5["url-tags"] === "string" ? args5["url-tags"] : void 0;
|
|
@@ -25615,7 +25614,7 @@ function duplicateCommand2(entity, label) {
|
|
|
25615
25614
|
const { apiPost: apiPost2 } = await import("./client-ODCNYCP4.js");
|
|
25616
25615
|
const { requireChatId: requireChatId2 } = await import("./env-BSG7TY5Y.js");
|
|
25617
25616
|
const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-2XBCYF7L.js");
|
|
25618
|
-
const { handleMetaError: handleMetaError2 } = await import("./shared-
|
|
25617
|
+
const { handleMetaError: handleMetaError2 } = await import("./shared-RYAF7Y7H.js");
|
|
25619
25618
|
try {
|
|
25620
25619
|
const accountId = bareAccountId2(args5);
|
|
25621
25620
|
const chatId = requireChatId2();
|
|
@@ -25680,8 +25679,7 @@ var listCommand4 = defineCommand58({
|
|
|
25680
25679
|
writeAdsOutput(data, fmt);
|
|
25681
25680
|
return;
|
|
25682
25681
|
}
|
|
25683
|
-
|
|
25684
|
-
writeJsonEnvelope(hints2.length > 0 ? { ok: true, data, hints: hints2 } : { ok: true, data });
|
|
25682
|
+
writeAdsJson({ ok: true, data });
|
|
25685
25683
|
} catch (err) {
|
|
25686
25684
|
handleMetaError(err);
|
|
25687
25685
|
}
|
|
@@ -25752,8 +25750,7 @@ var listCommand5 = defineCommand59({
|
|
|
25752
25750
|
writeAdsOutput(data, fmt);
|
|
25753
25751
|
return;
|
|
25754
25752
|
}
|
|
25755
|
-
|
|
25756
|
-
writeJsonEnvelope(hints2.length > 0 ? { ok: true, data, hints: hints2 } : { ok: true, data });
|
|
25753
|
+
writeAdsJson({ ok: true, data });
|
|
25757
25754
|
} catch (err) {
|
|
25758
25755
|
handleMetaError(err);
|
|
25759
25756
|
}
|
|
@@ -25903,8 +25900,7 @@ var listCommand7 = defineCommand62({
|
|
|
25903
25900
|
writeAdsOutput(data, fmt);
|
|
25904
25901
|
return;
|
|
25905
25902
|
}
|
|
25906
|
-
|
|
25907
|
-
writeJsonEnvelope(hints2.length > 0 ? { ok: true, data, hints: hints2 } : { ok: true, data });
|
|
25903
|
+
writeAdsJson({ ok: true, data });
|
|
25908
25904
|
} catch (err) {
|
|
25909
25905
|
handleMetaError(err);
|
|
25910
25906
|
}
|
|
@@ -33633,9 +33629,9 @@ import { defineCommand as defineCommand105 } from "citty";
|
|
|
33633
33629
|
import { defineCommand as defineCommand100 } from "citty";
|
|
33634
33630
|
|
|
33635
33631
|
// src/commands/avatars/casting.ts
|
|
33636
|
-
var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate`, `baker studio animate` and `baker canvas scaffold-
|
|
33632
|
+
var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate`, `baker studio animate`, `baker canvas scaffold-ad` and `baker canvas scaffold-video`. Do NOT hand-roll it by pasting `subjectDescription` into the prompt or by passing the sheet through `--reference` \u2014 both render something that merely resembles them. `--avatar` is also the ONLY thing that carries their ACCENT and delivery into a clip. An avatar has no pinned voice by design (ADR 0005): the video model renders the voice, and the region on their profile is the only thing steering it \u2014 without the flag the clip is performed flat, in whatever accent the model picks on its own. `baker canvas scaffold-video` casts only their FACE: a reproduced video keeps the source ad's accent direction, so never tell the user it speaks in theirs.";
|
|
33637
33633
|
var VERBATIM_RULE = "When you do write the description yourself \u2014 a hand-built canvas node, a landing image, anywhere `--avatar` does not exist \u2014 copy `subjectDescription` VERBATIM, word for word. Re-phrasing it per generation is the other reason a face drifts across a set.";
|
|
33638
|
-
var VIDEO_ROUTING = "In video, a photoreal presenter renders on `google/veo-3.1` (or `google/veo-3.1-fast`), never `bytedance/seedance-2.0` \u2014 it refuses photoreal human faces, AI-generated ones included.
|
|
33634
|
+
var VIDEO_ROUTING = "In video, a photoreal presenter renders on `google/veo-3.1` (or `google/veo-3.1-fast`), never `bytedance/seedance-2.0` \u2014 it refuses photoreal human faces, AI-generated ones included. Reproducing a video the user gave you: `baker canvas scaffold-video \u2026 --avatar <handle> --real-face`.";
|
|
33639
33635
|
function castingHints(avatar) {
|
|
33640
33636
|
if (avatar.status === "generating" && avatar.needsFace) {
|
|
33641
33637
|
return [
|
|
@@ -36653,6 +36649,30 @@ var RecurringElement = z38.object({
|
|
|
36653
36649
|
appears_in: z38.array(AppearsItem).optional()
|
|
36654
36650
|
}).loose();
|
|
36655
36651
|
var RecurringElements = z38.array(RecurringElement);
|
|
36652
|
+
function castAvatarsOnElements(elementsInput, avatars) {
|
|
36653
|
+
const elements = RecurringElements.parse(elementsInput);
|
|
36654
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
36655
|
+
let next = 0;
|
|
36656
|
+
const cast = elements.map((el) => {
|
|
36657
|
+
if (el.type.toLowerCase() !== "person" || el.same_as) return el;
|
|
36658
|
+
const avatar = avatars[next];
|
|
36659
|
+
if (!avatar) return el;
|
|
36660
|
+
next += 1;
|
|
36661
|
+
if (el.label) byLabel.set(el.label, avatar);
|
|
36662
|
+
const description = avatar.subjectDescription?.trim();
|
|
36663
|
+
return { ...el, ...description ? { description } : {}, source_image: avatar.sheetUrl, source_is_sheet: true };
|
|
36664
|
+
});
|
|
36665
|
+
return cast.map((el) => {
|
|
36666
|
+
const avatar = el.same_as ? byLabel.get(el.same_as) : void 0;
|
|
36667
|
+
return avatar ? { ...el, source_image: avatar.sheetUrl, source_is_sheet: true } : el;
|
|
36668
|
+
});
|
|
36669
|
+
}
|
|
36670
|
+
function avatarsNotCast(elementsInput, handles) {
|
|
36671
|
+
const people = RecurringElements.parse(elementsInput).filter(
|
|
36672
|
+
(el) => el.type.toLowerCase() === "person" && !el.same_as
|
|
36673
|
+
).length;
|
|
36674
|
+
return handles.slice(people);
|
|
36675
|
+
}
|
|
36656
36676
|
function sanitizeId(raw, fallback) {
|
|
36657
36677
|
const id = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
36658
36678
|
return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
|
|
@@ -36791,8 +36811,11 @@ function todoPath(el, label) {
|
|
|
36791
36811
|
const desc = el.description ? ` \u2014 ${el.description}` : "";
|
|
36792
36812
|
const expr = el.expression ? `, with a ${el.expression} expression` : "";
|
|
36793
36813
|
const t = el.type.toLowerCase();
|
|
36794
|
-
const fresh = t === "
|
|
36814
|
+
const fresh = t === "animal" || t === "location" ? " [SOURCE FRESH \u2014 a DIFFERENT animal/set than the original ad; do not reuse the source's]" : "";
|
|
36795
36815
|
const same = el.same_as ? ` [SAME INDIVIDUAL as ${el.same_as} \u2014 a different wardrobe/look of the same person; reuse that cast person, change only the outfit]` : "";
|
|
36816
|
+
if (t === "person") {
|
|
36817
|
+
return `[TODO: cast a company Avatar as ${label} (person)${desc}${expr}${same} \u2014 re-run scaffold-video with \`--avatar <handle>\` (\`baker avatars list\`; \`baker avatars create\` invents a new face). NEVER a photo of a real person found on the web or taken from the source ad: the image model reproduces that face]`;
|
|
36818
|
+
}
|
|
36796
36819
|
return `[TODO: drop one real source image for ${label} (${el.type})${desc}${expr} \u2014 reused across every frame it appears in${fresh}${same}; use a path relative to this canvas file, never an absolute path]`;
|
|
36797
36820
|
}
|
|
36798
36821
|
function buildElementSlots(elements) {
|
|
@@ -37043,7 +37066,7 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
|
|
|
37043
37066
|
const t = s.type.toLowerCase();
|
|
37044
37067
|
return t === "person" || t === "animal";
|
|
37045
37068
|
});
|
|
37046
|
-
const useOriginalAnchor = Boolean(url) && castSlots.length === 0;
|
|
37069
|
+
const useOriginalAnchor = Boolean(url) && castSlots.length === 0 && !ctx.sourceShowsPeople;
|
|
37047
37070
|
const hasOriginal = useOriginalAnchor;
|
|
37048
37071
|
const originalRef = useOriginalAnchor && url ? ingestFrameRef(url, edge, ctx, nodes) : void 0;
|
|
37049
37072
|
const presentOrdered = orderSlotsByIdentity(present2);
|
|
@@ -37509,6 +37532,7 @@ function buildCompositeScene(layout, regions, comp, scene, i, present2, mode, na
|
|
|
37509
37532
|
sceneIndex: i,
|
|
37510
37533
|
genAr: opts.genAr,
|
|
37511
37534
|
reuse: opts.reuse,
|
|
37535
|
+
sourceShowsPeople: opts.sourceShowsPeople,
|
|
37512
37536
|
imageModel: opts.imageModel,
|
|
37513
37537
|
shootMode: mode,
|
|
37514
37538
|
sceneStyle: sceneStyleOf(scene),
|
|
@@ -37784,6 +37808,9 @@ function onCameraDialogue(blueprint) {
|
|
|
37784
37808
|
const mode = blueprint.global?.voiceover?.mode;
|
|
37785
37809
|
return mode !== "voiceover" && mode !== "none";
|
|
37786
37810
|
}
|
|
37811
|
+
function sourceShowsPeople(blueprint, slots) {
|
|
37812
|
+
return (blueprint.global?.cast ?? []).length > 0 || slots.some((s) => s.type.toLowerCase() === "person");
|
|
37813
|
+
}
|
|
37787
37814
|
var castIdSet = (blueprint) => new Set((blueprint.global?.cast ?? []).map((c) => c.id).filter((id) => Boolean(id)));
|
|
37788
37815
|
var LANGUAGE_WORDS = [
|
|
37789
37816
|
[/\b(french|fran[çc]ais|francaise)\b/, "french"],
|
|
@@ -38099,6 +38126,7 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
38099
38126
|
sceneIndex: anchor,
|
|
38100
38127
|
genAr: env.genAr,
|
|
38101
38128
|
reuse: env.reuse,
|
|
38129
|
+
sourceShowsPeople: env.sourceShowsPeople,
|
|
38102
38130
|
imageModel: env.opts.imageModel,
|
|
38103
38131
|
shootMode: mode,
|
|
38104
38132
|
sceneStyle: sceneStyleOf(anchorScene),
|
|
@@ -38293,6 +38321,7 @@ function emitCompositeInTimeline(composite, scene, i, isLast, env, canonical2, e
|
|
|
38293
38321
|
outAr: env.outAr,
|
|
38294
38322
|
genAr: env.genAr,
|
|
38295
38323
|
reuse: env.reuse,
|
|
38324
|
+
sourceShowsPeople: env.sourceShowsPeople,
|
|
38296
38325
|
imageModel: env.opts.imageModel,
|
|
38297
38326
|
videoModel: env.opts.videoModel,
|
|
38298
38327
|
resolution: env.opts.resolution,
|
|
@@ -38389,6 +38418,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
38389
38418
|
sceneIndex: i,
|
|
38390
38419
|
genAr: env.genAr,
|
|
38391
38420
|
reuse: env.reuse,
|
|
38421
|
+
sourceShowsPeople: env.sourceShowsPeople,
|
|
38392
38422
|
imageModel: env.opts.imageModel,
|
|
38393
38423
|
shootMode: mode,
|
|
38394
38424
|
sceneStyle: sceneStyleOf(scene),
|
|
@@ -38489,6 +38519,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
38489
38519
|
outAr: aspect.outAr,
|
|
38490
38520
|
genAr: aspect.genAr,
|
|
38491
38521
|
reuse,
|
|
38522
|
+
sourceShowsPeople: sourceShowsPeople(blueprint, slots),
|
|
38492
38523
|
cameraOn: onCameraDialogue(blueprint),
|
|
38493
38524
|
casts: castIdSet(blueprint),
|
|
38494
38525
|
ingestCache: /* @__PURE__ */ new Map(),
|
|
@@ -39019,11 +39050,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
39019
39050
|
nodes.push({
|
|
39020
39051
|
id: slot.id,
|
|
39021
39052
|
type: "ingest",
|
|
39022
|
-
params: {
|
|
39023
|
-
source: "path",
|
|
39024
|
-
path: supplied || todoPath(element, slot.label),
|
|
39025
|
-
expect: "image"
|
|
39026
|
-
}
|
|
39053
|
+
params: /^https?:\/\//i.test(supplied) ? { source: "url", url: supplied, expect: "image" } : { source: "path", path: supplied || todoPath(element, slot.label), expect: "image" }
|
|
39027
39054
|
});
|
|
39028
39055
|
});
|
|
39029
39056
|
buildElementSheets(slots, nodes, opts.sheetModel);
|
|
@@ -39277,7 +39304,7 @@ var VIDEO_GUIDE = [
|
|
|
39277
39304
|
"0. RE-CRAFT THE SCRIPT FIRST (don't clone). This reference already won in-market, but copying a video is much harder than a static: the hook is targeting and may not transfer, and the message must become TRUE for our brand. Work the `metadata.todo.script_recraft` checklist \u2014 for each scene judge its role (hook/body/CTA), decide keep/cut/reorder/replace, and re-author every line for OUR customer's pain + OUR offer. See `references/script-craft.md` (hook/body/CTA framework) and the `meta-ads-playbook` skill. Most of the work lives here.",
|
|
39278
39305
|
"1. Edit each frame's prompt IN PLACE. Every `s<i>_start` keyframe node has its OWN self-contained `params.prompt` (the FRAME DESCRIPTION) \u2014 editing one changes only that frame. Rewrite the cast, product, claims, palette into the ad you want. The frame is RECAST to the el_* reference images you drop (the source ad's people are never reused), so describe pose/action/framing here and let the references carry identity.",
|
|
39279
39306
|
"1b. STORYBOARD FIRST \u2014 align the look on the cheap stills before clips bill. Each scene's keyframe IS your storyboard; `metadata.video.motion_board` lays out each scene's frame, time window, spoken line, and the graphics scheduled in it. Lock the keyframes + check each graphic lands on its spoken beat, THEN run the clips (images are cheap, videos aren't; the cache re-bills only what you change). See references/video-reproduce.md.",
|
|
39280
|
-
"2.
|
|
39307
|
+
"2. Fill each `el_*` ingest `[TODO]` path. A PERSON is a company Avatar's identity sheet, cast by re-scaffolding with `--avatar <handle>` (a sheet URL pasted by hand would be sheeted again) \u2014 never a photo of a real person, whose face the image model would reproduce; every other element is ONE real source image. Each is reused across every frame it appears in, so the same identity stays consistent. `baker canvas run` REFUSES to start until every `[TODO]` slot holds a real source \u2014 so this is mandatory, not optional.",
|
|
39281
39308
|
"3. Talking heads are NATIVE: a scene with one on-camera speaker is voiced by the video model itself (the line goes in the clip's prompt; models with a `generate_audio` toggle get it set, and the default model always renders audio), so lips and voice are generated together \u2014 no separate tts, no post-hoc lip-sync. Edit the line in the scene's `s<i>_clip` prompt to re-author the words TRUE for your brand. Off-camera narration scenes still use a sequenced `tts` per turn.",
|
|
39282
39309
|
"4. Voice consistency: every native talking clip's audio is re-voiced to ONE brand voice via `audio_voice_convert` (timing preserved \u2192 lips stay matched). Confirm the `voice_select` casting (one per speaker) \u2014 its `voice_id` is the brand voice; set its gender/language so the voice matches the creator.",
|
|
39283
39310
|
"5. Overlays are REAL HTML you paint. Open `video-overlay-composition/index.html`: the reference's overlays are seeded inside `#overlay-root` as plain elements (text + a `.pos-*` class + `data-start`/`data-dur`). Restyle the CSS freely \u2014 build lower-thirds, a ticker, whatever the look needs \u2014 and replace a logo placeholder with a real `<img>` you source (`baker images icon/sticker/gif/logo`) and drop in that dir. The runtime only shows/hides by timestamp; it makes no styling decisions. Drop `brand-bold.otf` / `brand-regular.otf` there for on-brand type.",
|
|
@@ -39345,7 +39372,7 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
|
|
|
39345
39372
|
review_storyboard_before_clips: "STORYBOARD FIRST. Each scene's keyframe (s<i>_start) IS your storyboard \u2014 align the LOOK on it before clips bill. Images are cheap, videos aren't, and the content-addressed cache re-bills only changed nodes, so iterate prompts here first and let the storyboard lock before the video_generate clips run. `metadata.video.motion_board` lists each scene's keyframe, window, spoken line, and the graphics scheduled in it \u2014 walk it scene by scene.",
|
|
39346
39373
|
motion_board: "`metadata.video.motion_board` maps which overlay/graphic fires on which spoken beat (per scene: window_s, spoken line, each graphic's at_s). Review it so every graphic lands on the word it punctuates \u2014 don't let an overlay drift off its beat. Adjust an overlay's data-start in video-overlay-composition/index.html (or appears_at_s in prompt.json) to retime it.",
|
|
39347
39374
|
assets_required: "MANDATORY: drop a real image at every el_* [TODO] ingest before running \u2014 `baker canvas run` refuses to start (and bills nothing) until each placeholder holds a real source.",
|
|
39348
|
-
sourcing: 'Resolve each el_* [TODO]: (
|
|
39375
|
+
sourcing: 'Resolve each el_* [TODO]: a PERSON is a company Avatar \u2014 pass `--avatar <handle>` to scaffold-video (`baker avatars list`; `baker avatars create` for a new invented face) \u2014 re-scaffold rather than paste a sheet URL at the slot, which would be sheeted again; never a photo of a real person from the web or the source ad, because the image model reproduces that face. Everything else: the client\'s OWN assets first \u2014 `baker images library "<subject>"` and `baker images logo <domain>` for the brand mark; otherwise a FRESH real reference \u2014 `baker images find "<subject>" --sources pinterest` (photo-real/candid, best for pets, sets and objects) or generate one with the image model. el_location and animals are [SOURCE FRESH]: a DIFFERENT set/animal than the original ad.',
|
|
39349
39376
|
recurring_elements_to_supply: report.elements,
|
|
39350
39377
|
text_strategy: "Decide per ad: text is either baked by the generated creative OR painted via the overlay HTML \u2014 not both. Default here is clean text-free frames + the HTML overlay layer (video-overlay-composition/index.html) as the single text source, which you fully control.",
|
|
39351
39378
|
timeline: "Automatic: each clip is generated at >= its scene length then trimmed back to the real scene duration, so the concatenated picture stays on the same timeline as the audio. You don't manage it.",
|
|
@@ -39454,7 +39481,8 @@ function videoReport(input, elementsInput) {
|
|
|
39454
39481
|
type: el.type,
|
|
39455
39482
|
description: el.description ?? null,
|
|
39456
39483
|
scenes: [...presenceOf(el).keys()].sort((a, b) => a - b),
|
|
39457
|
-
asset_todo: todoPath(el, label)
|
|
39484
|
+
asset_todo: el.source_image?.trim() ? null : todoPath(el, label),
|
|
39485
|
+
...el.source_image?.trim() ? { source_image: el.source_image.trim() } : {}
|
|
39458
39486
|
})),
|
|
39459
39487
|
dialogue,
|
|
39460
39488
|
sfx_count: sfxCount,
|
|
@@ -43884,6 +43912,20 @@ async function detectShotCutsBestEffort(videoPath, threshold) {
|
|
|
43884
43912
|
return void 0;
|
|
43885
43913
|
}
|
|
43886
43914
|
}
|
|
43915
|
+
async function resolveCastAvatar(handle) {
|
|
43916
|
+
try {
|
|
43917
|
+
const avatar = await readAvatars("/api/avatars/get", { handle });
|
|
43918
|
+
if (avatar.status && avatar.status !== "ready") {
|
|
43919
|
+
return { error: `Avatar \`${handle}\` is ${avatar.status}, not ready \u2014 \`baker avatars get ${handle} --wait\` returns the moment it is.` };
|
|
43920
|
+
}
|
|
43921
|
+
if (!avatar.sheetUrl) return { error: `Avatar \`${handle}\` has no identity sheet yet, so the video cannot ground on their face.` };
|
|
43922
|
+
return { sheetUrl: avatar.sheetUrl, subjectDescription: avatar.subjectDescription };
|
|
43923
|
+
} catch (e) {
|
|
43924
|
+
return {
|
|
43925
|
+
error: `Could not read avatar \`${handle}\`: ${e instanceof Error ? e.message : String(e)}. \`baker avatars list\` shows who there is.`
|
|
43926
|
+
};
|
|
43927
|
+
}
|
|
43928
|
+
}
|
|
43887
43929
|
function fail4(code, message) {
|
|
43888
43930
|
process.stderr.write(`${JSON.stringify({ ok: false, error: { code, message } }, null, 2)}
|
|
43889
43931
|
`);
|
|
@@ -44065,7 +44107,7 @@ async function runAnalysisPasses(deconstructCanvas, selectModel) {
|
|
|
44065
44107
|
var scaffoldVideoCommand = defineCommand116({
|
|
44066
44108
|
meta: {
|
|
44067
44109
|
name: "scaffold-video",
|
|
44068
|
-
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 a split, editable blueprint: a global prompt.json plus one small scenes/sNN.json per scene) 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 a scene's scenes/sNN.json (or prompt.json for global cast/palette/brand),
|
|
44110
|
+
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 a split, editable blueprint: a global prompt.json plus one small scenes/sNN.json per scene) 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 a scene's scenes/sNN.json (or prompt.json for global cast/palette/brand), fill the remaining [TODO] images, then `baker canvas run`. Start here: `baker avatars list`, then pass `--avatar <handle>` when the video shows a person \u2014 the reference's people are played by the company's avatars, never by a photo of anyone real."
|
|
44069
44111
|
},
|
|
44070
44112
|
args: {
|
|
44071
44113
|
file: {
|
|
@@ -44129,6 +44171,10 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
44129
44171
|
resolution: {
|
|
44130
44172
|
type: "string",
|
|
44131
44173
|
description: `Output resolution for generated clips (e.g. "1080p"). Default 1080p \u2014 the highest the video model supports \u2014 so clips keep the keyframe sharpness instead of the model's low default.`
|
|
44174
|
+
},
|
|
44175
|
+
avatar: {
|
|
44176
|
+
type: "string",
|
|
44177
|
+
description: "Cast company avatars as the reference's people \u2014 pass this whenever the video has a person in it. One handle, or several comma-separated in the order the people appear (`--avatar marta` / `--avatar marta,joan`). Each recurring person grounds on that avatar's identity sheet, so the ad shows the company's own cast instead of the source ad's actors. `baker avatars list` shows who there is; checked before anything is billed."
|
|
44132
44178
|
}
|
|
44133
44179
|
},
|
|
44134
44180
|
async run({ args: args5 }) {
|
|
@@ -44168,6 +44214,13 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
44168
44214
|
);
|
|
44169
44215
|
}
|
|
44170
44216
|
const { deconstructModel, selectModel, imageModel } = resolveModels2(args5);
|
|
44217
|
+
const avatars = [];
|
|
44218
|
+
const avatarHandles = String(args5.avatar ?? "").split(",").map((h) => h.trim()).filter(Boolean);
|
|
44219
|
+
for (const handle of avatarHandles) {
|
|
44220
|
+
const resolved = await resolveCastAvatar(handle);
|
|
44221
|
+
if ("error" in resolved) return fail4("avatar", resolved.error);
|
|
44222
|
+
avatars.push(resolved);
|
|
44223
|
+
}
|
|
44171
44224
|
const shotThreshold = args5["shot-threshold"] ? Number(args5["shot-threshold"]) : void 0;
|
|
44172
44225
|
const shotCuts = await detectShotCutsBestEffort(videoPath, shotThreshold);
|
|
44173
44226
|
const deconstructCanvas = buildDeconstructCanvas(videoPath, deconstructModel, {
|
|
@@ -44176,7 +44229,15 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
44176
44229
|
focus: args5.focus ? String(args5.focus) : void 0,
|
|
44177
44230
|
shotCuts
|
|
44178
44231
|
});
|
|
44179
|
-
const
|
|
44232
|
+
const analysis = await runAnalysisPasses(deconstructCanvas, selectModel);
|
|
44233
|
+
const { blueprint, transcript, creditsSpent } = analysis;
|
|
44234
|
+
const elements = castAvatarsOnElements(analysis.elements, avatars);
|
|
44235
|
+
const uncast = avatarsNotCast(analysis.elements, avatarHandles);
|
|
44236
|
+
const uncastHint = uncast.length ? {
|
|
44237
|
+
hints: [
|
|
44238
|
+
`NOT CAST: ${uncast.map((h) => `\`${h}\``).join(", ")} \u2014 the element selection found no more recurring people than it cast, so ${uncast.length === 1 ? "this avatar plays" : "these avatars play"} nobody in this canvas. A person shown in only one scene is not a recurring element, so those frames render whoever their scene file describes \u2014 tell the user who is cast and who is not.`
|
|
44239
|
+
]
|
|
44240
|
+
} : {};
|
|
44180
44241
|
const { videoModel, videoRouteReason } = resolveVideoModel(args5, elements);
|
|
44181
44242
|
if (videoRouteReason && !args5["video-model"] && !args5["real-face"] && hasPhotorealCast(elements)) {
|
|
44182
44243
|
process.stderr.write(`\u{1F3AC} Photoreal on-camera cast detected \u2192 generating clips on ${videoModel} (dodges Seedance's real-person filter).
|
|
@@ -44266,7 +44327,8 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
44266
44327
|
error: { code: "validation", issues: validation.issues },
|
|
44267
44328
|
canvas_path: outPath,
|
|
44268
44329
|
prompt_path: blueprintPath,
|
|
44269
|
-
note: "The canvas WAS written despite the blocking issue(s) \u2014 fix them in place, then `baker canvas validate` before running."
|
|
44330
|
+
note: "The canvas WAS written despite the blocking issue(s) \u2014 fix them in place, then `baker canvas validate` before running.",
|
|
44331
|
+
...uncastHint
|
|
44270
44332
|
},
|
|
44271
44333
|
null,
|
|
44272
44334
|
2
|
|
@@ -44315,6 +44377,7 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
44315
44377
|
source_reference: sourceRef.url,
|
|
44316
44378
|
composition_dir: compositionDest,
|
|
44317
44379
|
output: canvas.output,
|
|
44380
|
+
...uncastHint,
|
|
44318
44381
|
frames_mode: frames,
|
|
44319
44382
|
models: {
|
|
44320
44383
|
deconstruct: deconstructModel,
|
|
@@ -44358,7 +44421,7 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
44358
44421
|
...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
|
|
44359
44422
|
content_policy_risk: "You FORCED Seedance (`--video-model bytedance/seedance-2.0`) on an ad with a photoreal on-camera cast. ByteDance's real-person filter will reject those clips (content_policy_blocked, E005) \u2014 no prompt change clears it. Each clip auto-recovers on Veo at render time so the run still completes, but you burn a wasted Seedance attempt per clip. Drop `--video-model` to let the scaffold route the cast to Veo up front, or make the frames less photorealistic."
|
|
44360
44423
|
} : {},
|
|
44361
|
-
note: "
|
|
44424
|
+
note: "Fill each el_* [TODO] \u2014 a person with a company avatar's identity sheet (`--avatar <handle>`), never a photo of a real person; anything else with ONE real source image (reused across every frame that element appears in) \u2014 confirm each voice_select casting, then `baker canvas validate` and `baker canvas run`. Running generates many billed image/video/audio assets \u2014 it is not free."
|
|
44362
44425
|
}
|
|
44363
44426
|
},
|
|
44364
44427
|
null,
|