@koda-sl/baker-cli 0.135.1-dev.6a3e6978d → 0.136.0-dev.3ffe27f0b

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 CHANGED
@@ -2154,11 +2154,13 @@ baker videos tags
2154
2154
 
2155
2155
  Semantic search testimonials. For landing pages, query the product/offer and USP before filtering by rating so selected quotes match what the landing sells.
2156
2156
 
2157
+ Search reranks over the whole corpus with no language preference, so a corpus dominated by one language returns that language even for another-language query. Pass `--language <code>` to scope to a market. When results skew to one language and none was requested, the command emits a hint (in the `hints` array for `--output json`, on stderr for `--output md`/`files`).
2158
+
2157
2159
  ```bash
2158
2160
  baker testimonials search "great service"
2159
2161
  baker testimonials search "fast delivery" --rating-min 4 --tags quality
2160
2162
  baker testimonials search "competitor support" --source trustpilot --sentiment negative --rating-max 2
2161
- baker testimonials search "[product/offer] for [persona] that delivers [USP/outcome]" --rating-min 4 --output md --full --limit 12
2163
+ baker testimonials search "[product/offer] for [persona] that delivers [USP/outcome]" --rating-min 4 --language en --output md --full --limit 12
2162
2164
  ```
2163
2165
 
2164
2166
  ### `baker testimonials get <id>`
@@ -4058,6 +4060,8 @@ It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` inge
4058
4060
 
4059
4061
  **Automatic placement fan-out.** By default the canvas doesn't stop at one ratio: the hero renders once at the platform's primary ratio, then an `image_aspect_adapt` node recomposes it into that platform's full placement set, and the canvas `output` points at the adapted images. The placement set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or disable it with `--placements none` for a single base ad. The hero ratio passes through the adapt node **free**; each other ratio is a billed recomposition — and because adaptation re-generates, every ratio should be eyeballed for text and brand-color fidelity before publish (`ad-formats.md` in the scaffold skill covers this).
4060
4062
 
4063
+ The checklist `note` reminds you to fill **logo** slots from the company's committed brand logo in `src/brand/logos/` (per `BRAND.md`) — **not** from `baker images logo <domain>`, whose Brandfetch CDN often serves an outdated mark (a common cause of an old logo shipping on a live ad).
4064
+
4061
4065
  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.
4062
4066
 
4063
4067
  ```bash
