@koda-sl/baker-cli 0.131.0-dev.597b50181 → 0.131.0-dev.cad5e0bc7
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 +276 -82
- package/dist/{chunk-4QK7JC6O.js → chunk-5LCTMZJ5.js} +1 -1
- package/dist/chunk-5LCTMZJ5.js.map +1 -0
- package/dist/cli.js +2331 -2978
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-4QK7JC6O.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. |
|
|
@@ -2165,17 +2419,13 @@ baker testimonials tags
|
|
|
2165
2419
|
|
|
2166
2420
|
### Winning Ads (`baker winning-ads`)
|
|
2167
2421
|
|
|
2168
|
-
Search the **ad-dna** corpus of scored "winning" competitor ads for reference creatives to reproduce (e.g. with `baker canvas`)
|
|
2169
|
-
|
|
2170
|
-
> 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.
|
|
2171
2423
|
|
|
2172
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`.
|
|
2173
2425
|
|
|
2174
|
-
> Replaces the old `baker ads google library` tree, which has been removed. Competitor-by-keyword discovery still lives at `baker research advertisers`.
|
|
2175
|
-
|
|
2176
2426
|
### `baker winning-ads search <query>`
|
|
2177
2427
|
|
|
2178
|
-
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.
|
|
2179
2429
|
|
|
2180
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.
|
|
2181
2431
|
|
|
@@ -2197,7 +2447,7 @@ baker winning-ads search --ref-ad-id a_12345 --first-seen-after 2026-01-01T00:00
|
|
|
2197
2447
|
| `--limit <n>` | Max results 1–100 (**default 10** — shortlist size) |
|
|
2198
2448
|
| `--max-per-advertiser <n>` | Cap results per advertiser 1–50 (default 3) |
|
|
2199
2449
|
| `--min-relevance <0-1>` | Relevance floor; trims weak matches |
|
|
2200
|
-
| `--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 |
|
|
2201
2451
|
| `--format <list>` | `video,static,carousel` |
|
|
2202
2452
|
| `--winner-category <list>` | `winner,scaled_winner,evergreen,rising,untested,dud,…` (default: all) |
|
|
2203
2453
|
| `--awareness <list>` | `unaware,problem_aware,solution_aware,product_aware,most_aware` |
|
|
@@ -2211,21 +2461,20 @@ baker winning-ads search --ref-ad-id a_12345 --first-seen-after 2026-01-01T00:00
|
|
|
2211
2461
|
|
|
2212
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.
|
|
2213
2463
|
|
|
2214
|
-
### `baker winning-ads hooks|mechanisms|
|
|
2464
|
+
### `baker winning-ads hooks|mechanisms|patterns`
|
|
2215
2465
|
|
|
2216
|
-
Read the **winner-weighted rollup** of the ad-dna corpus
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
guessing. (Distinct from `winning-ads patterns`, which contrasts two ad cohorts.)
|
|
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.
|
|
2220
2469
|
|
|
2221
2470
|
```bash
|
|
2222
2471
|
# Top winning hook seeds for problem-aware SaaS on Meta:
|
|
2223
2472
|
baker winning-ads hooks --platform meta --awareness problem_aware --industry saas --output md
|
|
2224
2473
|
# Then narrow a reference search to that proven hook:
|
|
2225
2474
|
baker winning-ads search "onboarding demo" --hook-archetype callout --output md
|
|
2226
|
-
# Also: mechanisms (persuasion),
|
|
2475
|
+
# Also: mechanisms (persuasion), patterns (static ad layouts):
|
|
2227
2476
|
baker winning-ads mechanisms --platform linkedin --output md
|
|
2228
|
-
baker winning-ads
|
|
2477
|
+
baker winning-ads patterns --awareness solution_aware --output md
|
|
2229
2478
|
```
|
|
2230
2479
|
|
|
2231
2480
|
| Flag | Purpose |
|
|
@@ -2241,66 +2490,13 @@ proven grand-slam pattern floats to the top and a measured dud is floored.
|
|
|
2241
2490
|
|
|
2242
2491
|
### `baker winning-ads advertisers <brand>`
|
|
2243
2492
|
|
|
2244
|
-
|
|
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`.
|
|
2245
2494
|
|
|
2246
2495
|
```bash
|
|
2247
2496
|
baker winning-ads advertisers "Acme" --output md # find our own advertiser id
|
|
2248
2497
|
baker winning-ads advertisers "Deel" --platform meta --output md
|
|
2249
2498
|
```
|
|
2250
2499
|
|
|
2251
|
-
### `baker winning-ads follow "<domain | profile URL | brand>" --platform meta|linkedin`
|
|
2252
|
-
|
|
2253
|
-
Add **all** of a brand's ads to your library — every platform (Meta + LinkedIn) and every country → `POST /api/ad-library/follow`. `--platform` is **required**, but it only tells us how to read your `input` (Facebook vs LinkedIn URL); it does **not** limit what we track. A bare domain is best: we resolve both the Meta page and the LinkedIn company from it and track both. Discovery is never region-scoped. The result `status` is one of:
|
|
2254
|
-
|
|
2255
|
-
- `following` — the brand is already in the corpus; you're now subscribed (no wait).
|
|
2256
|
-
- `added` — a new brand was queued for ingestion; its ads appear as discovery completes (a `hints[]` note flags this).
|
|
2257
|
-
- `ambiguous` — the input mapped to multiple brands; pick one from `candidates` and re-run with a more specific domain/URL.
|
|
2258
|
-
|
|
2259
|
-
```bash
|
|
2260
|
-
baker winning-ads follow "deel.com" --platform meta
|
|
2261
|
-
baker winning-ads follow "https://www.linkedin.com/company/acme" --platform linkedin --label "Acme (competitor)"
|
|
2262
|
-
```
|
|
2263
|
-
|
|
2264
|
-
### `baker winning-ads following`
|
|
2265
|
-
|
|
2266
|
-
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`.
|
|
2267
|
-
|
|
2268
|
-
```bash
|
|
2269
|
-
baker winning-ads following --output md
|
|
2270
|
-
```
|
|
2271
|
-
|
|
2272
|
-
### `baker winning-ads winners <advertiser>`
|
|
2273
|
-
|
|
2274
|
-
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`.
|
|
2275
|
-
|
|
2276
|
-
```bash
|
|
2277
|
-
baker winning-ads winners adv_123 --top 15 --output md
|
|
2278
|
-
```
|
|
2279
|
-
|
|
2280
|
-
### `baker winning-ads unfollow <advertiser>`
|
|
2281
|
-
|
|
2282
|
-
Stop following a brand by advertiser id → `POST /api/ad-library/unfollow`. Returns `{ removed }`.
|
|
2283
|
-
|
|
2284
|
-
```bash
|
|
2285
|
-
baker winning-ads unfollow adv_123
|
|
2286
|
-
```
|
|
2287
|
-
|
|
2288
|
-
### `baker winning-ads brief`
|
|
2289
|
-
|
|
2290
|
-
Generate a creative brief grounded in strategically-similar winners → `POST /api/ad-library/brief`. Optionally describe the target creative with `--dna` (a JSON object), steer with `--notes`, and cap references with `--k`. Returns `brief_markdown` + `reference_ad_ids`.
|
|
2291
|
-
|
|
2292
|
-
```bash
|
|
2293
|
-
baker winning-ads brief --dna '{"angle":"cost savings","awareness_stage":"solution_aware"}' --notes "B2B, LinkedIn video" --k 8
|
|
2294
|
-
```
|
|
2295
|
-
|
|
2296
|
-
### `baker winning-ads patterns --winners <adIds> --duds <adIds>`
|
|
2297
|
-
|
|
2298
|
-
Mine what separates two cohorts of ads → `POST /api/ad-library/patterns`. Pass a comma-list of winning ad ids (`--winners`, cohort A) and weaker/dud ad ids (`--duds`, cohort B), plus optional `--top-n`. Returns each discriminating DNA field with the cohort it `favors` (`winners`/`duds`), a `score`, and the top values on each side.
|
|
2299
|
-
|
|
2300
|
-
```bash
|
|
2301
|
-
baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --top-n 10 --output md
|
|
2302
|
-
```
|
|
2303
|
-
|
|
2304
2500
|
---
|
|
2305
2501
|
|
|
2306
2502
|
### Scheduled Actions (`baker scheduled-actions`)
|
|
@@ -3871,18 +4067,16 @@ Validate, then execute the graph. Blocks until done. Logs one line per node. Ret
|
|
|
3871
4067
|
|
|
3872
4068
|
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:
|
|
3873
4069
|
|
|
3874
|
-
1. **`video_deconstruct`** (`~google/gemini-pro-latest`, full mode) — reverse-engineers the video into a scene-by-scene blueprint + word-level transcript
|
|
3875
|
-
|
|
3876
|
-
**Structural edits still need a re-scaffold.** The per-scene re-flow covers rewriting a scene's *content*. Changing scene *timing*, adding/removing/re-ordering scenes, or flipping a scene's shot type is a structural change the graph's timing/spine nodes don't track from a prompt edit — `validate`/`run` print a drift warning telling you to re-run `baker canvas scaffold-video`.
|
|
4070
|
+
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.
|
|
3877
4071
|
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.
|
|
3878
4072
|
|
|
3879
4073
|
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.
|
|
3880
4074
|
|
|
3881
4075
|
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`.
|
|
3882
4076
|
|
|
3883
|
-
It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`**
|
|
4077
|
+
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.
|
|
3884
4078
|
|
|
3885
|
-
**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
|
|
4079
|
+
**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.
|
|
3886
4080
|
|
|
3887
4081
|
**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).
|
|
3888
4082
|
|
|
@@ -3924,7 +4118,7 @@ The emitted canvas is validated (`validateCanvasDeep`) before it's written, so i
|
|
|
3924
4118
|
|
|
3925
4119
|
```bash
|
|
3926
4120
|
baker canvas scaffold-video ./reference-ad.mp4 --focus "competitor UGC ad for <brand>"
|
|
3927
|
-
# → writes ./reference-ad.video.canvas.json + ./prompt.json (
|
|
4121
|
+
# → writes ./reference-ad.video.canvas.json + ./prompt.json (+ video-overlay-composition/)
|
|
3928
4122
|
# edit each frame's own params.prompt + drop ONE real source image at each el_* [TODO], confirm voices, then:
|
|
3929
4123
|
baker canvas validate ./reference-ad.video.canvas.json
|
|
3930
4124
|
baker canvas run ./reference-ad.video.canvas.json
|
|
@@ -3956,7 +4150,7 @@ roster and prints the choice as `models.video_route_reason` in the run report (m
|
|
|
3956
4150
|
shifts a Veo pick to the fast tier. The scaffolder also bakes **per-intent param recipes** so the hero/reveal
|
|
3957
4151
|
beat claims the 1080p cine ceiling automatically and a Kling hook beat maxes prompt adherence.
|
|
3958
4152
|
|
|
3959
|
-
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
|
|
4153
|
+
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.
|
|
3960
4154
|
|
|
3961
4155
|
**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).
|
|
3962
4156
|
|