@koda-sl/baker-cli 0.125.0 → 0.129.1-dev.972f3c9aa
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 +447 -89
- package/canvas/tiktok-captions-composition/index.html +1 -1
- package/canvas/video-overlay-composition/index.html +78 -6
- package/canvas/video-overlay-composition/meta.json +30 -2
- package/dist/{chunk-4EPKHOTF.js → chunk-J2LYFDVC.js} +1630 -550
- package/dist/chunk-J2LYFDVC.js.map +1 -0
- package/dist/cli.js +6145 -4514
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +143 -2
- package/dist/engine/index.js +2 -2
- package/package.json +4 -2
- package/dist/chunk-4EPKHOTF.js.map +0 -1
package/README.md
CHANGED
|
@@ -223,8 +223,6 @@ baker ads google query --list-presets
|
|
|
223
223
|
| `keyword-analysis` | Keyword performance with match type | LAST_30_DAYS |
|
|
224
224
|
| `positive-keywords` | Positive (targeting) keywords only | ALL_TIME |
|
|
225
225
|
| `negative-keywords` | Negative (blocking) keywords only | ALL_TIME |
|
|
226
|
-
| `negative-keyword-lists` | Shared negative lists + their terms | ALL_TIME |
|
|
227
|
-
| `negative-list-attachments`| Which campaigns each shared list covers | ALL_TIME |
|
|
228
226
|
| `search-terms` | Actual user queries triggering ads | LAST_7_DAYS |
|
|
229
227
|
| `ad-copy-performance` | Ad headline/description effectiveness | LAST_30_DAYS |
|
|
230
228
|
| `asset-performance` | PMax asset performance labels | LAST_30_DAYS |
|
|
@@ -399,6 +397,262 @@ baker ads google keywords metrics --customer-id 1234567890 --keywords "running s
|
|
|
399
397
|
|
|
400
398
|
---
|
|
401
399
|
|
|
400
|
+
### Google Ads Library (`baker ads google library`)
|
|
401
|
+
|
|
402
|
+
Manage and search the Google Ads Transparency Center. Track competitor advertisers, browse their ad creatives, and discover who's bidding on keywords.
|
|
403
|
+
|
|
404
|
+
**Typical workflow:** `search-advertiser` → `track` → `search-ads`
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
### `baker ads google library search-advertiser "query"`
|
|
409
|
+
|
|
410
|
+
Search for an advertiser on the Google Ads Transparency Center.
|
|
411
|
+
|
|
412
|
+
> **Recommended:** use the domain running the ads (e.g. `example.com`) for more accurate results.
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
baker ads google library search-advertiser "example.com"
|
|
416
|
+
baker ads google library search-advertiser "Nike"
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Response:**
|
|
420
|
+
|
|
421
|
+
```json
|
|
422
|
+
{
|
|
423
|
+
"ok": true,
|
|
424
|
+
"data": {
|
|
425
|
+
"results": [
|
|
426
|
+
{ "advertiserId": "AR12345678901234567", "name": "Nike, Inc.", "region": "US", "format": "TEXT_IMAGE_VIDEO" }
|
|
427
|
+
]
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Flags:**
|
|
433
|
+
|
|
434
|
+
| Flag | Description |
|
|
435
|
+
|------------|--------------------------------|
|
|
436
|
+
| `--output` | Format: `json` \| `csv` \| `md` |
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
### `baker ads google library track <id> <name>`
|
|
441
|
+
|
|
442
|
+
Track a new Google advertiser and wait for the initial ad sync to complete. Polls every 5 seconds with a 10-minute timeout. Progress is written to stderr.
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
baker ads google library track AR12345678901234567 "Nike, Inc."
|
|
446
|
+
baker ads google library track AR12345678901234567 "Nike, Inc." --json
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**Response (with `--json`):**
|
|
450
|
+
|
|
451
|
+
```json
|
|
452
|
+
{
|
|
453
|
+
"ok": true,
|
|
454
|
+
"data": {
|
|
455
|
+
"advertiserId": "ar_abc123",
|
|
456
|
+
"accountId": "acc_def456",
|
|
457
|
+
"totalAdCount": 342,
|
|
458
|
+
"activeAdCount": 89
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Flags:**
|
|
464
|
+
|
|
465
|
+
| Flag | Description |
|
|
466
|
+
|----------|----------------------|
|
|
467
|
+
| `--json` | Output in JSON format |
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### `baker ads google library list-advertisers`
|
|
472
|
+
|
|
473
|
+
List all tracked Google advertisers and their accounts.
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
baker ads google library list-advertisers
|
|
477
|
+
baker ads google library list-advertisers --output md
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Flags:**
|
|
481
|
+
|
|
482
|
+
| Flag | Description |
|
|
483
|
+
|------------|--------------------------------|
|
|
484
|
+
| `--output` | Format: `json` \| `csv` \| `md` |
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### `baker ads google library sync-status <accountId>`
|
|
489
|
+
|
|
490
|
+
Check the sync status and ad counts of a tracked account.
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
baker ads google library sync-status acc_def456
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Response:**
|
|
497
|
+
|
|
498
|
+
```json
|
|
499
|
+
{
|
|
500
|
+
"ok": true,
|
|
501
|
+
"data": {
|
|
502
|
+
"syncStatus": null,
|
|
503
|
+
"totalAdCount": 342,
|
|
504
|
+
"activeAdCount": 89
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
`syncStatus` is `null` when idle, `"syncing"` during a sync, or `"error"` if the last sync failed.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
### `baker ads google library search-ads <accountId>`
|
|
514
|
+
|
|
515
|
+
Search and filter ads for a tracked account. Supports pagination.
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
baker ads google library search-ads acc_def456
|
|
519
|
+
baker ads google library search-ads acc_def456 --search "summer sale" --isActive --mediaType image
|
|
520
|
+
baker ads google library search-ads acc_def456 --sort newest --limit 50
|
|
521
|
+
baker ads google library search-ads acc_def456 --cursor "eyJwYWdl..."
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**Response:**
|
|
525
|
+
|
|
526
|
+
```json
|
|
527
|
+
{
|
|
528
|
+
"ok": true,
|
|
529
|
+
"data": {
|
|
530
|
+
"page": [
|
|
531
|
+
{
|
|
532
|
+
"_id": "abc123",
|
|
533
|
+
"platform": "google",
|
|
534
|
+
"externalId": "CR_1234567890",
|
|
535
|
+
"isActive": true,
|
|
536
|
+
"mediaType": "image",
|
|
537
|
+
"headline": "Summer Sale — 50% Off Everything",
|
|
538
|
+
"description": "Shop our biggest sale of the year. Free shipping on all orders.",
|
|
539
|
+
"destinationUrl": "https://example.com/summer-sale",
|
|
540
|
+
"bodyText": "Summer Sale — 50% Off Everything",
|
|
541
|
+
"pageName": "Example Store",
|
|
542
|
+
"impressionsMin": 100000,
|
|
543
|
+
"impressionsMax": 200000,
|
|
544
|
+
"startDate": "2025-06-01",
|
|
545
|
+
"endDate": "2025-06-30",
|
|
546
|
+
"firstSeenAt": 1717200000000,
|
|
547
|
+
"lastSeenAt": 1719792000000,
|
|
548
|
+
"publisherPlatforms": ["GOOGLE_ADS"],
|
|
549
|
+
"regionCodes": ["US", "GB"],
|
|
550
|
+
"variations": [
|
|
551
|
+
{
|
|
552
|
+
"headline": "Summer Sale — 50% Off",
|
|
553
|
+
"description": "Shop our biggest sale of the year.",
|
|
554
|
+
"destinationUrl": "https://example.com/summer-sale",
|
|
555
|
+
"imageUrl": "https://...",
|
|
556
|
+
"visibleUrl": "example.com"
|
|
557
|
+
}
|
|
558
|
+
],
|
|
559
|
+
"regions": [
|
|
560
|
+
{ "code": "US", "name": "United States" }
|
|
561
|
+
],
|
|
562
|
+
"analysisStatus": "completed",
|
|
563
|
+
"aiAnalysis": {
|
|
564
|
+
"aiSummary": "Promotional display ad for a seasonal sale with urgency-driven CTA",
|
|
565
|
+
"hookAngle": "Discount/Price",
|
|
566
|
+
"offerType": "Percentage Discount",
|
|
567
|
+
"ctaStrategy": "Shop Now",
|
|
568
|
+
"funnelStage": "Bottom",
|
|
569
|
+
"targetAudience": "Price-sensitive shoppers",
|
|
570
|
+
"adFormat": "responsive_display",
|
|
571
|
+
"tags": ["sale", "discount", "ecommerce"],
|
|
572
|
+
"trustSignals": ["Free shipping"],
|
|
573
|
+
"keyMessages": ["50% off", "Free shipping"],
|
|
574
|
+
"competitiveAngle": "Price leadership",
|
|
575
|
+
"dominantColors": ["#FF5733", "#FFFFFF"],
|
|
576
|
+
"analyzedAt": 1719792000000
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
],
|
|
580
|
+
"continueCursor": "eyJwYWdl...",
|
|
581
|
+
"isDone": false
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**Key response fields:**
|
|
587
|
+
|
|
588
|
+
| Field | Description |
|
|
589
|
+
|-------|-------------|
|
|
590
|
+
| `headline`, `description` | Top-level ad copy (first variation) |
|
|
591
|
+
| `variations[]` | All ad variations with copy, images, videos, and URLs |
|
|
592
|
+
| `regions[]` | Geographic targeting regions |
|
|
593
|
+
| `impressionsMin/Max` | Estimated impression range (Google Ads Transparency data) |
|
|
594
|
+
| `publisherPlatforms` | Where the ad ran (GOOGLE_ADS, YOUTUBE, etc.) |
|
|
595
|
+
| `analysisStatus` | AI analysis state: `pending`, `processing`, `completed`, `failed` |
|
|
596
|
+
| `aiAnalysis` | AI-generated creative analysis (only present when `analysisStatus` is `completed`) |
|
|
597
|
+
| `aiAnalysis.aiSummary` | One-line AI summary of the ad |
|
|
598
|
+
| `aiAnalysis.hookAngle` | Creative hook (Discount, Fear, Social Proof, etc.) |
|
|
599
|
+
| `aiAnalysis.funnelStage` | Funnel position: Top, Middle, Bottom |
|
|
600
|
+
| `aiAnalysis.tags` | AI-generated tags for filtering |
|
|
601
|
+
|
|
602
|
+
**Flags:**
|
|
603
|
+
|
|
604
|
+
| Flag | Description |
|
|
605
|
+
|---------------|------------------------------------------------|
|
|
606
|
+
| `--search` | Search term for ad text |
|
|
607
|
+
| `--isActive` | Filter by active ads only |
|
|
608
|
+
| `--mediaType` | Filter by media type: `image`, `video`, `text` |
|
|
609
|
+
| `--sort` | Sort: `newest` or `oldest` |
|
|
610
|
+
| `--limit` | Max results per page (default 20, max 100) |
|
|
611
|
+
| `--cursor` | Pagination cursor from previous response |
|
|
612
|
+
| `--output` | Format: `json` \| `csv` \| `md` |
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### `baker ads google library sync <accountId>`
|
|
617
|
+
|
|
618
|
+
Trigger an immediate re-sync for a tracked account. Polls every 5 seconds until complete (10-minute timeout). Progress is written to stderr.
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
baker ads google library sync acc_def456
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Response:**
|
|
625
|
+
|
|
626
|
+
```json
|
|
627
|
+
{
|
|
628
|
+
"ok": true,
|
|
629
|
+
"data": {
|
|
630
|
+
"totalAdCount": 350,
|
|
631
|
+
"activeAdCount": 92
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
### `baker ads google library search-competitors "keyword"`
|
|
639
|
+
|
|
640
|
+
Search for competitors running Google ads for a keyword. Uses DataForSEO (same data as `baker research advertisers`).
|
|
641
|
+
|
|
642
|
+
```bash
|
|
643
|
+
baker ads google library search-competitors "running shoes"
|
|
644
|
+
baker ads google library search-competitors "crm software" --location uk
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Flags:**
|
|
648
|
+
|
|
649
|
+
| Flag | Description |
|
|
650
|
+
|--------------|----------------------------|
|
|
651
|
+
| `--location` | Location name or code |
|
|
652
|
+
| `--json` | Output in JSON format |
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
402
656
|
### Staged writes (`baker ads google budgets|campaigns|...`)
|
|
403
657
|
|
|
404
658
|
Write commands **never touch the Google Ads API at stage time**. Each command stages a create/update/pause/resume/remove op against the current chat's draft (`BAKER_CHAT_ID`); the dashboard shows it as a pending "Google Ads" change, and the whole draft applies as one atomic `GoogleAdsService.Mutate` when the chat is published. Feature-flagged per company (`companies.googleAdsWriteEnabled`) — off by default = a fully simulated publish with zero real API calls.
|
|
@@ -1690,14 +1944,14 @@ baker images generate "flat geometric mascot, brand palette" \
|
|
|
1690
1944
|
--model recraft/recraft-v4.1-pro-vector --rgb-colors "[[10,10,10],[255,80,0]]" --bg-rgb "[255,255,255]"
|
|
1691
1945
|
```
|
|
1692
1946
|
|
|
1693
|
-
**Models** (`--model`, default `
|
|
1947
|
+
**Models** (`--model`, default `openai/gpt-5.4-image-2`):
|
|
1694
1948
|
|
|
1695
1949
|
| Model | Best for | Aspect ratios | Sizes |
|
|
1696
1950
|
|---|---|---|---|
|
|
1697
|
-
| `
|
|
1951
|
+
| `openai/gpt-5.4-image-2` **(default)** | Photoreal + cleanest in-image text — ad/landing reproduction | standard set | `1K` `2K` `4K` |
|
|
1952
|
+
| `google/gemini-3-pro-image-preview` | Highest fidelity (Nano Banana Pro) | standard set | `1K` `2K` `4K` |
|
|
1698
1953
|
| `google/gemini-3.5-flash` | Fast; extreme aspect ratios | standard **+** `1:4` `4:1` `1:8` `8:1` | `0.5K`–`4K` |
|
|
1699
|
-
| `google/gemini-3-
|
|
1700
|
-
| `openai/gpt-5.4-image-2` | Photoreal + cleanest in-image text — ad/landing reproduction | standard set | `1K` `2K` `4K` |
|
|
1954
|
+
| `google/gemini-3.1-flash-image-preview` | Same as 3.5 flash (preview) | extreme set | `0.5K`–`4K` |
|
|
1701
1955
|
| `recraft/recraft-v4.1-pro-vector` | Vector/flat/SVG-style with palette control | standard set | `1K` `2K` `4K` |
|
|
1702
1956
|
|
|
1703
1957
|
Standard aspect ratios: `1:1` `2:3` `3:2` `3:4` `4:3` `4:5` `5:4` `9:16` `16:9` `21:9`.
|
|
@@ -1706,7 +1960,7 @@ Standard aspect ratios: `1:1` `2:3` `3:2` `3:4` `4:3` `4:5` `5:4` `9:16` `16:9`
|
|
|
1706
1960
|
|
|
1707
1961
|
| Flag | Description |
|
|
1708
1962
|
|---|---|
|
|
1709
|
-
| `--model` | Model id (default `
|
|
1963
|
+
| `--model` | Model id (default `openai/gpt-5.4-image-2`) |
|
|
1710
1964
|
| `--aspect-ratio` | Output aspect ratio (default `1:1`) |
|
|
1711
1965
|
| `--image-size` | Resolution: `1K` (default) `2K` `4K` (Gemini flash also `0.5K`) |
|
|
1712
1966
|
| `--reference` | Comma-separated visual references, each either a **public image URL** (Pinterest / stock / library `imageUrl`) **or a local file path** (a sandbox image — brand logo, product shot, cropped photo, screenshot). Local files are downscaled (≤1536px) and inlined automatically — no manual upload. Applied in order; the biggest quality lever for photographed, on-brand output. Split is on `,`, so a URL containing a literal comma in its query string would be torn in two (rare for image CDNs — pass it alone if it occurs); a single `data:` URL is taken whole. |
|
|
@@ -1878,6 +2132,8 @@ Auto-ingests with prefetched bytes (no double-fetch). The screenshot bytes thems
|
|
|
1878
2132
|
|
|
1879
2133
|
ScreenshotOne caches captures for 30 days, so re-shotting the same URL within that window returns the cached capture.
|
|
1880
2134
|
|
|
2135
|
+
If the target page is unreachable or returns a non-2xx status (e.g. a 404 path or a login wall), the command fails with an actionable `VALIDATION_ERROR` naming the page and the status it returned, plus a `fix` hint — verify the URL and retry, or continue without the screenshot. It no longer surfaces a generic "Internal server error".
|
|
2136
|
+
|
|
1881
2137
|
**Flags:**
|
|
1882
2138
|
|
|
1883
2139
|
| Flag | Description |
|
|
@@ -2163,17 +2419,13 @@ baker testimonials tags
|
|
|
2163
2419
|
|
|
2164
2420
|
### Winning Ads (`baker winning-ads`)
|
|
2165
2421
|
|
|
2166
|
-
Search the **ad-dna** corpus of scored "winning" competitor ads for reference creatives to reproduce (e.g. with `baker canvas`)
|
|
2167
|
-
|
|
2168
|
-
> The corpus has **Meta + LinkedIn** connectors, so `--platform` inputs are limited to `meta,linkedin`. (Older result rows may still carry a legacy platform string.)
|
|
2422
|
+
Search the **ad-dna** corpus of scored "winning" competitor ads for reference creatives to reproduce (e.g. with `baker canvas`). Each result carries a presigned media URL (~1h TTL), the ad's DNA summary, and scores. The CLI authenticates with the normal `BAKER_API_KEY`; the Baker backend proxies the request to the ad-dna service with a server-held token — no extra credential in the sandbox.
|
|
2169
2423
|
|
|
2170
2424
|
> Backend env: the Convex deployment must have `AD_DNA_API_TOKEN` set (`npx convex env set AD_DNA_API_TOKEN …`). `AD_DNA_API_URL` is optional and defaults to `https://ads.withbaker.com`.
|
|
2171
2425
|
|
|
2172
|
-
> Replaces the old `baker ads google library` tree, which has been removed. Competitor-by-keyword discovery still lives at `baker research advertisers`.
|
|
2173
|
-
|
|
2174
2426
|
### `baker winning-ads search <query>`
|
|
2175
2427
|
|
|
2176
|
-
Semantic search (dense recall + BM25 + rerank)
|
|
2428
|
+
Semantic search (dense recall + BM25 + rerank). The CLI projects each result to a **lean, decision-focused** shape so the agent's context stays small — default fields: `advertiser`, `advertiser_id`, `platform`, `format`, `relevance`, `winner_score`, `summary` (what the ad is about), `media_url`; plus top-level `pool_size` and `match_confidence`. `--full` adds DNA detail (`angle`, `target_persona`, `hook_archetype`, `awareness_stage`, `industry`) + longevity (`days_active`, `reach`, `active`, `winner_category`, `media_kind`). `--output json` (default) returns the lean objects; `--output md` prints a table.
|
|
2177
2429
|
|
|
2178
2430
|
> `media_url` is the creative itself: for `static` it's the image, for `video` it's the video file. ad-dna stores **no separate poster** for videos, so a video result has only the video URL.
|
|
2179
2431
|
|
|
@@ -2195,7 +2447,7 @@ baker winning-ads search --ref-ad-id a_12345 --first-seen-after 2026-01-01T00:00
|
|
|
2195
2447
|
| `--limit <n>` | Max results 1–100 (**default 10** — shortlist size) |
|
|
2196
2448
|
| `--max-per-advertiser <n>` | Cap results per advertiser 1–50 (default 3) |
|
|
2197
2449
|
| `--min-relevance <0-1>` | Relevance floor; trims weak matches |
|
|
2198
|
-
| `--platform <list>` | `meta,linkedin` — pass a single value to search **only** that platform |
|
|
2450
|
+
| `--platform <list>` | One or many of `meta,tiktok,linkedin,google_search,google_display,youtube,reddit,x,pinterest,snapchat` — pass a single value to search **only** that platform |
|
|
2199
2451
|
| `--format <list>` | `video,static,carousel` |
|
|
2200
2452
|
| `--winner-category <list>` | `winner,scaled_winner,evergreen,rising,untested,dud,…` (default: all) |
|
|
2201
2453
|
| `--awareness <list>` | `unaware,problem_aware,solution_aware,product_aware,most_aware` |
|
|
@@ -2203,71 +2455,46 @@ baker winning-ads search --ref-ad-id a_12345 --first-seen-after 2026-01-01T00:00
|
|
|
2203
2455
|
| `--exclude-advertiser <list>` | **Drop** these advertiser ids — your own brand + already-used references |
|
|
2204
2456
|
| `--country <list>` / `--language <list>` | Filter by country / language codes |
|
|
2205
2457
|
| `--first-seen-after` / `--first-seen-before` | ISO datetime bounds (recency) |
|
|
2458
|
+
| `--hook-archetype <list>` | Narrow to ads on a proven hook **seed** (from `winning-ads hooks`) |
|
|
2206
2459
|
| `--output json\|md\|files` | Output format (default json) |
|
|
2207
2460
|
| `--full` | Include DNA detail + longevity |
|
|
2208
2461
|
|
|
2209
2462
|
Reading the scores: **`relevance`** (0–1) = match of the creative to your query; **`winner_score`** = how proven the ad is in-market. Pick references that are both relevant *and* proven.
|
|
2210
2463
|
|
|
2211
|
-
### `baker winning-ads
|
|
2464
|
+
### `baker winning-ads hooks|mechanisms|patterns`
|
|
2212
2465
|
|
|
2213
|
-
|
|
2466
|
+
Read the **winner-weighted rollup** of the ad-dna corpus — the hook / mechanism / static-format patterns that
|
|
2467
|
+
actually win, ranked by `winner_score`, keyed on the closed seeds/enums the extractor emits (no clustering).
|
|
2468
|
+
Ground a creative in a **proven** pattern for the exact segment instead of guessing.
|
|
2214
2469
|
|
|
2215
2470
|
```bash
|
|
2216
|
-
|
|
2217
|
-
baker winning-ads
|
|
2471
|
+
# Top winning hook seeds for problem-aware SaaS on Meta:
|
|
2472
|
+
baker winning-ads hooks --platform meta --awareness problem_aware --industry saas --output md
|
|
2473
|
+
# Then narrow a reference search to that proven hook:
|
|
2474
|
+
baker winning-ads search "onboarding demo" --hook-archetype callout --output md
|
|
2475
|
+
# Also: mechanisms (persuasion), patterns (static ad layouts):
|
|
2476
|
+
baker winning-ads mechanisms --platform linkedin --output md
|
|
2477
|
+
baker winning-ads patterns --awareness solution_aware --output md
|
|
2218
2478
|
```
|
|
2219
2479
|
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
```bash
|
|
2229
|
-
baker winning-ads follow "deel.com" --platform meta
|
|
2230
|
-
baker winning-ads follow "https://www.linkedin.com/company/acme" --platform linkedin --label "Acme (competitor)"
|
|
2231
|
-
```
|
|
2232
|
-
|
|
2233
|
-
### `baker winning-ads following`
|
|
2234
|
-
|
|
2235
|
-
List the brands you follow → `GET /api/ad-library/following`. Each row shows `status` (`ready` vs `adding…`) plus cached counts (`active_ad_count`, `total_ad_count`, `family_count`) and discovery progress (`adding_discovered`, `adding_enqueued`). `--full` adds `image_url` + `platforms`.
|
|
2236
|
-
|
|
2237
|
-
```bash
|
|
2238
|
-
baker winning-ads following --output md
|
|
2239
|
-
```
|
|
2240
|
-
|
|
2241
|
-
### `baker winning-ads winners <advertiser>`
|
|
2242
|
-
|
|
2243
|
-
Top winning ads for one advertiser id → `GET /api/ad-library/advertiser-winners`. Same lean winner cards as `search` (add `--full` for DNA + longevity). Supports `--top N` and `--platform meta|linkedin`.
|
|
2244
|
-
|
|
2245
|
-
```bash
|
|
2246
|
-
baker winning-ads winners adv_123 --top 15 --output md
|
|
2247
|
-
```
|
|
2248
|
-
|
|
2249
|
-
### `baker winning-ads unfollow <advertiser>`
|
|
2250
|
-
|
|
2251
|
-
Stop following a brand by advertiser id → `POST /api/ad-library/unfollow`. Returns `{ removed }`.
|
|
2252
|
-
|
|
2253
|
-
```bash
|
|
2254
|
-
baker winning-ads unfollow adv_123
|
|
2255
|
-
```
|
|
2256
|
-
|
|
2257
|
-
### `baker winning-ads brief`
|
|
2480
|
+
| Flag | Purpose |
|
|
2481
|
+
|---|---|
|
|
2482
|
+
| `--platform <p>` | Segment on a single platform |
|
|
2483
|
+
| `--awareness <stage>` | Segment on a single awareness stage |
|
|
2484
|
+
| `--industry <seed>` | Segment on a single industry seed |
|
|
2485
|
+
| `--limit <n>` | Max keys 1–100 (default 20) |
|
|
2486
|
+
| `--full` | Include the per-key segment breakdown |
|
|
2258
2487
|
|
|
2259
|
-
|
|
2488
|
+
Each key returns `count`, `mean_winner_score`, and `winner_share`. Ranking is winner-weighted volume, so a
|
|
2489
|
+
proven grand-slam pattern floats to the top and a measured dud is floored.
|
|
2260
2490
|
|
|
2261
|
-
|
|
2262
|
-
baker winning-ads brief --dna '{"angle":"cost savings","awareness_stage":"solution_aware"}' --notes "B2B, LinkedIn video" --k 8
|
|
2263
|
-
```
|
|
2264
|
-
|
|
2265
|
-
### `baker winning-ads patterns --winners <adIds> --duds <adIds>`
|
|
2491
|
+
### `baker winning-ads advertisers <brand>`
|
|
2266
2492
|
|
|
2267
|
-
|
|
2493
|
+
Resolve a brand name → `advertiser_id`(s) in the corpus. Use it to find **your own** advertiser (to `--exclude-advertiser`) or a **competitor** (to `--advertiser-id`). Returns `advertiser_id`, `label`, `active_ads`, `total_ads`.
|
|
2268
2494
|
|
|
2269
2495
|
```bash
|
|
2270
|
-
baker winning-ads
|
|
2496
|
+
baker winning-ads advertisers "Acme" --output md # find our own advertiser id
|
|
2497
|
+
baker winning-ads advertisers "Deel" --platform meta --output md
|
|
2271
2498
|
```
|
|
2272
2499
|
|
|
2273
2500
|
---
|
|
@@ -2497,13 +2724,36 @@ baker canvas run my-canvas.json
|
|
|
2497
2724
|
|
|
2498
2725
|
# 2b. Independent nodes ALREADY fan out in parallel — every node in the same
|
|
2499
2726
|
# topological layer (e.g. all the per-clip video_generate nodes) runs concurrently
|
|
2500
|
-
# up to a bound. Raise it with --parallel N (alias: --concurrency N; default
|
|
2727
|
+
# up to a bound. Raise it with --parallel N (alias: --concurrency N; default 8;
|
|
2501
2728
|
# env BAKER_CANVAS_CONCURRENCY). A single clip failing NO LONGER strands its
|
|
2502
2729
|
# siblings: the whole layer settles, cached siblings survive, and a re-run resumes
|
|
2503
2730
|
# from cache. There is no need to render clips one at a time or pin `output` to a
|
|
2504
2731
|
# single node — that old serial workaround is obsolete.
|
|
2505
2732
|
baker canvas run my-canvas.json --parallel 8
|
|
2506
2733
|
|
|
2734
|
+
# 2c. Runs persist across sandboxes/sessions by default: node results sync to a
|
|
2735
|
+
# company-scoped remote cache (small JSON pointers; bytes stay in R2), so a FRESH
|
|
2736
|
+
# sandbox re-runs an already-computed canvas at zero credits — assets rehydrate
|
|
2737
|
+
# from R2, sha-verified. Every run also posts a durable history record (per-node
|
|
2738
|
+
# outputs, credits, cached/fresh) that powers the dashboard's Creatives
|
|
2739
|
+
# generations timeline. Opt out with --remote-cache off (env
|
|
2740
|
+
# BAKER_CANVAS_REMOTE_CACHE=off) and --no-record. With --remote-cache off,
|
|
2741
|
+
# assets are not uploaded, so a recorded run keeps its stats but has no
|
|
2742
|
+
# browsable outputs — pass --no-record too if you want nothing persisted.
|
|
2743
|
+
baker canvas run my-canvas.json --remote-cache off --no-record
|
|
2744
|
+
|
|
2745
|
+
# 2d. Regenerate a node whose prompt is fine (force a fresh roll). The engine is
|
|
2746
|
+
# content-addressed: re-running an UNCHANGED node returns the identical cached
|
|
2747
|
+
# render, never a new draw. To re-roll a node without editing its prompt (a
|
|
2748
|
+
# color drifted, a face came out wrong), force it fresh two ways — NEVER
|
|
2749
|
+
# restructure the canvas (repointing output / deleting nodes) to trick the cache:
|
|
2750
|
+
# • One-shot flag — forces the named nodes + everything downstream fresh this
|
|
2751
|
+
# run, leaving every other node cached (unknown ids fail loudly before billing):
|
|
2752
|
+
baker canvas run my-canvas.json --regenerate gen_4x5,gen_9x16
|
|
2753
|
+
# • Persistent — add/bump a node's `regenerate` field in the canvas JSON
|
|
2754
|
+
# (e.g. "regenerate": 2) and re-run; the fresh render is reproducible in any
|
|
2755
|
+
# later session. Bump it again (3, 4, …) for each additional draw.
|
|
2756
|
+
|
|
2507
2757
|
# 3. Inspect a finished run (per-node timing, file list, optional video thumbs)
|
|
2508
2758
|
baker canvas inspect <run_id>
|
|
2509
2759
|
|
|
@@ -2603,6 +2853,20 @@ A literal string value. Use for prompts, descriptions, copy.
|
|
|
2603
2853
|
|
|
2604
2854
|
---
|
|
2605
2855
|
|
|
2856
|
+
##### `collect`
|
|
2857
|
+
|
|
2858
|
+
Gather images from multiple upstream nodes into one ordered array — the standard terminal for **multi-variant canvases** whose final output is several images (e.g. one artwork composited into N scene photos, one `image_generate` branch per scene). Point the canvas `output` at this node and every collected image becomes a final (`final#0`…`final#n-1`, capped at 10 in run records).
|
|
2859
|
+
|
|
2860
|
+
Pure ref passthrough: zero credits, no byte downloads, and each final carries a **label** into the run record — its producer node id (`$ref:gen_billboard_03.images#0` → `gen_billboard_03`) or an explicit `params.labels[i]` — so variants stay identifiable in the dashboard grid and per-output selection. Name branches after their scene/variant to get meaningful labels for free.
|
|
2861
|
+
|
|
2862
|
+
**Inputs:** `images` (`ImageRef[]`, min 1) — wire a literal array of refs, one per branch: `["$ref:gen_a.images#0", "$ref:gen_b.images#0", …]`.
|
|
2863
|
+
|
|
2864
|
+
**Params:** `labels` (string[], optional) — one unique label per wired image; overrides the producer-id default.
|
|
2865
|
+
|
|
2866
|
+
**Outputs:** `images` → `image[]`, same order as wired.
|
|
2867
|
+
|
|
2868
|
+
---
|
|
2869
|
+
|
|
2606
2870
|
##### `ffmpeg`
|
|
2607
2871
|
|
|
2608
2872
|
Local ffmpeg passthrough. Write the argv you'd type, declare outputs, the engine stages inputs and ingests results. See [Local CLI nodes](#local-cli-nodes) for the placeholder safety contract.
|
|
@@ -2748,7 +3012,7 @@ Pick a `source` discriminator and declare the kind you expect. See [Ingestion](#
|
|
|
2748
3012
|
|
|
2749
3013
|
**Outputs:** `asset` → `<params.expect>` / content-determined (URL strategy table) or extension-inferred (path).
|
|
2750
3014
|
|
|
2751
|
-
**Path-source notes:** the canvas is **not portable** to another machine without the file. Cache key folds the file's `mtime:size`, so editing the file invalidates the cache automatically. Supported extensions: `png`, `jpg`/`jpeg`, `webp`, `gif`, `avif`, `svg`, `mp4`, `webm`, `mov`, `m4v`, `mp3`, `wav`, `m4a`, `ogg`, `flac`, `json`, `txt`, `md`, `markdown`, `html`/`htm`, `csv`, `ttf`, `otf`, `woff`, `woff2`. Unknown extensions fall back to magic-byte sniffing for common image formats (and an SVG content sniff), else `kind_mismatch`. **
|
|
3015
|
+
**Path-source notes:** the canvas is **not portable** to another machine without the file. Cache key folds the file's `mtime:size`, so editing the file invalidates the cache automatically. Supported extensions: `png`, `jpg`/`jpeg`, `webp`, `gif`, `avif`, `svg`, `mp4`, `webm`, `mov`, `m4v`, `mp3`, `wav`, `m4a`, `ogg`, `flac`, `json`, `txt`, `md`, `markdown`, `html`/`htm`, `csv`, `ttf`, `otf`, `woff`, `woff2`. Unknown extensions fall back to magic-byte sniffing for common image formats (and an SVG content sniff), else `kind_mismatch`. **Any `expect: "image"` in a format image-generation models can't read (SVG, AVIF, HEIC, TIFF, BMP) is normalized to PNG on ingest** — model-safe rasters (`jpeg`/`png`/`gif`/`webp`) pass through untouched, everything else is transcoded so a reference can never 400 a generation. This applies to **both `source: "path"` and `source: "url"`** (URL images are fetched and normalized locally, since the backend can't run the rasterizer). SVG gets density-aware upscaling (longest edge near 2048px, transparency preserved). The normalized asset carries `metadata.rasterized_from` set to the source format (e.g. `"svg"`, `"avif"`). **Video (`expect: "video"`) duration is probed from the file's ISO-BMFF (`mp4`/`mov`/`m4v`) header** and stamped as the canonical `duration_ms` (and `metadata.duration_ms`); other containers (e.g. `webm`) leave it unset. Downstream `video_deconstruct` uses this declared duration to size its ingest-poll timeout and preflight — without it those fall back to worst-case budgets and a single deconstruct step can hit the action time limit.
|
|
2752
3016
|
|
|
2753
3017
|
**Cost:** 0 engine credits for direct fetch + yt-dlp + local file. Handinger charges per scrape.
|
|
2754
3018
|
|
|
@@ -2844,10 +3108,11 @@ Photorealistic generalist. Optional `reference` image.
|
|
|
2844
3108
|
| `prompt` | string | yes | non-empty |
|
|
2845
3109
|
| `aspect_ratio` | enum | no | STD AR |
|
|
2846
3110
|
| `image_size` | enum | no | `1K \| 2K \| 4K` |
|
|
3111
|
+
| `quality` | enum | no | `auto \| low \| medium \| high` — rendering quality; the scaffolder sets `high` for photoreal frames. (Do **not** pass `input_fidelity`: gpt-image-2 forces high fidelity automatically and the param can fail the request.) |
|
|
2847
3112
|
|
|
2848
3113
|
```json
|
|
2849
3114
|
{ "id": "hero", "type": "image_generate",
|
|
2850
|
-
"params": { "model": "openai/gpt-5.4-image-2", "prompt": "
|
|
3115
|
+
"params": { "model": "openai/gpt-5.4-image-2", "prompt": "Photorealistic photo of …", "aspect_ratio": "16:9", "image_size": "2K", "quality": "high" } }
|
|
2851
3116
|
```
|
|
2852
3117
|
|
|
2853
3118
|
###### Model: `google/gemini-3.5-flash`
|
|
@@ -3198,9 +3463,9 @@ Accepted ref-image MIMEs vary by model — see per-model sections below.
|
|
|
3198
3463
|
|
|
3199
3464
|
###### Model: `bytedance/seedance-2.0`
|
|
3200
3465
|
|
|
3201
|
-
Production-quality ad-creative model. Routed via **
|
|
3466
|
+
Production-quality ad-creative model. Routed via **Replicate** (`bytedance/seedance-2.0`). NOTE: ByteDance's upstream "real person" likeness filter still blocks photorealistic human reference frames on **any** reseller — the escape is a synthetic/AI presenter face or routing real faces to Veo, not the provider.
|
|
3202
3467
|
|
|
3203
|
-
Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp
|
|
3468
|
+
Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`.
|
|
3204
3469
|
|
|
3205
3470
|
| Name | Type | Required | Notes |
|
|
3206
3471
|
|---|---|---|---|
|
|
@@ -3250,11 +3515,17 @@ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`, `image/gif` (via OpenR
|
|
|
3250
3515
|
> **Switching a clip's model has cross-node constraints** — validate after every model edit. `validate` gates all of these before any billed run:
|
|
3251
3516
|
> - **One aspect_ratio for the whole canvas.** Every `video_generate` node must agree (`VIDEO_ASPECT_MISMATCH`), or the composite silently crops/letterboxes. Switching one clip to Veo (which only offers `16:9`/`9:16`) forces every other clip to a shared ratio too.
|
|
3252
3517
|
> - **`duration` is a per-model hard enum.** Seedance `4,5,6,8,10,12,15`; Veo `4,6,8` only. A value outside the model's set is rejected; a scene whose natural span exceeds the model's max is flagged (`VIDEO_SPAN_EXCEEDS_MODEL`, advisory) — a 9.2s scene can't fit Veo's 8s cap and would truncate; split the scene or keep it on a longer-max model.
|
|
3253
|
-
> - **`person_generation` differs.** Veo accepts only `allow_all
|
|
3518
|
+
> - **`person_generation` differs.** Veo **image-to-video** accepts only `allow_adult` (the sole legal value for keyframe-driven modes, and the only one permitted in the EU/UK); `allow_all` is text-to-video only. The scaffolder sets `allow_adult` on every Veo clip automatically (`VIDEO_PERSON_GENERATION_MISSING` warns if it's missing on a hand-edited canvas).
|
|
3254
3519
|
>
|
|
3255
3520
|
> A scaffolded canvas carries this table inline at `metadata.todo.model_constraints`.
|
|
3256
3521
|
|
|
3257
|
-
> **
|
|
3522
|
+
> **Per-model prompt & param profiles (applied automatically).** The scaffolder shapes each clip/frame prompt to the model's documented best practice and sets its request-param levers — see `packages/cli/src/engine/scaffold/lib/prompt-profiles.ts`:
|
|
3523
|
+
> - **Seedance 2.0** (default video): dialogue in quotes with delivery in **prose** (not `[brackets]`), a `Preserve the composition and colors` keyframe hold, and an **affirmative** stability tail — `Keep it clean and stable: crisp anatomically-correct hands, one consistent face, smooth steady motion` — instead of an `Avoid: …` list (Seedance has no `negative_prompt` param, and inline negation tends to summon the artifact it names). Prompt kept to the ~120-word quality budget (`VIDEO_PROMPT_OVER_BUDGET` warns past ~120).
|
|
3524
|
+
> - **Veo 3.1** (fallback): `person_generation=allow_adult` + a caption-suppressing `negative_prompt` set as params; durations snapped to `{4,6,8}`.
|
|
3525
|
+
> - **gpt-image** (default frames): clean-plate exclusions placed **LAST** (OpenAI weights early text as composition), an explicit `photorealistic` cue, identity-critical reference **first**, and `quality=high` (gpt-image-2 forces high input fidelity automatically — the `input_fidelity` param is deliberately **not** sent). Gemini keeps exclusions inline; Recraft drops the photoreal cue.
|
|
3526
|
+
> - New advisories: `VIDEO_RAW_FACE_KEYFRAME` (a Seedance clip animating a RAW ingested image — real-person 422 risk), `IMAGE_PROMPT_PROFILE_MISSING`, `VIDEO_PROMPT_PROFILE_MISSING`.
|
|
3527
|
+
|
|
3528
|
+
> **Content-policy blocks are deterministic, not flaky.** ByteDance's Seedance rejects any first/last frame that reads as a real-person likeness (even an AI-generated face) — surfaced as `content_policy_blocked` (HTTP 422, **non-retryable**). This is ByteDance's upstream filter, so it fires on **any** reseller (Replicate or otherwise) — a provider swap does **not** route around it. Retrying **never** succeeds and wastes credits. Fix the cause: use a **synthetic/AI-generated** (non-identifiable) presenter face, or route a real face to **Veo** (`person_generation: allow_adult`), or make the source frame less photorealistic.
|
|
3258
3529
|
|
|
3259
3530
|
---
|
|
3260
3531
|
|
|
@@ -3305,7 +3576,7 @@ None.
|
|
|
3305
3576
|
|
|
3306
3577
|
##### `video_lipsync`
|
|
3307
3578
|
|
|
3308
|
-
Lip-sync a video to an audio track via
|
|
3579
|
+
Lip-sync a video to an audio track via Sync Labs `sync/lipsync-2` (Replicate).
|
|
3309
3580
|
|
|
3310
3581
|
**Inputs**
|
|
3311
3582
|
|
|
@@ -3332,7 +3603,7 @@ Lip-sync a video to an audio track via VEED (fal.ai).
|
|
|
3332
3603
|
|
|
3333
3604
|
##### `video_background_remove`
|
|
3334
3605
|
|
|
3335
|
-
Strip a video's background → alpha WebM
|
|
3606
|
+
Strip a video's background → transparent alpha WebM (VP9) or MOV (ProRes 4444). Powered by `sprited/birefnet-video` (Replicate).
|
|
3336
3607
|
|
|
3337
3608
|
**Inputs**
|
|
3338
3609
|
|
|
@@ -3360,7 +3631,7 @@ Strip a video's background → alpha WebM/H264. Powered by fal.ai VEED.
|
|
|
3360
3631
|
|
|
3361
3632
|
##### `video_transcribe`
|
|
3362
3633
|
|
|
3363
|
-
Word-level transcription. Default `transcriber:"groq"` uses Groq Whisper Large v3 Turbo; `transcriber:"deepgram"` uses Deepgram Nova-3, which additionally emits a `rich` output with punctuated words + paragraph/sentence grouping (and speaker indices). Auto-extracts audio locally (mono 16 kHz, 64 kbps MP3) before uploading — payload shrinks ~100× vs. sending the full video.
|
|
3634
|
+
Word-level transcription of a video OR a bare audio track (the `video` input accepts either; an audio ref is sent as-is). Default `transcriber:"groq"` uses Groq Whisper Large v3 Turbo; `transcriber:"deepgram"` uses Deepgram Nova-3, which additionally emits a `rich` output with punctuated words + paragraph/sentence grouping (and speaker indices). Auto-extracts audio locally (mono 16 kHz, 64 kbps MP3) from a video input before uploading — payload shrinks ~100× vs. sending the full video.
|
|
3364
3635
|
|
|
3365
3636
|
**Inputs**
|
|
3366
3637
|
|
|
@@ -3497,7 +3768,7 @@ Place and mix several audio clips onto one timeline — a music bed plus timed v
|
|
|
3497
3768
|
|
|
3498
3769
|
| Name | Type | Required | Notes |
|
|
3499
3770
|
|---|---|---|---|
|
|
3500
|
-
| `tracks` | array | yes | `[{slot, start_s, gain_db?}]` — `slot` matches a wired input; `start_s` is absolute seconds; `gain_db` sets the static level (e.g. `-20` for a music bed) |
|
|
3771
|
+
| `tracks` | array | yes | `[{slot, start_s, duration_s?, gain_db?}]` — `slot` matches a wired input; `start_s` is absolute seconds; `duration_s` hard-caps the clip (trimmed BEFORE placement, so an over-long source can't bleed into the next track's window); `gain_db` sets the static level (e.g. `-20` for a music bed) |
|
|
3501
3772
|
| `total_ms` | number | no | pins the final length (pad/trim) |
|
|
3502
3773
|
| `output_format` | enum | no | `mp3` (default) / `wav` / `m4a` |
|
|
3503
3774
|
| `duck` | object | no | sidechain-duck one track under others: `{ track, against: [...], threshold?, ratio?, attack?, release? }` — the `track` (music) drops while any `against` track (voice) carries signal and recovers in the gaps |
|
|
@@ -3508,7 +3779,7 @@ Place and mix several audio clips onto one timeline — a music bed plus timed v
|
|
|
3508
3779
|
|
|
3509
3780
|
##### `image_background_remove`
|
|
3510
3781
|
|
|
3511
|
-
Strip background → transparent PNG
|
|
3782
|
+
Strip background → transparent PNG. Powered by `men1scus/birefnet` (Replicate). (Note: mask-only output is no longer produced.)
|
|
3512
3783
|
|
|
3513
3784
|
**Inputs**
|
|
3514
3785
|
|
|
@@ -3739,6 +4010,30 @@ $ baker canvas validate my-canvas.json
|
|
|
3739
4010
|
|
|
3740
4011
|
On error you get an array of `ValidationIssue`s, each with `path`, `code`, `message`, and optional `did_you_mean`.
|
|
3741
4012
|
|
|
4013
|
+
Advisory (warning-severity) video invariants include `VIDEO_PROMPT_DECISION_MISSING` (a thin clip prompt that
|
|
4014
|
+
names no camera/motion move **and** no exclusions — the six-decision doctrine) and `VIDEO_HOOK_LAYER_MISSING`
|
|
4015
|
+
(the hook, scene 0, is thin across two or more of the four hook layers — text/sound/visual/vibe). Warnings never
|
|
4016
|
+
block a run.
|
|
4017
|
+
|
|
4018
|
+
#### `baker canvas critique <file.json>`
|
|
4019
|
+
|
|
4020
|
+
Pre-spend creative critic — **advisory, never blocks**. Scores a scaffolded creative 0–1 before a billed `run`,
|
|
4021
|
+
so a weak spot gets fixed for free. It branches on modality: a **video** creative is scored on **hook strength**,
|
|
4022
|
+
**sound-off first-frame legibility**, **retention risk**, and **mechanism clarity**; a **static ad** on **baked-text
|
|
4023
|
+
legibility risk**, **identity grounding** (Rule 1 — every subject on a real reference), **brand-type fidelity** (a
|
|
4024
|
+
font specimen wired), and **mechanism clarity**. Reads the canvas only (no generation, no Convex). Ground a video
|
|
4025
|
+
hook against a proven pattern with `baker winning-ads hooks`.
|
|
4026
|
+
|
|
4027
|
+
```bash
|
|
4028
|
+
$ baker canvas critique my-video.json
|
|
4029
|
+
{ "ok": true, "advisory": true, "overall": 0.81,
|
|
4030
|
+
"dimensions": [ { "dimension": "hook_strength", "score": 1, "note": "hook works on 4/4 layers …" }, … ] }
|
|
4031
|
+
|
|
4032
|
+
$ baker canvas critique my-static-ad.json
|
|
4033
|
+
{ "ok": true, "advisory": true, "overall": 0.88,
|
|
4034
|
+
"dimensions": [ { "dimension": "text_legibility_risk", "score": 1, "note": "2 copy block(s) — reads cleanly" }, … ] }
|
|
4035
|
+
```
|
|
4036
|
+
|
|
3742
4037
|
#### `baker canvas run <file.json> [flags]`
|
|
3743
4038
|
|
|
3744
4039
|
Validate, then execute the graph. Blocks until done. Logs one line per node. Returns `{ ok, run_id, outputs_dir, output, stats }` on stdout.
|
|
@@ -3750,25 +4045,30 @@ Validate, then execute the graph. Blocks until done. Logs one line per node. Ret
|
|
|
3750
4045
|
| `--run-id <id>` | auto ULID | Override the generated run id. |
|
|
3751
4046
|
| `--cache-policy <policy>` | `read_write` | `read_write`, `bypass`, or `read_only`. |
|
|
3752
4047
|
| `--concurrency <n>` | `5` (or `BAKER_CANVAS_CONCURRENCY`) | Max nodes executing at once within a layer. |
|
|
4048
|
+
| `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Company-scoped remote cache + durable asset persistence. |
|
|
4049
|
+
| `--max-credits <n>` | uncapped (or `BAKER_CANVAS_MAX_CREDITS`) | Credit ceiling: aborts before billing when the estimate exceeds it, and at layer boundaries once actual spend does. Completed nodes stay cached, so retrying with a higher cap loses nothing. |
|
|
4050
|
+
| `--no-record` | records | Skip posting the durable run-history record (and its live progress). |
|
|
4051
|
+
|
|
4052
|
+
**Run history streams live.** The run posts its plan (every node + its dependency edges) the moment validation passes, then re-posts a progress snapshot as each node starts and settles — the dashboard's creative workflow graph shows nodes flipping pending → running → done in real time, with each node's outputs attached as they land. A failed run keeps its per-node trail (what completed, what died). All best-effort: an unreachable backend never changes the run's outcome.
|
|
3753
4053
|
|
|
3754
4054
|
**Failures don't abandon sibling work.** Nodes in a layer run under the concurrency cap and every one **settles** — a failed clip no longer kills its in-flight siblings, whose finished results still land in the content-addressed cache. One failure re-throws as-is; several are reported together (each failed node named). Re-running `baker canvas run` resumes from the cache and re-executes **only** the failed nodes and their descendants — never hand-orchestrate per-node renders. Long `video_generate` clips execute as **backend jobs** (the CLI polls; a CDN/proxy timeout can no longer kill a generation mid-flight).
|
|
3755
4055
|
|
|
3756
4056
|
#### `baker canvas scaffold-video <video> [flags]`
|
|
3757
4057
|
|
|
3758
|
-
Turn a reference video into a **runnable, self-validated reproduction canvas** in one command — the video counterpart of `scaffold-static-ad`. It runs **billed passes** up front:
|
|
4058
|
+
Turn a reference video into a **runnable, self-validated reproduction canvas** in one command — the video counterpart of `scaffold-static-ad`. The `<video>` positional is a **local path OR an http(s) URL** (a `baker winning-ads` `media_url`, a library URL, any reel link) — a URL is downloaded for you (no manual `curl` first), so pass `--slug`/`--out` with it to give the canvas a home. It runs **billed passes** up front:
|
|
3759
4059
|
|
|
3760
|
-
1. **`video_deconstruct`** (`~google/gemini-pro-latest`, full mode) — reverse-engineers the video into a scene-by-scene blueprint + word-level transcript, written next to the canvas as **`prompt.json
|
|
4060
|
+
1. **`video_deconstruct`** (`~google/gemini-pro-latest`, full mode) — reverse-engineers the video into a scene-by-scene blueprint + word-level transcript, written next to the canvas as **`prompt.json`** (the human-editable source of truth). Each scene's `start_frame_prompt`/`end_frame_prompt` are inlined into the frame nodes (see below); the shared **global style reference** every frame reads via `target_blueprint` is a **slim projection** written alongside as **`prompt.style.json`** (`global` cast/palette/brand + `reference_elements` only, no per-scene array). A 33-scene blueprint is ~200 KB — inlining it into every one of a dozen frame prompts was pure waste and let a frame blend in another scene's content; the slim is ~5 KB. `prompt.style.json` is a **derived file**: `baker canvas validate` and `run` regenerate it from `prompt.json` whenever they diverge (reported as `style_projection` in validate's output), so editing `prompt.json` is all it takes — global cast/palette/brand edits reach every frame on the next validate/run, and the affected frames re-bill. Never edit the derived file by hand. Per-scene `scene_setting`/`ambient` deltas are baked into that scene's own frame prompts as a `SCENE STYLE` block.
|
|
3761
4061
|
2. **recurring-element selection** (`~google/gemini-flash-latest`) — picks only the **recurring, identity-critical** elements (each `global.cast` person, a recurring animal, a showcased product, the brand logo) and the scene indices each appears in. One real reference image grounds each element across **every** frame it appears in, so the same actor stays consistent the whole video. This selection runs as a **second pass over a slimmed blueprint** (cast/branding + each scene's frame prompts only) — a long ad's full blueprint can exceed the engine's inline-prompt limit, so the heavy per-scene detail (dialogue, overlays, transcript) the selector never reads is dropped before the prompt.
|
|
3762
4062
|
|
|
3763
4063
|
Before the deconstruct it runs a **local shot-cut pass** on the source file with **[PySceneDetect](https://www.scenedetect.com)** (`scenedetect` CLI, `detect-content` — the battle-tested HSV content detector, installed in the canvas sandbox) and passes the cut timestamps as `video_deconstruct`'s `shot_cuts`. The deconstruct snaps its scene boundaries onto those real cuts and **splits any scene that spans one**, so a scene's frames can never straddle a hard cut (the failure where a scene's start frame was the couch and its end frame the b-roll). Two knobs tuned for fast social ads: the content **threshold defaults to 18** (PySceneDetect's own default of 27 misses soft reframes) and the **minimum scene length is dropped to 0.25s** (its default ~0.6s merges away rapid montage flashes) — so super-fast cuts survive and become cheap still-holds downstream. The threshold is **adaptive**: if the first pass looks like a continuous shot shredded into many close micro-cuts (a talking-head selfie's natural motion), it re-runs at PySceneDetect's own default of 27 and **merges the two passes** — the high-threshold set is the base, and the low pass's *isolated* extras (real soft blur-morph transitions that vanish at 27) are added back while clustered extras (motion shred) stay dropped. Pinning **`--shot-threshold N`** disables the re-check (lower = more cuts). The backend snap window is likewise **adaptive** (up to 1s onto an unambiguous nearest cut, shrinking around dense cut pairs so a boundary never jumps past the wrong cut), any scene spanning an interior cut is split, and the residual-sliver coalesce is **cut-aware**: a drift sliver folds backward across its non-cut edge and never re-merges across a real cut. If `scenedetect` is unavailable it warns loudly and degrades to LLM-only boundaries.
|
|
3764
4064
|
|
|
3765
4065
|
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`.
|
|
3766
4066
|
|
|
3767
|
-
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 self-contained `params.prompt`** — edit a frame node to change only that frame; `prompt.json` wired as the **
|
|
4067
|
+
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 self-contained `params.prompt`** — edit a frame node to change only that frame; 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) whenever identity is safely locked — i.e. a frame with no person/animal, OR every cast member present is **sheet-backed** (a multi-view turnaround owns identity, so the anchor can reproduce the source's framing without dictating the face). Since every base element is now sheet-backed by default, cast frames keep their framing anchor too — this is what reproduces the source's composition (a side-profile stays a side-profile, the camera angle holds scene to scene) instead of drifting to a fresh guess. The anchor's legend forbids taking identity/text/palette from it. It is dropped only when a cast member rests on a weak lone-snapshot reference (e.g. a `same_as` second-look slot), where the original frame could re-leak the source actor. 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.
|
|
3768
4068
|
|
|
3769
4069
|
**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 `prompt.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
|
|
3770
4070
|
|
|
3771
|
-
**Typed region kinds & real screen surfaces.** Each composition region now carries a `kind` — `camera` (filmed footage, re-generated), `screen_capture` (app/site/document screen recording), `static_graphic` (designed text/graphic panel), or `generated` (3D/motion graphics) — plus an optional `nested` list for video-in-video (a Loom-style camera bubble inside a screen share). `kind` is authoritative for routing (prose keywords remain the fallback for older blueprints): `screen_capture`/`static_graphic` regions are **never generated by the video model** — the scene renders as a clean background plate (its clip prompt is scrubbed of all screen narration and forbids rendering UI) and the real surface is composited on the overlay layer. The route is decided **once per persistent layout run** (consecutive scenes sharing one composition signature), so a layout that runs unbroken across many scenes can't flip between pipelines on wording differences. A persistent surface seeds **ONE grouped stub** in `video-overlay-composition/index.html` spanning its whole window, with a per-scene **state timeline** — build one continuous screen recording/mockup, not one screenshot per scene. A `screen_capture` region also carries `surface_id`: a source video routinely **splices two unrelated screen recordings** under one persistent layout (a live app-processing capture, then an unrelated pre-made demo note) — the deconstruct assigns a stable id while the SAME recording continues and a new one when the on-screen content genuinely changes, so the run splits into **separate stubs** at the splice instead of asking for one screenshot that can't cover both. `baker canvas validate` additionally warns (`VIDEO_UI_IN_PROMPT`) if any clip prompt still narrates a screen surface, and (`VIDEO_BRANDMARK_IN_PROMPT`) if a generate prompt asks the model to paint a brand logo/wordmark (generation garbles marks; source the real one with `baker images logo` and composite it on the overlay layer).
|
|
4071
|
+
**Typed region kinds & real screen surfaces.** Each composition region now carries a `kind` — `camera` (filmed footage, re-generated), `screen_capture` (app/site/document screen recording), `static_graphic` (designed text/graphic panel), or `generated` (3D/motion graphics) — plus an optional `nested` list for video-in-video (a Loom-style camera bubble inside a screen share). `kind` is authoritative for routing (prose keywords remain the fallback for older blueprints): `screen_capture`/`static_graphic` regions are **never generated by the video model** — the scene renders as a clean background plate (its clip prompt is scrubbed of all screen narration and forbids rendering UI) and the real surface is composited on the overlay layer. The route is decided **once per persistent layout run** (consecutive scenes sharing one composition signature), so a layout that runs unbroken across many scenes can't flip between pipelines on wording differences. A persistent surface seeds **ONE grouped stub** in `video-overlay-composition/index.html` spanning its whole window, with a per-scene **state timeline** — build one continuous screen recording/mockup, not one screenshot per scene. A `screen_capture` region also carries `surface_id`: a source video routinely **splices two unrelated screen recordings** under one persistent layout (a live app-processing capture, then an unrelated pre-made demo note) — the deconstruct assigns a stable id while the SAME recording continues and a new one when the on-screen content genuinely changes, so the run splits into **separate stubs** at the splice instead of asking for one screenshot that can't cover both. Full-frame screen scenes reuse the same `surface_id`: consecutive full-frame UI beats of one screen (e.g. a 3-scene import flow) share **ONE `s<i>_screen_ref` ingest** — the operator supplies that screenshot once instead of dropping the same capture into a dozen identical `[TODO]`s (distinct surfaces stay distinct). `baker canvas validate` additionally warns (`VIDEO_UI_IN_PROMPT`) if any clip prompt still narrates a screen surface, and (`VIDEO_BRANDMARK_IN_PROMPT`) if a generate prompt asks the model to paint a brand logo/wordmark (generation garbles marks; source the real one with `baker images logo` and composite it on the overlay layer).
|
|
3772
4072
|
|
|
3773
4073
|
**Designed graphics are rebuilt, not generated.** A `static_graphic` surface (a newspaper-collage panel, a meme card, a marketing composition) seeds a **GRAPHIC PANEL** stub — rebuild it as brand HTML or drop the design asset; it never gets the "screenshot the live page" instruction (there is no live page). A **full-frame** designed-graphic scene (the deconstruct emits one full-frame `static_graphic` region for meme/collage/motion-graphic beats) routes to a real design plate the same way screens do — no `image_generate`/`video_generate` — and dialogue over an all-graphic scene is voiceover by definition (nobody is on screen to lip-sync). A region typed `generated` whose own prose reads like a UI/designed panel is treated as a surface candidate too (the frame-grounded continuity checker delivers the verdict and corrects the kind), so one mistyped kind can't re-open the Seedance-paints-UI hole. Floating FX elements (hearts, sparkles, badges) ride the overlay layer: their narration is **scrubbed from clip briefs** and a categorical no-decorations directive is added, so the model can't bake a second, uneditable copy under the real composited one.
|
|
3774
4074
|
|
|
@@ -3780,29 +4080,31 @@ It then scaffolds the full pipeline like an **editing timeline**: each clip gets
|
|
|
3780
4080
|
|
|
3781
4081
|
**Montage flashes held as stills — unless the picture really moves.** A rapid-cut beat shorter than ~2s with no spoken line is a **flash** — Seedance's shortest clip is 4s, so generating one (then trimming away most of it) burns credits for motion no viewer perceives. The scaffold instead **holds one keyframe as a still** for the scene length (a cheap ffmpeg loop, no billed `video_generate`), same look at a fraction of the cost. The deconstruct now stamps each scene's **`motion_level`** (`static` / `subtle` / `dynamic`): a **dynamic** flash (pouring chocolate, hands working, walking) keeps a **real trimmed clip** — freezing a moving montage turns it into a slideshow — while genuinely static beats (a logo card, a pinned photo, a product still) keep the cheap hold. Talking/ambient beats always keep a real clip (they need motion + native audio). The deconstruct also stamps each dialogue line's **`on_camera`** flag — a voice playing over b-roll, a graphic, or a mere *photo* of the speaker stays voiceover, so the scaffold never lip-syncs a scene with no speaking face (the polaroid close-up failure).
|
|
3782
4082
|
|
|
3783
|
-
**
|
|
4083
|
+
**One clip per shot — separated at complete breaks.** A video is a sequence of clear **shots** with **complete breaks** (hard cuts) between them, and that is what the scaffold separates by: **two adjacent presenter shots at a hard cut become TWO clips**, never glued into one invented take just because the speech runs continuously across the cut. Each presenter shot is one Seedance clip (`s<anchor>_clip`, native lip-sync + audio) re-voiced to the brand voice. What is **NOT** split: a **voiceover** narration stays ONE ElevenLabs `tts` read across the b-roll it plays over, and a **b-roll cutaway** between two on-camera moments leaves the presenter shot continuous — the shown scenes aren't adjacent (the insert sits between them), so the clip covers both on-camera windows (sliced as `s<i>_seg`, an ffmpeg `-ss`/`-t` cut — video+audio from the *same* clip so lip-sync holds) while the cutaway plays its own silent clip over the continuing voice. "Shown" is decided by the **presenter element's per-scene presence**, not just who's speaking — a scene where a cast member narrates over b-roll (their element absent) is a cutaway, so the talking head never appears where the original cut away. A single shot longer than the **gateway-safe ~10s clip ceiling** (Seedance's *API* max is 15s, but the gateway often times out — **HTTP 524** — past ~10s) **splits into contiguous takes joined by a shared boundary frame**; the spine then **seam-dedups** that duplicated frame so the concat doesn't freeze on it (`--seam-dedup head|tail|off`, default `head` = drop the second clip's first frame), and clone-pads one frame back so the drop never shortens the picture against the absolute-timed audio. Timbre stays consistent across all the separate shot clips because every clip's native audio is re-voiced in **one merged per-speaker pass** (not per clip). A b-roll cutaway *inside* a phrase lands at an **approximate** time (Seedance exposes no word timing) — nudge the scene boundary if it's off its beat.
|
|
3784
4084
|
|
|
3785
4085
|
**A starting point, not a locked render.** The canvas mirrors the reference's structure to give you a faithful scaffold, but `metadata.todo.full_flexibility` makes explicit that the agent has **full editing freedom**: add / delete / reorder / split / merge scenes, re-prompt any frame or motion brief, change a scene's layout (full-frame ↔ composite), or rewrite any line — the content-addressed cache re-bills only what changes, and `baker canvas validate` re-checks timing/lip-sync after any edit.
|
|
3786
4086
|
|
|
3787
4087
|
**Sequenced audio.** Dialogue is a back-and-forth on one absolute timeline, so each **contiguous same-speaker turn** becomes its own `tts` placed at its real `start_s` — turns alternate and never stack (the earlier design concatenated each speaker's whole monologue at their earliest timestamp, so two voices played in parallel for the entire video). Each speaker is locked to one shared `voice_select` voice; a `sound_effect` per SFX and a `music` bed (conditioned on the **ad's own script + emotional arc** so the bed supports the message, styled after the AudD-identified track when available, ducked under the voices, and started at the reference's `music.starts_at_s` rather than always at 0) round out the mix (`audio_timeline`). The final mux normalizes the soundtrack to **−14 LUFS (stereo)** so the output plays loud in every player — the raw mix is quiet mono, which reads as "no sound."
|
|
3788
4088
|
|
|
3789
|
-
**Native talking heads + one voice per person (no post-hoc lip-sync).** Seedance 2.0 generates lip-synced speech **natively** — a presenter phrase puts the full phrase in the clip's prompt with `generate_audio`, so lips and voice are generated together (no `video_lipsync`/veed). Each presenter phrase's audio is extracted and re-voiced through a **
|
|
4089
|
+
**Native talking heads + one voice per person (no post-hoc lip-sync).** Seedance 2.0 generates lip-synced speech **natively** — a presenter phrase puts the full phrase in the clip's prompt with `generate_audio`, so lips and voice are generated together (no `video_lipsync`/veed). Each presenter phrase's audio is extracted (the spoken window only) and the extracts are merged **per speaker** onto one timeline, then re-voiced through a **single** `audio_voice_convert` (`<voice>_conv`, ElevenLabs Voice Changer) to the brand voice — one STS pass over the whole track instead of a convert node per clip, so it's fewer nodes, fewer calls, and a more consistent brand timbre (composite scenes already share this path); timing is preserved so the lips stay matched. There is **ONE voice per person**: a single `voice_select` is reused for all that person's phrases, and the deconstruct's `voiceover` label folds into the sole on-camera presenter (so on-camera and off-camera narration are the same voice, not two). A scene with **two speakers both on screen** can't be one clip — both lines become `tts` over a plain scene clip. But a scene with **one on-camera speaker trading lines with an OFF-camera voice** (an interviewer, a heard-but-not-shown assistant) keeps the on-camera speaker **native** (lip-synced) and reads the off-camera line as `tts` — "on screen" is decided by the speaker's element presence, so a heard-but-unshown voice no longer drops the whole scene to a silent clip. Every `tts` node is stamped with the spoken track's **`language_code`** when the blueprint states a language (cast localization note / voiceover persona / voice description), so numbers and units are read in the target tongue instead of ElevenLabs' English default (the "6900 read in English" bug). For **NATIVE (Seedance) lines** — which carry no language tag — the scaffold additionally **spells numerals into target-language words** across every part of the clip prompt Seedance can vocalize (the spoken line, the scene summary/action/motion, the transcript), so a French "6930 ?" becomes "six mille neuf cent trente ?" and is never read as English digits. Spelling covers **every language the blueprint can resolve** (fr, es, en, de, it, pt, nl, pl, ar, ja, ko, hi — via `n2words`); a language outside that set leaves digits (the `tts` path still localizes them via `language_code`).
|
|
3790
4090
|
|
|
3791
4091
|
**Same-shot lip-sync caution.** A single held shot can carry only ONE lip-synced clip (voiceover turns must not overlap, and Seedance generates one clip per shot), so when the on-camera speaker has further turns in that shot (a rapid "3000? … 4000?" with an off-camera "Plus" between), the first turn is native and the rest play as `tts` over the same clip — where the mouth no longer matches those words. This is inherent to reproducing sparse same-shot dialogue, not a wiring fault; the scaffold lists the affected scenes/lines in **`metadata.video.lip_sync_caution`** (advisory, never gated) so you can cut away to b-roll over those lines or rely on the burned-in captions that already show them.
|
|
3792
4092
|
|
|
3793
4093
|
**Timing-faithful clip + extract (no overlap).** Each phrase clip is generated to its **coverage window** (the deconstruct's real scene/line timing, capped at the gateway-safe ~10s ceiling) and its converted voice is extracted to the **spoken window** (pause to pause) — *not* padded to a word-count estimate. Padding past the window was what ran the voice the clip's whole length and overlapped the next phrase; trusting the deconstruct's timing keeps consecutive phrases back-to-back and lets Seedance pace the quoted text to fit. `metadata.video.talking_scenes` records each phrase's `scene_s` vs `est_speech_s`; on top of that the scaffold flags any scene whose estimated speech overruns its window by more than ~1.3× as **`metadata.todo.overstuffed_scenes`** (also in the stdout checklist) — a loud advisory to shorten the copy or lengthen the scene before rendering, since an over-stuffed line pushes the picture off the audio timeline. It similarly flags **`oversize_scenes`** — a single scene whose own footage exceeds the gateway-safe ~10s clip ceiling (a b-roll shot or one-shot monologue). The phrase splitter only breaks at scene boundaries, so it can't shrink a single over-long scene; its clip would 524 at the gateway, so the advisory tells you to split that scene into two before rendering.
|
|
3794
4094
|
|
|
3795
|
-
**Timeline-accurate picture.** Seedance can't render under 4s, so each clip is generated at the smallest allowed duration ≥ the scene length and then **trimmed back to the exact scene duration** before concat.
|
|
4095
|
+
**Timeline-accurate picture — ONE clock.** Seedance can't render under 4s, so each clip is generated at the smallest allowed duration ≥ the scene length and then **trimmed back to the exact scene duration** before concat. The spine is the ONE clock: every voice, SFX, music, and overlay placement is mapped onto where each scene's picture actually sits in the butted concat (not the reference video's own timestamps, which can carry dead air the picture doesn't reproduce), the audio mix's `total_ms` is pinned to the spine length, and every concat input is normalized to one raster clock (`yuv420p`, 30fps, square pixels, shared timebase) so a generated clip's unadvertised frame rate can never stretch the picture off the audio. The contract is stamped as `metadata.video.timeline` and `baker canvas validate` re-proves it from the live node params after any hand edit. Frames are also prompted as **clean text-free plates** (no baked captions/lower-thirds/tickers/logos-as-text) so the overlay layer is the single source of on-screen text.
|
|
3796
4096
|
|
|
3797
4097
|
**Scene transitions.** When the deconstruct flags a boundary as `fade`/`whip`/`zoom`/`dissolve`/`swipe` (`scene.transition_out`), the spine reproduces it as an ffmpeg **`xfade`** instead of a hard cut; plain `cut`/`match_cut` stay hard cuts. The overlap is consumed from **extra generated footage** (each transitioning clip is trimmed to `scene_s + transition` and the xfade `offset` lands on the cumulative scene start), so the total length still equals the sum of the scene lengths — the picture stays exactly on the audio timeline.
|
|
3798
4098
|
|
|
3799
4099
|
**One person, multiple looks.** If a single individual plays multiple personas/wardrobes (e.g. a creator as a skeptic then a believer), the selection pass emits **one element per look** linked via `same_as` — each outfit gets its own reference slot, but the frame legend and the `el_*` TODO tell the generator they are the **same person** (keep the face identical, change only wardrobe). The always-on `metadata.todo.completeness_check` reminds you to split a person collapsed into a single slot.
|
|
3800
4100
|
|
|
3801
|
-
**Overlays are agent-painted HTML, not props.** The clips are concatenated, then the `video-overlay` composition (copied next to the canvas) composites the overlay layer. The scaffold **bakes the reference's overlays into that composition's `index.html` as real, editable HTML** (each overlay is a plain element with its text, a `.pos-*` position class, and `data-start`/`data-dur` timing); a tiny generic runtime only shows/hides each element at its timestamp (with an optional `data-anim` entrance). It makes **no styling decisions** — bars, tickers, colors, fonts, and a real logo `<img>` you drop into the dir all live in the HTML/CSS you edit. Floating elements (logo bugs) are seeded as commented `<img>` stubs so an un-edited render stays clean. Drop `brand-bold.otf`/`brand-regular.otf` for on-brand type.
|
|
4101
|
+
**Overlays are agent-painted HTML, not props — and captions render in the SAME pass.** The clips are concatenated, then the `video-overlay` composition (copied next to the canvas) composites the overlay layer; when the ad has speech, the word-synced caption track renders inside this same composition (its optional `transcript` input) — one headless render and one less lossy encode than chaining a second captions pass. A captions-only ad (no overlay layer) keeps the standalone caption composition. The scaffold **bakes the reference's overlays into that composition's `index.html` as real, editable HTML** (each overlay is a plain element with its text, a `.pos-*` position class, and `data-start`/`data-dur` timing); a tiny generic runtime only shows/hides each element at its timestamp (with an optional `data-anim` entrance). It makes **no styling decisions** — bars, tickers, colors, fonts, and a real logo `<img>` you drop into the dir all live in the HTML/CSS you edit. Floating elements (logo bugs) are seeded as commented `<img>` stubs so an un-edited render stays clean. Drop `brand-bold.otf`/`brand-regular.otf` for on-brand type.
|
|
3802
4102
|
|
|
3803
4103
|
**Re-craft the script — the hook is the #1 decision.** A reproduction is *inspiration* from a proven ad, not a clone: its structure (hook → body → CTA) carries the persuasion, and the hook is *targeting*, so a competitor's hook often does **not** transfer. `metadata.todo.script_recraft` tags each scene with its `narrative_role` (from the deconstruct, else inferred) and carries the original line **flagged** so it is never shipped as-is — and the per-scene `recraft` instruction is **role-aware**: the **hook** scene's entry carries the diagnose → decide (keep/adapt/rebuild) → criteria (statement not question, benefit by ~2s, first frame legible **sound-off** in ~1s, no bait-and-switch) inline and routes to the skill's `references/hook-craft.md`. A dedicated top-level **`metadata.todo.hook`** key foregrounds it as the highest-leverage beat, mapped onto scene-0's artifacts (`s0_start` first frame, scene-0 overlay text, `s0_clip` line, micro-hook, hook-ramp).
|
|
3804
4104
|
|
|
3805
|
-
The
|
|
4105
|
+
**The inspiration video is preserved.** Like `scaffold-static-ad` keeps its reference image, the video command now auto-writes a **`_definition.md`** (so the creative joins the `creatives` collection) recording the source it was built from: `sourceKind: video`, `sourceAdvertiser` (the brand the deconstruct identified, or `--advertiser`), `platform` (`--platform`, default `meta`), and **`sourceReferenceUrl`** — the **durable, content-addressed R2 URL** the deconstruct already uploaded the source to (`prompt.json`'s `source.url`), which the dashboard's Inspiration card plays inline. Unlike the static flow it does **not** commit the video into `references/`: a reference clip can be up to 2 GiB and the video canvas never re-ingests the source at run time (it uses the extracted frame URLs), so a git copy would be pure bloat — the durable R2 URL is the reference. The `_definition.md` is preserved on re-scaffold, and the same `sourceReferenceUrl` is synced to the backend so the creative shows "built from this ad."
|
|
4106
|
+
|
|
4107
|
+
The emitted canvas is validated (`validateCanvasDeep`) before it's written, so it always runs. It also carries a **`metadata.video`** timing plan that `baker canvas validate` proves **statically, before any billed render**: no two voiceover turns overlap, the audio length ≈ the video length, every single-on-camera-speaker scene is a native talking head (its clip carries `generate_audio` and is wired to an `audio_voice_convert` node), **no re-crafted line physically overruns its clip** (`VIDEO_SPEECH_OVERRUN`) **or its extract window** (`VIDEO_SPEECH_EXCEEDS_EXTRACT` — the spoken window is the real audio budget; a 3s line in a 1.2s window passes the clip check and then gets cut on the spine), and **every clip agrees on one aspect ratio** (`VIDEO_ASPECT_MISMATCH`). The one-clock contract is proven from the LIVE graph after any hand edit: `VIDEO_TIMELINE_TOTAL_MISMATCH` (picture vs pinned audio length — what `-shortest` would silently truncate), `VIDEO_NATIVE_SEG_OVERLAP` (two same-speaker voice windows playing at once — echo), plus advisories `VIDEO_SPINE_UNNORMALIZED` (bare concat inputs), `VIDEO_ODD_DIMENSIONS` (libx264-fatal odd sizes), `VIDEO_REGION_DROPPED` (billed region clips a composite never consumes), `VIDEO_OVERLAY_OUT_OF_BOUNDS` (an overlay window past the video end), and `VIDEO_PROMPT_PROFILE_MISSING` (a video model with no clip-prompt profile — prompts are authored per model family now: Seedance's [brackets] delivery cues vs Veo's prose + no-subtitles rule). When a **photoreal on-camera cast** generates on **Seedance**, the checklist carries a **`content_policy_risk`** note: ByteDance's real-person-likeness filter can reject a photoreal AI face with a **non-retryable 422** (`content_policy_blocked`) that **no prompt reframe clears** — the escapes are regenerating on Veo (`--video-model google/veo-3.1-fast`) or a less-photoreal frame. Surfaced before the billed run so a face-heavy ad isn't discovered broken mid-render. The full editable checklist is embedded as **`metadata.todo`** (with a step-by-step guide in `metadata.description`). The checklist also carries a **`prompt_discipline`** note steering the edit toward the six-decision prompt structure (Route / Spec / Beats / Copy / Technique / Negatives) and on-brand **motion** — easing, transition (cut vs fade), pacing, and accent pulled from `BRAND.md` § Brand in Motion so the overlays move like the client's brand, not the reference's. stdout returns `{ ok, canvas_path, prompt_path, models, stats, checklist }`.
|
|
3806
4108
|
|
|
3807
4109
|
```bash
|
|
3808
4110
|
baker canvas scaffold-video ./reference-ad.mp4 --focus "competitor UGC ad for <brand>"
|
|
@@ -3815,20 +4117,32 @@ baker canvas run ./reference-ad.video.canvas.json
|
|
|
3815
4117
|
| Flag | Default | Effect |
|
|
3816
4118
|
|---|---|---|
|
|
3817
4119
|
| `--out <path>` | `<video-dir>/<name>.video.canvas.json` | Where to write the canvas (composition is copied alongside). |
|
|
4120
|
+
| `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. |
|
|
3818
4121
|
| `--frames <mode>` | `generate` | `generate` emits ONE recast keyframe per scene (the original frame is dropped so the dropped `el_*` assets drive identity); `reuse` wires the real extracted first+last frames straight into the clips (faithful, cheaper, no recast). |
|
|
3819
4122
|
| `--ambient` | off | Give silent **b-roll** scenes native diegetic ambient (Seedance `generate_audio`), mixed deep under the music bed. Talking scenes already carry voice; check levels don't muddy the mix before keeping it. |
|
|
4123
|
+
| `--seam-dedup <mode>` | `head` | How to dedup the boundary frame two clips SHARE when a long shot is split for length (the second clip's first frame IS the first clip's last frame, so a plain concat freezes on it for a frame). `head` drops the second clip's first frame, `tail` drops the first clip's last frame, `off` keeps both. Only touches shared-frame continuation joins — a hard cut between two shots shares no frame. |
|
|
3820
4124
|
| `--max-scenes <n>` | all source scenes | **Cost lever that reduces fidelity** — caps the deconstruct, MERGING away every scene beyond the cap (fewer cuts, lost beats). Prints a warning when set; omit it to reproduce every scene. |
|
|
3821
4125
|
| `--language <code>` | auto | Transcript/dialogue language hint (e.g. `fr`, `en`). |
|
|
3822
4126
|
| `--focus <text>` | — | Known provenance/emphasis to ground the deconstruct. |
|
|
3823
4127
|
| `--deconstruct-model <id>` | `~google/gemini-pro-latest` | Override the `video_deconstruct` model. |
|
|
3824
4128
|
| `--select-model <id>` | `~google/gemini-flash-latest` | Override the element-selection `text_generate` model. |
|
|
3825
4129
|
| `--image-model <id>` | `openai/gpt-5.4-image-2` | Override the per-frame `image_generate` model (defaults to the strongest, matching `scaffold-static-ad`). |
|
|
3826
|
-
| `--video-model <id>` | `bytedance/seedance-2.0`
|
|
4130
|
+
| `--video-model <id>` | scored router | Override the `video_generate` model **and skip the router**. Curated roster: `bytedance/seedance-2.0` (workhorse), `google/veo-3.1` (cine ceiling + real-face), `google/veo-3.1-fast` (cheap Veo), `kwaivgi/kling-v3.0-pro` (motion-transfer). |
|
|
4131
|
+
| `--real-face` | off | Brief needs a real human likeness → the router picks Veo (dodges the ByteDance real-person 422). |
|
|
4132
|
+
| `--motion-transfer` | off | Motion driven from a reference / hyper-dynamic → the router picks Kling. |
|
|
4133
|
+
| `--identity` | off | Same character/product across clips → the router picks the Seedance workhorse. |
|
|
4134
|
+
| `--budget <tier>` | `standard` | Router cost posture: `economy` (cheap Veo-fast tier) / `standard` / `premium`. |
|
|
3827
4135
|
| `--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. |
|
|
3828
4136
|
|
|
4137
|
+
**Scored model router.** When `--video-model` is omitted, a small weighted scorer picks from the curated
|
|
4138
|
+
roster and prints the choice as `models.video_route_reason` in the run report (mirroring Higgsfield's
|
|
4139
|
+
`models_explore`). Real-face → Veo, motion-transfer → Kling, else the Seedance workhorse; `--budget economy`
|
|
4140
|
+
shifts a Veo pick to the fast tier. The scaffolder also bakes **per-intent param recipes** so the hero/reveal
|
|
4141
|
+
beat claims the 1080p cine ceiling automatically and a Kling hook beat maxes prompt adherence.
|
|
4142
|
+
|
|
3829
4143
|
Each scene is captured in a **shoot mode** — `ugc_selfie` (talking heads, the default look), `ugc_broll`, `studio_product` (pack shot), `lifestyle_cinematic`, or `screen_ui`. The scaffold derives one per scene (UGC by default; the cinematic and screen lanes are opt-in) and bakes its capture block into the frame and a camera default into the clip; override per scene with a `shoot_mode` field in `prompt.json`. Capture aesthetic + depth-of-field follow the mode (UGC stays flat; studio/lifestyle allow shallow DoF). Clips also carry **diegetic native audio** — the scene's own ambience described in the Seedance prompt, never music (the music bed is a separate, ducked track); set a scene's `ambient` field to steer it.
|
|
3830
4144
|
|
|
3831
|
-
**Automatic by default (no flags).** Every recast **base element — person, pet, product, AND location/set** — is fused into ONE rich multi-view sheet (`image_reference_sheet`, one subject per sheet, **4K**, up to 8 cells) that every frame it appears in grounds on, so the same face/pet/pack/room is rendered from a multi-angle canvas instead of a lone flat snapshot (a one-scene hero element is sheeted too). Each sheet pairs a **full turnaround** (angles, for proportions/wardrobe/layout) with tight **close-ups** so the generator is prepared for ANY framing a scene needs: a **person** gets body cells + face close-ups (front/¾/profile) and a mid-sentence speaking expression (identity pinned, natural skin — no airbrushing); an **animal** gets a body turnaround + head close-ups + an eyes/face macro; a **product** gets a turnaround + label and material detail macros; a **location/set** gets several camera angles of the same room + a key-surface detail. Generated clips are pinned to **1080p** (see `--resolution`) so the video keeps the keyframe's sharpness, and each cast frame keeps the source frame as a **composition anchor** (identity stays on the sheet) so the original framing/camera is reproduced, not re-guessed. An **app/website/chat screen** is never sent to the video model — the scaffold drops the scene to a clean talking-head and seeds a phone-mockup PIP stub to fill with a real `baker images screenshot` or brand HTML block (Seedance garbles UI and a split leaves a seam). The **music bed is instrumental** (the script is never fed to the music model — it would sing over the voice), enters only after the hook, and is **sidechain-ducked** under the voice. **Word-synced TikTok captions** are wired whenever the ad has speech — and they are **transcribed from the rendered audio** (a `video_transcribe` of the
|
|
4145
|
+
**Automatic by default (no flags).** Every recast **base element — person, pet, product, AND location/set** — is fused into ONE rich multi-view sheet (`image_reference_sheet`, one subject per sheet, **4K**, up to 8 cells) that every frame it appears in grounds on, so the same face/pet/pack/room is rendered from a multi-angle canvas instead of a lone flat snapshot (a one-scene hero element is sheeted too). Each sheet pairs a **full turnaround** (angles, for proportions/wardrobe/layout) with tight **close-ups** so the generator is prepared for ANY framing a scene needs: a **person** gets body cells + face close-ups (front/¾/profile) and a mid-sentence speaking expression (identity pinned, natural skin — no airbrushing); an **animal** gets a body turnaround + head close-ups + an eyes/face macro; a **product** gets a turnaround + label and material detail macros; a **location/set** gets several camera angles of the same room + a key-surface detail. Generated clips are pinned to **1080p** (see `--resolution`) so the video keeps the keyframe's sharpness, and each cast frame keeps the source frame as a **composition anchor** (identity stays on the sheet) so the original framing/camera is reproduced, not re-guessed. An **app/website/chat screen** is never sent to the video model — the scaffold drops the scene to a clean talking-head and seeds a phone-mockup PIP stub to fill with a real `baker images screenshot` or brand HTML block (Seedance garbles UI and a split leaves a seam). The **music bed is instrumental** (the script is never fed to the music model — it would sing over the voice), enters only after the hook, and is **sidechain-ducked** under the voice. **Word-synced TikTok captions** are wired whenever the ad has speech — and they are **transcribed from the rendered audio's CLEAN VOICE BUS** (a `video_transcribe` of the vo tracks alone — no music bed, no SFX, which smear Whisper's word timings and hallucinate tokens), never the deconstruct's original transcript. This is a correctness boundary: wiring the source transcript would burn the **competitor's** words (their brand name, a claim we can't make) over the ad once the script is re-authored, whereas transcribing the generated audio can only ever show what is actually spoken, so the captions always track the re-written lines. Seeded overlays are pushed **off the subject's face** (dead-center → bottom band).
|
|
3832
4146
|
|
|
3833
4147
|
The two scaffold passes are billed (the full `video_deconstruct` is the heavy one); **running** the result then generates many image/video/audio assets and is not free. Defaults to vertical 1080×1920 overlays — copy + edit the composition for other aspect ratios. For on-brand overlay type, drop `brand-bold.otf`/`brand-regular.otf` into the copied `video-overlay-composition/` dir (wired via `@font-face`, with a system fallback). Richer transcription (punctuated words + paragraphs) is available via the deconstruct's `transcriber: "deepgram"` param when `DEEPGRAM_API_KEY` is set.
|
|
3834
4148
|
|
|
@@ -3839,10 +4153,12 @@ The two scaffold passes are billed (the full `video_deconstruct` is the heavy on
|
|
|
3839
4153
|
Turn a source/inspiration image into a **runnable, self-validated static-ad canvas** — the static counterpart of `scaffold-video`. Like the video scaffold, this runs **billed Gemini passes** up front:
|
|
3840
4154
|
|
|
3841
4155
|
1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image into a blueprint JSON, written next to the canvas as **`prompt.json`**. This is the editable "prompt": you rewrite it by hand into the ad you want (palette, copy, claims, subjects). It feeds the generator directly — there is **no automatic brand-transform step**. The blueprint also names the **`winning_mechanisms`** — the special sauce that makes the ad a candidate winner, each tagged `kind` (verbal: rhyme/pun/rhythm; visual: unexpected crop, visual gag, juxtaposition, pattern interrupt, before/after; structural: hook order/reveal) with a `device` and `why_it_works` — so your rewrite rebuilds the mechanism that makes the ad win instead of adapting only the surface and losing it.
|
|
3842
|
-
2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. Background extras are dropped. Each is stamped back onto its blueprint entry as a `reference_image` label so the JSON self-documents which slot grounds which subject.
|
|
4156
|
+
2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`) so you drop the right file in each slot instead of stretching one logo to cover both. The describe pass also records the ad's **typography** under a `fonts` block (each typeface's classification, a best-guess family, and its weight/case) so you know exactly what to drop at the brand-font slot. Background extras are dropped. Each element is stamped back onto its blueprint entry as a `reference_image` label so the JSON self-documents which slot grounds which subject.
|
|
3843
4157
|
3. **global layout** (`~google/gemini-flash-latest`) — produces a structured `layout` block in `prompt.json`: the column/row grid, each region's `x_pct`/`y_pct` bounds, panel splits, background/shape, and every text block's relative size/weight/case/alignment. This is what gives the generator a precise composition to rebuild.
|
|
3844
4158
|
|
|
3845
|
-
It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` ingest slot per detected element** (plus an optional brand-font → type-specimen) into `image_generate`, and wires the original image in for composition only. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, layout_regions, stats, checklist }` — the **checklist** lists every real asset to drop in.
|
|
4159
|
+
It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` ingest slot per detected element** (plus an optional brand-font → type-specimen) into `image_generate`, and wires the original image in for composition only. Each **person/animal hero** is additionally fused into a generated **multi-view reference sheet** (`image_reference_sheet`, a turnaround built from the one dropped photo) that the render grounds on instead of the lone flat snapshot — the same identity lock the video scaffold uses, so the face/muzzle stays consistent and artifact-free from a single reference. Pass `--skip-actor-sheets` to ground straight on the dropped photo. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, layout_regions, stats, checklist }` — the **checklist** lists every real asset to drop in (and which heroes get a sheet).
|
|
4160
|
+
|
|
4161
|
+
The generation prompt is **model-aware and typography-hardened** (a static ad is the only path that bakes text *into* the image — video frames are clean plates). It resolves the image model's profile, so for **gpt-image** the exclusions (no invented badges/watermark) are hoisted to the very *end* of the prompt where the model reads them as pure exclusions rather than composition, and it quotes every blueprint string with an instruction to render each **once, verbatim**, lock brand names letter-by-letter, and keep type crisp and legible — the fix for the #1 static-ad defect (garbled/duplicated/dropped copy). Run `baker canvas critique` on the scaffolded canvas for an advisory read on text-legibility risk, identity grounding, and brand-type fidelity before you spend.
|
|
3846
4162
|
|
|
3847
4163
|
```bash
|
|
3848
4164
|
baker canvas scaffold-static-ad ./reference-ad.png --context "competitor ad for <brand>, <category>, <market>"
|
|
@@ -3856,15 +4172,37 @@ baker canvas run ./static-ad.canvas.json
|
|
|
3856
4172
|
|---|---|---|
|
|
3857
4173
|
| `--context <text>` | — | Known provenance (advertiser, category, market) to ground the describe. |
|
|
3858
4174
|
| `--out <path>` | `<image-dir>/static-ad.canvas.json` (cwd when `<image>` is a URL) | Where to write the canvas (`prompt.json` is written alongside). |
|
|
4175
|
+
| `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. With a slug, the reference image is **downloaded into `src/creatives/<slug>/references/` and normalized to a model-safe format** (SVG/AVIF/HEIC/… → PNG), named from the actual bytes (not the URL string) so a presigned/extensionless URL never lands as PNG-bytes-in-`.jpg` — the canvas ingests that committed, portable path instead of the expiring URL. |
|
|
3859
4176
|
| `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
|
|
3860
4177
|
| `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
|
|
3861
4178
|
| `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
|
|
3862
4179
|
| `--gen-model <id>` | registry default (`openai/gpt-5.4-image-2`) | Override the `image_generate` model. |
|
|
3863
4180
|
| `--aspect <ratio>` | inferred from the image, else `9:16` | Force the output aspect ratio. |
|
|
3864
4181
|
| `--skip-font` | off | Skip the brand-font → type-specimen slot. |
|
|
4182
|
+
| `--skip-actor-sheets` | off | Ground each person/animal on its lone dropped photo instead of a generated multi-view reference sheet. |
|
|
3865
4183
|
|
|
3866
4184
|
Scaffolding runs (and bills) the two vision passes; **running** the result generates a billed image. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
|
|
3867
4185
|
|
|
4186
|
+
**Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted and an interrupted (or stale) run of the exact same canvas is adopted automatically. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
|
|
4187
|
+
|
|
4188
|
+
#### `baker canvas rerun <slug> [flags]`
|
|
4189
|
+
|
|
4190
|
+
Re-run a creative's latest recorded canvas **from run history** — no local files needed. Every `canvas run` of a creative uploads a portable snapshot (the canvas JSON plus the local files it ingests: prompt blueprints, composition dirs, reference images) to durable storage and records it on the run. `rerun` restores those files into `src/creatives/<slug>/` (sha-verified; files already matching are left untouched) and then executes the normal run flow — so an interrupted run **resumes** (in-flight jobs re-attach) and a completed one re-renders from the cache at zero credits.
|
|
4191
|
+
|
|
4192
|
+
| Flag | Default | Meaning |
|
|
4193
|
+
| --- | --- | --- |
|
|
4194
|
+
| `--force-remote` | off | Overwrite local files whose content differs from the snapshot (otherwise a conflict aborts with the differing paths). |
|
|
4195
|
+
| `--fresh` | off | Start a new run id instead of resuming an interrupted one. |
|
|
4196
|
+
| `--regenerate <ids>` | — | Same as `canvas run --regenerate`. |
|
|
4197
|
+
| `--concurrency <n>` | — | Same as `canvas run --concurrency`. |
|
|
4198
|
+
|
|
4199
|
+
```bash
|
|
4200
|
+
baker canvas rerun spring-offer-4x5
|
|
4201
|
+
baker canvas rerun spring-offer-4x5 --force-remote --regenerate gen_4x5
|
|
4202
|
+
```
|
|
4203
|
+
|
|
4204
|
+
Use it when a creative was built in another conversation (or its sandbox is gone) and you need to continue or re-render it here. Files the snapshot could not include (missing at run time, or oversized) are listed as warnings — supply those locally only if the run actually needs to regenerate the nodes that read them.
|
|
4205
|
+
|
|
3868
4206
|
#### `baker canvas inspect <run_id> [--thumbnails]`
|
|
3869
4207
|
|
|
3870
4208
|
One-page summary of a completed run: per-node duration + cache status, list of files in the run dir, optional video thumbnails (start/middle/end frames extracted via ffmpeg).
|
|
@@ -4327,6 +4665,26 @@ import {
|
|
|
4327
4665
|
} from "@koda-sl/baker-cli/engine";
|
|
4328
4666
|
```
|
|
4329
4667
|
|
|
4668
|
+
## Creatives
|
|
4669
|
+
|
|
4670
|
+
Publish an approved canvas render as a first-class Baker creative. The image uploads to the Baker image library (tagged `creative`), a creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
|
|
4671
|
+
|
|
4672
|
+
```bash
|
|
4673
|
+
baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" \
|
|
4674
|
+
--slug spring-offer-4x5 --run-id r_01JXYZ... \
|
|
4675
|
+
--source-reference-url "https://www.facebook.com/ads/library/?id=..."
|
|
4676
|
+
```
|
|
4677
|
+
|
|
4678
|
+
| Flag | Effect |
|
|
4679
|
+
|---|---|
|
|
4680
|
+
| `--title <text>` | Required. Human title for the creative. |
|
|
4681
|
+
| `--slug <slug>` | Creative slug (`src/creatives/<slug>/`) — attaches the image to that creative's row, marks it `published`. |
|
|
4682
|
+
| `--run-id <r_…>` | Pins the approved generation from the creative's run history as the published one. |
|
|
4683
|
+
| `--source-reference-url <url>` | Original reference ad URL, recorded on the creative. |
|
|
4684
|
+
| `--context <text>` | Optional describe context for the uploaded image asset. |
|
|
4685
|
+
|
|
4686
|
+
Without `--slug` the command behaves as before (one creative record per published image). With `--slug` it upserts the repo-convention row — the same one the dashboard's Creatives tab and the `src/creatives/{slug}/` folder describe — so publish, repo sync, and run history all land on a single record regardless of order.
|
|
4687
|
+
|
|
4330
4688
|
## Help & Discovery
|
|
4331
4689
|
|
|
4332
4690
|
Every command supports `--help` for usage info:
|