@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.
Files changed (99) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-links.d.ts +17 -0
  3. package/src/lib/components/ai-job-links.js +63 -0
  4. package/src/lib/components/ai-job-links.js.map +1 -1
  5. package/src/lib/components/ai-job-plan.component.js +8 -1
  6. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  7. package/src/lib/components/ai-job-requests.d.ts +5 -0
  8. package/src/lib/components/ai-job-requests.js +4 -2
  9. package/src/lib/components/ai-job-requests.js.map +1 -1
  10. package/src/lib/components/ai-site-build-page.component.js +7 -1
  11. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  12. package/src/lib/components/ai-store-finish-card.component.d.ts +28 -0
  13. package/src/lib/components/ai-store-finish-card.component.js +116 -0
  14. package/src/lib/components/ai-store-finish-card.component.js.map +1 -0
  15. package/src/lib/components/assist-build-card.component.js +12 -1
  16. package/src/lib/components/assist-build-card.component.js.map +1 -1
  17. package/src/lib/components/assist-panel.component.js +14 -2
  18. package/src/lib/components/assist-panel.component.js.map +1 -1
  19. package/src/lib/jobs/ai-job-build-step.d.ts +45 -1
  20. package/src/lib/jobs/ai-job-build-step.js +283 -7
  21. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  22. package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
  23. package/src/lib/jobs/ai-job-drafts.js +4 -1
  24. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  25. package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
  26. package/src/lib/jobs/ai-job-layout-language.js +11 -4
  27. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  28. package/src/lib/jobs/ai-job-layout-step.js +3 -1
  29. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  30. package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
  31. package/src/lib/jobs/ai-job-page-language.js +26 -9
  32. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  33. package/src/lib/jobs/ai-job-page-sections.d.ts +11 -0
  34. package/src/lib/jobs/ai-job-page-sections.js +53 -5
  35. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  36. package/src/lib/jobs/ai-job-page-step.d.ts +2 -1
  37. package/src/lib/jobs/ai-job-page-step.js +51 -10
  38. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  39. package/src/lib/jobs/ai-job-plan-step.d.ts +12 -0
  40. package/src/lib/jobs/ai-job-plan-step.js +41 -7
  41. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-site-datasets.d.ts +89 -2
  43. package/src/lib/jobs/ai-job-site-datasets.js +239 -15
  44. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  45. package/src/lib/jobs/ai-job-site-step.d.ts +18 -2
  46. package/src/lib/jobs/ai-job-site-step.js +109 -22
  47. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  48. package/src/lib/jobs/ai-job-site-store-pages.d.ts +75 -0
  49. package/src/lib/jobs/ai-job-site-store-pages.js +195 -0
  50. package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -0
  51. package/src/lib/jobs/ai-job-store-links-layout.d.ts +71 -0
  52. package/src/lib/jobs/ai-job-store-links-layout.js +95 -0
  53. package/src/lib/jobs/ai-job-store-links-layout.js.map +1 -0
  54. package/src/lib/jobs/ai-layout-stock-photos.d.ts +69 -3
  55. package/src/lib/jobs/ai-layout-stock-photos.js +603 -42
  56. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  57. package/src/lib/jobs/ai-site-publish.d.ts +18 -0
  58. package/src/lib/jobs/ai-site-publish.js +76 -15
  59. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  60. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.d.ts +43 -0
  61. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js +61 -0
  62. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js.map +1 -0
  63. package/src/lib/layout-language/ai-layout-compiler.js +25 -2
  64. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  65. package/src/lib/layout-language/ai-layout-frame.d.ts +10 -0
  66. package/src/lib/layout-language/ai-layout-frame.js +58 -4
  67. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  68. package/src/lib/layout-language/ai-layout-listings.d.ts +6 -0
  69. package/src/lib/layout-language/ai-layout-listings.js +3 -0
  70. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  71. package/src/lib/layout-language/ai-layout-pictures.js +12 -4
  72. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  73. package/src/lib/model/ai-build-job.d.ts +2 -0
  74. package/src/lib/model/ai-build-job.js +23 -3
  75. package/src/lib/model/ai-build-job.js.map +1 -1
  76. package/src/lib/model/ai-build-plan.d.ts +7 -0
  77. package/src/lib/model/ai-build-plan.js.map +1 -1
  78. package/src/lib/model/ai-build-progress.js +3 -1
  79. package/src/lib/model/ai-build-progress.js.map +1 -1
  80. package/src/lib/model/ai-build-store-pages.d.ts +133 -0
  81. package/src/lib/model/ai-build-store-pages.js +209 -0
  82. package/src/lib/model/ai-build-store-pages.js.map +1 -0
  83. package/src/lib/model/ai-layout-store-links.d.ts +64 -0
  84. package/src/lib/model/ai-layout-store-links.js +165 -0
  85. package/src/lib/model/ai-layout-store-links.js.map +1 -0
  86. package/src/lib/model/ai-site-inventory.d.ts +5 -0
  87. package/src/lib/model/ai-site-inventory.js.map +1 -1
  88. package/src/lib/model/ai-site-store-pages.d.ts +192 -0
  89. package/src/lib/model/ai-site-store-pages.js +383 -0
  90. package/src/lib/model/ai-site-store-pages.js.map +1 -0
  91. package/src/lib/runtime/ai-doctrine-validators.d.ts +44 -0
  92. package/src/lib/runtime/ai-doctrine-validators.js +177 -0
  93. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  94. package/src/lib/runtime/ai-doctrine.js +3 -2
  95. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  96. package/src/lib/runtime/site-inventory.js +8 -3
  97. package/src/lib/runtime/site-inventory.js.map +1 -1
  98. package/src/lib/testing/stand-in-dataset-index.js +8 -4
  99. 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
- const world = making ? kept.slice(0, -1) : kept;
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
- for (const clause of alt.toLowerCase().split(CLAUSE_BREAK)){
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
- */ export function aiStockProductSearches(slot, craft) {
511
- var _ref;
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 raw of (_ref = (_slot_product = slot.product) == null ? void 0 : _slot_product.subjects) != null ? _ref : []){
522
- const subject = aiStockSubjectWords(raw, '');
523
- if (!subject) continue;
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: craft && !says ? `${craft} ${subject}` : subject,
527
- subject
985
+ query,
986
+ subject,
987
+ object
528
988
  });
529
989
  }
530
- if (craft) queries.push({
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
- }, craft ? {
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
- const broad = terms.broad.map((query)=>({
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
- ] : subject ? [
589
- {
590
- query: qualify(subject)
591
- },
592
- {
593
- query: qualify(object)
594
- },
1096
+ ] : [
1097
+ ...part === 'story' ? making : [],
1098
+ ...thing,
595
1099
  ...broad
596
- ] : broad;
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
- rejected
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 picture of a thing only when the hit
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
- if (required.some((stem)=>!hit.all.has(stem))) return 0;
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.some((phrase)=>` ${phrase} `.includes(` ${pair} `));
1216
+ const phrased = Boolean(pair) && among(hit.phrases, pair);
674
1217
  let score = 0;
675
1218
  if (judgement.strict && object) {
676
- if (!hit.all.has(object)) return 0;
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 += hit.lead.has(object) ? 4 : 2;
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 subject = aiStockSubjectWords(slot.alt, (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '');
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];