@koda-sl/baker-cli 0.445.0-dev.71ff4e44a → 0.449.0-dev.b6179c7f5
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/{chunk-CYZ7H4F5.js → chunk-Y33XAZ5M.js} +189 -110
- package/dist/chunk-Y33XAZ5M.js.map +1 -0
- package/dist/cli.js +176 -81
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +3 -6
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-CYZ7H4F5.js.map +0 -1
package/README.md
CHANGED
|
@@ -1814,10 +1814,10 @@ baker creatives make <idea> [--slug <s>] [--adset <id>] [--reference <adId>] [--
|
|
|
1814
1814
|
baker creatives learn [--days <n>] [--dry-run] # what each idea's ads did, written back onto the idea
|
|
1815
1815
|
```
|
|
1816
1816
|
|
|
1817
|
-
- `brief` returns a verdict, not raw numbers: `account.bottleneck` is `hook`, `after_click`, `none` or `too_little_data`. A part that cannot be read says `unavailable
|
|
1817
|
+
- `brief` returns a verdict, not raw numbers: `account.bottleneck` is `hook`, `after_click`, `none` or `too_little_data`. A part that cannot be read says `unavailable`; the market search gets 10 seconds, so a slow ad library leaves it out rather than holding the brief. With no ideas on file, its hint lists the values `awareness`, `angle` and `format` take. `market` says what competitors' ads already say (the followed brands, else the library), how sophisticated the market is, and which awareness stages nobody speaks to.
|
|
1818
1818
|
- `ideas` is free and local. An idea's `evidence` resolves to `src/content/insights/` entries, or it is an `explore` bet. Numbers must be in the Knowledge or our own customers' quotes (a Reddit, forum or web quote never sources a claim), no two ideas may share an angle and format, and a review or testimonial format needs a quote from our own customer.
|
|
1819
1819
|
- `learn` pools each built idea's creatives, judges them against the rest of the account's cost per result and writes `status` (`winner`, `failed`, `inconclusive`, `testing`) and a frozen `result` onto the idea. `ideas` then warns when a new idea repeats a failed one.
|
|
1820
|
-
- `make` builds one idea. A static searches the idea's container for ads made for our viewer, checks the closest three with `winning-ads fit` and scaffolds from the first that fits. A video idea takes its beats spec. Either way it records the idea on `_definition.md`, sets `prompt.json`'s `message` and syncs the definition. Drafted copy lines claiming a figure the company never wrote come back first in `hints`. When no proven ad fits (or with `--from-idea`), a static starts from the idea itself: `prompt.json` holds the message and placeholders, and `canvas run` refuses to render until the scene and copy are written.
|
|
1820
|
+
- `make` builds one idea. A static searches the idea's container for ads made for our viewer, checks the closest three with `winning-ads fit` and scaffolds from the first that fits. For a new creative it starts scaffolding the best-ranked one while the fits are asked (kept when it fits, stopped and removed whole when another or none is kept), so the reference's describe pass no longer waits for them. A creative whose ad is already written is never built over (`ALREADY_MADE`): edit its `prompt.json`, or pass `--slug` for another ad of the same idea. A video idea takes its beats spec. Either way it records the idea on `_definition.md`, sets `prompt.json`'s `message` and syncs the definition. Drafted copy lines claiming a figure the company never wrote come back first in `hints`. When no proven ad fits (or with `--from-idea`), a static starts from the idea itself: `prompt.json` holds the message and placeholders, and `canvas run` refuses to render until the scene and copy are written.
|
|
1821
1821
|
|
|
1822
1822
|
### Competitive Intelligence (`baker research`)
|
|
1823
1823
|
|
|
@@ -4627,7 +4627,7 @@ Open record keyed by the composition's `meta.json` inputs.
|
|
|
4627
4627
|
|
|
4628
4628
|
##### `static_prefilter`
|
|
4629
4629
|
|
|
4630
|
-
Local; no credits (one box-reader call per variant, about $0.005, which rides on the run record's review spend). Ranks a static ad's variants before the judge by the deterministic checks (blocking defects, then every line of the ad spec's copy read back with its box, the brand's logo found, the button's words, every word and the logo inside the master's band) and passes on only the best-ranked, so the judge only breaks an exact tie. A variant the reader could not read ties with the best. **Inputs:** `images` (the variants' whole list), `spec` (the ad spec, `$ref:prompt.asset`). **Params:** `formats` (the delivered sizes). **Outputs:** `images` (the best-ranked variants, in an order seeded by the pictures rather than the render order; never empty: when every variant fails, the one with the fewest defects passes as `flagged`), `order` (which variant each of `images` is), `dropped` (each variant set aside, with its index and reasons), `flagged` (the variant kept when all failed) and `usage` (the reader's spend). Scaffolded by `scaffold-static-ad` between the variants and the judge.
|
|
4630
|
+
Local; no credits (one box-reader call per variant, about $0.005, which rides on the run record's review spend). Ranks a static ad's variants before the judge by the deterministic checks (blocking defects, then every line of the ad spec's copy read back with its box, the brand's logo found, the button's words, every word and the logo inside the master's band) and passes on only the best-ranked, so the judge only breaks an exact tie. A variant the reader could not read ties with the best. **Inputs:** `images` (the variants' whole list), `spec` (the ad spec, `$ref:prompt.asset`). **Params:** `formats` (the delivered sizes). **Outputs:** `images` (the best-ranked variants, in an order seeded by the pictures rather than the render order; never empty: when every variant fails, the one with the fewest defects passes as `flagged`), `order` (which variant each of `images` is), `dropped` (each variant set aside, with its index and reasons), `flagged` (the variant kept when all failed), `image` (the chosen picture: when a variant rendered at another shape, a 3:4 for a 4:5, would lose a word or the button to a cut about its centre, it comes cut to the master's frame where every word stays whole, else as rendered) and `usage` (the reader's spend). Scaffolded by `scaffold-static-ad` between the variants and the judge.
|
|
4631
4631
|
|
|
4632
4632
|
##### `static_safe_fit`
|
|
4633
4633
|
|
|
@@ -5879,7 +5879,7 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
|
|
|
5879
5879
|
|
|
5880
5880
|
**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. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. 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`).
|
|
5881
5881
|
|
|
5882
|
-
**Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 2 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint only the margins that leaves and lays the ad's own pixels back over the painting, so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear is laid out again instead); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size the review flagged follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it, and a size it found nothing blocking on is delivered as it was (`keep`, nothing in it made again or placed); anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`). A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`) and is not read again: the review reads only the sizes the round changed. Each round is one render (a fix, or one request per re-laid size) and prints `[autofix] round N of 2: …` on stdout; the final JSON's hints start with "Corrected automatically before this result, …" and its review is of the corrected version. Blocking findings left after the rounds keep the hints below. A round that would only repeat the correction that just left its defect in place (a chosen option, `--use-variant`, is only ever corrected) is not run: that version is reported as it is. `--no-autofix` reports after the first render.
|
|
5882
|
+
**Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 2 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint only the margins that leaves and lays the ad's own pixels back over the painting, so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear is laid out again instead); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size the review flagged follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it, and a size it found nothing blocking on is delivered as it was (`keep`, nothing in it made again or placed); anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`); sizes that do not show the same ad (`SIZES:`) are always the re-laid ones' fault, since every size follows the chosen picture, so they are laid out again to show exactly what it shows, never the picture edited to match them. A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`) and is not read again: the review reads only the sizes the round changed. Each round is one render (a fix, or one request per re-laid size) and prints `[autofix] round N of 2: …` on stdout; the final JSON's hints start with "Corrected automatically before this result, …" and its review is of the corrected version. Blocking findings left after the rounds keep the hints below. A round that would only repeat the correction that just left its defect in place (a chosen option, `--use-variant`, is only ever corrected) is not run: that version is reported as it is. `--no-autofix` reports after the first render.
|
|
5883
5883
|
|
|
5884
5884
|
**Static ads: correct a version, or use another of its variants.** Correct a finished version with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word; it replaces the variants and the judge, and every size is made again from the corrected picture. **A `prompt.json` changed only in its words, their positions or its colours** since the version shown is made the same way, as an edit of that version (`[edit]` in the output, the instruction read off the change); three new options are asked for only with `--regenerate static_variants`. A `--fix` whose defect is still blocking afterwards is recorded, and asking for it again with the same words exits 2 with `no_progress`. When every blocking finding is on re-laid-out sizes, the first hint says to lay out those sizes again (`--regenerate adapt_<size>`) before any `--fix`. Every static run's hints say what it cost in credits and what for. There is no render limit per creative. Use another variant with `--use-variant <sha256>` (`--from <run_id>`, default the last render): when the person chose another of a version's variants over the judge's pick, every size is made again from that picture, and the only requests re-lay it out for each other size; `--regenerate adapt_9x16` lays out one size again from the same chosen picture when only that size came out wrong (after a `--fix` or `--use-variant`, from that corrected or chosen picture, so the size never brings back the uncorrected one; `--from <run_id>` names another version); `baker creatives feedback --slug <slug>` prints the choice (`variantChoice`) and the exact command.
|
|
5885
5885
|
|
|
@@ -6009,7 +6009,7 @@ The canvas is validated before it is written. stdout returns `{ ok, canvas_path,
|
|
|
6009
6009
|
|
|
6010
6010
|
**Who sees the ad.** `--target-adset <id>` names the Meta ad set the ad will run in. Before anything is billed, the viewer its targeting reaches is read exactly as `baker ads meta adsets --audience` reads it (`temperature`, what they already `knows`, `placements`) and written to `_definition.md` as `targetAdSet` and `viewer` (an existing definition keeps every other field). The sizes, the brief and the prefilter keep clear of that viewer's placements (`staticSafeArea`; `automatic`, a bare `facebook`/`instagram` or no placements is Advantage+, Reels included), and every `baker canvas run` checks the ad against that viewer. With no ad account, give the viewer from the Knowledge: `--viewer-temperature cold|warm|hot [--viewer-knows brand,offer,product] [--viewer-placements <placement,…>]`, recorded with `source: knowledge`; next to `--target-adset` they correct the read, and its `reasons` say so. A bad value exits 2 (`invalid_viewer`) before any billed pass; an ad set that cannot be read never stops the scaffold (a hint says why, and the sizes keep clear of every placement). With neither flag, a `viewer` (or `targetAdSet`) already in `_definition.md` is used as it is. stdout carries `target_adset`, `viewer` and a `checklist.viewer` line.
|
|
6011
6011
|
|
|
6012
|
-
**After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, as does a capital I read as an l (one stroke in a sans-serif), accents do not; words the picture adds are blocking only when set at the copy's usual size or making a claim (a figure, a price, an offer), and otherwise a note, as a screen's code, a poster or a mug in the scene; a copy line is "on the ad twice" only when most of its words repeat, never a word or two echoed on a prop; on a native screen, which code typesets from the spec, a line read differently is the reader's error and only a missing one counts); **the ad's own copy and its logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas; `must-show.ts` decides what is measured: never what the scene prints, nor a copy line printed again in the scene while the line itself sits clear). On a 9:16, which runs in Stories and Reels alike, what Stories' header or bottom bar covers (and any other app's interface named) is said as "where Stories and Reels cover it" ("where Reels covers it" for an ad set that runs only in Reels) and blocks for the logo, the button, the headline, the figure and the offer, while a supporting line (body, caption, label, small print) there is a note; what only Reels' taller footer covers is one note, and only for the lead line, since Stories shows it and the 9:16 is laid out in Stories' band; words merely close to where an app draws are no finding. The steps that place a size clear of an interface (`static_safe_fit`, `static_place`) are told the same copy (`prompt.json`'s lines, their roles, and the brand's name and the logo's words, a wordmark read close to them counting as the logo) and move the ad for those alone, a supporting line included only where the frame cuts it; on a feed image such as a 4:5, only a design margin, so words close to the edge are a warning and only words the frame cuts are blocking); a copy line written over two lines (`"15%\noff"`) is matched part by part wherever the reader finds each; designed type in a colour far from the brand palette is a warning; supporting lines too small to read on a phone are one note per size; the main line set smaller than a supporting one is a note, the brand's wordmark never counted as part of a line that starts with its name; the logo set twice is a note, a far smaller mark printed on something in the scene (a hoodie, a bag) is not. When the copy names a real person ("Lautaro Suarez, CTO at Vibiz"), the run casts them as themselves before it renders (`baker avatars research --create`, their avatar's sheet added to `prompt.json` `references` as a `PERSON_n`), and a face shown as a named person the ad has no photo of is blocking. When the reader cannot answer, the vision pass transcribes the copy instead and judges the safe zones by eye. Then one vision pass for clipping, overlaps, artifacts, one dominant idea, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-candidate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed, and the run is recorded with `presentable: false` so it is never shown as a version; every fix is an edit to `prompt.json` and a re-run (wrong words: the lines in `copy`; wrong picture: the `scene`; a bad roll: `--regenerate static_variants`; one size wrong while the chosen picture is right: `--regenerate adapt_<size>`, which re-lays it out, named in the hints); a blocking defect is first corrected by the run itself (`--autofix`, above). The checks rank the variants before the pick and the judge only breaks an exact tie, comparing pairwise against the compiled brief, knowing the brand and the viewer; every variant stays on offer to the person in Baker. Within one chat turn an ad gets two runs you start (each with its automatic corrections), and none once it is `ready` or `ready_with_notes`: the next is refused (`CONFLICT`, exit 2) with what to show instead; the person's next message starts a new turn (`turn-renders.ts`). When a defect still stands after the automatic rounds, the hints say to fix it with a `--fix`, then by changing the spec or the approach, and if it still cannot be fixed to show the best version and name what is left in one plain sentence, without a question.
|
|
6012
|
+
**After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, as does a capital I read as an l (one stroke in a sans-serif), accents do not; words the picture adds are blocking only when set at the copy's usual size or making a claim (a figure, a price, an offer), and otherwise a note, as a screen's code, a poster or a mug in the scene, except a price (a currency sign beside a figure) the copy does not hold, blocking at any size and blurred by the correction, the document it is on kept; every render is asked for no readable prices on a bill, quote, receipt, screen or price tag beyond the copy; a copy line is "on the ad twice" only when most of its words repeat, never a word or two echoed on a prop; on a native screen, which code typesets from the spec, a line read differently is the reader's error and only a missing one counts); **the ad's own copy and its logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas; `must-show.ts` decides what is measured: never what the scene prints, nor a copy line printed again in the scene while the line itself sits clear). On a 9:16, which runs in Stories and Reels alike, what Stories' header or bottom bar covers (and any other app's interface named) is said as "where Stories and Reels cover it" ("where Reels covers it" for an ad set that runs only in Reels) and blocks for the logo, the button, the headline, the figure and the offer, while a supporting line (body, caption, label, small print) there is a note; what only Reels' taller footer covers is one note, and only for the lead line, since Stories shows it and the 9:16 is laid out in Stories' band; words merely close to where an app draws are no finding. The steps that place a size clear of an interface (`static_safe_fit`, `static_place`) are told the same copy (`prompt.json`'s lines, their roles, and the brand's name and the logo's words, a wordmark read close to them counting as the logo) and move the ad for those alone, a supporting line included only where the frame cuts it; on a feed image such as a 4:5, only a design margin, so words close to the edge are a warning and only words the frame cuts are blocking); a copy line written over two lines (`"15%\noff"`) is matched part by part wherever the reader finds each; designed type in a colour far from the brand palette is a warning; supporting lines too small to read on a phone are one note per size; the main line set smaller than a supporting one is a note, the brand's wordmark never counted as part of a line that starts with its name; the logo set twice is a note, a far smaller mark printed on something in the scene (a hoodie, a bag) is not. When the copy names a real person ("Lautaro Suarez, CTO at Vibiz"), the run casts them as themselves before it renders (`baker avatars research --create`, their avatar's sheet added to `prompt.json` `references` as a `PERSON_n`), and a face shown as a named person the ad has no photo of is blocking. When the reader cannot answer, the vision pass transcribes the copy instead and judges the safe zones by eye. Then one vision pass for clipping, overlaps, artifacts, one dominant idea, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-candidate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed, and the run is recorded with `presentable: false` so it is never shown as a version; every fix is an edit to `prompt.json` and a re-run (wrong words: the lines in `copy`; wrong picture: the `scene`; a bad roll: `--regenerate static_variants`; one size wrong while the chosen picture is right: `--regenerate adapt_<size>`, which re-lays it out, named in the hints); a blocking defect is first corrected by the run itself (`--autofix`, above). The checks rank the variants before the pick and the judge only breaks an exact tie, comparing pairwise against the compiled brief, knowing the brand and the viewer; every variant stays on offer to the person in Baker. Within one chat turn an ad gets two runs you start (each with its automatic corrections), and none once it is `ready` or `ready_with_notes`: the next is refused (`CONFLICT`, exit 2) with what to show instead; the person's next message starts a new turn (`turn-renders.ts`). When a defect still stands after the automatic rounds, the hints say to fix it with a `--fix`, then by changing the spec or the approach, and if it still cannot be fixed to show the best version and name what is left in one plain sentence, without a question.
|
|
6013
6013
|
|
|
6014
6014
|
The render prompt is **model-aware**: each model's profile (`src/engine/lib/prompt-profiles.ts`) sets its shape, word budget, picture order and request params. gpt-image gets short labelled sections with the constraints last; Gemini gets sentences that give the reason and a positive closing list of the only words allowed. A profile change, or a new model on the router (`src/engine/lib/image-model-router.ts`), is re-run on the static bench (`scripts/creative-evals`, `--job static`) before it ships. Run `baker canvas critique` on the scaffolded canvas for an advisory read before you spend.
|
|
6015
6015
|
|
|
@@ -3594,23 +3594,6 @@ function resolveConcurrency(...candidates) {
|
|
|
3594
3594
|
}
|
|
3595
3595
|
return DEFAULT_CONCURRENCY;
|
|
3596
3596
|
}
|
|
3597
|
-
async function mapWithConcurrency(items, limit, fn) {
|
|
3598
|
-
const results = new Array(items.length);
|
|
3599
|
-
let next = 0;
|
|
3600
|
-
const worker = async () => {
|
|
3601
|
-
while (next < items.length) {
|
|
3602
|
-
const index = next++;
|
|
3603
|
-
try {
|
|
3604
|
-
results[index] = { status: "fulfilled", value: await fn(items[index], index) };
|
|
3605
|
-
} catch (reason) {
|
|
3606
|
-
results[index] = { status: "rejected", reason };
|
|
3607
|
-
}
|
|
3608
|
-
}
|
|
3609
|
-
};
|
|
3610
|
-
const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
|
|
3611
|
-
await Promise.all(workers);
|
|
3612
|
-
return results;
|
|
3613
|
-
}
|
|
3614
3597
|
|
|
3615
3598
|
// src/engine/lib/json-walk.ts
|
|
3616
3599
|
function isContainerObject(value) {
|
|
@@ -6706,6 +6689,7 @@ function textFidelityDirective(strings2, hasFont) {
|
|
|
6706
6689
|
return [
|
|
6707
6690
|
exact,
|
|
6708
6691
|
"Render NO other words anywhere in the image \u2014 no invented labels, captions, sub-headers, or filler text.",
|
|
6692
|
+
"Any bill, quote, receipt, screen or price tag in the scene shows no readable prices or amounts beyond these strings: its figures are blurred or out of focus.",
|
|
6709
6693
|
"For any brand name or uncommon word, lock its letters exactly as written (spell it out letter-by-letter if that helps the characters render correctly).",
|
|
6710
6694
|
"Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
|
|
6711
6695
|
].join("\n");
|
|
@@ -6956,8 +6940,9 @@ var FIELD_SHAPES = {
|
|
|
6956
6940
|
references: '"references": [{"id": "PRODUCT_1", "role": "product", "what": "the solar panel kit"}]',
|
|
6957
6941
|
third_party_brands: '"third_party_brands": [{"name": "Trustpilot", "role": "proof", "logo": "BADGE_1"}]',
|
|
6958
6942
|
mechanism: '"mechanism": [{"device": "a before and after", "why_it_works": "the change is seen, not told"}]',
|
|
6959
|
-
viewer: '"viewer": {"who": "homeowners with high energy bills"}'
|
|
6943
|
+
viewer: '"viewer": {"who": "homeowners with high energy bills", "knows": "nothing about us yet"}'
|
|
6960
6944
|
};
|
|
6945
|
+
var SPEC_SHAPES_HINT = `prompt.json shapes: ${["copy", "colors", "viewer"].map((f) => FIELD_SHAPES[f]).join("; ")}.`;
|
|
6961
6946
|
function adSpecErrorText(e) {
|
|
6962
6947
|
if (!(e instanceof z7.ZodError)) return e instanceof Error ? e.message : String(e);
|
|
6963
6948
|
const items = /* @__PURE__ */ new Map();
|
|
@@ -10180,7 +10165,7 @@ function checkOrphanNodes(ctx) {
|
|
|
10180
10165
|
path: `nodes[${ctx.idToIndex.get(n.id)}]`,
|
|
10181
10166
|
code: STAGE_CODES.ORPHAN_NODE,
|
|
10182
10167
|
severity: "warning",
|
|
10183
|
-
message: `node "${n.id}" (${n.type}) is not reachable from output "${out.node}"
|
|
10168
|
+
message: `node "${n.id}" (${n.type}) is not reachable from output "${out.node}": a run skips it, so nothing it would make is used; wire it in or remove it`
|
|
10184
10169
|
});
|
|
10185
10170
|
}
|
|
10186
10171
|
}
|
|
@@ -10251,6 +10236,17 @@ function unwrap(schema) {
|
|
|
10251
10236
|
}
|
|
10252
10237
|
|
|
10253
10238
|
// src/engine/engine/executor.ts
|
|
10239
|
+
function throwRunOutcome(signal, failures, held) {
|
|
10240
|
+
if (signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
|
|
10241
|
+
if (failures.length === 1 && failures[0]) throw failures[0].reason;
|
|
10242
|
+
if (failures.length > 1) throw new LayerExecutionError([...failures]);
|
|
10243
|
+
if (held.length > 0) {
|
|
10244
|
+
throw new RunAbortedError(
|
|
10245
|
+
"checkpoint",
|
|
10246
|
+
`stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
|
|
10247
|
+
);
|
|
10248
|
+
}
|
|
10249
|
+
}
|
|
10254
10250
|
function heldByCheckpoint(canvas, layer, kinds) {
|
|
10255
10251
|
if (!kinds?.size) return [];
|
|
10256
10252
|
return layer.filter((id) => {
|
|
@@ -10339,45 +10335,61 @@ var Engine = class {
|
|
|
10339
10335
|
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
|
|
10340
10336
|
}
|
|
10341
10337
|
async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
|
|
10342
|
-
const
|
|
10338
|
+
const order = topologicalLayers(graph).flat();
|
|
10343
10339
|
const limit = resolveConcurrency(opts.concurrency);
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
duration_ms: r.durationMs,
|
|
10361
|
-
credits: r.credits
|
|
10362
|
-
};
|
|
10363
|
-
nodeRuns.push(run);
|
|
10364
|
-
this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
|
|
10365
|
-
}
|
|
10366
|
-
});
|
|
10367
|
-
});
|
|
10368
|
-
const failures = [];
|
|
10369
|
-
settled.forEach((result, i) => {
|
|
10370
|
-
const nodeId = layer[i];
|
|
10371
|
-
if (result.status === "rejected" && nodeId) {
|
|
10372
|
-
if (result.reason instanceof RunAbortedError) return;
|
|
10373
|
-
failures.push({ nodeId, reason: result.reason });
|
|
10374
|
-
this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(result.reason) });
|
|
10340
|
+
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before dispatch");
|
|
10341
|
+
const waitingOn = new Map(order.map((id) => [id, new Set(graph.get(id) ?? [])]));
|
|
10342
|
+
const running = /* @__PURE__ */ new Map();
|
|
10343
|
+
const failures = [];
|
|
10344
|
+
const held = [];
|
|
10345
|
+
const dispatch = (nodeId) => {
|
|
10346
|
+
waitingOn.delete(nodeId);
|
|
10347
|
+
this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
|
|
10348
|
+
const settled = this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then(
|
|
10349
|
+
(r) => {
|
|
10350
|
+
this.recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts);
|
|
10351
|
+
for (const deps of waitingOn.values()) deps.delete(nodeId);
|
|
10352
|
+
},
|
|
10353
|
+
(reason) => {
|
|
10354
|
+
failures.push({ nodeId, reason });
|
|
10355
|
+
this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(reason) });
|
|
10375
10356
|
}
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10357
|
+
);
|
|
10358
|
+
running.set(
|
|
10359
|
+
nodeId,
|
|
10360
|
+
settled.finally(() => running.delete(nodeId))
|
|
10361
|
+
);
|
|
10362
|
+
};
|
|
10363
|
+
const isHeld = (nodeId) => heldByCheckpoint(canvas, [nodeId], opts.stop_before_kinds).length > 0;
|
|
10364
|
+
for (; ; ) {
|
|
10365
|
+
const ready = opts.signal?.aborted ? [] : order.filter((id) => waitingOn.get(id)?.size === 0);
|
|
10366
|
+
for (const nodeId of ready) {
|
|
10367
|
+
if (running.size >= limit) break;
|
|
10368
|
+
if (isHeld(nodeId)) {
|
|
10369
|
+
waitingOn.delete(nodeId);
|
|
10370
|
+
held.push(nodeId);
|
|
10371
|
+
} else dispatch(nodeId);
|
|
10372
|
+
}
|
|
10373
|
+
if (running.size === 0) break;
|
|
10374
|
+
await Promise.race(running.values());
|
|
10380
10375
|
}
|
|
10376
|
+
throwRunOutcome(opts.signal, failures, held);
|
|
10377
|
+
}
|
|
10378
|
+
/** A settled node counted, recorded and announced. */
|
|
10379
|
+
recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts) {
|
|
10380
|
+
if (r.cached) counters.cachedNodes++;
|
|
10381
|
+
counters.totalCredits += r.credits;
|
|
10382
|
+
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
10383
|
+
if (!node) return;
|
|
10384
|
+
const run = {
|
|
10385
|
+
node_id: nodeId,
|
|
10386
|
+
node_type: node.type,
|
|
10387
|
+
cached: r.cached,
|
|
10388
|
+
duration_ms: r.durationMs,
|
|
10389
|
+
credits: r.credits
|
|
10390
|
+
};
|
|
10391
|
+
nodeRuns.push(run);
|
|
10392
|
+
this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
|
|
10381
10393
|
}
|
|
10382
10394
|
/** Progress consumers are observers only — an exception there must never fail the run. */
|
|
10383
10395
|
emitProgress(opts, event) {
|
|
@@ -10469,20 +10481,6 @@ var Engine = class {
|
|
|
10469
10481
|
this.logOutputWarnings(node.id, outputsObj);
|
|
10470
10482
|
return { cached: false, credits, durationMs: elapsed };
|
|
10471
10483
|
}
|
|
10472
|
-
/**
|
|
10473
|
-
* Every reason a layer must not dispatch, checked before any of its nodes starts, so a stop
|
|
10474
|
-
* never leaves a layer half billed: a signal, the cost cap and a checkpoint.
|
|
10475
|
-
*/
|
|
10476
|
-
assertLayerMayRun(canvas, layer, opts) {
|
|
10477
|
-
if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
|
|
10478
|
-
const held = heldByCheckpoint(canvas, layer, opts.stop_before_kinds);
|
|
10479
|
-
if (held.length > 0) {
|
|
10480
|
-
throw new RunAbortedError(
|
|
10481
|
-
"checkpoint",
|
|
10482
|
-
`stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
|
|
10483
|
-
);
|
|
10484
|
-
}
|
|
10485
|
-
}
|
|
10486
10484
|
/** A cache hit replayed as this node's result: its outputs written, and free. */
|
|
10487
10485
|
async replayCached(node, cacheKey, outputs, writer) {
|
|
10488
10486
|
const cacheT0 = Date.now();
|
|
@@ -15752,7 +15750,7 @@ function contextItems(opts) {
|
|
|
15752
15750
|
opts.thumbnails ? "7. THUMBNAIL: at thumbnail size the lead line (the largest words) cannot be read. Judge only whether those words can be read, nothing else. That is blocking: most people only ever see the ad that small." : "7. THUMBNAIL: the lead line (the largest words) would be unreadable on a phone feed. That is blocking."
|
|
15753
15751
|
],
|
|
15754
15752
|
sizes: opts.formats.length > 1 ? [
|
|
15755
|
-
|
|
15753
|
+
`12. SIZES: one size loses what the others are built on: the letter sheet, the email or chat window, the post card, the comic's panels and their order, the product the ad is about, or a size that shows something the others do not (blocking; start the finding with "SIZES:" and report it on the size that differs). Rearranged for its shape is fine.`
|
|
15756
15754
|
] : []
|
|
15757
15755
|
};
|
|
15758
15756
|
}
|
|
@@ -15805,6 +15803,7 @@ function mostOfALine(chunk, copy) {
|
|
|
15805
15803
|
});
|
|
15806
15804
|
}
|
|
15807
15805
|
var CLAIM = /[%€$£]|\b(?:free|gratis|gratuit[oa]?s?|off|sale|oferta|descuento|rebajas?|promo)\b/i;
|
|
15806
|
+
var PRICE = /[€$£]\s?\d|\d\s?[€$£]/;
|
|
15808
15807
|
var COPY_SIZE_SHARE = 0.8;
|
|
15809
15808
|
function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
15810
15809
|
if (extra.length === 0) return [];
|
|
@@ -15822,7 +15821,8 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
|
15822
15821
|
const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!CLAIM.test(chunk) || scenery(chunk));
|
|
15823
15822
|
const quote = (chunks) => chunks.map((e) => `"${e}"`).join(", ");
|
|
15824
15823
|
const asCopy = extra.filter((chunk) => !printed(chunk));
|
|
15825
|
-
const
|
|
15824
|
+
const prices = extra.filter((chunk) => printed(chunk) && PRICE.test(chunk));
|
|
15825
|
+
const inScene = extra.filter((chunk) => printed(chunk) && !prices.includes(chunk));
|
|
15826
15826
|
return [
|
|
15827
15827
|
...asCopy.length > 0 ? [
|
|
15828
15828
|
{
|
|
@@ -15831,6 +15831,13 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
|
15831
15831
|
what: `${format}: the picture adds words that aren't in the copy: ${quote(asCopy)}. Remove them, or add them to the copy.`
|
|
15832
15832
|
}
|
|
15833
15833
|
] : [],
|
|
15834
|
+
...prices.length > 0 ? [
|
|
15835
|
+
{
|
|
15836
|
+
severity: "blocking",
|
|
15837
|
+
// `static-autofix.ts` SCENE_PRICES reads this sentence to blur them.
|
|
15838
|
+
what: `${format}: the picture shows prices the copy does not hold: ${quote(prices)}. Make them unreadable.`
|
|
15839
|
+
}
|
|
15840
|
+
] : [],
|
|
15834
15841
|
...inScene.length > 0 ? [
|
|
15835
15842
|
{
|
|
15836
15843
|
severity: "warning",
|
|
@@ -16637,12 +16644,64 @@ import { z as z26 } from "zod";
|
|
|
16637
16644
|
// src/engine/nodes/local/lib/variant-prefilter.ts
|
|
16638
16645
|
import { createHash as createHash3 } from "crypto";
|
|
16639
16646
|
import sharp8 from "sharp";
|
|
16640
|
-
function
|
|
16647
|
+
function coverOf(from, to) {
|
|
16641
16648
|
const scale = Math.max(to.width / from.width, to.height / from.height);
|
|
16642
|
-
const
|
|
16643
|
-
const
|
|
16644
|
-
|
|
16645
|
-
|
|
16649
|
+
const width = from.width * scale;
|
|
16650
|
+
const height = from.height * scale;
|
|
16651
|
+
return { scale, width, height, centre: { left: (width - to.width) / 2, top: (height - to.height) / 2 } };
|
|
16652
|
+
}
|
|
16653
|
+
function framingOf(from, to) {
|
|
16654
|
+
let frame;
|
|
16655
|
+
return {
|
|
16656
|
+
place: (boxes) => {
|
|
16657
|
+
frame = frameKeeping(boxes, from, to);
|
|
16658
|
+
return coverFitBoxes(boxes, from, to, frame);
|
|
16659
|
+
},
|
|
16660
|
+
moved: () => {
|
|
16661
|
+
const { centre } = coverOf(from, to);
|
|
16662
|
+
return frame && (Math.abs(frame.left - centre.left) >= 1 || Math.abs(frame.top - centre.top) >= 1) ? frame : void 0;
|
|
16663
|
+
}
|
|
16664
|
+
};
|
|
16665
|
+
}
|
|
16666
|
+
async function cutToFrame(bytes, frame, to) {
|
|
16667
|
+
const { width = 0, height = 0 } = await sharp8(bytes).metadata();
|
|
16668
|
+
const { scale } = coverOf({ width, height }, to);
|
|
16669
|
+
const w = Math.min(width, Math.round(to.width / scale));
|
|
16670
|
+
const h = Math.min(height, Math.round(to.height / scale));
|
|
16671
|
+
const left = Math.min(width - w, Math.max(0, Math.round(frame.left / scale)));
|
|
16672
|
+
const top = Math.min(height - h, Math.max(0, Math.round(frame.top / scale)));
|
|
16673
|
+
return sharp8(bytes).extract({ left, top, width: w, height: h }).png().toBuffer();
|
|
16674
|
+
}
|
|
16675
|
+
var FRAME_MARGIN = 0.02;
|
|
16676
|
+
function frameKeeping(boxes, from, to) {
|
|
16677
|
+
const cover = coverOf(from, to);
|
|
16678
|
+
if (boxes.length === 0) return cover.centre;
|
|
16679
|
+
const along = (offset2, extent, frame, lo, hi) => {
|
|
16680
|
+
const low = lo * extent;
|
|
16681
|
+
const high = hi * extent;
|
|
16682
|
+
const room = extent - frame;
|
|
16683
|
+
const margin = FRAME_MARGIN * frame;
|
|
16684
|
+
for (const m of [margin, 0]) {
|
|
16685
|
+
const min = Math.max(0, high + m - frame);
|
|
16686
|
+
const max = Math.min(room, low - m);
|
|
16687
|
+
if (min <= max) return Math.min(max, Math.max(min, offset2));
|
|
16688
|
+
}
|
|
16689
|
+
return offset2;
|
|
16690
|
+
};
|
|
16691
|
+
const x0 = Math.min(...boxes.map((b) => b.x0));
|
|
16692
|
+
const x1 = Math.max(...boxes.map((b) => b.x1));
|
|
16693
|
+
const y0 = Math.min(...boxes.map((b) => b.y0));
|
|
16694
|
+
const y1 = Math.max(...boxes.map((b) => b.y1));
|
|
16695
|
+
return {
|
|
16696
|
+
left: along(cover.centre.left, cover.width, to.width, x0, x1),
|
|
16697
|
+
top: along(cover.centre.top, cover.height, to.height, y0, y1)
|
|
16698
|
+
};
|
|
16699
|
+
}
|
|
16700
|
+
function coverFitBoxes(boxes, from, to, frame) {
|
|
16701
|
+
const cover = coverOf(from, to);
|
|
16702
|
+
const { left: offX, top: offY } = frame ?? cover.centre;
|
|
16703
|
+
const x = (f) => (f * cover.width - offX) / to.width;
|
|
16704
|
+
const y = (f) => (f * cover.height - offY) / to.height;
|
|
16646
16705
|
return boxes.map((b) => ({ ...b, x0: x(b.x0), y0: y(b.y0), x1: x(b.x1), y1: y(b.y1) }));
|
|
16647
16706
|
}
|
|
16648
16707
|
var NO_LOGO = "The brand's logo was not found on it.";
|
|
@@ -16696,34 +16755,45 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
|
|
|
16696
16755
|
});
|
|
16697
16756
|
const format = plan.master.format;
|
|
16698
16757
|
const grade = plan.masterShape === "relayout" ? plan.placements ? { placements: plan.placements } : {} : { band: plan.band };
|
|
16758
|
+
const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
|
|
16699
16759
|
const checked = await Promise.all(
|
|
16700
|
-
variants.map(
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
try {
|
|
16704
|
-
const bytes = await bytesOf(source);
|
|
16705
|
-
const { width = 0, height = 0 } = await sharp8(bytes).metadata();
|
|
16706
|
-
const check = await checkStaticImage(bytes, {
|
|
16707
|
-
format,
|
|
16708
|
-
lines,
|
|
16709
|
-
allowed: brand ? [brand] : [],
|
|
16710
|
-
...grade,
|
|
16711
|
-
toFrame: (boxes) => coverFitBoxes(boxes, { width, height }, plan.master.size),
|
|
16712
|
-
...opts.read ? { read: opts.read } : {},
|
|
16713
|
-
...opts.onSpend ? { onSpend: opts.onSpend } : {}
|
|
16714
|
-
});
|
|
16715
|
-
if (check.status === "not_checked") return { index: variant.index, blocking: null };
|
|
16716
|
-
const prefix = `${format}: `;
|
|
16717
|
-
const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
|
|
16718
|
-
const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
|
|
16719
|
-
return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want) };
|
|
16720
|
-
} catch {
|
|
16721
|
-
return { index: variant.index, blocking: null };
|
|
16722
|
-
}
|
|
16723
|
-
})
|
|
16760
|
+
variants.map(
|
|
16761
|
+
(variant) => checkVariant(variant, { format, size: plan.master.size, lines, brand, grade, want, opts })
|
|
16762
|
+
)
|
|
16724
16763
|
);
|
|
16725
16764
|
const outcome = prefilterOutcome(checked);
|
|
16726
|
-
|
|
16765
|
+
const frames = Object.fromEntries(checked.flatMap((c) => "frame" in c && c.frame ? [[c.index, c.frame]] : []));
|
|
16766
|
+
return {
|
|
16767
|
+
...outcome,
|
|
16768
|
+
kept: seededOrder(outcome.kept, variants),
|
|
16769
|
+
...Object.keys(frames).length > 0 ? { frames, masterSize: plan.master.size } : {}
|
|
16770
|
+
};
|
|
16771
|
+
}
|
|
16772
|
+
async function checkVariant(variant, c) {
|
|
16773
|
+
const { format, lines, brand, grade, want, opts } = c;
|
|
16774
|
+
const source = variant.path ?? variant.url;
|
|
16775
|
+
if (!source) return { index: variant.index, blocking: null };
|
|
16776
|
+
try {
|
|
16777
|
+
const bytes = await bytesOf(source);
|
|
16778
|
+
const { width = 0, height = 0 } = await sharp8(bytes).metadata();
|
|
16779
|
+
const framing = framingOf({ width, height }, c.size);
|
|
16780
|
+
const check = await checkStaticImage(bytes, {
|
|
16781
|
+
format,
|
|
16782
|
+
lines,
|
|
16783
|
+
allowed: brand ? [brand] : [],
|
|
16784
|
+
...grade,
|
|
16785
|
+
toFrame: framing.place,
|
|
16786
|
+
...opts.read ? { read: opts.read } : {},
|
|
16787
|
+
...opts.onSpend ? { onSpend: opts.onSpend } : {}
|
|
16788
|
+
});
|
|
16789
|
+
if (check.status === "not_checked") return { index: variant.index, blocking: null };
|
|
16790
|
+
const prefix = `${format}: `;
|
|
16791
|
+
const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
|
|
16792
|
+
const frame = framing.moved();
|
|
16793
|
+
return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want), ...frame ? { frame } : {} };
|
|
16794
|
+
} catch {
|
|
16795
|
+
return { index: variant.index, blocking: null };
|
|
16796
|
+
}
|
|
16727
16797
|
}
|
|
16728
16798
|
function seededOrder(indexes, variants) {
|
|
16729
16799
|
const shaOf = new Map(variants.map((v) => [v.index, v.sha256]));
|
|
@@ -16799,7 +16869,7 @@ async function readSpec(ref) {
|
|
|
16799
16869
|
}
|
|
16800
16870
|
var staticPrefilterNode = defineNode({
|
|
16801
16871
|
id: "static_prefilter",
|
|
16802
|
-
version: "1.
|
|
16872
|
+
version: "1.1.0",
|
|
16803
16873
|
category: "image",
|
|
16804
16874
|
location: "local",
|
|
16805
16875
|
// Reads each variant's pixels itself, from its file or its durable URL, so the variants
|
|
@@ -16832,7 +16902,8 @@ var staticPrefilterNode = defineNode({
|
|
|
16832
16902
|
images: z26.array(ImageRef).min(1),
|
|
16833
16903
|
// Which variant (its place among the inputs) each of `images` is, so the judge's "Image k" maps back.
|
|
16834
16904
|
order: z26.array(z26.number().int().min(0)).optional(),
|
|
16835
|
-
// The picture every size is made from: the checks' first, or on a tie the judge's.
|
|
16905
|
+
// The picture every size is made from: the checks' first, or on a tie the judge's. Cut to the
|
|
16906
|
+
// master's frame where every word stays whole when its centre would cut one, else as rendered.
|
|
16836
16907
|
image: ImageRef.optional(),
|
|
16837
16908
|
// Which variant it is (its place among the inputs).
|
|
16838
16909
|
chosen: z26.number().int().min(0).optional(),
|
|
@@ -16845,12 +16916,12 @@ var staticPrefilterNode = defineNode({
|
|
|
16845
16916
|
}).strict(),
|
|
16846
16917
|
outputKinds: { images: "image", image: "image" },
|
|
16847
16918
|
cost: () => ({ credits: 0, seconds_estimate: 1 }),
|
|
16848
|
-
async execute({ inputs, params }) {
|
|
16849
|
-
const variants = inputs.images.map((
|
|
16919
|
+
async execute({ inputs, params, ctx }) {
|
|
16920
|
+
const variants = inputs.images.map((image2, index) => ({
|
|
16850
16921
|
index,
|
|
16851
|
-
sha256:
|
|
16852
|
-
...
|
|
16853
|
-
...
|
|
16922
|
+
sha256: image2.sha256,
|
|
16923
|
+
...image2.path ? { path: image2.path } : {},
|
|
16924
|
+
...image2.url ? { url: image2.url } : {}
|
|
16854
16925
|
}));
|
|
16855
16926
|
const usage = [];
|
|
16856
16927
|
const result = await prefilterVariants(variants, params.checks ?? await readSpec(inputs.spec), params.formats, {
|
|
@@ -16866,10 +16937,11 @@ var staticPrefilterNode = defineNode({
|
|
|
16866
16937
|
{ model: params.judge_model, prompt: params.judge_prompt, onSpend: (u) => usage.push(u) }
|
|
16867
16938
|
) : null;
|
|
16868
16939
|
const chosen = tie?.chosen ?? shown[0] ?? 0;
|
|
16940
|
+
const image = await framed(inputs.images[chosen], result.frames?.[chosen], result.masterSize, ctx);
|
|
16869
16941
|
return {
|
|
16870
16942
|
images: kept.length > 0 ? kept : inputs.images,
|
|
16871
16943
|
order: shown,
|
|
16872
|
-
...
|
|
16944
|
+
...image ? { image } : {},
|
|
16873
16945
|
chosen,
|
|
16874
16946
|
...tie ? { reasoning: tie.reasoning } : {},
|
|
16875
16947
|
dropped: result.dropped,
|
|
@@ -16878,6 +16950,12 @@ var staticPrefilterNode = defineNode({
|
|
|
16878
16950
|
};
|
|
16879
16951
|
}
|
|
16880
16952
|
});
|
|
16953
|
+
async function framed(picture, frame, size2, ctx) {
|
|
16954
|
+
const source = picture?.path ?? picture?.url;
|
|
16955
|
+
if (!picture || !frame || !size2 || !source) return picture;
|
|
16956
|
+
const bytes = await cutToFrame(await bytesOf(source), frame, size2);
|
|
16957
|
+
return await ctx.assets.ingestBytes({ bytes, kind: "image", mime: "image/png" });
|
|
16958
|
+
}
|
|
16881
16959
|
|
|
16882
16960
|
// src/engine/nodes/local/staticSafeFit.ts
|
|
16883
16961
|
import { z as z27 } from "zod";
|
|
@@ -18531,6 +18609,7 @@ export {
|
|
|
18531
18609
|
conceptProblems,
|
|
18532
18610
|
unwrittenProblems,
|
|
18533
18611
|
sceneWordProblems,
|
|
18612
|
+
SPEC_SHAPES_HINT,
|
|
18534
18613
|
adSpecErrorText,
|
|
18535
18614
|
readAdSpec,
|
|
18536
18615
|
withRenderedLettering,
|
|
@@ -18619,4 +18698,4 @@ export {
|
|
|
18619
18698
|
defaultRegistry,
|
|
18620
18699
|
createEngineFromEnv
|
|
18621
18700
|
};
|
|
18622
|
-
//# sourceMappingURL=chunk-
|
|
18701
|
+
//# sourceMappingURL=chunk-Y33XAZ5M.js.map
|