@aglyn/plugins-ai 1.0.0-beta.240 → 1.0.0-beta.242
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/package.json +17 -17
- package/src/lib/components/ai-job-links.d.ts +17 -0
- package/src/lib/components/ai-job-links.js +63 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +8 -1
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +7 -1
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-store-finish-card.component.d.ts +28 -0
- package/src/lib/components/ai-store-finish-card.component.js +116 -0
- package/src/lib/components/ai-store-finish-card.component.js.map +1 -0
- package/src/lib/components/assist-build-card.component.js +12 -1
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +45 -1
- package/src/lib/jobs/ai-job-build-step.js +283 -7
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
- package/src/lib/jobs/ai-job-drafts.js +4 -1
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
- package/src/lib/jobs/ai-job-layout-language.js +4 -2
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +3 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-language.js +19 -7
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +11 -0
- package/src/lib/jobs/ai-job-page-sections.js +40 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-step.js +19 -8
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +12 -0
- package/src/lib/jobs/ai-job-plan-step.js +41 -7
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.js +3 -2
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +76 -2
- package/src/lib/jobs/ai-job-site-datasets.js +206 -13
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +18 -2
- package/src/lib/jobs/ai-job-site-step.js +109 -22
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.d.ts +75 -0
- package/src/lib/jobs/ai-job-site-store-pages.js +195 -0
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -0
- package/src/lib/jobs/ai-job-store-links-layout.d.ts +71 -0
- package/src/lib/jobs/ai-job-store-links-layout.js +95 -0
- package/src/lib/jobs/ai-job-store-links-layout.js.map +1 -0
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +129 -8
- package/src/lib/jobs/ai-layout-stock-photos.js +919 -75
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +18 -0
- package/src/lib/jobs/ai-site-publish.js +76 -15
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.d.ts +43 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js +61 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-saltmarsh-recordings.d.ts +39 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-saltmarsh-recordings.js +508 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-saltmarsh-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.js +25 -2
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +10 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -4
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-listings.js +3 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +2 -0
- package/src/lib/model/ai-build-job.js +23 -3
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +7 -0
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-build-store-pages.d.ts +133 -0
- package/src/lib/model/ai-build-store-pages.js +209 -0
- package/src/lib/model/ai-build-store-pages.js.map +1 -0
- package/src/lib/model/ai-layout-store-links.d.ts +64 -0
- package/src/lib/model/ai-layout-store-links.js +165 -0
- package/src/lib/model/ai-layout-store-links.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +5 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.d.ts +192 -0
- package/src/lib/model/ai-site-store-pages.js +383 -0
- package/src/lib/model/ai-site-store-pages.js.map +1 -0
- package/src/lib/runtime/site-inventory.js +8 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +8 -4
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
|
@@ -17,6 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import { pluginMediaIngest } from "@aglyn/aglyn/plugin-manager/plugin-media-ingest";
|
|
18
18
|
import { STOCK_PHOTO_QUERY_MAX_CHARS, stockPhotoProvider, stockPhotoSourceKey } from "@aglyn/aglyn/plugin-manager/stock-photo-provider";
|
|
19
19
|
import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
20
|
+
import { aiStorefrontSays } from "../layout-language/ai-layout-storefront.js";
|
|
20
21
|
/**
|
|
21
22
|
* STOCK PHOTOS FOR A LANGUAGE PAGE'S PICTURE SLOTS (AGL-3660).
|
|
22
23
|
*
|
|
@@ -70,6 +71,17 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
70
71
|
* ranked: the object in the hit's first tags (what the photo is OF) over
|
|
71
72
|
* a mention in its last, the subject's other words, the domain. Among
|
|
72
73
|
* equals the job's seed picks, so two sites differ and one job repeats.
|
|
74
|
+
* What the beta.239 Ember & Oak start added: "hand-poured", "handmade"
|
|
75
|
+
* and "by hand" never make "hands" a subject, and a story of the craft
|
|
76
|
+
* asks for the maker at work ("candle making"); a hit carrying a topic
|
|
77
|
+
* the brief never named (a cross, a grave, a flag, a gun, wine, a child,
|
|
78
|
+
* a mannequin) never fills anything; a product, and a collection tile,
|
|
79
|
+
* take only a hit naming its head noun among the first tags, held to the
|
|
80
|
+
* shop's category; and an object whose word is not enough ("melt") is
|
|
81
|
+
* named and searched by its kin ("wax melt", "melt warmer"), never by the
|
|
82
|
+
* category alone. And from the Kiln & Clover portfolio: nudity never fills
|
|
83
|
+
* anything, for any business; a hit naming a rival craft (glass, for a
|
|
84
|
+
* potter) counts only when it names the craft's own words too.
|
|
73
85
|
* 4. **Never twice.** A photo is placed once per job: not twice on a page,
|
|
74
86
|
* and not on a page when another page (or another pass, in this process)
|
|
75
87
|
* of the same job already placed it. The caller hands in what the job's
|
|
@@ -83,6 +95,18 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
83
95
|
* to its photographer. A copy that fails tries the slot's next accepted
|
|
84
96
|
* hit.
|
|
85
97
|
*
|
|
98
|
+
* 6. **Fall back to the craft, never off it** (the beta.241 Saltmarsh Soap
|
|
99
|
+
* Co start, a store "for handmade cold-process soap": its craft read as
|
|
100
|
+
* nothing, its hero searched "shop" and took a florist's bouquet, and four
|
|
101
|
+
* of five products took the starters' laptop desk and living room). A
|
|
102
|
+
* business that is only a place takes its craft from what it is for
|
|
103
|
+
* ("soap shop"); a hero names the craft itself ({@link AiStockSiteTerms.core});
|
|
104
|
+
* a product, after its own searches, tries its head noun anywhere among
|
|
105
|
+
* the tags, its gift wrapper with the craft and the craft's broad
|
|
106
|
+
* searches, and then — a product or a hero only — reuses an on-topic photo
|
|
107
|
+
* the job already shows rather than take a starter. Every slot of a
|
|
108
|
+
* craft's site tries the craft's broad searches last.
|
|
109
|
+
*
|
|
86
110
|
* A slot nothing acceptable answered — the library down, refusing, or out
|
|
87
111
|
* of time — is left `null` for the starter photos. A named object is never
|
|
88
112
|
* given one while the library answers anything of the site's world: the
|
|
@@ -98,12 +122,18 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
98
122
|
* The unit job input naming the screen drafts the job's other pages were
|
|
99
123
|
* written to, whose photos a page does not place again (AGL-3660).
|
|
100
124
|
*/ export const AI_STOCK_PHOTO_PAGES_INPUT = 'stockPhotoPages';
|
|
125
|
+
/**
|
|
126
|
+
* The unit job input naming library photos (`media:` references) another
|
|
127
|
+
* part of the job already shows outside its pages — a dataset's records
|
|
128
|
+
* (AGL-3616) — which a page does not place again.
|
|
129
|
+
*/ export const AI_STOCK_PHOTO_AVOID_INPUT = 'stockPhotoAvoid';
|
|
101
130
|
const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
102
131
|
/** Words that carry no subject. */ const STOPWORDS = words('a an the and or of in on at to for with from by into onto over under near our your their my his her its ' + 'this that these those is are be being was were who whom which where while as about than then very just ' + 'small local best new top quality premium family-owned family owned independent professional ' + 'business company services service page photo picture image showing shows show ' + 'someone people person one two three some many several');
|
|
103
132
|
/**
|
|
104
133
|
* Words that describe a picture's look, not what it shows, dropped from a
|
|
105
134
|
* subject. A word that is as often a noun ("light", "set") is not one.
|
|
106
|
-
*/ const FILLER = words('beautiful beautifully stunning gorgeous lovely pretty elegant modern contemporary rustic cozy cosy warm cool ' + 'soft bright natural simple minimal minimalist delicate unique special artisan artisanal handmade ' + 'hand-made hand-thrown handthrown hand-crafted handcrafted crafted hand-built handbuilt hand-poured ' + 'hand-painted hand-dyed hand-stitched poured finished styled perfect ' + 'fresh clean calm serene quiet peaceful vibrant colorful colourful sunlit sunny golden glossy matte textured ' + 'close-up closeup up view views detail details detailed shot shots overhead flat lay flatlay flat-lay ' + 'top-down wide angle featuring featured displayed arranged sitting resting placed standing lined ' + 'single pair collection assortment selection group few various array range large tiny big little ' + 'tall short wide narrow everyday classic signature original favorite favourite bestselling best-selling ' + 'small-batch batch limited edition custom bespoke ' +
|
|
135
|
+
*/ const FILLER = words('beautiful beautifully stunning gorgeous lovely pretty elegant modern contemporary rustic cozy cosy warm cool ' + 'soft bright natural simple minimal minimalist delicate unique special artisan artisanal handmade ' + 'hand-made hand-thrown handthrown hand-crafted handcrafted crafted hand-built handbuilt hand-poured ' + 'hand-painted hand-dyed hand-stitched poured finished styled perfect wheel-thrown wheelthrown thrown ' + 'fresh clean calm serene quiet peaceful vibrant colorful colourful sunlit sunny golden glossy matte textured ' + 'close-up closeup up view views detail details detailed shot shots overhead flat lay flatlay flat-lay ' + 'top-down wide angle featuring featured displayed arranged sitting resting placed standing lined ' + 'single pair collection assortment selection group few various array range large tiny big little ' + 'tall short wide narrow everyday classic signature original favorite favourite bestselling best-selling ' + 'small-batch batch limited edition custom bespoke ' + // How a soap is made, never what it shows: "cold" and "process" named a supermarket fridge (Saltmarsh, 2026-10-10).
|
|
136
|
+
'cold-process cold-processed hot-process hot-processed melt-and-pour ' + 'white black grey gray beige cream ivory brown tan blue green red pink yellow orange purple speckled ' + 'gentle airy inviting welcoming cheerful happy smiling friendly');
|
|
107
137
|
/** The nouns that name a place of business, not its craft: "studio" in "ceramics studio". */ const VENUES = words('studio studios shop shops store stores boutique company co workshop gallery atelier agency brand practice ' + 'salon house collective lab market services service firm clinic center centre parlor parlour bar ' + 'class classes lessons school academy');
|
|
108
138
|
/** The person who does the craft, not the craft: "artist" in "ceramic artist". */ const ROLES = words('artist artists maker makers artisan artisans owner owners founder founders creator creators');
|
|
109
139
|
/** Words naming what kind of SITE it is, never what it shows: "portfolio" in "a portfolio for a ceramic artist". */ const SITE_KINDS = words('portfolio portfolios website websites site sites blog blogs online web webshop ecommerce e-commerce ' + 'landing personal homepage storefront');
|
|
@@ -119,6 +149,374 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
119
149
|
* an empty frame on the page (the Juniper Clay hero), a template or a
|
|
120
150
|
* graphic is not a photo of anything.
|
|
121
151
|
*/ const REJECTED_TAGS = words('mockup mock-up mock blank template templates clipart clip-art illustration vector logo icon ' + 'ai-generated generated render rendering 3d');
|
|
152
|
+
/**
|
|
153
|
+
* Topics a business's photos never carry unless its own words name them
|
|
154
|
+
* (AGL-3660, the beta.239 Ember & Oak start, whose "Gifts for melt fans" tile
|
|
155
|
+
* showed devotional candles with crosses and names). A hit tagged with any
|
|
156
|
+
* word of a topic is rejected unless the site's business type or brief says
|
|
157
|
+
* one of the topic's words or its `allow` words. The last topic is not
|
|
158
|
+
* sensitive but is never the thing: the same start's story band showed bronze
|
|
159
|
+
* mannequin hands tagged "wax".
|
|
160
|
+
*/ const SENSITIVE_TOPICS = [
|
|
161
|
+
{
|
|
162
|
+
// Religion.
|
|
163
|
+
words: [
|
|
164
|
+
'cross',
|
|
165
|
+
'crosses',
|
|
166
|
+
'crucifix',
|
|
167
|
+
'church',
|
|
168
|
+
'chapel',
|
|
169
|
+
'cathedral',
|
|
170
|
+
'jesus',
|
|
171
|
+
'christ',
|
|
172
|
+
'christian',
|
|
173
|
+
'christianity',
|
|
174
|
+
'god',
|
|
175
|
+
'bible',
|
|
176
|
+
'rosary',
|
|
177
|
+
'prayer',
|
|
178
|
+
'pray',
|
|
179
|
+
'praying',
|
|
180
|
+
'worship',
|
|
181
|
+
'worships',
|
|
182
|
+
'religion',
|
|
183
|
+
'religious',
|
|
184
|
+
'faith',
|
|
185
|
+
'islam',
|
|
186
|
+
'islamic',
|
|
187
|
+
'muslim',
|
|
188
|
+
'mosque',
|
|
189
|
+
'quran',
|
|
190
|
+
'temple',
|
|
191
|
+
'buddha',
|
|
192
|
+
'buddhism',
|
|
193
|
+
'buddhist',
|
|
194
|
+
'hindu',
|
|
195
|
+
'hinduism',
|
|
196
|
+
'synagogue',
|
|
197
|
+
'jewish',
|
|
198
|
+
'judaism',
|
|
199
|
+
'ecclesiastical',
|
|
200
|
+
'saint',
|
|
201
|
+
'altar',
|
|
202
|
+
'sacrificial',
|
|
203
|
+
'nativity',
|
|
204
|
+
'priest',
|
|
205
|
+
'nun',
|
|
206
|
+
'monk'
|
|
207
|
+
],
|
|
208
|
+
allow: [
|
|
209
|
+
'ministry',
|
|
210
|
+
'parish',
|
|
211
|
+
'congregation',
|
|
212
|
+
'wedding'
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
// Death and mourning.
|
|
217
|
+
words: [
|
|
218
|
+
'funeral',
|
|
219
|
+
'memorial',
|
|
220
|
+
'grave',
|
|
221
|
+
'gravestone',
|
|
222
|
+
'cemetery',
|
|
223
|
+
'tomb',
|
|
224
|
+
'death',
|
|
225
|
+
'dead',
|
|
226
|
+
'grief',
|
|
227
|
+
'mourning',
|
|
228
|
+
'condolence',
|
|
229
|
+
'coffin',
|
|
230
|
+
'skull'
|
|
231
|
+
],
|
|
232
|
+
allow: [
|
|
233
|
+
'hospice',
|
|
234
|
+
'mortuary',
|
|
235
|
+
'cremation',
|
|
236
|
+
'obituary',
|
|
237
|
+
'halloween'
|
|
238
|
+
]
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
// Politics.
|
|
242
|
+
words: [
|
|
243
|
+
'political',
|
|
244
|
+
'politics',
|
|
245
|
+
'protest',
|
|
246
|
+
'protester',
|
|
247
|
+
'demonstration',
|
|
248
|
+
'rally',
|
|
249
|
+
'flag',
|
|
250
|
+
'election',
|
|
251
|
+
'vote',
|
|
252
|
+
'voting',
|
|
253
|
+
'parliament',
|
|
254
|
+
'government'
|
|
255
|
+
],
|
|
256
|
+
allow: [
|
|
257
|
+
'advocacy',
|
|
258
|
+
'campaign',
|
|
259
|
+
'candidate',
|
|
260
|
+
'council',
|
|
261
|
+
'nonprofit'
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
// Weapons and war.
|
|
266
|
+
words: [
|
|
267
|
+
'gun',
|
|
268
|
+
'rifle',
|
|
269
|
+
'pistol',
|
|
270
|
+
'weapon',
|
|
271
|
+
'firearm',
|
|
272
|
+
'ammunition',
|
|
273
|
+
'bullet',
|
|
274
|
+
'sword',
|
|
275
|
+
'war',
|
|
276
|
+
'military',
|
|
277
|
+
'army',
|
|
278
|
+
'soldier'
|
|
279
|
+
],
|
|
280
|
+
allow: [
|
|
281
|
+
'hunting',
|
|
282
|
+
'archery',
|
|
283
|
+
'martial',
|
|
284
|
+
'fencing',
|
|
285
|
+
'veteran'
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
// Alcohol, for a business that does not serve it.
|
|
290
|
+
words: [
|
|
291
|
+
'alcohol',
|
|
292
|
+
'wine',
|
|
293
|
+
'beer',
|
|
294
|
+
'whiskey',
|
|
295
|
+
'whisky',
|
|
296
|
+
'vodka',
|
|
297
|
+
'cocktail',
|
|
298
|
+
'liquor',
|
|
299
|
+
'rum',
|
|
300
|
+
'gin',
|
|
301
|
+
'champagne',
|
|
302
|
+
'drunk'
|
|
303
|
+
],
|
|
304
|
+
allow: [
|
|
305
|
+
'winery',
|
|
306
|
+
'vineyard',
|
|
307
|
+
'brewery',
|
|
308
|
+
'distillery',
|
|
309
|
+
'pub',
|
|
310
|
+
'tavern',
|
|
311
|
+
'taproom',
|
|
312
|
+
'bartender',
|
|
313
|
+
'sommelier',
|
|
314
|
+
'spirits'
|
|
315
|
+
]
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
// Tobacco and drugs.
|
|
319
|
+
words: [
|
|
320
|
+
'cigarette',
|
|
321
|
+
'smoking',
|
|
322
|
+
'tobacco',
|
|
323
|
+
'cannabis',
|
|
324
|
+
'marijuana',
|
|
325
|
+
'drug',
|
|
326
|
+
'drugs'
|
|
327
|
+
],
|
|
328
|
+
allow: [
|
|
329
|
+
'dispensary',
|
|
330
|
+
'vape',
|
|
331
|
+
'cigar'
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
// Children, for a business that is not about them.
|
|
336
|
+
words: [
|
|
337
|
+
'child',
|
|
338
|
+
'children',
|
|
339
|
+
'kid',
|
|
340
|
+
'kids',
|
|
341
|
+
'baby',
|
|
342
|
+
'babies',
|
|
343
|
+
'toddler',
|
|
344
|
+
'infant'
|
|
345
|
+
],
|
|
346
|
+
allow: [
|
|
347
|
+
'nursery',
|
|
348
|
+
'daycare',
|
|
349
|
+
'preschool',
|
|
350
|
+
'kindergarten',
|
|
351
|
+
'school',
|
|
352
|
+
'tutor',
|
|
353
|
+
'tutoring',
|
|
354
|
+
'parent',
|
|
355
|
+
'parents',
|
|
356
|
+
'parenting',
|
|
357
|
+
'family',
|
|
358
|
+
'families',
|
|
359
|
+
'pediatric',
|
|
360
|
+
'paediatric',
|
|
361
|
+
'newborn',
|
|
362
|
+
'maternity'
|
|
363
|
+
]
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
// Nudity, for every business: no word of a brief lets it in (Kiln &
|
|
367
|
+
// Clover, 2026-10-10: a nude torso sculpture filled "About the artist").
|
|
368
|
+
// Never bare "body": a skincare shop's body lotion is its product.
|
|
369
|
+
words: [
|
|
370
|
+
'nude',
|
|
371
|
+
'nudes',
|
|
372
|
+
'nudity',
|
|
373
|
+
'naked',
|
|
374
|
+
'torso',
|
|
375
|
+
'breast',
|
|
376
|
+
'breasts',
|
|
377
|
+
'erotic',
|
|
378
|
+
'sensual',
|
|
379
|
+
'lingerie',
|
|
380
|
+
'bikini'
|
|
381
|
+
],
|
|
382
|
+
allow: [],
|
|
383
|
+
always: true
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
// Figures, never the maker: mannequin hands are not a candlemaker's.
|
|
387
|
+
words: [
|
|
388
|
+
'mannequin',
|
|
389
|
+
'mannequins',
|
|
390
|
+
'dummy',
|
|
391
|
+
'statue',
|
|
392
|
+
'sculpture',
|
|
393
|
+
'figurine',
|
|
394
|
+
'doll',
|
|
395
|
+
'dolls',
|
|
396
|
+
'waxwork'
|
|
397
|
+
],
|
|
398
|
+
allow: [
|
|
399
|
+
'sculptor',
|
|
400
|
+
'museum',
|
|
401
|
+
'fashion',
|
|
402
|
+
'boutique',
|
|
403
|
+
'clothing',
|
|
404
|
+
'apparel',
|
|
405
|
+
'toy',
|
|
406
|
+
'toys'
|
|
407
|
+
]
|
|
408
|
+
}
|
|
409
|
+
];
|
|
410
|
+
/** Each topic's words and the words that allow it, as stems. */ const SENSITIVE_STEMS = SENSITIVE_TOPICS.map((topic)=>{
|
|
411
|
+
var _topic_allow;
|
|
412
|
+
return {
|
|
413
|
+
words: new Set(topic.words.map(aiStockStem)),
|
|
414
|
+
allow: new Set(topic.always ? [] : [
|
|
415
|
+
...topic.words,
|
|
416
|
+
...(_topic_allow = topic.allow) != null ? _topic_allow : []
|
|
417
|
+
].map(aiStockStem))
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
/**
|
|
421
|
+
* Hands are never a picture's subject (AGL-3660): "hand-poured", "handmade"
|
|
422
|
+
* and "by hand" say how a thing was made, and a search for "hands" found
|
|
423
|
+
* bronze mannequin hands for the beta.239 Ember & Oak story band. Each of
|
|
424
|
+
* these is dropped from a picture's words; a hand only stays where it names
|
|
425
|
+
* a thing sold for hands ("hand cream").
|
|
426
|
+
*/ const HAND_IDIOMS = [
|
|
427
|
+
/\b(?:made\s+|poured\s+|crafted\s+|finished\s+|done\s+)?by\s+(?:our\s+|my\s+|their\s+)?(?:own\s+)?hands?\b/g,
|
|
428
|
+
/\bwith\s+(?:our|my|their)\s+(?:own\s+)?(?:two\s+)?hands\b/g,
|
|
429
|
+
/\bhand[- ]?(?:poured|made|crafted|thrown|built|painted|dyed|stitched|sewn|knit|knitted|carved|tied|lettered|blown|bound|rolled|dipped|picked|forged|woven|cut|finished|mixed|cast|formed|shaped|pressed|printed|wrapped|selected|blended|turned|glazed)\b/g,
|
|
430
|
+
/\bhands?\b(?!\s+(?:creams?|soaps?|lotions?|wash|towels?|saniti[sz]ers?|salves?|balms?|tools?|saws?|planes?|drills?|mixers?|puppets?|bags?|warmers?))/g
|
|
431
|
+
];
|
|
432
|
+
/** A text without its hands. */ function withoutHands(text) {
|
|
433
|
+
return HAND_IDIOMS.reduce((out, idiom)=>out.replace(idiom, ' '), text.toLowerCase());
|
|
434
|
+
}
|
|
435
|
+
/** Whether a text says how a thing was made by hand: a maker at work. */ function saysMadeByHand(text) {
|
|
436
|
+
const lower = text.toLowerCase();
|
|
437
|
+
return HAND_IDIOMS.some((idiom)=>{
|
|
438
|
+
idiom.lastIndex = 0;
|
|
439
|
+
const found = idiom.test(lower);
|
|
440
|
+
idiom.lastIndex = 0;
|
|
441
|
+
return found;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* The kin of a few objects whose own word is not enough (AGL-3660): a "melt"
|
|
446
|
+
* is any melting thing (the beta.239 "Wax melts" tile showed resin fluid
|
|
447
|
+
* art), so a wax melt names one of these phrases. `within` is the world they
|
|
448
|
+
* are of; `qualifier` the word a search adds; `queries` the searches for the
|
|
449
|
+
* object itself, which stand in for the shop's category alone: a candle is
|
|
450
|
+
* not a wax melt.
|
|
451
|
+
*/ const OBJECT_KIN = [
|
|
452
|
+
{
|
|
453
|
+
objects: [
|
|
454
|
+
'melt',
|
|
455
|
+
'tart',
|
|
456
|
+
'cube'
|
|
457
|
+
],
|
|
458
|
+
within: [
|
|
459
|
+
'wax',
|
|
460
|
+
'candle',
|
|
461
|
+
'soy'
|
|
462
|
+
],
|
|
463
|
+
qualifier: 'wax',
|
|
464
|
+
kin: [
|
|
465
|
+
'wax melt',
|
|
466
|
+
'wax melts',
|
|
467
|
+
'wax cube',
|
|
468
|
+
'wax cubes',
|
|
469
|
+
'wax tart',
|
|
470
|
+
'wax tarts',
|
|
471
|
+
'melt warmer',
|
|
472
|
+
'wax warmer',
|
|
473
|
+
'tart warmer',
|
|
474
|
+
'oil burner',
|
|
475
|
+
'oil warmer',
|
|
476
|
+
'aroma burner',
|
|
477
|
+
'scent warmer',
|
|
478
|
+
'fragrance warmer'
|
|
479
|
+
],
|
|
480
|
+
queries: [
|
|
481
|
+
'wax melts',
|
|
482
|
+
'wax melt warmer',
|
|
483
|
+
'scented wax cubes'
|
|
484
|
+
]
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
// A "bar" is a pub's before it is a soap's (the Saltmarsh "cold-process
|
|
488
|
+
// soap bar" search answered a pint of beer second): a soap bar is named
|
|
489
|
+
// by soap, and searched by it.
|
|
490
|
+
objects: [
|
|
491
|
+
'bar'
|
|
492
|
+
],
|
|
493
|
+
within: [
|
|
494
|
+
'soap',
|
|
495
|
+
'soapmaker',
|
|
496
|
+
'skincare'
|
|
497
|
+
],
|
|
498
|
+
qualifier: 'soap',
|
|
499
|
+
kin: [
|
|
500
|
+
'soap bar',
|
|
501
|
+
'soap bars',
|
|
502
|
+
'bar of soap',
|
|
503
|
+
'bars of soap',
|
|
504
|
+
'soap',
|
|
505
|
+
'soaps',
|
|
506
|
+
'handmade soap',
|
|
507
|
+
'soap block',
|
|
508
|
+
'soap cubes'
|
|
509
|
+
],
|
|
510
|
+
queries: [
|
|
511
|
+
'soap bars',
|
|
512
|
+
'handmade soap bar'
|
|
513
|
+
]
|
|
514
|
+
}
|
|
515
|
+
];
|
|
516
|
+
/** An object's kin where the site or the subject is of its world, else `null`. */ function objectKin(object, world) {
|
|
517
|
+
var _OBJECT_KIN_find;
|
|
518
|
+
return (_OBJECT_KIN_find = OBJECT_KIN.find((entry)=>entry.objects.some((word)=>aiStockStem(word) === object) && entry.within.some((word)=>world.includes(aiStockStem(word))))) != null ? _OBJECT_KIN_find : null;
|
|
519
|
+
}
|
|
122
520
|
/**
|
|
123
521
|
* The kin of a few common crafts: the words a photo of that world is
|
|
124
522
|
* tagged with, and the broad searches that find it. A craft not listed
|
|
@@ -150,6 +548,33 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
150
548
|
broad: [
|
|
151
549
|
'handmade ceramics',
|
|
152
550
|
'pottery'
|
|
551
|
+
],
|
|
552
|
+
rivals: {
|
|
553
|
+
words: [
|
|
554
|
+
'glass',
|
|
555
|
+
'glassware',
|
|
556
|
+
'glasswork',
|
|
557
|
+
'glassblowing',
|
|
558
|
+
'blown',
|
|
559
|
+
'stained',
|
|
560
|
+
'murano',
|
|
561
|
+
'crystal'
|
|
562
|
+
],
|
|
563
|
+
core: [
|
|
564
|
+
'ceramic',
|
|
565
|
+
'ceramics',
|
|
566
|
+
'pottery',
|
|
567
|
+
'potter',
|
|
568
|
+
'stoneware',
|
|
569
|
+
'porcelain',
|
|
570
|
+
'earthenware',
|
|
571
|
+
'clay',
|
|
572
|
+
'terracotta'
|
|
573
|
+
]
|
|
574
|
+
},
|
|
575
|
+
making: [
|
|
576
|
+
'pottery wheel',
|
|
577
|
+
'potter at work'
|
|
153
578
|
]
|
|
154
579
|
},
|
|
155
580
|
{
|
|
@@ -171,6 +596,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
171
596
|
broad: [
|
|
172
597
|
'handmade candles',
|
|
173
598
|
'scented candles'
|
|
599
|
+
],
|
|
600
|
+
making: [
|
|
601
|
+
'candle making',
|
|
602
|
+
'pouring wax'
|
|
174
603
|
]
|
|
175
604
|
},
|
|
176
605
|
{
|
|
@@ -196,6 +625,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
196
625
|
broad: [
|
|
197
626
|
'handmade jewelry',
|
|
198
627
|
'jewelry'
|
|
628
|
+
],
|
|
629
|
+
making: [
|
|
630
|
+
'jewelry making',
|
|
631
|
+
'jeweler at work'
|
|
199
632
|
]
|
|
200
633
|
},
|
|
201
634
|
{
|
|
@@ -204,19 +637,53 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
204
637
|
'soapmaker',
|
|
205
638
|
'skincare'
|
|
206
639
|
],
|
|
640
|
+
// Never "natural": a florist's bouquet tagged "natural" was the
|
|
641
|
+
// Saltmarsh Soap Co hero (2026-10-10).
|
|
207
642
|
kin: [
|
|
208
643
|
'soap',
|
|
209
644
|
'skincare',
|
|
210
645
|
'lotion',
|
|
646
|
+
'cream',
|
|
211
647
|
'bath',
|
|
212
648
|
'spa',
|
|
213
|
-
'natural',
|
|
214
649
|
'cosmetic'
|
|
215
650
|
],
|
|
216
651
|
broad: [
|
|
217
652
|
'handmade soap',
|
|
218
653
|
'natural skincare'
|
|
219
|
-
]
|
|
654
|
+
],
|
|
655
|
+
// "Soap making" is answered with soap bubbles; a bar is what a soapmaker makes.
|
|
656
|
+
making: [
|
|
657
|
+
'soap making',
|
|
658
|
+
'handmade soap'
|
|
659
|
+
],
|
|
660
|
+
// Soap bubbles, a dispenser and detergent are soap, never a soapmaker's:
|
|
661
|
+
// the same start's story band showed bubbles in flight.
|
|
662
|
+
rivals: {
|
|
663
|
+
words: [
|
|
664
|
+
'bubble',
|
|
665
|
+
'bubbles',
|
|
666
|
+
'soapy',
|
|
667
|
+
'dispenser',
|
|
668
|
+
'detergent',
|
|
669
|
+
'dishwashing',
|
|
670
|
+
'laundry'
|
|
671
|
+
],
|
|
672
|
+
core: [
|
|
673
|
+
'bar',
|
|
674
|
+
'bars',
|
|
675
|
+
'handmade',
|
|
676
|
+
'homemade',
|
|
677
|
+
'loaf',
|
|
678
|
+
'lye',
|
|
679
|
+
'glycerin',
|
|
680
|
+
'skincare',
|
|
681
|
+
'lotion',
|
|
682
|
+
'cosmetic',
|
|
683
|
+
'spa',
|
|
684
|
+
'artisan'
|
|
685
|
+
]
|
|
686
|
+
}
|
|
220
687
|
},
|
|
221
688
|
{
|
|
222
689
|
match: [
|
|
@@ -242,6 +709,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
242
709
|
broad: [
|
|
243
710
|
'woodworking',
|
|
244
711
|
'wooden furniture'
|
|
712
|
+
],
|
|
713
|
+
making: [
|
|
714
|
+
'woodworking workshop',
|
|
715
|
+
'carpenter at work'
|
|
245
716
|
]
|
|
246
717
|
},
|
|
247
718
|
{
|
|
@@ -266,6 +737,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
266
737
|
broad: [
|
|
267
738
|
'bakery',
|
|
268
739
|
'fresh bread'
|
|
740
|
+
],
|
|
741
|
+
making: [
|
|
742
|
+
'baker kneading dough',
|
|
743
|
+
'baking bread'
|
|
269
744
|
]
|
|
270
745
|
},
|
|
271
746
|
{
|
|
@@ -288,6 +763,9 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
288
763
|
broad: [
|
|
289
764
|
'coffee shop',
|
|
290
765
|
'coffee'
|
|
766
|
+
],
|
|
767
|
+
making: [
|
|
768
|
+
'barista making coffee'
|
|
291
769
|
]
|
|
292
770
|
},
|
|
293
771
|
{
|
|
@@ -307,6 +785,9 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
307
785
|
broad: [
|
|
308
786
|
'florist',
|
|
309
787
|
'flower bouquet'
|
|
788
|
+
],
|
|
789
|
+
making: [
|
|
790
|
+
'florist arranging flowers'
|
|
310
791
|
]
|
|
311
792
|
},
|
|
312
793
|
{
|
|
@@ -336,6 +817,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
336
817
|
broad: [
|
|
337
818
|
'handmade textiles',
|
|
338
819
|
'yarn'
|
|
820
|
+
],
|
|
821
|
+
making: [
|
|
822
|
+
'knitting',
|
|
823
|
+
'weaving loom'
|
|
339
824
|
]
|
|
340
825
|
},
|
|
341
826
|
{
|
|
@@ -354,6 +839,9 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
354
839
|
broad: [
|
|
355
840
|
'leather craft',
|
|
356
841
|
'leather goods'
|
|
842
|
+
],
|
|
843
|
+
making: [
|
|
844
|
+
'leather crafting'
|
|
357
845
|
]
|
|
358
846
|
}
|
|
359
847
|
];
|
|
@@ -422,10 +910,11 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
422
910
|
return craft ? aiStockStem(craft) : '';
|
|
423
911
|
}
|
|
424
912
|
/** The site's terms, read from its business type. */ export function aiStockSiteTerms(businessType) {
|
|
425
|
-
|
|
426
|
-
|
|
913
|
+
let business = aiStockBusinessWords(businessType);
|
|
914
|
+
let craft = aiStockCraftWords(business);
|
|
427
915
|
const domain = new Set();
|
|
428
916
|
/** The things it makes, by their head nouns. */ const made = new Set();
|
|
917
|
+
/** The last word of the first clause that names the world: "soap" of "for handmade cold-process soap". */ let firstWorld = '';
|
|
429
918
|
let named = false;
|
|
430
919
|
let making = false;
|
|
431
920
|
for (const { opener, words: clause } of businessClauses(businessType)){
|
|
@@ -440,18 +929,44 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
440
929
|
// What it makes names its world by what the things are made of, never
|
|
441
930
|
// the things: "stoneware" of "stoneware bowls", "soy" of "soy candles".
|
|
442
931
|
// A bowl is not a ceramic world; a robin's food bowl is not pottery.
|
|
443
|
-
|
|
932
|
+
// A set or a gift is never a world: "gift" of "gift sets" is not one.
|
|
933
|
+
const world = (making ? kept.slice(0, -1) : kept).filter((word)=>!WRAPPERS.has(word));
|
|
444
934
|
for (const word of world)for (const stem of stemList(word))domain.add(stem);
|
|
935
|
+
const last = world[world.length - 1];
|
|
936
|
+
if (!firstWorld && last) firstWorld = aiStockStem(last);
|
|
445
937
|
if (making) for (const word of kept.slice(-1))for (const stem of stemList(word))made.add(stem);
|
|
446
938
|
}
|
|
447
|
-
if (craft) domain.add(craft);
|
|
448
939
|
// A business named only by what it sells ("a shop selling pottery") is of that world.
|
|
449
940
|
if (!domain.size) for (const stem of made)domain.add(stem);
|
|
941
|
+
const groups = CRAFT_KIN.filter((group)=>group.match.some((word)=>domain.has(aiStockStem(word)) || made.has(aiStockStem(word)) || aiStockStem(word) === craft));
|
|
942
|
+
// A business that is only a place ("an online shop for handmade
|
|
943
|
+
// cold-process soap") takes its craft from what it is for: the Saltmarsh
|
|
944
|
+
// Soap Co start (2026-10-10) read its craft as nothing, so its hero searched
|
|
945
|
+
// "shop" (a florist's bouquet) and no product was held to soap.
|
|
946
|
+
if (!craft) {
|
|
947
|
+
var _groups_;
|
|
948
|
+
const listed = (_groups_ = groups[0]) == null ? void 0 : _groups_.match.map(aiStockStem).find((stem)=>domain.has(stem) || made.has(stem));
|
|
949
|
+
craft = listed || firstWorld || [
|
|
950
|
+
...made
|
|
951
|
+
][0] || '';
|
|
952
|
+
if (craft && !stemList(business).includes(craft)) business = `${craft} ${business}`.trim();
|
|
953
|
+
}
|
|
954
|
+
if (craft) domain.add(craft);
|
|
450
955
|
const broad = [];
|
|
451
|
-
|
|
452
|
-
|
|
956
|
+
const makingSearches = [];
|
|
957
|
+
const rivals = [];
|
|
958
|
+
const core = new Set(craft ? [
|
|
959
|
+
craft
|
|
960
|
+
] : []);
|
|
961
|
+
for (const group of groups){
|
|
962
|
+
for (const word of group.match)core.add(aiStockStem(word));
|
|
453
963
|
for (const word of group.kin)domain.add(aiStockStem(word));
|
|
454
964
|
broad.push(...group.broad);
|
|
965
|
+
makingSearches.push(...group.making);
|
|
966
|
+
if (group.rivals) rivals.push({
|
|
967
|
+
words: group.rivals.words.map(aiStockStem),
|
|
968
|
+
core: group.rivals.core.map(aiStockStem)
|
|
969
|
+
});
|
|
455
970
|
}
|
|
456
971
|
if (!broad.length) broad.push(business, craft);
|
|
457
972
|
return {
|
|
@@ -462,6 +977,16 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
462
977
|
].filter((stem)=>stem.length > 1),
|
|
463
978
|
broad: [
|
|
464
979
|
...new Set(broad.map((query)=>query.trim()).filter(Boolean))
|
|
980
|
+
],
|
|
981
|
+
making: [
|
|
982
|
+
...new Set(makingSearches)
|
|
983
|
+
],
|
|
984
|
+
named: [
|
|
985
|
+
...new Set(stemList(businessType))
|
|
986
|
+
],
|
|
987
|
+
rivals,
|
|
988
|
+
core: [
|
|
989
|
+
...core
|
|
465
990
|
]
|
|
466
991
|
};
|
|
467
992
|
}
|
|
@@ -474,11 +999,12 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
474
999
|
* preposition starts the setting, filler dropped, its last three words (the
|
|
475
1000
|
* head noun is the last) — else its section's name.
|
|
476
1001
|
*/ export function aiStockSubjectWords(alt, sectionName) {
|
|
477
|
-
|
|
1002
|
+
// "Hand-poured", "by hand" and "hands" say how it was made, never what it shows.
|
|
1003
|
+
for (const clause of withoutHands(alt).split(CLAUSE_BREAK)){
|
|
478
1004
|
const found = subjectOf(contentWords(clause != null ? clause : ''));
|
|
479
1005
|
if (found.length) return found.slice(-3).join(' ');
|
|
480
1006
|
}
|
|
481
|
-
return subjectOf(contentWords(sectionName)).slice(0, 2).join(' ');
|
|
1007
|
+
return subjectOf(contentWords(withoutHands(sectionName))).slice(0, 2).join(' ');
|
|
482
1008
|
}
|
|
483
1009
|
/**
|
|
484
1010
|
* The thing a subject shows, one singular word: its last word that is not
|
|
@@ -499,16 +1025,54 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
499
1025
|
if (aspect < 0.87) return 'vertical';
|
|
500
1026
|
return 'any';
|
|
501
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* A slot's part (AGL-3660): a tile of a collections band is product-like,
|
|
1030
|
+
* held to the shop's category and its own object; a picture of a story of
|
|
1031
|
+
* the craft, or of a thing made by hand, shows the maker at work.
|
|
1032
|
+
*/ export function aiStockSlotPart(slot, sectionName) {
|
|
1033
|
+
if (slot.role === 'hero') return undefined;
|
|
1034
|
+
if (slot.role === 'gallery' && aiStorefrontSays('collections', sectionName)) return 'collection';
|
|
1035
|
+
if (aiStorefrontSays('story', sectionName) || saysMadeByHand(slot.alt) || saysMadeByHand(sectionName)) return 'story';
|
|
1036
|
+
return undefined;
|
|
1037
|
+
}
|
|
1038
|
+
/** An object's kin phrases as stems, or none. */ const kinStems = (entry)=>entry ? [
|
|
1039
|
+
...new Set(entry.kin.map((phrase)=>stemList(phrase).join(' ')))
|
|
1040
|
+
] : [];
|
|
1041
|
+
/** The wrapper words a gift is named by, searched with the craft: "soap gift box". */ const GIFT_WRAPPERS = words('gift gifts box boxes basket baskets hamper hampers');
|
|
502
1042
|
/**
|
|
503
1043
|
* A product's searches (AGL-3676), most specific first: each of its subjects
|
|
504
1044
|
* — its name's noun phrase, then its photo's — with the shop's category
|
|
505
|
-
* where the phrase does not already
|
|
506
|
-
* category alone, a plain photo of what the shop sells. Every
|
|
507
|
-
* hit naming the category
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
|
|
511
|
-
|
|
1045
|
+
* where the phrase does not already name the shop's world ("candle gift
|
|
1046
|
+
* box"), then the category alone, a plain photo of what the shop sells. Every
|
|
1047
|
+
* one must find a hit naming the category — or the product's own word of the
|
|
1048
|
+
* shop's world, "lotion" in a soap shop — so no product is ever filled with a
|
|
1049
|
+
* lifestyle shot of something else: the beta.237 Willow Wick start put a
|
|
1050
|
+
* laptop and roses under "Candle Wick Trimmer" and an antique tea set under
|
|
1051
|
+
* "Candle Gift Set".
|
|
1052
|
+
*
|
|
1053
|
+
* And each must find a hit naming the product's own head noun among its
|
|
1054
|
+
* first tags (AGL-3660): the beta.239 Ember & Oak start put a bed with a book,
|
|
1055
|
+
* a mug and a camera (its candle a last tag) under "Hand-Poured Soy Candle".
|
|
1056
|
+
* An object whose own word is not enough ({@link OBJECT_KIN}) is searched by
|
|
1057
|
+
* its kin instead of the category alone, and named by them: a single taper
|
|
1058
|
+
* candle filled "Wax Melt Gift Set".
|
|
1059
|
+
*
|
|
1060
|
+
* Then, before a product is ever given a photo of something else, it falls
|
|
1061
|
+
* back to its own craft (AGL-3660). The Saltmarsh Soap Co start (2026-10-10)
|
|
1062
|
+
* found no hit naming "bar" or "box" among the first tags, and four of its
|
|
1063
|
+
* five products took the starter photos — a laptop and roses, a living room
|
|
1064
|
+
* of plants. So, in order:
|
|
1065
|
+
*
|
|
1066
|
+
* 1. The same searches again, the head noun named anywhere among the tags
|
|
1067
|
+
* (no extra search: the page remembers its answers), and a gift's own
|
|
1068
|
+
* wrapper with the craft ("soap gift box").
|
|
1069
|
+
* 2. The site's broad searches ("handmade soap", "natural skincare"), any hit
|
|
1070
|
+
* naming the craft itself ({@link AiStockSiteTerms.core}).
|
|
1071
|
+
*
|
|
1072
|
+
* The source then reuses an on-topic photo another slot of the job already
|
|
1073
|
+
* shows before it gives up ({@link aiLayoutStockPhotoSource}).
|
|
1074
|
+
*/ export function aiStockProductSearches(slot, craft, domain = [], fallback = {}) {
|
|
1075
|
+
var _ref, _subjects_, _fallback_broad, _fallback_core;
|
|
512
1076
|
var _slot_product;
|
|
513
1077
|
const orientation = aiStockOrientation(slot.aspect);
|
|
514
1078
|
const size = orientation === 'vertical' ? {
|
|
@@ -517,38 +1081,131 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
517
1081
|
minWidth: 900
|
|
518
1082
|
};
|
|
519
1083
|
const craftStems = stemList(craft);
|
|
1084
|
+
const world = new Set([
|
|
1085
|
+
...domain,
|
|
1086
|
+
...craftStems
|
|
1087
|
+
]);
|
|
1088
|
+
const subjects = ((_ref = (_slot_product = slot.product) == null ? void 0 : _slot_product.subjects) != null ? _ref : []).map((raw)=>aiStockSubjectWords(raw, '')).filter(Boolean);
|
|
1089
|
+
// The product's head noun is its name's: "candle" of "Hand-Poured Soy Candle".
|
|
1090
|
+
const own = (_subjects_ = subjects[0]) != null ? _subjects_ : '';
|
|
1091
|
+
const object = aiStockObjectWord(own);
|
|
1092
|
+
const kin = object ? objectKin(object, [
|
|
1093
|
+
...craftStems,
|
|
1094
|
+
...domain,
|
|
1095
|
+
...stemList(subjects.join(' '))
|
|
1096
|
+
]) : null;
|
|
1097
|
+
const kinPhrases = kinStems(kin);
|
|
1098
|
+
// A product of the shop's world beside its category is held to either:
|
|
1099
|
+
// a soap shop's body lotion names "lotion", never "soap".
|
|
1100
|
+
const ownWorld = stemList(own).filter((stem)=>world.has(stem) && !WRAPPERS.has(stem) && !craftStems.includes(stem));
|
|
1101
|
+
const category = craft && ownWorld.length ? [
|
|
1102
|
+
...craftStems,
|
|
1103
|
+
...ownWorld
|
|
1104
|
+
] : [];
|
|
520
1105
|
const queries = [];
|
|
521
|
-
for (const
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
const says = craftStems.every((stem)=>stemList(subject).includes(stem));
|
|
1106
|
+
for (const subject of subjects){
|
|
1107
|
+
const stems = stemList(subject);
|
|
1108
|
+
const query = kin ? stems.includes(aiStockStem(kin.qualifier)) ? subject : `${kin.qualifier} ${subject}` : craft && !stems.some((stem)=>world.has(stem)) ? `${craft} ${subject}` : subject;
|
|
525
1109
|
queries.push({
|
|
526
|
-
query
|
|
527
|
-
subject
|
|
1110
|
+
query,
|
|
1111
|
+
subject,
|
|
1112
|
+
object,
|
|
1113
|
+
lead: true
|
|
528
1114
|
});
|
|
529
1115
|
}
|
|
530
|
-
|
|
1116
|
+
// The category alone stands in only for a thing that is no more than of
|
|
1117
|
+
// it; a thing that is its own word of the shop's world is searched by that
|
|
1118
|
+
// word ("lotion"). Its hits are still scored by the product's own words: a
|
|
1119
|
+
// soy candle over a taper.
|
|
1120
|
+
if (kin) for (const query of kin.queries)queries.push({
|
|
1121
|
+
query,
|
|
1122
|
+
subject: own || query,
|
|
1123
|
+
object,
|
|
1124
|
+
lead: true
|
|
1125
|
+
});
|
|
1126
|
+
else if (object && ownWorld.includes(object)) queries.push({
|
|
1127
|
+
query: object,
|
|
1128
|
+
subject: own,
|
|
1129
|
+
object,
|
|
1130
|
+
lead: true
|
|
1131
|
+
});
|
|
1132
|
+
else if (craft) queries.push({
|
|
531
1133
|
query: craft,
|
|
532
|
-
subject: craft
|
|
1134
|
+
subject: own || craft,
|
|
1135
|
+
object: craft,
|
|
1136
|
+
lead: true
|
|
1137
|
+
});
|
|
1138
|
+
// 1. The head noun anywhere among the tags, by the same searches.
|
|
1139
|
+
for (const entry of [
|
|
1140
|
+
...queries
|
|
1141
|
+
])queries.push(_extends({}, entry, {
|
|
1142
|
+
lead: false
|
|
1143
|
+
}));
|
|
1144
|
+
// A gift is its craft's: "bath gift box" asks for a "soap gift box".
|
|
1145
|
+
const wrapped = own.split(/\s+/).filter((word)=>WRAPPERS.has(word) && aiStockStem(word) !== object);
|
|
1146
|
+
const gift = wrapped.find((word)=>GIFT_WRAPPERS.has(word));
|
|
1147
|
+
if (craft && gift && !kin) queries.push({
|
|
1148
|
+
query: `${craft} ${wrapped.join(' ')}`,
|
|
1149
|
+
subject: own,
|
|
1150
|
+
object: aiStockStem(gift),
|
|
1151
|
+
lead: false
|
|
533
1152
|
});
|
|
1153
|
+
// 2. The craft's own broad searches, any hit naming the craft.
|
|
1154
|
+
for (const query of (_fallback_broad = fallback.broad) != null ? _fallback_broad : [])queries.push({
|
|
1155
|
+
query,
|
|
1156
|
+
subject: own,
|
|
1157
|
+
object: '',
|
|
1158
|
+
lead: false,
|
|
1159
|
+
broad: true
|
|
1160
|
+
});
|
|
1161
|
+
const core = [
|
|
1162
|
+
...new Set([
|
|
1163
|
+
...craftStems,
|
|
1164
|
+
...(_fallback_core = fallback.core) != null ? _fallback_core : []
|
|
1165
|
+
])
|
|
1166
|
+
];
|
|
534
1167
|
const seen = new Set();
|
|
535
1168
|
const searches = [];
|
|
536
1169
|
for (const entry of queries){
|
|
537
1170
|
const query = clip(entry.query);
|
|
538
|
-
|
|
539
|
-
seen.
|
|
1171
|
+
const key = `${query}|${entry.lead}|${entry.object}|${entry.broad === true}`;
|
|
1172
|
+
if (!query || seen.has(key)) continue;
|
|
1173
|
+
seen.add(key);
|
|
1174
|
+
if (entry.broad) {
|
|
1175
|
+
searches.push(_extends({
|
|
1176
|
+
query,
|
|
1177
|
+
orientation
|
|
1178
|
+
}, size, {
|
|
1179
|
+
broad: true,
|
|
1180
|
+
subject: entry.subject
|
|
1181
|
+
}, core.length ? {
|
|
1182
|
+
category: core
|
|
1183
|
+
} : {}));
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
540
1186
|
searches.push(_extends({
|
|
541
1187
|
query,
|
|
542
1188
|
orientation
|
|
543
1189
|
}, size, {
|
|
544
1190
|
subject: entry.subject
|
|
545
|
-
},
|
|
1191
|
+
}, entry.lead ? {
|
|
1192
|
+
lead: true
|
|
1193
|
+
} : {
|
|
1194
|
+
leadWorld: true
|
|
1195
|
+
}, entry.object ? {
|
|
1196
|
+
object: entry.object
|
|
1197
|
+
} : {}, kinPhrases.length && entry.object === object ? {
|
|
1198
|
+
kin: kinPhrases
|
|
1199
|
+
} : {}, craft ? {
|
|
546
1200
|
requires: craft
|
|
1201
|
+
} : {}, category.length ? {
|
|
1202
|
+
category
|
|
547
1203
|
} : {}));
|
|
548
1204
|
}
|
|
549
1205
|
return searches;
|
|
550
1206
|
}
|
|
551
|
-
/** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, terms, subject) {
|
|
1207
|
+
/** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, terms, subject, part) {
|
|
1208
|
+
var _terms_core, _terms_making;
|
|
552
1209
|
const orientation = aiStockOrientation(slot.aspect);
|
|
553
1210
|
const size = slot.role === 'hero' ? {
|
|
554
1211
|
minWidth: 1600
|
|
@@ -560,13 +1217,50 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
560
1217
|
const domain = new Set(terms.domain);
|
|
561
1218
|
const object = aiStockObjectWord(subject);
|
|
562
1219
|
const subjectStems = stemList(subject);
|
|
1220
|
+
const kin = object ? objectKin(object, [
|
|
1221
|
+
...terms.domain,
|
|
1222
|
+
...subjectStems
|
|
1223
|
+
]) : null;
|
|
1224
|
+
const kinPhrases = kinStems(kin);
|
|
563
1225
|
// The craft qualifies a subject that does not already name the site's world.
|
|
564
1226
|
const namesWorld = subjectStems.some((stem)=>domain.has(stem));
|
|
1227
|
+
// A tile of the shop's world beside its category is held to either: a soap
|
|
1228
|
+
// shop's "Body care" tile names "body", never "soap" (AGL-3660).
|
|
1229
|
+
const ownWorld = subjectStems.filter((stem)=>domain.has(stem) && !WRAPPERS.has(stem) && stem !== terms.craft);
|
|
1230
|
+
// A hero of a craft names the craft itself: the Saltmarsh Soap Co hero
|
|
1231
|
+
// (2026-10-10) was a florist's bouquet tagged "natural" (AGL-3660).
|
|
1232
|
+
const core = (_terms_core = terms.core) != null ? _terms_core : [];
|
|
565
1233
|
const qualify = (phrase)=>terms.craft && !namesWorld && !stemList(phrase).includes(terms.craft) ? `${terms.craft} ${phrase}` : phrase;
|
|
566
|
-
|
|
1234
|
+
// A collection tile is product-like: held to the shop's category and its
|
|
1235
|
+
// own object among the first tags, and searched by its object's kin, never
|
|
1236
|
+
// the category's world alone (AGL-3660).
|
|
1237
|
+
const collection = part === 'collection' && Boolean(terms.craft);
|
|
1238
|
+
const broad = kin && collection ? [] : terms.broad.map((query)=>({
|
|
567
1239
|
query,
|
|
568
|
-
broad: true
|
|
1240
|
+
broad: true,
|
|
1241
|
+
category: collection
|
|
1242
|
+
}));
|
|
1243
|
+
// The maker at work, held to the craft: never "hands" (AGL-3660).
|
|
1244
|
+
const making = ((_terms_making = terms.making) != null ? _terms_making : []).map((query)=>({
|
|
1245
|
+
query,
|
|
1246
|
+
broad: true,
|
|
1247
|
+
category: true
|
|
569
1248
|
}));
|
|
1249
|
+
const thing = subject ? [
|
|
1250
|
+
{
|
|
1251
|
+
query: qualify(subject),
|
|
1252
|
+
held: true
|
|
1253
|
+
},
|
|
1254
|
+
...kin ? kin.queries.map((query)=>({
|
|
1255
|
+
query,
|
|
1256
|
+
held: true
|
|
1257
|
+
})) : [
|
|
1258
|
+
{
|
|
1259
|
+
query: qualify(object),
|
|
1260
|
+
held: true
|
|
1261
|
+
}
|
|
1262
|
+
]
|
|
1263
|
+
] : [];
|
|
570
1264
|
const queries = slot.role === 'hero' ? [
|
|
571
1265
|
{
|
|
572
1266
|
query: terms.business,
|
|
@@ -574,6 +1268,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
574
1268
|
},
|
|
575
1269
|
...broad
|
|
576
1270
|
] : slot.role === 'about' ? [
|
|
1271
|
+
...part === 'story' ? making : [],
|
|
577
1272
|
{
|
|
578
1273
|
query: `${terms.business} ${subject}`,
|
|
579
1274
|
broad: true,
|
|
@@ -584,16 +1279,13 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
584
1279
|
broad: true,
|
|
585
1280
|
people: true
|
|
586
1281
|
},
|
|
1282
|
+
...part === 'story' ? [] : making,
|
|
587
1283
|
...broad
|
|
588
|
-
] :
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
query: qualify(object)
|
|
594
|
-
},
|
|
1284
|
+
] : [
|
|
1285
|
+
...part === 'story' ? making : [],
|
|
1286
|
+
...thing,
|
|
595
1287
|
...broad
|
|
596
|
-
]
|
|
1288
|
+
];
|
|
597
1289
|
const seen = new Set();
|
|
598
1290
|
const searches = [];
|
|
599
1291
|
for (const raw of queries){
|
|
@@ -601,6 +1293,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
601
1293
|
const key = `${query}|${raw.people === true}`;
|
|
602
1294
|
if (!query || seen.has(key)) continue;
|
|
603
1295
|
seen.add(key);
|
|
1296
|
+
const held = raw.held === true && (collection || kinPhrases.length > 0);
|
|
604
1297
|
searches.push(_extends({
|
|
605
1298
|
query,
|
|
606
1299
|
orientation
|
|
@@ -608,6 +1301,19 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
608
1301
|
people: true
|
|
609
1302
|
} : {}, raw.broad ? {
|
|
610
1303
|
broad: true
|
|
1304
|
+
} : {}, held && kinPhrases.length ? {
|
|
1305
|
+
kin: kinPhrases
|
|
1306
|
+
} : {}, held && collection ? {
|
|
1307
|
+
lead: true
|
|
1308
|
+
} : {}, terms.craft && (raw.category || raw.held && collection) ? _extends({
|
|
1309
|
+
requires: terms.craft
|
|
1310
|
+
}, ownWorld.length ? {
|
|
1311
|
+
category: [
|
|
1312
|
+
terms.craft,
|
|
1313
|
+
...ownWorld
|
|
1314
|
+
]
|
|
1315
|
+
} : {}) : {}, slot.role === 'hero' && core.length ? {
|
|
1316
|
+
category: core
|
|
611
1317
|
} : {}));
|
|
612
1318
|
}
|
|
613
1319
|
return searches;
|
|
@@ -622,7 +1328,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
622
1328
|
}
|
|
623
1329
|
}
|
|
624
1330
|
/** A hit's words: the first few (what it is OF), every one, and each tag's phrase. */ function hitWords(photo) {
|
|
625
|
-
var _photo_tags, _photo_alt, _photo_alt1;
|
|
1331
|
+
var _photo_tags, _photo_alt, _photo_alt1, _photo_alt2;
|
|
626
1332
|
const slug = pageSlug(photo.pageUrl);
|
|
627
1333
|
const tags = ((_photo_tags = photo.tags) != null ? _photo_tags : []).map((tag)=>tag.toLowerCase().trim()).filter(Boolean);
|
|
628
1334
|
const lead = new Set(stemList([
|
|
@@ -639,17 +1345,42 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
639
1345
|
stemList((_photo_alt1 = photo.alt) != null ? _photo_alt1 : '').join(' '),
|
|
640
1346
|
stemList(slug).join(' ')
|
|
641
1347
|
];
|
|
1348
|
+
const leadPhrases = [
|
|
1349
|
+
...tags.slice(0, 3).map((tag)=>stemList(tag).join(' ')),
|
|
1350
|
+
stemList(slug).join(' ')
|
|
1351
|
+
];
|
|
642
1352
|
const rejected = tags.some((tag)=>REJECTED_TAGS.has(tag) || tag.split(/\s+/).some((word)=>REJECTED_TAGS.has(word)));
|
|
1353
|
+
// Whole words only, a hyphenated one kept whole: "cross-stitch" is not a cross.
|
|
1354
|
+
const words = new Set([
|
|
1355
|
+
...tags,
|
|
1356
|
+
((_photo_alt2 = photo.alt) != null ? _photo_alt2 : '').toLowerCase()
|
|
1357
|
+
].join(' ').split(/[^a-zÀ-ɏ-]+/).filter((word)=>word.length > 1).map(aiStockStem));
|
|
643
1358
|
return {
|
|
644
1359
|
lead,
|
|
645
1360
|
all,
|
|
646
1361
|
phrases,
|
|
647
|
-
|
|
1362
|
+
leadPhrases,
|
|
1363
|
+
rejected,
|
|
1364
|
+
words
|
|
648
1365
|
};
|
|
649
1366
|
}
|
|
1367
|
+
/** Whether a phrase is among a hit's phrases, word for word. */ const among = (phrases, phrase)=>phrases.some((found)=>` ${found} `.includes(` ${phrase} `));
|
|
1368
|
+
/**
|
|
1369
|
+
* Whether a hit carries a topic its site never named ({@link SENSITIVE_TOPICS}):
|
|
1370
|
+
* a cross, a grave, a flag, a gun, a glass of wine, a child, a mannequin.
|
|
1371
|
+
*/ export function aiStockSensitive(photo, named = []) {
|
|
1372
|
+
const { words: found } = hitWords(photo);
|
|
1373
|
+
const site = new Set(named);
|
|
1374
|
+
return SENSITIVE_STEMS.some((topic)=>[
|
|
1375
|
+
...topic.words
|
|
1376
|
+
].some((word)=>found.has(word)) && ![
|
|
1377
|
+
...topic.allow
|
|
1378
|
+
].some((word)=>site.has(word)));
|
|
1379
|
+
}
|
|
650
1380
|
/**
|
|
651
1381
|
* How well a hit shows what a slot needs (AGL-3660), or 0 when it must not
|
|
652
|
-
* fill it: a mockup or a blank never; a
|
|
1382
|
+
* fill it: a mockup or a blank never; a topic the site never named never
|
|
1383
|
+
* ({@link aiStockSensitive}); a picture of a thing only when the hit
|
|
653
1384
|
* names its object AND either the site's world or the subject's own
|
|
654
1385
|
* two-word phrase ("bud vase", "wick trimmer"); anything else only when it
|
|
655
1386
|
* names the site's world (when the site names one). Among the accepted, the
|
|
@@ -657,25 +1388,48 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
657
1388
|
* the subject's two-word phrase 3, each of its other words 1, and the
|
|
658
1389
|
* site's world 1, plus a quarter when it is among the first tags.
|
|
659
1390
|
*/ export function aiStockRelevance(photo, judgement) {
|
|
660
|
-
var _judgement_requires, _judgement_subject, _judgement_domain;
|
|
1391
|
+
var _judgement_rivals, _judgement_kin, _judgement_requires, _judgement_category, _judgement_subject, _judgement_object, _judgement_domain;
|
|
661
1392
|
const hit = hitWords(photo);
|
|
662
1393
|
if (hit.rejected) return 0;
|
|
1394
|
+
if (aiStockSensitive(photo, judgement.named)) return 0;
|
|
1395
|
+
// Glass art is not pottery: a rival craft's photo names this craft's own words.
|
|
1396
|
+
for (const rival of (_judgement_rivals = judgement.rivals) != null ? _judgement_rivals : [])if (rival.words.some((word)=>hit.all.has(word)) && !rival.core.some((word)=>hit.all.has(word))) return 0;
|
|
1397
|
+
const kin = (_judgement_kin = judgement.kin) != null ? _judgement_kin : [];
|
|
1398
|
+
const kinNamed = kin.some((phrase)=>among(hit.phrases, phrase));
|
|
1399
|
+
const kinLead = kin.some((phrase)=>among(hit.leadPhrases, phrase));
|
|
663
1400
|
const required = stemList((_judgement_requires = judgement.requires) != null ? _judgement_requires : '');
|
|
664
|
-
|
|
1401
|
+
const category = ((_judgement_category = judgement.category) != null ? _judgement_category : []).flatMap(stemList);
|
|
1402
|
+
const anyOf = category.length ? [
|
|
1403
|
+
...new Set([
|
|
1404
|
+
...required,
|
|
1405
|
+
...category
|
|
1406
|
+
])
|
|
1407
|
+
] : [];
|
|
1408
|
+
const demanded = required.length > 0 || anyOf.length > 0;
|
|
1409
|
+
const meets = anyOf.length ? anyOf.some((stem)=>hit.all.has(stem)) : required.every((stem)=>hit.all.has(stem));
|
|
1410
|
+
// An object's kin stands for the category: a wax melt is the candle shop's.
|
|
1411
|
+
if (demanded && !kinNamed && !meets) return 0;
|
|
665
1412
|
const subject = stemList((_judgement_subject = judgement.subject) != null ? _judgement_subject : '');
|
|
666
|
-
const object = judgement.subject ? aiStockObjectWord(judgement.subject) : '';
|
|
1413
|
+
const object = (_judgement_object = judgement.object) != null ? _judgement_object : judgement.subject ? aiStockObjectWord(judgement.subject) : '';
|
|
667
1414
|
const domain = ((_judgement_domain = judgement.domain) != null ? _judgement_domain : []).filter((stem)=>stem !== object);
|
|
668
1415
|
// A hit naming the word it must name is of the site's world: a product's
|
|
669
1416
|
// category is that world even where it is the product's own object.
|
|
670
|
-
const world =
|
|
671
|
-
const worldLead = required.length > 0 && required.every((stem)=>hit.lead.has(stem)) || domain.some((stem)=>hit.lead.has(stem));
|
|
1417
|
+
const world = demanded || kinNamed || domain.some((stem)=>hit.all.has(stem));
|
|
1418
|
+
const worldLead = (anyOf.length ? anyOf.some((stem)=>hit.lead.has(stem)) : required.length > 0 && required.every((stem)=>hit.lead.has(stem))) || kinLead || domain.some((stem)=>hit.lead.has(stem));
|
|
1419
|
+
if (judgement.leadWorld && !worldLead) return 0;
|
|
672
1420
|
const pair = subject.length >= 2 ? subject.slice(-2).join(' ') : '';
|
|
673
|
-
const phrased = Boolean(pair) && hit.phrases
|
|
1421
|
+
const phrased = Boolean(pair) && among(hit.phrases, pair);
|
|
674
1422
|
let score = 0;
|
|
675
1423
|
if (judgement.strict && object) {
|
|
676
|
-
|
|
1424
|
+
// An object with kin is named only by them: "melt" alone is resin art.
|
|
1425
|
+
const named = kin.length ? kinNamed : hit.all.has(object);
|
|
1426
|
+
if (!named) return 0;
|
|
1427
|
+
const leading = kin.length ? kinLead : hit.lead.has(object);
|
|
1428
|
+
// A lifestyle scene whose object is a last tag is not a photo OF it.
|
|
1429
|
+
if (judgement.lead && !leading) return 0;
|
|
677
1430
|
if (domain.length && !world && !phrased) return 0;
|
|
678
|
-
score +=
|
|
1431
|
+
score += leading ? 4 : 2;
|
|
1432
|
+
if (kinNamed) score += 3;
|
|
679
1433
|
} else if (domain.length) {
|
|
680
1434
|
if (!world) return 0;
|
|
681
1435
|
}
|
|
@@ -793,6 +1547,11 @@ const EXTENSIONS = {
|
|
|
793
1547
|
delete request.broad;
|
|
794
1548
|
delete request.subject;
|
|
795
1549
|
delete request.requires;
|
|
1550
|
+
delete request.object;
|
|
1551
|
+
delete request.kin;
|
|
1552
|
+
delete request.lead;
|
|
1553
|
+
delete request.leadWorld;
|
|
1554
|
+
delete request.category;
|
|
796
1555
|
const key = JSON.stringify([
|
|
797
1556
|
request.query,
|
|
798
1557
|
request.orientation,
|
|
@@ -811,7 +1570,7 @@ const EXTENSIONS = {
|
|
|
811
1570
|
answers.set(key, asked);
|
|
812
1571
|
return asked;
|
|
813
1572
|
};
|
|
814
|
-
/** The photo placed in the site's library, a copy it holds or a new one; `null` when it cannot be placed. */ const keep = async (photo, slot, query)=>{
|
|
1573
|
+
/** The photo placed in the site's library, a copy it holds or a new one; `null` when it cannot be placed. */ const keep = async (photo, slot, query, repeat = false)=>{
|
|
815
1574
|
const sourceKey = stockPhotoSourceKey(photo);
|
|
816
1575
|
try {
|
|
817
1576
|
var _EXTENSIONS_bytes_contentType;
|
|
@@ -822,7 +1581,7 @@ const EXTENSIONS = {
|
|
|
822
1581
|
if (kept) {
|
|
823
1582
|
var _kept_width, _kept_height;
|
|
824
1583
|
// Held because a slot of this page or another page of the job shows it.
|
|
825
|
-
if (placedSrcs.has(kept.src) || elsewhere(kept.src)) return null;
|
|
1584
|
+
if (!repeat && (placedSrcs.has(kept.src) || elsewhere(kept.src))) return null;
|
|
826
1585
|
remember(sourceKey, kept.src);
|
|
827
1586
|
return {
|
|
828
1587
|
src: kept.src,
|
|
@@ -885,6 +1644,50 @@ const EXTENSIONS = {
|
|
|
885
1644
|
}
|
|
886
1645
|
return null;
|
|
887
1646
|
};
|
|
1647
|
+
/** How a search's hits are judged for a slot. */ const judgementFor = (slot, request, subject)=>{
|
|
1648
|
+
var _request_subject;
|
|
1649
|
+
return _extends({
|
|
1650
|
+
subject: (_request_subject = request.subject) != null ? _request_subject : request.broad ? '' : subject,
|
|
1651
|
+
domain: terms.domain,
|
|
1652
|
+
strict: slot.role === 'gallery' && !request.broad || !!request.requires,
|
|
1653
|
+
named: terms.named,
|
|
1654
|
+
rivals: terms.rivals
|
|
1655
|
+
}, request.requires ? {
|
|
1656
|
+
requires: request.requires
|
|
1657
|
+
} : {}, request.category ? {
|
|
1658
|
+
category: request.category
|
|
1659
|
+
} : {}, request.object ? {
|
|
1660
|
+
object: request.object
|
|
1661
|
+
} : {}, request.kin ? {
|
|
1662
|
+
kin: request.kin
|
|
1663
|
+
} : {}, request.lead ? {
|
|
1664
|
+
lead: true
|
|
1665
|
+
} : {}, request.leadWorld ? {
|
|
1666
|
+
leadWorld: true
|
|
1667
|
+
} : {});
|
|
1668
|
+
};
|
|
1669
|
+
/**
|
|
1670
|
+
* The craft's own photos, the last thing a slot of a craft's site is
|
|
1671
|
+
* filled from (AGL-3660): its broad searches, any hit naming the craft
|
|
1672
|
+
* itself. Never a desk, a laptop or a living room for a soap shop.
|
|
1673
|
+
*/ const craftSearches = (slot)=>{
|
|
1674
|
+
if (!terms.core.length) return [];
|
|
1675
|
+
const orientation = aiStockOrientation(slot.aspect);
|
|
1676
|
+
const size = slot.role === 'hero' ? {
|
|
1677
|
+
minWidth: 1600
|
|
1678
|
+
} : orientation === 'vertical' ? {
|
|
1679
|
+
minHeight: 900
|
|
1680
|
+
} : {
|
|
1681
|
+
minWidth: 900
|
|
1682
|
+
};
|
|
1683
|
+
return terms.broad.map((query)=>_extends({
|
|
1684
|
+
query: clip(query),
|
|
1685
|
+
orientation
|
|
1686
|
+
}, size, {
|
|
1687
|
+
broad: true,
|
|
1688
|
+
category: terms.core
|
|
1689
|
+
}));
|
|
1690
|
+
};
|
|
888
1691
|
// 1. Choose, slot by slot, so the page's choices never collide: each
|
|
889
1692
|
// slot claims its best accepted hit before the next slot looks.
|
|
890
1693
|
const choices = [];
|
|
@@ -894,26 +1697,33 @@ const EXTENSIONS = {
|
|
|
894
1697
|
choices.push(null);
|
|
895
1698
|
continue;
|
|
896
1699
|
}
|
|
897
|
-
const
|
|
1700
|
+
const sectionName = (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '';
|
|
1701
|
+
const subject = aiStockSubjectWords(slot.alt, sectionName);
|
|
898
1702
|
let choice = null;
|
|
899
|
-
const requests =
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1703
|
+
const requests = [
|
|
1704
|
+
...slot.product ? aiStockProductSearches(slot, terms.craft, terms.domain, {
|
|
1705
|
+
broad: terms.broad,
|
|
1706
|
+
core: terms.core
|
|
1707
|
+
}) : aiStockSearchesFor(slot, terms, subject, aiStockSlotPart(slot, sectionName)),
|
|
1708
|
+
...craftSearches(slot)
|
|
1709
|
+
];
|
|
1710
|
+
/** The slot's accepted hits for a search; `null` once the page's searches are spent. */ const accepted = async (request, repeat)=>{
|
|
903
1711
|
const found = await search(request);
|
|
904
|
-
if (found === null)
|
|
905
|
-
const blocked = new Set(
|
|
906
|
-
|
|
907
|
-
const key
|
|
908
|
-
|
|
1712
|
+
if (found === null) return null;
|
|
1713
|
+
const blocked = new Set();
|
|
1714
|
+
if (!repeat) {
|
|
1715
|
+
for (const key of claimed)blocked.add(key);
|
|
1716
|
+
for (const hit of found){
|
|
1717
|
+
const key = stockPhotoSourceKey(hit);
|
|
1718
|
+
if (elsewhere(key)) blocked.add(key);
|
|
1719
|
+
}
|
|
909
1720
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
} : {}));
|
|
1721
|
+
return aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, judgementFor(slot, request, subject));
|
|
1722
|
+
};
|
|
1723
|
+
for (const request of requests){
|
|
1724
|
+
if (choice || signal.aborted) break;
|
|
1725
|
+
const ranked = await accepted(request, false);
|
|
1726
|
+
if (ranked === null) break;
|
|
917
1727
|
if (!ranked.length) continue;
|
|
918
1728
|
const first = ranked[0];
|
|
919
1729
|
claimed.add(stockPhotoSourceKey(first));
|
|
@@ -922,11 +1732,30 @@ const EXTENSIONS = {
|
|
|
922
1732
|
query: request.query
|
|
923
1733
|
};
|
|
924
1734
|
}
|
|
1735
|
+
// 2. Nothing new of its own: a product or a hero reuses an on-topic
|
|
1736
|
+
// photo another slot of the job already shows (a product may repeat
|
|
1737
|
+
// a collection tile's photo) before the starters' desk or living
|
|
1738
|
+
// room fills it (AGL-3660). Only the searches already answered are
|
|
1739
|
+
// asked again, from memory. Any other picture keeps "never twice".
|
|
1740
|
+
if (!choice && (slot.product || slot.role === 'hero')) {
|
|
1741
|
+
for (const request of requests){
|
|
1742
|
+
if (choice || signal.aborted) break;
|
|
1743
|
+
const ranked = await accepted(request, true);
|
|
1744
|
+
if (!(ranked == null ? void 0 : ranked.length)) continue;
|
|
1745
|
+
choice = {
|
|
1746
|
+
candidates: ranked.slice(0, AI_LAYOUT_STOCK_TRIES_PER_SLOT),
|
|
1747
|
+
query: request.query,
|
|
1748
|
+
repeat: true
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
925
1752
|
choices.push(choice);
|
|
926
1753
|
}
|
|
927
|
-
//
|
|
928
|
-
// accepted hit no other slot has claimed.
|
|
1754
|
+
// 3. Keep, a few at a time; a copy that fails tries the slot's next
|
|
1755
|
+
// accepted hit no other slot has claimed. A repeat waits for the
|
|
1756
|
+
// slot it repeats, so the library never holds a photo twice.
|
|
929
1757
|
const photos = slots.map(()=>null);
|
|
1758
|
+
/** What this pass placed, by source key. */ const placedByKey = new Map();
|
|
930
1759
|
const fill = async (index)=>{
|
|
931
1760
|
const choice = choices[index];
|
|
932
1761
|
const slot = slots[index];
|
|
@@ -934,28 +1763,43 @@ const EXTENSIONS = {
|
|
|
934
1763
|
for (const [rank, candidate] of choice.candidates.entries()){
|
|
935
1764
|
if (signal.aborted) return;
|
|
936
1765
|
const key = stockPhotoSourceKey(candidate);
|
|
1766
|
+
if (choice.repeat) {
|
|
1767
|
+
var _placedByKey_get;
|
|
1768
|
+
const placed = (_placedByKey_get = placedByKey.get(key)) != null ? _placedByKey_get : await keep(candidate, slot, choice.query, true);
|
|
1769
|
+
if (placed) {
|
|
1770
|
+
photos[index] = placed;
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
937
1775
|
if (rank > 0) {
|
|
938
1776
|
if (claimed.has(key)) continue;
|
|
939
1777
|
claimed.add(key);
|
|
940
1778
|
}
|
|
941
1779
|
const placed = await keep(candidate, slot, choice.query);
|
|
942
1780
|
if (placed) {
|
|
1781
|
+
placedByKey.set(key, placed);
|
|
943
1782
|
photos[index] = placed;
|
|
944
1783
|
return;
|
|
945
1784
|
}
|
|
946
1785
|
}
|
|
947
1786
|
};
|
|
1787
|
+
const firsts = slots.map((_slot, index)=>index).filter((index)=>{
|
|
1788
|
+
var _choices_index;
|
|
1789
|
+
return !((_choices_index = choices[index]) == null ? void 0 : _choices_index.repeat);
|
|
1790
|
+
});
|
|
948
1791
|
let next = 0;
|
|
949
1792
|
const worker = async ()=>{
|
|
950
|
-
while(next <
|
|
951
|
-
const index = next;
|
|
1793
|
+
while(next < firsts.length){
|
|
1794
|
+
const index = firsts[next];
|
|
952
1795
|
next += 1;
|
|
953
1796
|
await fill(index);
|
|
954
1797
|
}
|
|
955
1798
|
};
|
|
956
1799
|
await Promise.all(Array.from({
|
|
957
|
-
length: Math.min(AI_LAYOUT_STOCK_COPIES_AT_ONCE,
|
|
1800
|
+
length: Math.min(AI_LAYOUT_STOCK_COPIES_AT_ONCE, firsts.length)
|
|
958
1801
|
}, worker));
|
|
1802
|
+
for (const [index, choice] of choices.entries())if (choice == null ? void 0 : choice.repeat) await fill(index);
|
|
959
1803
|
return photos;
|
|
960
1804
|
};
|
|
961
1805
|
}
|