package/dist/cli.js CHANGED
@@ -20335,7 +20335,7 @@ var scaffoldStaticAdCommand = defineCommand91({
20335
20335
  font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path (the describe pass recorded the ad's typefaces under `fonts` in prompt.json \u2014 match those). The font is wired into the render as a TYPE SPECIMEN reference so generated text takes the brand letterforms. Delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
20336
20336
  actor_sheets: report.actor_sheets.length > 0 ? `Each living hero (${report.actor_sheets.join(", ")}) is fused into a generated multi-view reference sheet (image_reference_sheet) that the render grounds on \u2014 so drop ONE clean photo at that hero's ingest and the sheet builds the consistent turnaround. Pass --skip-actor-sheets to ground on the lone photo instead.` : "none (no person/animal heroes detected, or --skip-actor-sheets)",
20337
20337
  placements: report.placements === null ? `Single format (${report.aspect_ratio}). Pass --placements meta_feed_stories|meta_all|linkedin|\u2026 to auto-fan-out to a platform's set.` : `Auto-fans the approved hero out to ${report.placements} \u2192 ${report.output_formats.join(", ")}. The hero ratio (${report.aspect_ratio}) passes through free; each other ratio is a billed render, AI-recomposed (not cropped). Re-read the copy and confirm the brand-color treatment on EVERY ratio before publishing \u2014 adaptation re-generates and can silently mangle text or flatten a color device. Pass --placements none for a single base ad.`,
20338
- note: "Populate as you go: for each [TODO] ingest slot, source its real asset and wire it into the slot right away \u2014 one at a time, not all sourced first then reconciled at the end. When every slot is filled, `baker canvas validate` then `baker canvas run`. Running generates a billed image \u2014 it is not free."
20338
+ note: "Populate as you go: for each [TODO] ingest slot, source its real asset and wire it into the slot right away \u2014 one at a time, not all sourced first then reconciled at the end. For any LOGO slot, use the company's committed brand logo from `src/brand/logos/` (per BRAND.md's Logos map) \u2014 do NOT re-fetch it with `baker images logo <domain>` when a brand logo exists, because the Brandfetch CDN often serves an OUTDATED mark and that is how an old logo ships on a live ad. When every slot is filled, `baker canvas validate` then `baker canvas run`. Running generates a billed image \u2014 it is not free."
20339
20339
  }
20340
20340
  },
20341
20341
  null,
@@ -27037,6 +27037,25 @@ var listCommand10 = defineCommand154({
27037
27037
 
27038
27038
  // src/commands/testimonials/search.ts
27039
27039
  import { defineCommand as defineCommand155 } from "citty";
27040
+ function languageBiasHint(results, requestedLanguage) {
27041
+ if (requestedLanguage) {
27042
+ return null;
27043
+ }
27044
+ const withLang = results.filter((r) => typeof r.language === "string" && r.language.length > 0);
27045
+ if (withLang.length < 3) {
27046
+ return null;
27047
+ }
27048
+ const counts = /* @__PURE__ */ new Map();
27049
+ for (const r of withLang) {
27050
+ const lang = r.language;
27051
+ counts.set(lang, (counts.get(lang) ?? 0) + 1);
27052
+ }
27053
+ const [topLang, topCount] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
27054
+ if (topLang && topCount / withLang.length >= 0.8) {
27055
+ return `Results skew ${topLang} (${topCount}/${withLang.length}). If you're sourcing quotes for a specific market, pass --language <code> (e.g. --language en) so the ad's language matches its audience.`;
27056
+ }
27057
+ return null;
27058
+ }
27040
27059
  registerSchema({
27041
27060
  command: "testimonials.search",
27042
27061
  description: "Search testimonials by text query. Uses hybrid BM25 + vector + reranking.",
@@ -27067,6 +27086,34 @@ registerSchema({
27067
27086
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
27068
27087
  }
27069
27088
  });
27089
+ function buildSearchRequest(query, args) {
27090
+ const body = { query };
27091
+ if (args.limit !== void 0) {
27092
+ body.limit = Number(args.limit);
27093
+ }
27094
+ if (args.source) {
27095
+ body.source = args.source;
27096
+ }
27097
+ if (args["rating-min"] !== void 0) {
27098
+ body.rating_min = Number(args["rating-min"]);
27099
+ }
27100
+ if (args["rating-max"] !== void 0) {
27101
+ body.rating_max = Number(args["rating-max"]);
27102
+ }
27103
+ if (args.status) {
27104
+ body.status = args.status;
27105
+ }
27106
+ if (args.sentiment) {
27107
+ body.sentiment = args.sentiment;
27108
+ }
27109
+ if (args.language) {
27110
+ body.language = args.language;
27111
+ }
27112
+ if (args.tags) {
27113
+ body.tags = args.tags.split(",").filter(Boolean);
27114
+ }
27115
+ return body;
27116
+ }
27070
27117
  var searchCommand2 = defineCommand155({
27071
27118
  meta: {
27072
27119
  name: "search",
@@ -27093,39 +27140,21 @@ var searchCommand2 = defineCommand155({
27093
27140
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Search query is required" } });
27094
27141
  process.exit(1);
27095
27142
  }
27096
- const body = { query };
27097
- if (args.limit !== void 0) {
27098
- body.limit = Number(args.limit);
27099
- }
27100
- if (args.source) {
27101
- body.source = args.source;
27102
- }
27103
- if (args["rating-min"] !== void 0) {
27104
- body.rating_min = Number(args["rating-min"]);
27105
- }
27106
- if (args["rating-max"] !== void 0) {
27107
- body.rating_max = Number(args["rating-max"]);
27108
- }
27109
- if (args.status) {
27110
- body.status = args.status;
27111
- }
27112
- if (args.sentiment) {
27113
- body.sentiment = args.sentiment;
27114
- }
27115
- if (args.language) {
27116
- body.language = args.language;
27117
- }
27118
- if (args.tags) {
27119
- body.tags = args.tags.split(",").filter(Boolean);
27120
- }
27143
+ const body = buildSearchRequest(query, args);
27121
27144
  const data = await apiPost("/api/testimonials/search", body);
27145
+ const hint = languageBiasHint(data, body.language);
27146
+ const outputFormat = args.output || "json";
27122
27147
  writeOutput(
27123
- { ok: true, data },
27124
- args.output || "json",
27148
+ { ok: true, data, ...hint ? { hints: [hint] } : {} },
27149
+ outputFormat,
27125
27150
  args.fields ? args.fields.split(",") : void 0,
27126
27151
  args.full,
27127
27152
  testimonialNormalizer
27128
27153
  );
27154
+ if (hint && outputFormat !== "json") {
27155
+ process.stderr.write(`${hint}
27156
+ `);
27157
+ }
27129
27158
  } catch (err) {
27130
27159
  if (err instanceof ApiError) {
27131
27160
  writeJson({ ok: false, error: { code: err.code, message: err.message } });