@aglyn/plugins-ai 1.0.0-beta.239 → 1.0.0-beta.241
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-job-requests.d.ts +5 -0
- package/src/lib/components/ai-job-requests.js +4 -2
- package/src/lib/components/ai-job-requests.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/components/assist-panel.component.js +14 -2
- package/src/lib/components/assist-panel.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 +11 -4
- 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 +26 -9
- 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 +53 -5
- 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 +51 -10
- 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-datasets.d.ts +89 -2
- package/src/lib/jobs/ai-job-site-datasets.js +239 -15
- 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 +69 -3
- package/src/lib/jobs/ai-layout-stock-photos.js +603 -42
- 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/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/layout-language/ai-layout-pictures.js +12 -4
- package/src/lib/layout-language/ai-layout-pictures.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/ai-doctrine-validators.d.ts +44 -0
- package/src/lib/runtime/ai-doctrine-validators.js +177 -0
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +3 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- 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
|
|
@@ -98,12 +110,17 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
98
110
|
* The unit job input naming the screen drafts the job's other pages were
|
|
99
111
|
* written to, whose photos a page does not place again (AGL-3660).
|
|
100
112
|
*/ export const AI_STOCK_PHOTO_PAGES_INPUT = 'stockPhotoPages';
|
|
113
|
+
/**
|
|
114
|
+
* The unit job input naming library photos (`media:` references) another
|
|
115
|
+
* part of the job already shows outside its pages — a dataset's records
|
|
116
|
+
* (AGL-3616) — which a page does not place again.
|
|
117
|
+
*/ export const AI_STOCK_PHOTO_AVOID_INPUT = 'stockPhotoAvoid';
|
|
101
118
|
const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
102
119
|
/** 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
120
|
/**
|
|
104
121
|
* Words that describe a picture's look, not what it shows, dropped from a
|
|
105
122
|
* 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 ' + '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');
|
|
123
|
+
*/ 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 ' + '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
124
|
/** 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
125
|
/** 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
126
|
/** 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 +136,345 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
119
136
|
* an empty frame on the page (the Juniper Clay hero), a template or a
|
|
120
137
|
* graphic is not a photo of anything.
|
|
121
138
|
*/ const REJECTED_TAGS = words('mockup mock-up mock blank template templates clipart clip-art illustration vector logo icon ' + 'ai-generated generated render rendering 3d');
|
|
139
|
+
/**
|
|
140
|
+
* Topics a business's photos never carry unless its own words name them
|
|
141
|
+
* (AGL-3660, the beta.239 Ember & Oak start, whose "Gifts for melt fans" tile
|
|
142
|
+
* showed devotional candles with crosses and names). A hit tagged with any
|
|
143
|
+
* word of a topic is rejected unless the site's business type or brief says
|
|
144
|
+
* one of the topic's words or its `allow` words. The last topic is not
|
|
145
|
+
* sensitive but is never the thing: the same start's story band showed bronze
|
|
146
|
+
* mannequin hands tagged "wax".
|
|
147
|
+
*/ const SENSITIVE_TOPICS = [
|
|
148
|
+
{
|
|
149
|
+
// Religion.
|
|
150
|
+
words: [
|
|
151
|
+
'cross',
|
|
152
|
+
'crosses',
|
|
153
|
+
'crucifix',
|
|
154
|
+
'church',
|
|
155
|
+
'chapel',
|
|
156
|
+
'cathedral',
|
|
157
|
+
'jesus',
|
|
158
|
+
'christ',
|
|
159
|
+
'christian',
|
|
160
|
+
'christianity',
|
|
161
|
+
'god',
|
|
162
|
+
'bible',
|
|
163
|
+
'rosary',
|
|
164
|
+
'prayer',
|
|
165
|
+
'pray',
|
|
166
|
+
'praying',
|
|
167
|
+
'worship',
|
|
168
|
+
'worships',
|
|
169
|
+
'religion',
|
|
170
|
+
'religious',
|
|
171
|
+
'faith',
|
|
172
|
+
'islam',
|
|
173
|
+
'islamic',
|
|
174
|
+
'muslim',
|
|
175
|
+
'mosque',
|
|
176
|
+
'quran',
|
|
177
|
+
'temple',
|
|
178
|
+
'buddha',
|
|
179
|
+
'buddhism',
|
|
180
|
+
'buddhist',
|
|
181
|
+
'hindu',
|
|
182
|
+
'hinduism',
|
|
183
|
+
'synagogue',
|
|
184
|
+
'jewish',
|
|
185
|
+
'judaism',
|
|
186
|
+
'ecclesiastical',
|
|
187
|
+
'saint',
|
|
188
|
+
'altar',
|
|
189
|
+
'sacrificial',
|
|
190
|
+
'nativity',
|
|
191
|
+
'priest',
|
|
192
|
+
'nun',
|
|
193
|
+
'monk'
|
|
194
|
+
],
|
|
195
|
+
allow: [
|
|
196
|
+
'ministry',
|
|
197
|
+
'parish',
|
|
198
|
+
'congregation',
|
|
199
|
+
'wedding'
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
// Death and mourning.
|
|
204
|
+
words: [
|
|
205
|
+
'funeral',
|
|
206
|
+
'memorial',
|
|
207
|
+
'grave',
|
|
208
|
+
'gravestone',
|
|
209
|
+
'cemetery',
|
|
210
|
+
'tomb',
|
|
211
|
+
'death',
|
|
212
|
+
'dead',
|
|
213
|
+
'grief',
|
|
214
|
+
'mourning',
|
|
215
|
+
'condolence',
|
|
216
|
+
'coffin',
|
|
217
|
+
'skull'
|
|
218
|
+
],
|
|
219
|
+
allow: [
|
|
220
|
+
'hospice',
|
|
221
|
+
'mortuary',
|
|
222
|
+
'cremation',
|
|
223
|
+
'obituary',
|
|
224
|
+
'halloween'
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
// Politics.
|
|
229
|
+
words: [
|
|
230
|
+
'political',
|
|
231
|
+
'politics',
|
|
232
|
+
'protest',
|
|
233
|
+
'protester',
|
|
234
|
+
'demonstration',
|
|
235
|
+
'rally',
|
|
236
|
+
'flag',
|
|
237
|
+
'election',
|
|
238
|
+
'vote',
|
|
239
|
+
'voting',
|
|
240
|
+
'parliament',
|
|
241
|
+
'government'
|
|
242
|
+
],
|
|
243
|
+
allow: [
|
|
244
|
+
'advocacy',
|
|
245
|
+
'campaign',
|
|
246
|
+
'candidate',
|
|
247
|
+
'council',
|
|
248
|
+
'nonprofit'
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
// Weapons and war.
|
|
253
|
+
words: [
|
|
254
|
+
'gun',
|
|
255
|
+
'rifle',
|
|
256
|
+
'pistol',
|
|
257
|
+
'weapon',
|
|
258
|
+
'firearm',
|
|
259
|
+
'ammunition',
|
|
260
|
+
'bullet',
|
|
261
|
+
'sword',
|
|
262
|
+
'war',
|
|
263
|
+
'military',
|
|
264
|
+
'army',
|
|
265
|
+
'soldier'
|
|
266
|
+
],
|
|
267
|
+
allow: [
|
|
268
|
+
'hunting',
|
|
269
|
+
'archery',
|
|
270
|
+
'martial',
|
|
271
|
+
'fencing',
|
|
272
|
+
'veteran'
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
// Alcohol, for a business that does not serve it.
|
|
277
|
+
words: [
|
|
278
|
+
'alcohol',
|
|
279
|
+
'wine',
|
|
280
|
+
'beer',
|
|
281
|
+
'whiskey',
|
|
282
|
+
'whisky',
|
|
283
|
+
'vodka',
|
|
284
|
+
'cocktail',
|
|
285
|
+
'liquor',
|
|
286
|
+
'rum',
|
|
287
|
+
'gin',
|
|
288
|
+
'champagne',
|
|
289
|
+
'drunk'
|
|
290
|
+
],
|
|
291
|
+
allow: [
|
|
292
|
+
'winery',
|
|
293
|
+
'vineyard',
|
|
294
|
+
'brewery',
|
|
295
|
+
'distillery',
|
|
296
|
+
'pub',
|
|
297
|
+
'tavern',
|
|
298
|
+
'taproom',
|
|
299
|
+
'bartender',
|
|
300
|
+
'sommelier',
|
|
301
|
+
'spirits'
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
// Tobacco and drugs.
|
|
306
|
+
words: [
|
|
307
|
+
'cigarette',
|
|
308
|
+
'smoking',
|
|
309
|
+
'tobacco',
|
|
310
|
+
'cannabis',
|
|
311
|
+
'marijuana',
|
|
312
|
+
'drug',
|
|
313
|
+
'drugs'
|
|
314
|
+
],
|
|
315
|
+
allow: [
|
|
316
|
+
'dispensary',
|
|
317
|
+
'vape',
|
|
318
|
+
'cigar'
|
|
319
|
+
]
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
// Children, for a business that is not about them.
|
|
323
|
+
words: [
|
|
324
|
+
'child',
|
|
325
|
+
'children',
|
|
326
|
+
'kid',
|
|
327
|
+
'kids',
|
|
328
|
+
'baby',
|
|
329
|
+
'babies',
|
|
330
|
+
'toddler',
|
|
331
|
+
'infant'
|
|
332
|
+
],
|
|
333
|
+
allow: [
|
|
334
|
+
'nursery',
|
|
335
|
+
'daycare',
|
|
336
|
+
'preschool',
|
|
337
|
+
'kindergarten',
|
|
338
|
+
'school',
|
|
339
|
+
'tutor',
|
|
340
|
+
'tutoring',
|
|
341
|
+
'parent',
|
|
342
|
+
'parents',
|
|
343
|
+
'parenting',
|
|
344
|
+
'family',
|
|
345
|
+
'families',
|
|
346
|
+
'pediatric',
|
|
347
|
+
'paediatric',
|
|
348
|
+
'newborn',
|
|
349
|
+
'maternity'
|
|
350
|
+
]
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
// Nudity, for every business: no word of a brief lets it in (Kiln &
|
|
354
|
+
// Clover, 2026-10-10: a nude torso sculpture filled "About the artist").
|
|
355
|
+
// Never bare "body": a skincare shop's body lotion is its product.
|
|
356
|
+
words: [
|
|
357
|
+
'nude',
|
|
358
|
+
'nudes',
|
|
359
|
+
'nudity',
|
|
360
|
+
'naked',
|
|
361
|
+
'torso',
|
|
362
|
+
'breast',
|
|
363
|
+
'breasts',
|
|
364
|
+
'erotic',
|
|
365
|
+
'sensual',
|
|
366
|
+
'lingerie',
|
|
367
|
+
'bikini'
|
|
368
|
+
],
|
|
369
|
+
allow: [],
|
|
370
|
+
always: true
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
// Figures, never the maker: mannequin hands are not a candlemaker's.
|
|
374
|
+
words: [
|
|
375
|
+
'mannequin',
|
|
376
|
+
'mannequins',
|
|
377
|
+
'dummy',
|
|
378
|
+
'statue',
|
|
379
|
+
'sculpture',
|
|
380
|
+
'figurine',
|
|
381
|
+
'doll',
|
|
382
|
+
'dolls',
|
|
383
|
+
'waxwork'
|
|
384
|
+
],
|
|
385
|
+
allow: [
|
|
386
|
+
'sculptor',
|
|
387
|
+
'museum',
|
|
388
|
+
'fashion',
|
|
389
|
+
'boutique',
|
|
390
|
+
'clothing',
|
|
391
|
+
'apparel',
|
|
392
|
+
'toy',
|
|
393
|
+
'toys'
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
];
|
|
397
|
+
/** Each topic's words and the words that allow it, as stems. */ const SENSITIVE_STEMS = SENSITIVE_TOPICS.map((topic)=>{
|
|
398
|
+
var _topic_allow;
|
|
399
|
+
return {
|
|
400
|
+
words: new Set(topic.words.map(aiStockStem)),
|
|
401
|
+
allow: new Set(topic.always ? [] : [
|
|
402
|
+
...topic.words,
|
|
403
|
+
...(_topic_allow = topic.allow) != null ? _topic_allow : []
|
|
404
|
+
].map(aiStockStem))
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
/**
|
|
408
|
+
* Hands are never a picture's subject (AGL-3660): "hand-poured", "handmade"
|
|
409
|
+
* and "by hand" say how a thing was made, and a search for "hands" found
|
|
410
|
+
* bronze mannequin hands for the beta.239 Ember & Oak story band. Each of
|
|
411
|
+
* these is dropped from a picture's words; a hand only stays where it names
|
|
412
|
+
* a thing sold for hands ("hand cream").
|
|
413
|
+
*/ const HAND_IDIOMS = [
|
|
414
|
+
/\b(?:made\s+|poured\s+|crafted\s+|finished\s+|done\s+)?by\s+(?:our\s+|my\s+|their\s+)?(?:own\s+)?hands?\b/g,
|
|
415
|
+
/\bwith\s+(?:our|my|their)\s+(?:own\s+)?(?:two\s+)?hands\b/g,
|
|
416
|
+
/\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,
|
|
417
|
+
/\bhands?\b(?!\s+(?:creams?|soaps?|lotions?|wash|towels?|saniti[sz]ers?|salves?|balms?|tools?|saws?|planes?|drills?|mixers?|puppets?|bags?|warmers?))/g
|
|
418
|
+
];
|
|
419
|
+
/** A text without its hands. */ function withoutHands(text) {
|
|
420
|
+
return HAND_IDIOMS.reduce((out, idiom)=>out.replace(idiom, ' '), text.toLowerCase());
|
|
421
|
+
}
|
|
422
|
+
/** Whether a text says how a thing was made by hand: a maker at work. */ function saysMadeByHand(text) {
|
|
423
|
+
const lower = text.toLowerCase();
|
|
424
|
+
return HAND_IDIOMS.some((idiom)=>{
|
|
425
|
+
idiom.lastIndex = 0;
|
|
426
|
+
const found = idiom.test(lower);
|
|
427
|
+
idiom.lastIndex = 0;
|
|
428
|
+
return found;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* The kin of a few objects whose own word is not enough (AGL-3660): a "melt"
|
|
433
|
+
* is any melting thing (the beta.239 "Wax melts" tile showed resin fluid
|
|
434
|
+
* art), so a wax melt names one of these phrases. `within` is the world they
|
|
435
|
+
* are of; `qualifier` the word a search adds; `queries` the searches for the
|
|
436
|
+
* object itself, which stand in for the shop's category alone: a candle is
|
|
437
|
+
* not a wax melt.
|
|
438
|
+
*/ const OBJECT_KIN = [
|
|
439
|
+
{
|
|
440
|
+
objects: [
|
|
441
|
+
'melt',
|
|
442
|
+
'tart',
|
|
443
|
+
'cube'
|
|
444
|
+
],
|
|
445
|
+
within: [
|
|
446
|
+
'wax',
|
|
447
|
+
'candle',
|
|
448
|
+
'soy'
|
|
449
|
+
],
|
|
450
|
+
qualifier: 'wax',
|
|
451
|
+
kin: [
|
|
452
|
+
'wax melt',
|
|
453
|
+
'wax melts',
|
|
454
|
+
'wax cube',
|
|
455
|
+
'wax cubes',
|
|
456
|
+
'wax tart',
|
|
457
|
+
'wax tarts',
|
|
458
|
+
'melt warmer',
|
|
459
|
+
'wax warmer',
|
|
460
|
+
'tart warmer',
|
|
461
|
+
'oil burner',
|
|
462
|
+
'oil warmer',
|
|
463
|
+
'aroma burner',
|
|
464
|
+
'scent warmer',
|
|
465
|
+
'fragrance warmer'
|
|
466
|
+
],
|
|
467
|
+
queries: [
|
|
468
|
+
'wax melts',
|
|
469
|
+
'wax melt warmer',
|
|
470
|
+
'scented wax cubes'
|
|
471
|
+
]
|
|
472
|
+
}
|
|
473
|
+
];
|
|
474
|
+
/** An object's kin where the site or the subject is of its world, else `null`. */ function objectKin(object, world) {
|
|
475
|
+
var _OBJECT_KIN_find;
|
|
476
|
+
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;
|
|
477
|
+
}
|
|
122
478
|
/**
|
|
123
479
|
* The kin of a few common crafts: the words a photo of that world is
|
|
124
480
|
* tagged with, and the broad searches that find it. A craft not listed
|
|
@@ -150,6 +506,33 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
150
506
|
broad: [
|
|
151
507
|
'handmade ceramics',
|
|
152
508
|
'pottery'
|
|
509
|
+
],
|
|
510
|
+
rivals: {
|
|
511
|
+
words: [
|
|
512
|
+
'glass',
|
|
513
|
+
'glassware',
|
|
514
|
+
'glasswork',
|
|
515
|
+
'glassblowing',
|
|
516
|
+
'blown',
|
|
517
|
+
'stained',
|
|
518
|
+
'murano',
|
|
519
|
+
'crystal'
|
|
520
|
+
],
|
|
521
|
+
core: [
|
|
522
|
+
'ceramic',
|
|
523
|
+
'ceramics',
|
|
524
|
+
'pottery',
|
|
525
|
+
'potter',
|
|
526
|
+
'stoneware',
|
|
527
|
+
'porcelain',
|
|
528
|
+
'earthenware',
|
|
529
|
+
'clay',
|
|
530
|
+
'terracotta'
|
|
531
|
+
]
|
|
532
|
+
},
|
|
533
|
+
making: [
|
|
534
|
+
'pottery wheel',
|
|
535
|
+
'potter at work'
|
|
153
536
|
]
|
|
154
537
|
},
|
|
155
538
|
{
|
|
@@ -171,6 +554,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
171
554
|
broad: [
|
|
172
555
|
'handmade candles',
|
|
173
556
|
'scented candles'
|
|
557
|
+
],
|
|
558
|
+
making: [
|
|
559
|
+
'candle making',
|
|
560
|
+
'pouring wax'
|
|
174
561
|
]
|
|
175
562
|
},
|
|
176
563
|
{
|
|
@@ -196,6 +583,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
196
583
|
broad: [
|
|
197
584
|
'handmade jewelry',
|
|
198
585
|
'jewelry'
|
|
586
|
+
],
|
|
587
|
+
making: [
|
|
588
|
+
'jewelry making',
|
|
589
|
+
'jeweler at work'
|
|
199
590
|
]
|
|
200
591
|
},
|
|
201
592
|
{
|
|
@@ -216,6 +607,9 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
216
607
|
broad: [
|
|
217
608
|
'handmade soap',
|
|
218
609
|
'natural skincare'
|
|
610
|
+
],
|
|
611
|
+
making: [
|
|
612
|
+
'soap making'
|
|
219
613
|
]
|
|
220
614
|
},
|
|
221
615
|
{
|
|
@@ -242,6 +636,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
242
636
|
broad: [
|
|
243
637
|
'woodworking',
|
|
244
638
|
'wooden furniture'
|
|
639
|
+
],
|
|
640
|
+
making: [
|
|
641
|
+
'woodworking workshop',
|
|
642
|
+
'carpenter at work'
|
|
245
643
|
]
|
|
246
644
|
},
|
|
247
645
|
{
|
|
@@ -266,6 +664,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
266
664
|
broad: [
|
|
267
665
|
'bakery',
|
|
268
666
|
'fresh bread'
|
|
667
|
+
],
|
|
668
|
+
making: [
|
|
669
|
+
'baker kneading dough',
|
|
670
|
+
'baking bread'
|
|
269
671
|
]
|
|
270
672
|
},
|
|
271
673
|
{
|
|
@@ -288,6 +690,9 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
288
690
|
broad: [
|
|
289
691
|
'coffee shop',
|
|
290
692
|
'coffee'
|
|
693
|
+
],
|
|
694
|
+
making: [
|
|
695
|
+
'barista making coffee'
|
|
291
696
|
]
|
|
292
697
|
},
|
|
293
698
|
{
|
|
@@ -307,6 +712,9 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
307
712
|
broad: [
|
|
308
713
|
'florist',
|
|
309
714
|
'flower bouquet'
|
|
715
|
+
],
|
|
716
|
+
making: [
|
|
717
|
+
'florist arranging flowers'
|
|
310
718
|
]
|
|
311
719
|
},
|
|
312
720
|
{
|
|
@@ -336,6 +744,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
336
744
|
broad: [
|
|
337
745
|
'handmade textiles',
|
|
338
746
|
'yarn'
|
|
747
|
+
],
|
|
748
|
+
making: [
|
|
749
|
+
'knitting',
|
|
750
|
+
'weaving loom'
|
|
339
751
|
]
|
|
340
752
|
},
|
|
341
753
|
{
|
|
@@ -354,6 +766,9 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
354
766
|
broad: [
|
|
355
767
|
'leather craft',
|
|
356
768
|
'leather goods'
|
|
769
|
+
],
|
|
770
|
+
making: [
|
|
771
|
+
'leather crafting'
|
|
357
772
|
]
|
|
358
773
|
}
|
|
359
774
|
];
|
|
@@ -440,7 +855,8 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
440
855
|
// What it makes names its world by what the things are made of, never
|
|
441
856
|
// the things: "stoneware" of "stoneware bowls", "soy" of "soy candles".
|
|
442
857
|
// A bowl is not a ceramic world; a robin's food bowl is not pottery.
|
|
443
|
-
|
|
858
|
+
// A set or a gift is never a world: "gift" of "gift sets" is not one.
|
|
859
|
+
const world = (making ? kept.slice(0, -1) : kept).filter((word)=>!WRAPPERS.has(word));
|
|
444
860
|
for (const word of world)for (const stem of stemList(word))domain.add(stem);
|
|
445
861
|
if (making) for (const word of kept.slice(-1))for (const stem of stemList(word))made.add(stem);
|
|
446
862
|
}
|
|
@@ -448,10 +864,17 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
448
864
|
// A business named only by what it sells ("a shop selling pottery") is of that world.
|
|
449
865
|
if (!domain.size) for (const stem of made)domain.add(stem);
|
|
450
866
|
const broad = [];
|
|
867
|
+
const makingSearches = [];
|
|
868
|
+
const rivals = [];
|
|
451
869
|
for (const group of CRAFT_KIN){
|
|
452
870
|
if (!group.match.some((word)=>domain.has(aiStockStem(word)) || made.has(aiStockStem(word)))) continue;
|
|
453
871
|
for (const word of group.kin)domain.add(aiStockStem(word));
|
|
454
872
|
broad.push(...group.broad);
|
|
873
|
+
makingSearches.push(...group.making);
|
|
874
|
+
if (group.rivals) rivals.push({
|
|
875
|
+
words: group.rivals.words.map(aiStockStem),
|
|
876
|
+
core: group.rivals.core.map(aiStockStem)
|
|
877
|
+
});
|
|
455
878
|
}
|
|
456
879
|
if (!broad.length) broad.push(business, craft);
|
|
457
880
|
return {
|
|
@@ -462,7 +885,14 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
462
885
|
].filter((stem)=>stem.length > 1),
|
|
463
886
|
broad: [
|
|
464
887
|
...new Set(broad.map((query)=>query.trim()).filter(Boolean))
|
|
465
|
-
]
|
|
888
|
+
],
|
|
889
|
+
making: [
|
|
890
|
+
...new Set(makingSearches)
|
|
891
|
+
],
|
|
892
|
+
named: [
|
|
893
|
+
...new Set(stemList(businessType))
|
|
894
|
+
],
|
|
895
|
+
rivals
|
|
466
896
|
};
|
|
467
897
|
}
|
|
468
898
|
/** A phrase's content words without its filler; all filler, and nothing is left. */ function subjectOf(list) {
|
|
@@ -474,11 +904,12 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
474
904
|
* preposition starts the setting, filler dropped, its last three words (the
|
|
475
905
|
* head noun is the last) — else its section's name.
|
|
476
906
|
*/ export function aiStockSubjectWords(alt, sectionName) {
|
|
477
|
-
|
|
907
|
+
// "Hand-poured", "by hand" and "hands" say how it was made, never what it shows.
|
|
908
|
+
for (const clause of withoutHands(alt).split(CLAUSE_BREAK)){
|
|
478
909
|
const found = subjectOf(contentWords(clause != null ? clause : ''));
|
|
479
910
|
if (found.length) return found.slice(-3).join(' ');
|
|
480
911
|
}
|
|
481
|
-
return subjectOf(contentWords(sectionName)).slice(0, 2).join(' ');
|
|
912
|
+
return subjectOf(contentWords(withoutHands(sectionName))).slice(0, 2).join(' ');
|
|
482
913
|
}
|
|
483
914
|
/**
|
|
484
915
|
* The thing a subject shows, one singular word: its last word that is not
|
|
@@ -499,6 +930,19 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
499
930
|
if (aspect < 0.87) return 'vertical';
|
|
500
931
|
return 'any';
|
|
501
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* A slot's part (AGL-3660): a tile of a collections band is product-like,
|
|
935
|
+
* held to the shop's category and its own object; a picture of a story of
|
|
936
|
+
* the craft, or of a thing made by hand, shows the maker at work.
|
|
937
|
+
*/ export function aiStockSlotPart(slot, sectionName) {
|
|
938
|
+
if (slot.role === 'hero') return undefined;
|
|
939
|
+
if (slot.role === 'gallery' && aiStorefrontSays('collections', sectionName)) return 'collection';
|
|
940
|
+
if (aiStorefrontSays('story', sectionName) || saysMadeByHand(slot.alt) || saysMadeByHand(sectionName)) return 'story';
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
/** An object's kin phrases as stems, or none. */ const kinStems = (entry)=>entry ? [
|
|
944
|
+
...new Set(entry.kin.map((phrase)=>stemList(phrase).join(' ')))
|
|
945
|
+
] : [];
|
|
502
946
|
/**
|
|
503
947
|
* A product's searches (AGL-3676), most specific first: each of its subjects
|
|
504
948
|
* — its name's noun phrase, then its photo's — with the shop's category
|
|
@@ -507,8 +951,15 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
507
951
|
* hit naming the category, so no product is ever filled with a lifestyle shot
|
|
508
952
|
* of something else: the beta.237 Willow Wick start put a laptop and roses
|
|
509
953
|
* under "Candle Wick Trimmer" and an antique tea set under "Candle Gift Set".
|
|
510
|
-
|
|
511
|
-
|
|
954
|
+
*
|
|
955
|
+
* And every one must find a hit naming the product's own head noun among its
|
|
956
|
+
* first tags (AGL-3660): the beta.239 Ember & Oak start put a bed with a book,
|
|
957
|
+
* a mug and a camera (its candle a last tag) under "Hand-Poured Soy Candle".
|
|
958
|
+
* An object whose own word is not enough ({@link OBJECT_KIN}) is searched by
|
|
959
|
+
* its kin instead of the category alone, and named by them: a single taper
|
|
960
|
+
* candle filled "Wax Melt Gift Set".
|
|
961
|
+
*/ export function aiStockProductSearches(slot, craft, domain = []) {
|
|
962
|
+
var _ref, _subjects_, _subjects_1;
|
|
512
963
|
var _slot_product;
|
|
513
964
|
const orientation = aiStockOrientation(slot.aspect);
|
|
514
965
|
const size = orientation === 'vertical' ? {
|
|
@@ -517,19 +968,37 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
517
968
|
minWidth: 900
|
|
518
969
|
};
|
|
519
970
|
const craftStems = stemList(craft);
|
|
971
|
+
const subjects = ((_ref = (_slot_product = slot.product) == null ? void 0 : _slot_product.subjects) != null ? _ref : []).map((raw)=>aiStockSubjectWords(raw, '')).filter(Boolean);
|
|
972
|
+
// The product's head noun is its name's: "candle" of "Hand-Poured Soy Candle".
|
|
973
|
+
const object = aiStockObjectWord((_subjects_ = subjects[0]) != null ? _subjects_ : '');
|
|
974
|
+
const kin = object ? objectKin(object, [
|
|
975
|
+
...craftStems,
|
|
976
|
+
...domain,
|
|
977
|
+
...stemList(subjects.join(' '))
|
|
978
|
+
]) : null;
|
|
979
|
+
const kinPhrases = kinStems(kin);
|
|
520
980
|
const queries = [];
|
|
521
|
-
for (const
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
const says = craftStems.every((stem)=>stemList(subject).includes(stem));
|
|
981
|
+
for (const subject of subjects){
|
|
982
|
+
const stems = stemList(subject);
|
|
983
|
+
const query = kin ? stems.includes(aiStockStem(kin.qualifier)) ? subject : `${kin.qualifier} ${subject}` : craft && !craftStems.every((stem)=>stems.includes(stem)) ? `${craft} ${subject}` : subject;
|
|
525
984
|
queries.push({
|
|
526
|
-
query
|
|
527
|
-
subject
|
|
985
|
+
query,
|
|
986
|
+
subject,
|
|
987
|
+
object
|
|
528
988
|
});
|
|
529
989
|
}
|
|
530
|
-
|
|
990
|
+
// The category alone stands in only for a thing that is no more than of it.
|
|
991
|
+
// Its hits are still scored by the product's own words: a soy candle over a taper.
|
|
992
|
+
const own = (_subjects_1 = subjects[0]) != null ? _subjects_1 : '';
|
|
993
|
+
if (kin) for (const query of kin.queries)queries.push({
|
|
994
|
+
query,
|
|
995
|
+
subject: own || query,
|
|
996
|
+
object
|
|
997
|
+
});
|
|
998
|
+
else if (craft) queries.push({
|
|
531
999
|
query: craft,
|
|
532
|
-
subject: craft
|
|
1000
|
+
subject: own || craft,
|
|
1001
|
+
object: craft
|
|
533
1002
|
});
|
|
534
1003
|
const seen = new Set();
|
|
535
1004
|
const searches = [];
|
|
@@ -541,14 +1010,20 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
541
1010
|
query,
|
|
542
1011
|
orientation
|
|
543
1012
|
}, size, {
|
|
544
|
-
subject: entry.subject
|
|
545
|
-
|
|
1013
|
+
subject: entry.subject,
|
|
1014
|
+
lead: true
|
|
1015
|
+
}, entry.object ? {
|
|
1016
|
+
object: entry.object
|
|
1017
|
+
} : {}, kinPhrases.length ? {
|
|
1018
|
+
kin: kinPhrases
|
|
1019
|
+
} : {}, craft ? {
|
|
546
1020
|
requires: craft
|
|
547
1021
|
} : {}));
|
|
548
1022
|
}
|
|
549
1023
|
return searches;
|
|
550
1024
|
}
|
|
551
|
-
/** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, terms, subject) {
|
|
1025
|
+
/** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, terms, subject, part) {
|
|
1026
|
+
var _terms_making;
|
|
552
1027
|
const orientation = aiStockOrientation(slot.aspect);
|
|
553
1028
|
const size = slot.role === 'hero' ? {
|
|
554
1029
|
minWidth: 1600
|
|
@@ -560,13 +1035,44 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
560
1035
|
const domain = new Set(terms.domain);
|
|
561
1036
|
const object = aiStockObjectWord(subject);
|
|
562
1037
|
const subjectStems = stemList(subject);
|
|
1038
|
+
const kin = object ? objectKin(object, [
|
|
1039
|
+
...terms.domain,
|
|
1040
|
+
...subjectStems
|
|
1041
|
+
]) : null;
|
|
1042
|
+
const kinPhrases = kinStems(kin);
|
|
563
1043
|
// The craft qualifies a subject that does not already name the site's world.
|
|
564
1044
|
const namesWorld = subjectStems.some((stem)=>domain.has(stem));
|
|
565
1045
|
const qualify = (phrase)=>terms.craft && !namesWorld && !stemList(phrase).includes(terms.craft) ? `${terms.craft} ${phrase}` : phrase;
|
|
566
|
-
|
|
1046
|
+
// A collection tile is product-like: held to the shop's category and its
|
|
1047
|
+
// own object among the first tags, and searched by its object's kin, never
|
|
1048
|
+
// the category's world alone (AGL-3660).
|
|
1049
|
+
const collection = part === 'collection' && Boolean(terms.craft);
|
|
1050
|
+
const broad = kin && collection ? [] : terms.broad.map((query)=>({
|
|
567
1051
|
query,
|
|
568
|
-
broad: true
|
|
1052
|
+
broad: true,
|
|
1053
|
+
category: collection
|
|
1054
|
+
}));
|
|
1055
|
+
// The maker at work, held to the craft: never "hands" (AGL-3660).
|
|
1056
|
+
const making = ((_terms_making = terms.making) != null ? _terms_making : []).map((query)=>({
|
|
1057
|
+
query,
|
|
1058
|
+
broad: true,
|
|
1059
|
+
category: true
|
|
569
1060
|
}));
|
|
1061
|
+
const thing = subject ? [
|
|
1062
|
+
{
|
|
1063
|
+
query: qualify(subject),
|
|
1064
|
+
held: true
|
|
1065
|
+
},
|
|
1066
|
+
...kin ? kin.queries.map((query)=>({
|
|
1067
|
+
query,
|
|
1068
|
+
held: true
|
|
1069
|
+
})) : [
|
|
1070
|
+
{
|
|
1071
|
+
query: qualify(object),
|
|
1072
|
+
held: true
|
|
1073
|
+
}
|
|
1074
|
+
]
|
|
1075
|
+
] : [];
|
|
570
1076
|
const queries = slot.role === 'hero' ? [
|
|
571
1077
|
{
|
|
572
1078
|
query: terms.business,
|
|
@@ -574,6 +1080,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
574
1080
|
},
|
|
575
1081
|
...broad
|
|
576
1082
|
] : slot.role === 'about' ? [
|
|
1083
|
+
...part === 'story' ? making : [],
|
|
577
1084
|
{
|
|
578
1085
|
query: `${terms.business} ${subject}`,
|
|
579
1086
|
broad: true,
|
|
@@ -584,16 +1091,13 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
584
1091
|
broad: true,
|
|
585
1092
|
people: true
|
|
586
1093
|
},
|
|
1094
|
+
...part === 'story' ? [] : making,
|
|
587
1095
|
...broad
|
|
588
|
-
] :
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
query: qualify(object)
|
|
594
|
-
},
|
|
1096
|
+
] : [
|
|
1097
|
+
...part === 'story' ? making : [],
|
|
1098
|
+
...thing,
|
|
595
1099
|
...broad
|
|
596
|
-
]
|
|
1100
|
+
];
|
|
597
1101
|
const seen = new Set();
|
|
598
1102
|
const searches = [];
|
|
599
1103
|
for (const raw of queries){
|
|
@@ -601,6 +1105,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
601
1105
|
const key = `${query}|${raw.people === true}`;
|
|
602
1106
|
if (!query || seen.has(key)) continue;
|
|
603
1107
|
seen.add(key);
|
|
1108
|
+
const held = raw.held === true && (collection || kinPhrases.length > 0);
|
|
604
1109
|
searches.push(_extends({
|
|
605
1110
|
query,
|
|
606
1111
|
orientation
|
|
@@ -608,6 +1113,12 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
608
1113
|
people: true
|
|
609
1114
|
} : {}, raw.broad ? {
|
|
610
1115
|
broad: true
|
|
1116
|
+
} : {}, held && kinPhrases.length ? {
|
|
1117
|
+
kin: kinPhrases
|
|
1118
|
+
} : {}, held && collection ? {
|
|
1119
|
+
lead: true
|
|
1120
|
+
} : {}, terms.craft && (raw.category || raw.held && collection) ? {
|
|
1121
|
+
requires: terms.craft
|
|
611
1122
|
} : {}));
|
|
612
1123
|
}
|
|
613
1124
|
return searches;
|
|
@@ -622,7 +1133,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
622
1133
|
}
|
|
623
1134
|
}
|
|
624
1135
|
/** 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;
|
|
1136
|
+
var _photo_tags, _photo_alt, _photo_alt1, _photo_alt2;
|
|
626
1137
|
const slug = pageSlug(photo.pageUrl);
|
|
627
1138
|
const tags = ((_photo_tags = photo.tags) != null ? _photo_tags : []).map((tag)=>tag.toLowerCase().trim()).filter(Boolean);
|
|
628
1139
|
const lead = new Set(stemList([
|
|
@@ -639,17 +1150,42 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
639
1150
|
stemList((_photo_alt1 = photo.alt) != null ? _photo_alt1 : '').join(' '),
|
|
640
1151
|
stemList(slug).join(' ')
|
|
641
1152
|
];
|
|
1153
|
+
const leadPhrases = [
|
|
1154
|
+
...tags.slice(0, 3).map((tag)=>stemList(tag).join(' ')),
|
|
1155
|
+
stemList(slug).join(' ')
|
|
1156
|
+
];
|
|
642
1157
|
const rejected = tags.some((tag)=>REJECTED_TAGS.has(tag) || tag.split(/\s+/).some((word)=>REJECTED_TAGS.has(word)));
|
|
1158
|
+
// Whole words only, a hyphenated one kept whole: "cross-stitch" is not a cross.
|
|
1159
|
+
const words = new Set([
|
|
1160
|
+
...tags,
|
|
1161
|
+
((_photo_alt2 = photo.alt) != null ? _photo_alt2 : '').toLowerCase()
|
|
1162
|
+
].join(' ').split(/[^a-zÀ-ɏ-]+/).filter((word)=>word.length > 1).map(aiStockStem));
|
|
643
1163
|
return {
|
|
644
1164
|
lead,
|
|
645
1165
|
all,
|
|
646
1166
|
phrases,
|
|
647
|
-
|
|
1167
|
+
leadPhrases,
|
|
1168
|
+
rejected,
|
|
1169
|
+
words
|
|
648
1170
|
};
|
|
649
1171
|
}
|
|
1172
|
+
/** Whether a phrase is among a hit's phrases, word for word. */ const among = (phrases, phrase)=>phrases.some((found)=>` ${found} `.includes(` ${phrase} `));
|
|
1173
|
+
/**
|
|
1174
|
+
* Whether a hit carries a topic its site never named ({@link SENSITIVE_TOPICS}):
|
|
1175
|
+
* a cross, a grave, a flag, a gun, a glass of wine, a child, a mannequin.
|
|
1176
|
+
*/ export function aiStockSensitive(photo, named = []) {
|
|
1177
|
+
const { words: found } = hitWords(photo);
|
|
1178
|
+
const site = new Set(named);
|
|
1179
|
+
return SENSITIVE_STEMS.some((topic)=>[
|
|
1180
|
+
...topic.words
|
|
1181
|
+
].some((word)=>found.has(word)) && ![
|
|
1182
|
+
...topic.allow
|
|
1183
|
+
].some((word)=>site.has(word)));
|
|
1184
|
+
}
|
|
650
1185
|
/**
|
|
651
1186
|
* 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
|
|
1187
|
+
* fill it: a mockup or a blank never; a topic the site never named never
|
|
1188
|
+
* ({@link aiStockSensitive}); a picture of a thing only when the hit
|
|
653
1189
|
* names its object AND either the site's world or the subject's own
|
|
654
1190
|
* two-word phrase ("bud vase", "wick trimmer"); anything else only when it
|
|
655
1191
|
* names the site's world (when the site names one). Among the accepted, the
|
|
@@ -657,25 +1193,38 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
657
1193
|
* the subject's two-word phrase 3, each of its other words 1, and the
|
|
658
1194
|
* site's world 1, plus a quarter when it is among the first tags.
|
|
659
1195
|
*/ export function aiStockRelevance(photo, judgement) {
|
|
660
|
-
var _judgement_requires, _judgement_subject, _judgement_domain;
|
|
1196
|
+
var _judgement_rivals, _judgement_kin, _judgement_requires, _judgement_subject, _judgement_object, _judgement_domain;
|
|
661
1197
|
const hit = hitWords(photo);
|
|
662
1198
|
if (hit.rejected) return 0;
|
|
1199
|
+
if (aiStockSensitive(photo, judgement.named)) return 0;
|
|
1200
|
+
// Glass art is not pottery: a rival craft's photo names this craft's own words.
|
|
1201
|
+
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;
|
|
1202
|
+
const kin = (_judgement_kin = judgement.kin) != null ? _judgement_kin : [];
|
|
1203
|
+
const kinNamed = kin.some((phrase)=>among(hit.phrases, phrase));
|
|
1204
|
+
const kinLead = kin.some((phrase)=>among(hit.leadPhrases, phrase));
|
|
663
1205
|
const required = stemList((_judgement_requires = judgement.requires) != null ? _judgement_requires : '');
|
|
664
|
-
|
|
1206
|
+
// An object's kin stands for the category: a wax melt is the candle shop's.
|
|
1207
|
+
if (!kinNamed && required.some((stem)=>!hit.all.has(stem))) return 0;
|
|
665
1208
|
const subject = stemList((_judgement_subject = judgement.subject) != null ? _judgement_subject : '');
|
|
666
|
-
const object = judgement.subject ? aiStockObjectWord(judgement.subject) : '';
|
|
1209
|
+
const object = (_judgement_object = judgement.object) != null ? _judgement_object : judgement.subject ? aiStockObjectWord(judgement.subject) : '';
|
|
667
1210
|
const domain = ((_judgement_domain = judgement.domain) != null ? _judgement_domain : []).filter((stem)=>stem !== object);
|
|
668
1211
|
// A hit naming the word it must name is of the site's world: a product's
|
|
669
1212
|
// category is that world even where it is the product's own object.
|
|
670
|
-
const world = required.length > 0 || domain.some((stem)=>hit.all.has(stem));
|
|
671
|
-
const worldLead = required.length > 0 && required.every((stem)=>hit.lead.has(stem)) || domain.some((stem)=>hit.lead.has(stem));
|
|
1213
|
+
const world = required.length > 0 || kinNamed || domain.some((stem)=>hit.all.has(stem));
|
|
1214
|
+
const worldLead = required.length > 0 && required.every((stem)=>hit.lead.has(stem)) || kinLead || domain.some((stem)=>hit.lead.has(stem));
|
|
672
1215
|
const pair = subject.length >= 2 ? subject.slice(-2).join(' ') : '';
|
|
673
|
-
const phrased = Boolean(pair) && hit.phrases
|
|
1216
|
+
const phrased = Boolean(pair) && among(hit.phrases, pair);
|
|
674
1217
|
let score = 0;
|
|
675
1218
|
if (judgement.strict && object) {
|
|
676
|
-
|
|
1219
|
+
// An object with kin is named only by them: "melt" alone is resin art.
|
|
1220
|
+
const named = kin.length ? kinNamed : hit.all.has(object);
|
|
1221
|
+
if (!named) return 0;
|
|
1222
|
+
const leading = kin.length ? kinLead : hit.lead.has(object);
|
|
1223
|
+
// A lifestyle scene whose object is a last tag is not a photo OF it.
|
|
1224
|
+
if (judgement.lead && !leading) return 0;
|
|
677
1225
|
if (domain.length && !world && !phrased) return 0;
|
|
678
|
-
score +=
|
|
1226
|
+
score += leading ? 4 : 2;
|
|
1227
|
+
if (kinNamed) score += 3;
|
|
679
1228
|
} else if (domain.length) {
|
|
680
1229
|
if (!world) return 0;
|
|
681
1230
|
}
|
|
@@ -793,6 +1342,9 @@ const EXTENSIONS = {
|
|
|
793
1342
|
delete request.broad;
|
|
794
1343
|
delete request.subject;
|
|
795
1344
|
delete request.requires;
|
|
1345
|
+
delete request.object;
|
|
1346
|
+
delete request.kin;
|
|
1347
|
+
delete request.lead;
|
|
796
1348
|
const key = JSON.stringify([
|
|
797
1349
|
request.query,
|
|
798
1350
|
request.orientation,
|
|
@@ -894,9 +1446,10 @@ const EXTENSIONS = {
|
|
|
894
1446
|
choices.push(null);
|
|
895
1447
|
continue;
|
|
896
1448
|
}
|
|
897
|
-
const
|
|
1449
|
+
const sectionName = (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '';
|
|
1450
|
+
const subject = aiStockSubjectWords(slot.alt, sectionName);
|
|
898
1451
|
let choice = null;
|
|
899
|
-
const requests = slot.product ? aiStockProductSearches(slot, terms.craft) : aiStockSearchesFor(slot, terms, subject);
|
|
1452
|
+
const requests = slot.product ? aiStockProductSearches(slot, terms.craft, terms.domain) : aiStockSearchesFor(slot, terms, subject, aiStockSlotPart(slot, sectionName));
|
|
900
1453
|
for (const request of requests){
|
|
901
1454
|
var _request_subject;
|
|
902
1455
|
if (choice || signal.aborted) break;
|
|
@@ -910,9 +1463,17 @@ const EXTENSIONS = {
|
|
|
910
1463
|
const ranked = aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, _extends({
|
|
911
1464
|
subject: (_request_subject = request.subject) != null ? _request_subject : request.broad ? '' : subject,
|
|
912
1465
|
domain: terms.domain,
|
|
913
|
-
strict: slot.role === 'gallery' && !request.broad || !!request.requires
|
|
1466
|
+
strict: slot.role === 'gallery' && !request.broad || !!request.requires,
|
|
1467
|
+
named: terms.named,
|
|
1468
|
+
rivals: terms.rivals
|
|
914
1469
|
}, request.requires ? {
|
|
915
1470
|
requires: request.requires
|
|
1471
|
+
} : {}, request.object ? {
|
|
1472
|
+
object: request.object
|
|
1473
|
+
} : {}, request.kin ? {
|
|
1474
|
+
kin: request.kin
|
|
1475
|
+
} : {}, request.lead ? {
|
|
1476
|
+
lead: true
|
|
916
1477
|
} : {}));
|
|
917
1478
|
if (!ranked.length) continue;
|
|
918
1479
|
const first = ranked[0];
|