@koda-sl/baker-cli 0.354.0-dev.faae9de67 → 0.355.0-dev.c2de9a6fe
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 +5 -5
- package/dist/cli.js +220 -124
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -742,7 +742,7 @@ Use `--no-cache` on any command to bypass **both** the local file cache and the
|
|
|
742
742
|
|------|-----------|---------|
|
|
743
743
|
| `FIELD_NOT_FOUND` | No | Invalid field in GAQL query |
|
|
744
744
|
| `WRONG_RESOURCE` | No | Field queried from wrong resource |
|
|
745
|
-
| `INVALID_OPERATOR` | No | Unsupported operator (e.g. CONTAINS) |
|
|
745
|
+
| `INVALID_OPERATOR` | No | Unsupported operator (e.g. CONTAINS(), or =, IN or IS NOT NULL on a list field such as `final_urls` or `labels`, which takes only CONTAINS ALL/ANY/NONE). List-field operators are refused before the query is sent, with a corrected command that rewrites =/IN to CONTAINS ANY and drops a null test |
|
|
746
746
|
| `CUSTOMER_NOT_FOUND` | No | Invalid customer ID — run `baker ads google accounts` to find valid IDs |
|
|
747
747
|
| `MISSING_MANAGER_ID` | No | Account not accessible (may need MCC login-customer-id) |
|
|
748
748
|
| `INCOMPATIBLE_FIELDS` | No | Fields can't be in same query |
|
|
@@ -1421,8 +1421,8 @@ baker experiment plan --landing oferta --variant oferta--1 --goal submit:book-a-
|
|
|
1421
1421
|
baker experiment start --landing oferta --variant oferta--1 --because "60% of visitors never scroll past the hero" --change "put the price in the headline"
|
|
1422
1422
|
baker experiment update --id temp_exp_1 --goal submit:book-a-call # a staged test can still change what it measures
|
|
1423
1423
|
baker experiment update --id a3f91c2b --because "…" # a live one can only change its wording
|
|
1424
|
-
baker experiment status # the verdict on every test
|
|
1425
|
-
baker experiment status --id a3f91c2b --full # one test, with
|
|
1424
|
+
baker experiment status # the verdict on every test
|
|
1425
|
+
baker experiment status --id a3f91c2b --full # one test, with the confidence and posteriors behind it
|
|
1426
1426
|
baker experiment history --landing oferta # every finished test on a page: claim, numbers, decision, learning
|
|
1427
1427
|
baker experiment pause --id a3f91c2b # everyone sees the original for now
|
|
1428
1428
|
baker experiment resume --id a3f91c2b # the split picks up with the same sides
|
|
@@ -5412,7 +5412,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
|
|
|
5412
5412
|
|
|
5413
5413
|
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`.
|
|
5414
5414
|
|
|
5415
|
-
It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) 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.
|
|
5415
|
+
It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) 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 — only on a video with nobody in it: when the deconstruct found a cast or a recurring person, reuse is refused (`frames_reuse_refused`) and every frame is generated, so the source's actors never reach the ad.
|
|
5416
5416
|
|
|
5417
5417
|
**Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
|
|
5418
5418
|
|
|
@@ -5466,7 +5466,7 @@ baker canvas run ./reference-ad.video.canvas.json
|
|
|
5466
5466
|
|---|---|---|
|
|
5467
5467
|
| `--out <path>` | `<video-dir>/<name>.video.canvas.json` | Where to write the canvas (composition is copied alongside). |
|
|
5468
5468
|
| `--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`. |
|
|
5469
|
-
| `--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). |
|
|
5469
|
+
| `--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) — only on a video with nobody in it; on one showing people it is refused and every frame is generated. |
|
|
5470
5470
|
| `--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. |
|
|
5471
5471
|
| `--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. |
|
|
5472
5472
|
| `--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. |
|
package/dist/cli.js
CHANGED
|
@@ -10564,6 +10564,102 @@ function hintsFor(scope, days) {
|
|
|
10564
10564
|
return hints2;
|
|
10565
10565
|
}
|
|
10566
10566
|
|
|
10567
|
+
// src/commands/ads/google/list-fields.ts
|
|
10568
|
+
var LIST_FIELDS = /* @__PURE__ */ new Set([
|
|
10569
|
+
"ad.final_mobile_urls",
|
|
10570
|
+
"ad.final_urls",
|
|
10571
|
+
"ad_group.excluded_parent_asset_field_types",
|
|
10572
|
+
"ad_group.excluded_parent_asset_set_types",
|
|
10573
|
+
"ad_group.labels",
|
|
10574
|
+
"ad_group.primary_status_reasons",
|
|
10575
|
+
"ad_group_ad.ad.final_mobile_urls",
|
|
10576
|
+
"ad_group_ad.ad.final_urls",
|
|
10577
|
+
"ad_group_ad.labels",
|
|
10578
|
+
"ad_group_ad.primary_status_reasons",
|
|
10579
|
+
"ad_group_asset.primary_status_reasons",
|
|
10580
|
+
"ad_group_criterion.disapproval_reasons",
|
|
10581
|
+
"ad_group_criterion.final_mobile_urls",
|
|
10582
|
+
"ad_group_criterion.final_urls",
|
|
10583
|
+
"ad_group_criterion.labels",
|
|
10584
|
+
"ad_group_criterion.primary_status_reasons",
|
|
10585
|
+
"ad_group_criterion.topic.path",
|
|
10586
|
+
"ad_group_criterion.webpage.sample.sample_urls",
|
|
10587
|
+
"asset.dynamic_custom_asset.contextual_keywords",
|
|
10588
|
+
"asset.dynamic_custom_asset.similar_ids",
|
|
10589
|
+
"asset.dynamic_education_asset.contextual_keywords",
|
|
10590
|
+
"asset.dynamic_education_asset.similar_program_ids",
|
|
10591
|
+
"asset.dynamic_flights_asset.similar_destination_ids",
|
|
10592
|
+
"asset.dynamic_hotels_and_rentals_asset.contextual_keywords",
|
|
10593
|
+
"asset.dynamic_hotels_and_rentals_asset.similar_property_ids",
|
|
10594
|
+
"asset.dynamic_jobs_asset.contextual_keywords",
|
|
10595
|
+
"asset.dynamic_jobs_asset.similar_job_ids",
|
|
10596
|
+
"asset.dynamic_local_asset.contextual_keywords",
|
|
10597
|
+
"asset.dynamic_local_asset.similar_deal_ids",
|
|
10598
|
+
"asset.dynamic_real_estate_asset.contextual_keywords",
|
|
10599
|
+
"asset.dynamic_real_estate_asset.similar_listing_ids",
|
|
10600
|
+
"asset.dynamic_travel_asset.contextual_keywords",
|
|
10601
|
+
"asset.dynamic_travel_asset.similar_destination_ids",
|
|
10602
|
+
"asset.final_mobile_urls",
|
|
10603
|
+
"asset.final_urls",
|
|
10604
|
+
"asset.page_feed_asset.labels",
|
|
10605
|
+
"asset.structured_snippet_asset.values",
|
|
10606
|
+
"asset_group.final_mobile_urls",
|
|
10607
|
+
"asset_group.final_urls",
|
|
10608
|
+
"asset_group.primary_status_reasons",
|
|
10609
|
+
"asset_group_asset.primary_status_reasons",
|
|
10610
|
+
"asset_group_signal.disapproval_reasons",
|
|
10611
|
+
"asset_set.business_profile_location_group.dynamic_business_profile_location_group_filter.label_filters",
|
|
10612
|
+
"asset_set.business_profile_location_group.dynamic_business_profile_location_group_filter.listing_id_filters",
|
|
10613
|
+
"asset_set.location_set.business_profile_location_set.label_filters",
|
|
10614
|
+
"asset_set.location_set.business_profile_location_set.listing_id_filters",
|
|
10615
|
+
"bidding_data_exclusion.advertising_channel_types",
|
|
10616
|
+
"bidding_data_exclusion.campaigns",
|
|
10617
|
+
"bidding_data_exclusion.devices",
|
|
10618
|
+
"bidding_seasonality_adjustment.advertising_channel_types",
|
|
10619
|
+
"bidding_seasonality_adjustment.campaigns",
|
|
10620
|
+
"bidding_seasonality_adjustment.devices",
|
|
10621
|
+
"campaign.excluded_parent_asset_field_types",
|
|
10622
|
+
"campaign.excluded_parent_asset_set_types",
|
|
10623
|
+
"campaign.feed_types",
|
|
10624
|
+
"campaign.labels",
|
|
10625
|
+
"campaign.optimization_goal_setting.optimization_goal_types",
|
|
10626
|
+
"campaign.primary_status_reasons",
|
|
10627
|
+
"campaign.selective_optimization.conversion_actions",
|
|
10628
|
+
"campaign.shopping_setting.advertising_partner_ids",
|
|
10629
|
+
"campaign.text_guidelines.term_exclusions",
|
|
10630
|
+
"campaign_asset.primary_status_reasons",
|
|
10631
|
+
"campaign_criterion.location_group.geo_target_constants",
|
|
10632
|
+
"campaign_criterion.topic.path",
|
|
10633
|
+
"campaign_criterion.webpage.sample.sample_urls",
|
|
10634
|
+
"conversion_value_rule.audience_condition.user_interests",
|
|
10635
|
+
"conversion_value_rule.audience_condition.user_lists",
|
|
10636
|
+
"conversion_value_rule.device_condition.device_types",
|
|
10637
|
+
"conversion_value_rule.geo_location_condition.excluded_geo_target_constants",
|
|
10638
|
+
"conversion_value_rule.geo_location_condition.geo_target_constants",
|
|
10639
|
+
"conversion_value_rule_set.conversion_value_rules",
|
|
10640
|
+
"conversion_value_rule_set.dimensions",
|
|
10641
|
+
"custom_conversion_goal.conversion_actions",
|
|
10642
|
+
"customer.pay_per_conversion_eligibility_failure_reasons",
|
|
10643
|
+
"customer_asset.primary_status_reasons",
|
|
10644
|
+
"customer_client.applied_labels",
|
|
10645
|
+
"experiment_arm.campaigns",
|
|
10646
|
+
"experiment_arm.in_design_campaigns",
|
|
10647
|
+
"keyword_plan_campaign.language_constants",
|
|
10648
|
+
"local_services_employee.category_ids",
|
|
10649
|
+
"local_services_employee.languages_spoken",
|
|
10650
|
+
"metrics.interaction_event_types",
|
|
10651
|
+
"recommendation.campaigns",
|
|
10652
|
+
"shared_criterion.webpage.sample.sample_urls",
|
|
10653
|
+
"shopping_product.target_countries",
|
|
10654
|
+
"topic_constant.path",
|
|
10655
|
+
"user_list.lookalike_user_list.country_codes",
|
|
10656
|
+
"user_list.lookalike_user_list.seed_user_list_ids"
|
|
10657
|
+
]);
|
|
10658
|
+
var LIST_FIELD_OPERATORS = "CONTAINS ALL, CONTAINS ANY, CONTAINS NONE";
|
|
10659
|
+
function listFieldGuidance(field) {
|
|
10660
|
+
return `${field} is a list field: it takes only ${LIST_FIELD_OPERATORS} \u2014 no comparison (=, !=, IN), pattern match or null test. To find rows that have it, drop that condition, keep ${field} in SELECT and filter the rows (rows without it omit the key); to match values, use ${field} CONTAINS ANY ('value')`;
|
|
10661
|
+
}
|
|
10662
|
+
|
|
10567
10663
|
// src/commands/ads/google/correction-table.ts
|
|
10568
10664
|
function buildCommand(query, ctx) {
|
|
10569
10665
|
return `baker ads google query "${query}" --customer-id ${ctx.customerId}`;
|
|
@@ -10591,6 +10687,27 @@ var CORRECTION_RULES = [
|
|
|
10591
10687
|
};
|
|
10592
10688
|
}
|
|
10593
10689
|
},
|
|
10690
|
+
// 0b. OPERATOR_FIELD_MISMATCH — the field refuses this operator, and Google names the
|
|
10691
|
+
// ones it takes. Kept ahead of the field-specific rules for the same reason as rule 0.
|
|
10692
|
+
// A repeated field (final_urls, labels, …) takes only CONTAINS ALL/ANY/NONE, so it has
|
|
10693
|
+
// no null test at all: read it unfiltered and filter the rows instead.
|
|
10694
|
+
{
|
|
10695
|
+
matchApiError: /OPERATOR_FIELD_MISMATCH|is not a valid operator to use with/i,
|
|
10696
|
+
fix: (ctx) => {
|
|
10697
|
+
const message = ctx.apiErrorMessage ?? "";
|
|
10698
|
+
const field = message.match(/valid operator to use with '([\w.]+)'/i)?.[1] ?? "this field";
|
|
10699
|
+
const operator = message.match(/([A-Z=!<>][A-Z_ =!<>]*?) is not a valid operator/)?.[1]?.trim();
|
|
10700
|
+
const valid = message.match(/Valid operators: (.+?)\.?(?:"|$)/i)?.[1]?.trim();
|
|
10701
|
+
const refused = operator ? `${operator} cannot be used on ${field}` : `${field} does not take that operator`;
|
|
10702
|
+
const allowed = valid ? `; its valid operators are ${valid}` : "";
|
|
10703
|
+
const repeated = valid?.split(",").every((op) => /^CONTAINS (ALL|ANY|NONE)$/i.test(op.trim())) ?? false;
|
|
10704
|
+
const guidance = repeated ? `. ${listFieldGuidance(field)}` : ". Rewrite that condition with one of them, or drop it and filter the rows";
|
|
10705
|
+
return {
|
|
10706
|
+
action: "change_operator",
|
|
10707
|
+
explanation: `${refused}${allowed}${guidance}`
|
|
10708
|
+
};
|
|
10709
|
+
}
|
|
10710
|
+
},
|
|
10594
10711
|
// 1. bare keyword.text → ad_group_criterion.keyword.text (leaves <resource>.keyword.text untouched)
|
|
10595
10712
|
{
|
|
10596
10713
|
matchQuery: /(?<![\w.])keyword\.text\b/,
|
|
@@ -10633,10 +10750,11 @@ var CORRECTION_RULES = [
|
|
|
10633
10750
|
};
|
|
10634
10751
|
}
|
|
10635
10752
|
},
|
|
10636
|
-
// 4. CONTAINS → LIKE
|
|
10753
|
+
// 4. CONTAINS → LIKE. Keyed on the query alone: Google's error envelope ("Request contains an
|
|
10754
|
+
// invalid argument … Valid operators: CONTAINS ANY") matches any loose error pattern, and this
|
|
10755
|
+
// advice only makes sense when the query actually wrote CONTAINS(…).
|
|
10637
10756
|
{
|
|
10638
10757
|
matchQuery: /CONTAINS\s*\(/i,
|
|
10639
|
-
matchApiError: /CONTAINS.*not.*supported|invalid.*operator.*CONTAINS/i,
|
|
10640
10758
|
fix: (ctx) => {
|
|
10641
10759
|
const match = ctx.originalQuery.match(/CONTAINS\s*\(\s*([^,]+),\s*'([^']+)'\s*\)/i);
|
|
10642
10760
|
if (match) {
|
|
@@ -13082,6 +13200,73 @@ function rejectOversizedLimit(query, customerId) {
|
|
|
13082
13200
|
}
|
|
13083
13201
|
};
|
|
13084
13202
|
}
|
|
13203
|
+
var WHERE_OPERATOR = /(?<![\w.])([a-z_]+(?:\.[a-z_]+)+)\s*(CONTAINS\s+(?:ALL|ANY|NONE)|IS\s+NOT\s+NULL|IS\s+NULL|NOT\s+LIKE|NOT\s+IN|NOT\s+REGEXP_MATCH|REGEXP_MATCH|LIKE|IN|BETWEEN|DURING|!=|<=|>=|=|<|>)/gi;
|
|
13204
|
+
function escapeRegExp(text2) {
|
|
13205
|
+
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13206
|
+
}
|
|
13207
|
+
function withoutNullTest(query, field) {
|
|
13208
|
+
const test = `${escapeRegExp(field)}\\s+IS\\s+(?:NOT\\s+)?NULL\\b`;
|
|
13209
|
+
const dropped = query.replace(new RegExp(`\\bWHERE\\s+${test}\\s+AND\\s+`, "i"), "WHERE ").replace(new RegExp(`\\s+AND\\s+${test}`, "i"), "").replace(new RegExp(`\\s*\\bWHERE\\s+${test}`, "i"), "");
|
|
13210
|
+
return mentions(selectClause(dropped), field) ? dropped : dropped.replace(/SELECT\s+/i, `SELECT ${field}, `);
|
|
13211
|
+
}
|
|
13212
|
+
function asListCondition(query, field) {
|
|
13213
|
+
const condition = new RegExp(
|
|
13214
|
+
`(?<![\\w.])${escapeRegExp(field)}\\s*(NOT\\s+IN\\b|IN\\b|!=|=)\\s*('[^']*'|\\([^)]*\\))`,
|
|
13215
|
+
"i"
|
|
13216
|
+
);
|
|
13217
|
+
const match = query.match(condition);
|
|
13218
|
+
if (!match) return null;
|
|
13219
|
+
const [, operator = "", value = ""] = match;
|
|
13220
|
+
const contains = /^(=|IN)$/i.test(operator.trim()) ? "CONTAINS ANY" : "CONTAINS NONE";
|
|
13221
|
+
const values = value.startsWith("(") ? value : `(${value})`;
|
|
13222
|
+
return query.replace(condition, `${field} ${contains} ${values}`);
|
|
13223
|
+
}
|
|
13224
|
+
function rejectListFieldOperator(query, customerId) {
|
|
13225
|
+
const where = whereClause(query).replace(/'[^']*'/g, "''");
|
|
13226
|
+
const refusals = [...where.matchAll(WHERE_OPERATOR)].map(([, field = "", operator = ""]) => ({
|
|
13227
|
+
field: field.toLowerCase(),
|
|
13228
|
+
operator: operator.toUpperCase().replace(/\s+/g, " ")
|
|
13229
|
+
})).filter(({ field, operator }) => LIST_FIELDS.has(field) && !operator.startsWith("CONTAINS"));
|
|
13230
|
+
const first = refusals[0];
|
|
13231
|
+
if (!first) return null;
|
|
13232
|
+
const fixed = refusals.reduce(
|
|
13233
|
+
(corrected, { field, operator }) => corrected === null ? null : operator.startsWith("IS ") ? withoutNullTest(corrected, field) : asListCondition(corrected, field),
|
|
13234
|
+
query
|
|
13235
|
+
);
|
|
13236
|
+
return {
|
|
13237
|
+
ok: false,
|
|
13238
|
+
error: {
|
|
13239
|
+
code: "INVALID_OPERATOR",
|
|
13240
|
+
message: `${first.operator} cannot be used on ${first.field}: it is a list field, so Google would refuse this query.`,
|
|
13241
|
+
fix: {
|
|
13242
|
+
action: "change_operator",
|
|
13243
|
+
...fixed ? { correctedCommand: buildCommand2(fixed, customerId) } : {},
|
|
13244
|
+
explanation: listFieldGuidance(first.field)
|
|
13245
|
+
},
|
|
13246
|
+
retryable: false
|
|
13247
|
+
}
|
|
13248
|
+
};
|
|
13249
|
+
}
|
|
13250
|
+
function rejectContainsCall(query, customerId) {
|
|
13251
|
+
const containsMatch = query.match(/CONTAINS\s*\(\s*([^,]+),\s*'([^']+)'\s*\)/i);
|
|
13252
|
+
if (!containsMatch) return null;
|
|
13253
|
+
const field = containsMatch[1]?.trim();
|
|
13254
|
+
const value = containsMatch[2] ?? "";
|
|
13255
|
+
const fixed = query.replace(/CONTAINS\s*\(\s*[^,]+,\s*'[^']+'\s*\)/i, `${field} LIKE '%${value}%'`);
|
|
13256
|
+
return {
|
|
13257
|
+
ok: false,
|
|
13258
|
+
error: {
|
|
13259
|
+
code: "INVALID_OPERATOR",
|
|
13260
|
+
message: "GAQL does not support CONTAINS. Use LIKE with % wildcards.",
|
|
13261
|
+
fix: {
|
|
13262
|
+
action: "change_operator",
|
|
13263
|
+
correctedCommand: buildCommand2(fixed, customerId),
|
|
13264
|
+
explanation: "GAQL uses LIKE '%value%' for substring matching, not CONTAINS()"
|
|
13265
|
+
},
|
|
13266
|
+
retryable: false
|
|
13267
|
+
}
|
|
13268
|
+
};
|
|
13269
|
+
}
|
|
13085
13270
|
function validatePreflight(query, customerId, limit) {
|
|
13086
13271
|
const warnings = [];
|
|
13087
13272
|
const corrected = query;
|
|
@@ -13104,29 +13289,6 @@ function validatePreflight(query, customerId, limit) {
|
|
|
13104
13289
|
}
|
|
13105
13290
|
};
|
|
13106
13291
|
}
|
|
13107
|
-
const containsMatch = corrected.match(/CONTAINS\s*\(\s*([^,]+),\s*'([^']+)'\s*\)/i);
|
|
13108
|
-
if (containsMatch) {
|
|
13109
|
-
const field = containsMatch[1]?.trim();
|
|
13110
|
-
const value = containsMatch[2] ?? "";
|
|
13111
|
-
const fixed = corrected.replace(/CONTAINS\s*\(\s*[^,]+,\s*'[^']+'\s*\)/i, `${field} LIKE '%${value}%'`);
|
|
13112
|
-
return {
|
|
13113
|
-
valid: false,
|
|
13114
|
-
warnings: [],
|
|
13115
|
-
error: {
|
|
13116
|
-
ok: false,
|
|
13117
|
-
error: {
|
|
13118
|
-
code: "INVALID_OPERATOR",
|
|
13119
|
-
message: "GAQL does not support CONTAINS. Use LIKE with % wildcards.",
|
|
13120
|
-
fix: {
|
|
13121
|
-
action: "change_operator",
|
|
13122
|
-
correctedCommand: buildCommand2(fixed, customerId),
|
|
13123
|
-
explanation: "GAQL uses LIKE '%value%' for substring matching, not CONTAINS()"
|
|
13124
|
-
},
|
|
13125
|
-
retryable: false
|
|
13126
|
-
}
|
|
13127
|
-
}
|
|
13128
|
-
};
|
|
13129
|
-
}
|
|
13130
13292
|
const openDateMatch = corrected.match(/segments\.date\s*>=\s*'(\d{4}-\d{2}-\d{2})'/i);
|
|
13131
13293
|
if (openDateMatch && !/BETWEEN/i.test(corrected)) {
|
|
13132
13294
|
const startDate = openDateMatch[1] ?? "";
|
|
@@ -13153,10 +13315,10 @@ function validatePreflight(query, customerId, limit) {
|
|
|
13153
13315
|
}
|
|
13154
13316
|
};
|
|
13155
13317
|
}
|
|
13156
|
-
const
|
|
13157
|
-
|
|
13158
|
-
|
|
13159
|
-
|
|
13318
|
+
for (const reject of [rejectContainsCall, rejectListFieldOperator, rejectUnsupportedDuring, rejectOversizedLimit]) {
|
|
13319
|
+
const rejection2 = reject(corrected, customerId);
|
|
13320
|
+
if (rejection2) return { valid: false, warnings: [], error: rejection2 };
|
|
13321
|
+
}
|
|
13160
13322
|
if (/\bsegments\.conversion_action_name\b/i.test(corrected) && /\bmetrics\.cost_micros\b/i.test(corrected)) {
|
|
13161
13323
|
return {
|
|
13162
13324
|
valid: false,
|
|
@@ -39734,10 +39896,18 @@ function dropUncastVoices(nodes) {
|
|
|
39734
39896
|
if (node && node.type === "voice_select" && !referenced.has(node.id)) nodes.splice(i, 1);
|
|
39735
39897
|
}
|
|
39736
39898
|
}
|
|
39737
|
-
function
|
|
39899
|
+
function framesReuseRefusal(blueprint, elements, requested) {
|
|
39900
|
+
if (requested !== "reuse") return void 0;
|
|
39901
|
+
const showsPeople = (blueprint.global?.cast ?? []).length > 0 || elements.some((e) => e.type.toLowerCase() === "person");
|
|
39902
|
+
if (!showsPeople) return void 0;
|
|
39903
|
+
return "--frames reuse was not applied: this video shows people, and reusing its frames would put the source's actors into our ad. Every frame is generated instead, recast to the el_* references. Reuse applies only to a video with nobody in it.";
|
|
39904
|
+
}
|
|
39905
|
+
function scaffoldVideoCanvas(input, elementsInput, requested) {
|
|
39738
39906
|
const blueprint = VideoBlueprint.parse(input);
|
|
39739
39907
|
injectHookPhysicality(blueprint);
|
|
39740
39908
|
const elements = RecurringElements.parse(elementsInput);
|
|
39909
|
+
const reuseRefused = framesReuseRefusal(blueprint, elements, requested.frames);
|
|
39910
|
+
const opts = reuseRefused ? { ...requested, frames: "generate" } : requested;
|
|
39741
39911
|
const clock = spineClockFor(blueprint);
|
|
39742
39912
|
const nodes = [];
|
|
39743
39913
|
emitBlueprintIngests(opts, nodes);
|
|
@@ -39844,6 +40014,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
39844
40014
|
}
|
|
39845
40015
|
const todo = {
|
|
39846
40016
|
...buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts, blueprint),
|
|
40017
|
+
...reuseRefused ? { frames_reuse_refused: reuseRefused } : {},
|
|
39847
40018
|
...aspectRemapTodo(resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel))),
|
|
39848
40019
|
model_constraints: buildModelConstraints()
|
|
39849
40020
|
};
|
|
@@ -44651,6 +44822,12 @@ function videoDefinitionDescription(blueprint) {
|
|
|
44651
44822
|
const product = g.branding?.product;
|
|
44652
44823
|
return typeof product === "string" && product.trim() ? product.trim() : void 0;
|
|
44653
44824
|
}
|
|
44825
|
+
function framesModeReport(canvas, requested) {
|
|
44826
|
+
const todo = canvas.metadata?.todo;
|
|
44827
|
+
const mode = typeof todo?.frames_mode === "string" ? todo.frames_mode : requested;
|
|
44828
|
+
const refused = typeof todo?.frames_reuse_refused === "string" ? todo.frames_reuse_refused : void 0;
|
|
44829
|
+
return { frames_mode: mode, ...refused ? { frames_reuse_refused: refused } : {} };
|
|
44830
|
+
}
|
|
44654
44831
|
async function materializeReferenceVideo(fileArg2) {
|
|
44655
44832
|
if (!/^https?:\/\//i.test(fileArg2)) return path26.resolve(fileArg2);
|
|
44656
44833
|
let bytes;
|
|
@@ -44808,7 +44985,10 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
44808
44985
|
type: "string",
|
|
44809
44986
|
description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
|
|
44810
44987
|
},
|
|
44811
|
-
frames: {
|
|
44988
|
+
frames: {
|
|
44989
|
+
type: "string",
|
|
44990
|
+
description: `"generate" (default, every frame regenerated for our ad) or "reuse" (wire the source's real frames in). Reuse applies only to a video with nobody in it \u2014 on a video showing people it is refused and every frame is generated, so the source's actors never reach our ad.`
|
|
44991
|
+
},
|
|
44812
44992
|
ambient: {
|
|
44813
44993
|
type: "boolean",
|
|
44814
44994
|
description: "Give silent b-roll scenes native diegetic ambient mixed deep under the music bed (off by default)"
|
|
@@ -44996,6 +45176,7 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
44996
45176
|
error: { code: "validation", issues: validation.issues },
|
|
44997
45177
|
canvas_path: outPath,
|
|
44998
45178
|
prompt_path: blueprintPath,
|
|
45179
|
+
...framesModeReport(canvas, frames),
|
|
44999
45180
|
note: "The canvas WAS written despite the blocking issue(s) \u2014 fix them in place, then `baker canvas validate` before running."
|
|
45000
45181
|
},
|
|
45001
45182
|
null,
|
|
@@ -45045,7 +45226,7 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
45045
45226
|
source_reference: sourceRef.url,
|
|
45046
45227
|
composition_dir: compositionDest,
|
|
45047
45228
|
output: canvas.output,
|
|
45048
|
-
|
|
45229
|
+
...framesModeReport(canvas, frames),
|
|
45049
45230
|
models: {
|
|
45050
45231
|
deconstruct: deconstructModel,
|
|
45051
45232
|
select: selectModel,
|
|
@@ -47115,80 +47296,6 @@ async function declarationsOf(root, files) {
|
|
|
47115
47296
|
return sources.flatMap(eventDeclarationsIn);
|
|
47116
47297
|
}
|
|
47117
47298
|
|
|
47118
|
-
// src/commands/experiment/readout.ts
|
|
47119
|
-
function isoMoment(ms) {
|
|
47120
|
-
return ms === null ? null : new Date(ms).toISOString();
|
|
47121
|
-
}
|
|
47122
|
-
function isoDay2(ms) {
|
|
47123
|
-
return ms === null ? null : new Date(ms).toISOString().slice(0, 10);
|
|
47124
|
-
}
|
|
47125
|
-
function compactArm(arm) {
|
|
47126
|
-
return { visitors: arm.visitors, conversions: arm.conversions, rate: Math.round(arm.rate * 1e4) / 1e4 };
|
|
47127
|
-
}
|
|
47128
|
-
function compactMetric(metric) {
|
|
47129
|
-
return {
|
|
47130
|
-
name: metric.name,
|
|
47131
|
-
reading: metric.reading,
|
|
47132
|
-
control: metric.control.conversions,
|
|
47133
|
-
variant: metric.variant.conversions
|
|
47134
|
-
};
|
|
47135
|
-
}
|
|
47136
|
-
function fullArm(arm) {
|
|
47137
|
-
return {
|
|
47138
|
-
...arm,
|
|
47139
|
-
...arm.snapshotCapturedAt === void 0 ? {} : { snapshotCapturedAt: isoMoment(arm.snapshotCapturedAt) }
|
|
47140
|
-
};
|
|
47141
|
-
}
|
|
47142
|
-
function statusReadout(row, { full, now }) {
|
|
47143
|
-
const decisionWhen = {
|
|
47144
|
-
earliestDecisionAt: isoDay2(row.earliestDecisionAt),
|
|
47145
|
-
...row.earliestDecisionAt === null ? {} : { earliestDecisionIn: describeWait(row.earliestDecisionAt, now) }
|
|
47146
|
-
};
|
|
47147
|
-
const moments = {
|
|
47148
|
-
startedAt: isoMoment(row.startedAt),
|
|
47149
|
-
pausedAt: isoMoment(row.pausedAt),
|
|
47150
|
-
finishedAt: isoMoment(row.finishedAt)
|
|
47151
|
-
};
|
|
47152
|
-
if (full) {
|
|
47153
|
-
return {
|
|
47154
|
-
...row,
|
|
47155
|
-
...moments,
|
|
47156
|
-
...decisionWhen,
|
|
47157
|
-
control: fullArm(row.control),
|
|
47158
|
-
variant: fullArm(row.variant)
|
|
47159
|
-
};
|
|
47160
|
-
}
|
|
47161
|
-
return {
|
|
47162
|
-
experimentId: row.experimentId,
|
|
47163
|
-
landingSlug: row.landingSlug,
|
|
47164
|
-
variantSlug: row.variantSlug,
|
|
47165
|
-
status: row.status,
|
|
47166
|
-
verdict: row.verdict,
|
|
47167
|
-
summary: row.summary,
|
|
47168
|
-
...row.winner === null ? {} : { winner: row.winner },
|
|
47169
|
-
...row.invalidReason === null ? {} : { invalidReason: row.invalidReason },
|
|
47170
|
-
stillNeeded: row.stillNeeded,
|
|
47171
|
-
...decisionWhen,
|
|
47172
|
-
willNotConclude: row.willNotConclude,
|
|
47173
|
-
control: compactArm(row.control),
|
|
47174
|
-
variant: compactArm(row.variant),
|
|
47175
|
-
secondary: row.secondary.map(compactMetric),
|
|
47176
|
-
hypothesisStatement: row.hypothesisStatement,
|
|
47177
|
-
minDetectableRelativeLift: row.minDetectableRelativeLift,
|
|
47178
|
-
goal: row.goal,
|
|
47179
|
-
...row.variantGoal === null ? {} : { variantGoal: row.variantGoal },
|
|
47180
|
-
goalLabel: row.goalLabel,
|
|
47181
|
-
...moments,
|
|
47182
|
-
...row.status === "finished" ? {
|
|
47183
|
-
survivor: row.survivor,
|
|
47184
|
-
hypothesisOutcome: row.hypothesisOutcome,
|
|
47185
|
-
fold: row.fold,
|
|
47186
|
-
learning: row.learning,
|
|
47187
|
-
endReason: row.endReason
|
|
47188
|
-
} : {}
|
|
47189
|
-
};
|
|
47190
|
-
}
|
|
47191
|
-
|
|
47192
47299
|
// src/commands/experiment/update.ts
|
|
47193
47300
|
function buildUpdateRequest(args5, rawArgs) {
|
|
47194
47301
|
const expect = args5.expect === void 0 ? void 0 : String(args5.expect);
|
|
@@ -47430,26 +47537,18 @@ registerSchema({
|
|
|
47430
47537
|
args: {
|
|
47431
47538
|
id: { type: "string", description: "One test, by its id", required: false },
|
|
47432
47539
|
landing: LANDING_FILTER_ARG,
|
|
47433
|
-
full: {
|
|
47434
|
-
type: "boolean",
|
|
47435
|
-
description: "Every field the dashboard reads too \u2014 the hypothesis in parts, what each version owns, the preview pictures and the posteriors behind the verdict",
|
|
47436
|
-
required: false
|
|
47437
|
-
}
|
|
47540
|
+
full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
|
|
47438
47541
|
}
|
|
47439
47542
|
});
|
|
47440
47543
|
var statusCommand6 = defineCommand124({
|
|
47441
47544
|
meta: {
|
|
47442
47545
|
name: "status",
|
|
47443
|
-
description: "The verdict on each test. Read `verdict` and nothing else to decide: keep_running | winner | no_difference | cannot_win | invalid. `cannot_win` means even the best case is under the lift the test was looking for \u2014 end it and keep the original. `stillNeeded` is the data (visitors, conversions) the test still needs; a test is gated on data, never on days. `
|
|
47546
|
+
description: "The verdict on each test. Read `verdict` and nothing else to decide: keep_running | winner | no_difference | cannot_win | invalid. `cannot_win` means even the best case is under the lift the test was looking for \u2014 end it and keep the original. `stillNeeded` is the data (visitors, conversions) the test still needs; a test is gated on data, never on days. `summary` is one sentence you can show the client, and `secondary` lists every other conversion the company counts on the same two arms \u2014 context, never a second decision. The numbers under `--full` are for printing, never for deciding \u2014 a test that says keep_running has not finished, however good its numbers look."
|
|
47444
47547
|
},
|
|
47445
47548
|
args: {
|
|
47446
47549
|
id: { type: "string", description: "One test, by its id", required: false },
|
|
47447
47550
|
landing: LANDING_FILTER_ARG,
|
|
47448
|
-
full: {
|
|
47449
|
-
type: "boolean",
|
|
47450
|
-
description: "Every field the dashboard reads too \u2014 the hypothesis in parts, what each version owns, the preview pictures and the posteriors behind the verdict",
|
|
47451
|
-
required: false
|
|
47452
|
-
}
|
|
47551
|
+
full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
|
|
47453
47552
|
},
|
|
47454
47553
|
run: async ({ args: args5 }) => {
|
|
47455
47554
|
try {
|
|
@@ -47458,13 +47557,10 @@ var statusCommand6 = defineCommand124({
|
|
|
47458
47557
|
...args5.landing ? { landingSlug: String(args5.landing) } : {},
|
|
47459
47558
|
...args5.full === true ? { full: true } : {}
|
|
47460
47559
|
});
|
|
47461
|
-
const now = Date.now();
|
|
47462
47560
|
writeJsonEnvelope({
|
|
47463
47561
|
ok: true,
|
|
47464
|
-
data:
|
|
47465
|
-
|
|
47466
|
-
},
|
|
47467
|
-
hints: buildStatusHints(response.data.experiments, now)
|
|
47562
|
+
data: response.data,
|
|
47563
|
+
hints: buildStatusHints(response.data.experiments)
|
|
47468
47564
|
});
|
|
47469
47565
|
} catch (error) {
|
|
47470
47566
|
reportApiError(error);
|
|
@@ -57608,7 +57704,7 @@ function mentionsInSource(source, names) {
|
|
|
57608
57704
|
const out = [];
|
|
57609
57705
|
for (const name of names) {
|
|
57610
57706
|
const seen = /* @__PURE__ */ new Set();
|
|
57611
|
-
for (const m of visible.matchAll(new RegExp(`\\b${
|
|
57707
|
+
for (const m of visible.matchAll(new RegExp(`\\b${escapeRegExp2(name)}\\b`, "gi"))) {
|
|
57612
57708
|
const at = m.index ?? 0;
|
|
57613
57709
|
if (tables.some(([from, to]) => at >= from && at < to)) continue;
|
|
57614
57710
|
const start = sentenceStart(visible, at);
|
|
@@ -57637,7 +57733,7 @@ function blankHtmlKeepingOffsets(text2) {
|
|
|
57637
57733
|
}
|
|
57638
57734
|
return out.join("");
|
|
57639
57735
|
}
|
|
57640
|
-
function
|
|
57736
|
+
function escapeRegExp2(value) {
|
|
57641
57737
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
57642
57738
|
}
|
|
57643
57739
|
function detectSource(source) {
|