@aglyn/plugins-ai 1.0.0-beta.241 → 1.0.0-beta.243
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/jobs/ai-job-site-content.js +3 -2
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +107 -8
- package/src/lib/jobs/ai-layout-stock-photos.js +639 -77
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-saltmarsh-recordings.d.ts +39 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-saltmarsh-recordings.js +508 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-saltmarsh-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-tidewater-recordings.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-tidewater-recordings.js +261 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-tidewater-recordings.js.map +1 -0
|
@@ -95,6 +95,28 @@ import { aiStorefrontSays } from "../layout-language/ai-layout-storefront.js";
|
|
|
95
95
|
* to its photographer. A copy that fails tries the slot's next accepted
|
|
96
96
|
* hit.
|
|
97
97
|
*
|
|
98
|
+
* 6. **Fall back to the craft, never off it** (the beta.241 Saltmarsh Soap
|
|
99
|
+
* Co start, a store "for handmade cold-process soap": its craft read as
|
|
100
|
+
* nothing, its hero searched "shop" and took a florist's bouquet, and four
|
|
101
|
+
* of five products took the starters' laptop desk and living room). A
|
|
102
|
+
* business that is only a place takes its craft from what it is for
|
|
103
|
+
* ("soap shop"); a hero names the craft itself ({@link AiStockSiteTerms.core});
|
|
104
|
+
* a product, after its own searches, tries its head noun anywhere among
|
|
105
|
+
* the tags, its gift wrapper with the craft and the craft's broad
|
|
106
|
+
* searches, and then — a product or a hero only — reuses an on-topic photo
|
|
107
|
+
* the job already shows rather than take a starter. Every slot of a
|
|
108
|
+
* craft's site tries the craft's broad searches last.
|
|
109
|
+
* 7. **A product takes the surest photo, even a borrowed one** (the beta.242
|
|
110
|
+
* Tidewater Soap Works start: a sauna for "Bath Gift Set", colognes for
|
|
111
|
+
* "Natural Body Lotion", a bathroom shelf for "Soap Bar Trio" and a soap
|
|
112
|
+
* dispenser for its starter size, while the collection tiles beside them
|
|
113
|
+
* were good). A product's searches are ranked by how sure they are: its
|
|
114
|
+
* own photo (the head noun or its kin among the first tags), then a photo
|
|
115
|
+
* OF its category, new or one the job already shows, then a weak hit of
|
|
116
|
+
* its own. No product shows a room or a scene, a fragrance bottle, or what
|
|
117
|
+
* its kin shuns (a pump for a bar) unless it names it; "bath" in a bath
|
|
118
|
+
* gift set is what the set is for; a size is never the product.
|
|
119
|
+
*
|
|
98
120
|
* A slot nothing acceptable answered — the library down, refusing, or out
|
|
99
121
|
* of time — is left `null` for the starter photos. A named object is never
|
|
100
122
|
* given one while the library answers anything of the site's world: the
|
|
@@ -120,7 +142,8 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
120
142
|
/**
|
|
121
143
|
* Words that describe a picture's look, not what it shows, dropped from a
|
|
122
144
|
* subject. A word that is as often a noun ("light", "set") is not one.
|
|
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 ' +
|
|
145
|
+
*/ const FILLER = words('beautiful beautifully stunning gorgeous lovely pretty elegant modern contemporary rustic cozy cosy warm cool ' + 'soft bright natural simple minimal minimalist delicate unique special artisan artisanal handmade ' + 'hand-made hand-thrown handthrown hand-crafted handcrafted crafted hand-built handbuilt hand-poured ' + 'hand-painted hand-dyed hand-stitched poured finished styled perfect wheel-thrown wheelthrown thrown ' + 'fresh clean calm serene quiet peaceful vibrant colorful colourful sunlit sunny golden glossy matte textured ' + 'close-up closeup up view views detail details detailed shot shots overhead flat lay flatlay flat-lay ' + 'top-down wide angle featuring featured displayed arranged sitting resting placed standing lined ' + 'single pair collection assortment selection group few various array range large tiny big little ' + 'tall short wide narrow everyday classic signature original favorite favourite bestselling best-selling ' + 'small-batch batch limited edition custom bespoke ' + // How a soap is made, never what it shows: "cold" and "process" named a supermarket fridge (Saltmarsh, 2026-10-10).
|
|
146
|
+
'cold-process cold-processed hot-process hot-processed melt-and-pour ' + 'white black grey gray beige cream ivory brown tan blue green red pink yellow orange purple speckled ' + 'gentle airy inviting welcoming cheerful happy smiling friendly');
|
|
124
147
|
/** 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');
|
|
125
148
|
/** 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');
|
|
126
149
|
/** 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');
|
|
@@ -136,6 +159,47 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
136
159
|
* an empty frame on the page (the Juniper Clay hero), a template or a
|
|
137
160
|
* graphic is not a photo of anything.
|
|
138
161
|
*/ const REJECTED_TAGS = words('mockup mock-up mock blank template templates clipart clip-art illustration vector logo icon ' + 'ai-generated generated render rendering 3d');
|
|
162
|
+
/**
|
|
163
|
+
* Rooms and scenes, never a product (AGL-3660): the beta.242 Tidewater Soap
|
|
164
|
+
* Works start put a wooden sauna under "Bath Gift Set" and a bathroom shelf of
|
|
165
|
+
* baskets under "Soap Bar Trio". A product's hit whose first tags name one of
|
|
166
|
+
* these is turned down unless the product or the brief names it too.
|
|
167
|
+
*/ const PRODUCT_SCENES = new Set([
|
|
168
|
+
'sauna',
|
|
169
|
+
'bathroom',
|
|
170
|
+
'bathtub',
|
|
171
|
+
'shower',
|
|
172
|
+
'toilet',
|
|
173
|
+
'sink',
|
|
174
|
+
'washbasin',
|
|
175
|
+
'faucet',
|
|
176
|
+
'tap',
|
|
177
|
+
'shelf',
|
|
178
|
+
'shelves',
|
|
179
|
+
'interior',
|
|
180
|
+
'bedroom',
|
|
181
|
+
'kitchen',
|
|
182
|
+
'room',
|
|
183
|
+
'livingroom',
|
|
184
|
+
'hotel',
|
|
185
|
+
'lobby'
|
|
186
|
+
].map(aiStockStem));
|
|
187
|
+
/**
|
|
188
|
+
* A fragrance's bottle, never a lotion, a cream or a soap (AGL-3660): the same
|
|
189
|
+
* start's "Natural Body Lotion" showed blue cologne bottles tagged "perfume,
|
|
190
|
+
* bottle, lotion". Turned down among a product's first tags unless the
|
|
191
|
+
* product or the brief is a fragrance.
|
|
192
|
+
*/ const FRAGRANCE_BOTTLES = new Set([
|
|
193
|
+
'perfume',
|
|
194
|
+
'perfumes',
|
|
195
|
+
'perfumery',
|
|
196
|
+
'cologne',
|
|
197
|
+
'colognes',
|
|
198
|
+
'parfum',
|
|
199
|
+
'eau',
|
|
200
|
+
'atomizer',
|
|
201
|
+
'fragrance'
|
|
202
|
+
].map(aiStockStem));
|
|
139
203
|
/**
|
|
140
204
|
* Topics a business's photos never carry unless its own words name them
|
|
141
205
|
* (AGL-3660, the beta.239 Ember & Oak start, whose "Gifts for melt fans" tile
|
|
@@ -303,6 +367,7 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
303
367
|
},
|
|
304
368
|
{
|
|
305
369
|
// Tobacco and drugs.
|
|
370
|
+
// "cbd" and "cannabinoid": a hemp cream tube was the next "body lotion" answer (Tidewater, 2026-10-11).
|
|
306
371
|
words: [
|
|
307
372
|
'cigarette',
|
|
308
373
|
'smoking',
|
|
@@ -310,7 +375,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
310
375
|
'cannabis',
|
|
311
376
|
'marijuana',
|
|
312
377
|
'drug',
|
|
313
|
-
'drugs'
|
|
378
|
+
'drugs',
|
|
379
|
+
'cbd',
|
|
380
|
+
'cannabinoid',
|
|
381
|
+
'thc'
|
|
314
382
|
],
|
|
315
383
|
allow: [
|
|
316
384
|
'dispensary',
|
|
@@ -469,6 +537,129 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
469
537
|
'wax melt warmer',
|
|
470
538
|
'scented wax cubes'
|
|
471
539
|
]
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
// A "bar" is a pub's before it is a soap's (the Saltmarsh "cold-process
|
|
543
|
+
// soap bar" search answered a pint of beer second): a soap bar is named
|
|
544
|
+
// by soap, and searched by it.
|
|
545
|
+
objects: [
|
|
546
|
+
'bar'
|
|
547
|
+
],
|
|
548
|
+
within: [
|
|
549
|
+
'soap',
|
|
550
|
+
'soapmaker',
|
|
551
|
+
'skincare'
|
|
552
|
+
],
|
|
553
|
+
qualifier: 'soap',
|
|
554
|
+
kin: [
|
|
555
|
+
'soap bar',
|
|
556
|
+
'soap bars',
|
|
557
|
+
'bar of soap',
|
|
558
|
+
'bars of soap',
|
|
559
|
+
'soap',
|
|
560
|
+
'soaps',
|
|
561
|
+
'handmade soap',
|
|
562
|
+
'soap block',
|
|
563
|
+
'soap cubes'
|
|
564
|
+
],
|
|
565
|
+
queries: [
|
|
566
|
+
'soap bars',
|
|
567
|
+
'handmade soap bar'
|
|
568
|
+
],
|
|
569
|
+
// A bar is never a pump or a liquid soap (the beta.242 Tidewater start).
|
|
570
|
+
shun: [
|
|
571
|
+
'pump',
|
|
572
|
+
'pumps',
|
|
573
|
+
'liquid',
|
|
574
|
+
'dispenser',
|
|
575
|
+
'dispensers'
|
|
576
|
+
]
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
// "Bath" in "bath gift set", "bath bomb" or "bath salts" is what the
|
|
580
|
+
// products are for, never the room: the beta.242 Tidewater Soap Works
|
|
581
|
+
// start (2026-10-11) put a wooden sauna under "Bath Gift Set", its tags
|
|
582
|
+
// "sauna, steam, bath". A bath set is named by soap and bath products,
|
|
583
|
+
// and searched with soap.
|
|
584
|
+
objects: [
|
|
585
|
+
'bath'
|
|
586
|
+
],
|
|
587
|
+
within: [
|
|
588
|
+
'soap',
|
|
589
|
+
'soapmaker',
|
|
590
|
+
'skincare',
|
|
591
|
+
'spa',
|
|
592
|
+
'cosmetic'
|
|
593
|
+
],
|
|
594
|
+
qualifier: 'soap',
|
|
595
|
+
after: true,
|
|
596
|
+
kin: [
|
|
597
|
+
'soap',
|
|
598
|
+
'soaps',
|
|
599
|
+
'soap bar',
|
|
600
|
+
'soap bars',
|
|
601
|
+
'handmade soap',
|
|
602
|
+
'bath bomb',
|
|
603
|
+
'bath bombs',
|
|
604
|
+
'bath salt',
|
|
605
|
+
'bath salts',
|
|
606
|
+
'bath product',
|
|
607
|
+
'bath products',
|
|
608
|
+
'bath set',
|
|
609
|
+
'toiletries'
|
|
610
|
+
],
|
|
611
|
+
queries: [
|
|
612
|
+
'soap gift set',
|
|
613
|
+
'bath products',
|
|
614
|
+
'bath bombs'
|
|
615
|
+
],
|
|
616
|
+
shun: [
|
|
617
|
+
'pump',
|
|
618
|
+
'pumps',
|
|
619
|
+
'liquid',
|
|
620
|
+
'dispenser',
|
|
621
|
+
'dispensers'
|
|
622
|
+
]
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
// A lotion is any cream for the skin; a perfume bottle tagged "lotion"
|
|
626
|
+
// is not one (the same start's "Natural Body Lotion" showed colognes).
|
|
627
|
+
objects: [
|
|
628
|
+
'lotion',
|
|
629
|
+
'moisturizer',
|
|
630
|
+
'moisturiser'
|
|
631
|
+
],
|
|
632
|
+
within: [
|
|
633
|
+
'soap',
|
|
634
|
+
'soapmaker',
|
|
635
|
+
'skincare',
|
|
636
|
+
'lotion',
|
|
637
|
+
'cosmetic',
|
|
638
|
+
'beauty',
|
|
639
|
+
'spa'
|
|
640
|
+
],
|
|
641
|
+
kin: [
|
|
642
|
+
'lotion',
|
|
643
|
+
'lotions',
|
|
644
|
+
'body lotion',
|
|
645
|
+
'hand lotion',
|
|
646
|
+
'cream',
|
|
647
|
+
'creams',
|
|
648
|
+
'hand cream',
|
|
649
|
+
'body cream',
|
|
650
|
+
'skin cream',
|
|
651
|
+
'face cream',
|
|
652
|
+
'cream jar',
|
|
653
|
+
'body butter',
|
|
654
|
+
'moisturizer',
|
|
655
|
+
'moisturiser'
|
|
656
|
+
],
|
|
657
|
+
queries: [
|
|
658
|
+
'body lotion',
|
|
659
|
+
'hand cream',
|
|
660
|
+
'cream jar',
|
|
661
|
+
'moisturizer'
|
|
662
|
+
]
|
|
472
663
|
}
|
|
473
664
|
];
|
|
474
665
|
/** An object's kin where the site or the subject is of its world, else `null`. */ function objectKin(object, world) {
|
|
@@ -595,21 +786,56 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
595
786
|
'soapmaker',
|
|
596
787
|
'skincare'
|
|
597
788
|
],
|
|
789
|
+
// Never "natural": a florist's bouquet tagged "natural" was the
|
|
790
|
+
// Saltmarsh Soap Co hero (2026-10-10).
|
|
598
791
|
kin: [
|
|
599
792
|
'soap',
|
|
600
793
|
'skincare',
|
|
601
794
|
'lotion',
|
|
795
|
+
'cream',
|
|
602
796
|
'bath',
|
|
603
797
|
'spa',
|
|
604
|
-
'natural',
|
|
605
798
|
'cosmetic'
|
|
606
799
|
],
|
|
607
800
|
broad: [
|
|
608
801
|
'handmade soap',
|
|
609
802
|
'natural skincare'
|
|
610
803
|
],
|
|
804
|
+
// "Soap making" is answered with soap bubbles; a bar is what a soapmaker makes.
|
|
611
805
|
making: [
|
|
612
|
-
'soap making'
|
|
806
|
+
'soap making',
|
|
807
|
+
'handmade soap'
|
|
808
|
+
],
|
|
809
|
+
// Soap bubbles, a dispenser and detergent are soap, never a soapmaker's:
|
|
810
|
+
// the same start's story band showed bubbles in flight.
|
|
811
|
+
rivals: {
|
|
812
|
+
words: [
|
|
813
|
+
'bubble',
|
|
814
|
+
'bubbles',
|
|
815
|
+
'soapy',
|
|
816
|
+
'dispenser',
|
|
817
|
+
'detergent',
|
|
818
|
+
'dishwashing',
|
|
819
|
+
'laundry'
|
|
820
|
+
],
|
|
821
|
+
core: [
|
|
822
|
+
'bar',
|
|
823
|
+
'bars',
|
|
824
|
+
'handmade',
|
|
825
|
+
'homemade',
|
|
826
|
+
'loaf',
|
|
827
|
+
'lye',
|
|
828
|
+
'glycerin',
|
|
829
|
+
'skincare',
|
|
830
|
+
'lotion',
|
|
831
|
+
'cosmetic',
|
|
832
|
+
'spa',
|
|
833
|
+
'artisan'
|
|
834
|
+
]
|
|
835
|
+
},
|
|
836
|
+
shun: [
|
|
837
|
+
'dispenser',
|
|
838
|
+
'dispensers'
|
|
613
839
|
]
|
|
614
840
|
},
|
|
615
841
|
{
|
|
@@ -837,10 +1063,11 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
837
1063
|
return craft ? aiStockStem(craft) : '';
|
|
838
1064
|
}
|
|
839
1065
|
/** The site's terms, read from its business type. */ export function aiStockSiteTerms(businessType) {
|
|
840
|
-
|
|
841
|
-
|
|
1066
|
+
let business = aiStockBusinessWords(businessType);
|
|
1067
|
+
let craft = aiStockCraftWords(business);
|
|
842
1068
|
const domain = new Set();
|
|
843
1069
|
/** The things it makes, by their head nouns. */ const made = new Set();
|
|
1070
|
+
/** The last word of the first clause that names the world: "soap" of "for handmade cold-process soap". */ let firstWorld = '';
|
|
844
1071
|
let named = false;
|
|
845
1072
|
let making = false;
|
|
846
1073
|
for (const { opener, words: clause } of businessClauses(businessType)){
|
|
@@ -858,16 +1085,37 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
858
1085
|
// A set or a gift is never a world: "gift" of "gift sets" is not one.
|
|
859
1086
|
const world = (making ? kept.slice(0, -1) : kept).filter((word)=>!WRAPPERS.has(word));
|
|
860
1087
|
for (const word of world)for (const stem of stemList(word))domain.add(stem);
|
|
1088
|
+
const last = world[world.length - 1];
|
|
1089
|
+
if (!firstWorld && last) firstWorld = aiStockStem(last);
|
|
861
1090
|
if (making) for (const word of kept.slice(-1))for (const stem of stemList(word))made.add(stem);
|
|
862
1091
|
}
|
|
863
|
-
if (craft) domain.add(craft);
|
|
864
1092
|
// A business named only by what it sells ("a shop selling pottery") is of that world.
|
|
865
1093
|
if (!domain.size) for (const stem of made)domain.add(stem);
|
|
1094
|
+
const groups = CRAFT_KIN.filter((group)=>group.match.some((word)=>domain.has(aiStockStem(word)) || made.has(aiStockStem(word)) || aiStockStem(word) === craft));
|
|
1095
|
+
// A business that is only a place ("an online shop for handmade
|
|
1096
|
+
// cold-process soap") takes its craft from what it is for: the Saltmarsh
|
|
1097
|
+
// Soap Co start (2026-10-10) read its craft as nothing, so its hero searched
|
|
1098
|
+
// "shop" (a florist's bouquet) and no product was held to soap.
|
|
1099
|
+
if (!craft) {
|
|
1100
|
+
var _groups_;
|
|
1101
|
+
const listed = (_groups_ = groups[0]) == null ? void 0 : _groups_.match.map(aiStockStem).find((stem)=>domain.has(stem) || made.has(stem));
|
|
1102
|
+
craft = listed || firstWorld || [
|
|
1103
|
+
...made
|
|
1104
|
+
][0] || '';
|
|
1105
|
+
if (craft && !stemList(business).includes(craft)) business = `${craft} ${business}`.trim();
|
|
1106
|
+
}
|
|
1107
|
+
if (craft) domain.add(craft);
|
|
866
1108
|
const broad = [];
|
|
867
1109
|
const makingSearches = [];
|
|
868
1110
|
const rivals = [];
|
|
869
|
-
|
|
870
|
-
|
|
1111
|
+
const core = new Set(craft ? [
|
|
1112
|
+
craft
|
|
1113
|
+
] : []);
|
|
1114
|
+
const shun = new Set();
|
|
1115
|
+
for (const group of groups){
|
|
1116
|
+
var _group_shun;
|
|
1117
|
+
for (const word of (_group_shun = group.shun) != null ? _group_shun : [])shun.add(aiStockStem(word));
|
|
1118
|
+
for (const word of group.match)core.add(aiStockStem(word));
|
|
871
1119
|
for (const word of group.kin)domain.add(aiStockStem(word));
|
|
872
1120
|
broad.push(...group.broad);
|
|
873
1121
|
makingSearches.push(...group.making);
|
|
@@ -892,7 +1140,13 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
892
1140
|
named: [
|
|
893
1141
|
...new Set(stemList(businessType))
|
|
894
1142
|
],
|
|
895
|
-
rivals
|
|
1143
|
+
rivals,
|
|
1144
|
+
core: [
|
|
1145
|
+
...core
|
|
1146
|
+
],
|
|
1147
|
+
shun: [
|
|
1148
|
+
...shun
|
|
1149
|
+
]
|
|
896
1150
|
};
|
|
897
1151
|
}
|
|
898
1152
|
/** A phrase's content words without its filler; all filler, and nothing is left. */ function subjectOf(list) {
|
|
@@ -943,23 +1197,59 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
943
1197
|
/** An object's kin phrases as stems, or none. */ const kinStems = (entry)=>entry ? [
|
|
944
1198
|
...new Set(entry.kin.map((phrase)=>stemList(phrase).join(' ')))
|
|
945
1199
|
] : [];
|
|
1200
|
+
/**
|
|
1201
|
+
* A size never names a product (AGL-3660): the Tidewater start searched
|
|
1202
|
+
* "soap set starter size" for "Bath Gift Set Starter Size", its head noun
|
|
1203
|
+
* read as "size", and took a soap dispenser.
|
|
1204
|
+
*/ const PRODUCT_SIZES = /\b(?:(?:starter|travel|trial|sample|mini|full|half|family|value|jumbo|large|small|regular|standard|big)[- ])?sized?s?\b/gi;
|
|
1205
|
+
/** The wrapper words a gift is named by, searched with the craft: "soap gift box". */ const GIFT_WRAPPERS = words('gift gifts box boxes basket baskets hamper hampers');
|
|
946
1206
|
/**
|
|
947
1207
|
* A product's searches (AGL-3676), most specific first: each of its subjects
|
|
948
1208
|
* — its name's noun phrase, then its photo's — with the shop's category
|
|
949
|
-
* where the phrase does not already
|
|
950
|
-
* category alone, a plain photo of what the shop sells. Every
|
|
951
|
-
* hit naming the category
|
|
952
|
-
*
|
|
953
|
-
*
|
|
1209
|
+
* where the phrase does not already name the shop's world ("candle gift
|
|
1210
|
+
* box"), then the category alone, a plain photo of what the shop sells. Every
|
|
1211
|
+
* one must find a hit naming the category — or the product's own word of the
|
|
1212
|
+
* shop's world, "lotion" in a soap shop — so no product is ever filled with a
|
|
1213
|
+
* lifestyle shot of something else: the beta.237 Willow Wick start put a
|
|
1214
|
+
* laptop and roses under "Candle Wick Trimmer" and an antique tea set under
|
|
1215
|
+
* "Candle Gift Set".
|
|
954
1216
|
*
|
|
955
|
-
* And
|
|
1217
|
+
* And each must find a hit naming the product's own head noun among its
|
|
956
1218
|
* first tags (AGL-3660): the beta.239 Ember & Oak start put a bed with a book,
|
|
957
1219
|
* a mug and a camera (its candle a last tag) under "Hand-Poured Soy Candle".
|
|
958
1220
|
* An object whose own word is not enough ({@link OBJECT_KIN}) is searched by
|
|
959
1221
|
* its kin instead of the category alone, and named by them: a single taper
|
|
960
1222
|
* candle filled "Wax Melt Gift Set".
|
|
961
|
-
|
|
962
|
-
|
|
1223
|
+
*
|
|
1224
|
+
* Then, before a product is ever given a photo of something else, it falls
|
|
1225
|
+
* back to its own craft (AGL-3660). The Saltmarsh Soap Co start (2026-10-10)
|
|
1226
|
+
* found no hit naming "bar" or "box" among the first tags, and four of its
|
|
1227
|
+
* five products took the starter photos — a laptop and roses, a living room
|
|
1228
|
+
* of plants. And the beta.242 Tidewater Soap Works start (2026-10-11) showed
|
|
1229
|
+
* that the head noun anywhere is not enough: its own weak hits put a sauna
|
|
1230
|
+
* under "Bath Gift Set", colognes under "Natural Body Lotion", a bathroom
|
|
1231
|
+
* shelf under "Soap Bar Trio" and a soap dispenser under its starter size,
|
|
1232
|
+
* while the collection tiles beside them, searched for the category, were
|
|
1233
|
+
* good. So the searches are ranked by how sure they are ({@link AiStockSearch.tier}):
|
|
1234
|
+
*
|
|
1235
|
+
* 1. Its own: the head noun (or its kin) among the first tags, as above.
|
|
1236
|
+
* 2. `borrow`: a photo OF its category — the category alone, then the craft's
|
|
1237
|
+
* broad searches ("handmade soap", "natural skincare"), one of the
|
|
1238
|
+
* product's kin or the craft's own names among the first tags. A product
|
|
1239
|
+
* may repeat one the job already shows, a collection tile's photo or
|
|
1240
|
+
* another product's ({@link aiLayoutStockPhotoSource}): a borrowed
|
|
1241
|
+
* on-topic photo beats a weak specific one.
|
|
1242
|
+
* 3. `weak`: the same searches again, the head noun named anywhere among the
|
|
1243
|
+
* tags (no extra search: the page remembers its answers), a gift's own
|
|
1244
|
+
* wrapper with the craft ("soap gift box"), and the craft's broad searches,
|
|
1245
|
+
* any hit naming the craft itself ({@link AiStockSiteTerms.core}).
|
|
1246
|
+
*
|
|
1247
|
+
* Every one of them turns down a room or a scene, a fragrance bottle, and
|
|
1248
|
+
* what the product's kin shuns (a pump for a bar of soap), unless the
|
|
1249
|
+
* product names it. A size never names the product: "Bath Gift Set Starter
|
|
1250
|
+
* Size" is a bath gift set.
|
|
1251
|
+
*/ export function aiStockProductSearches(slot, craft, domain = [], fallback = {}) {
|
|
1252
|
+
var _ref, _subjects_, _fallback_core, _ref1, _fallback_broad, _fallback_broad1;
|
|
963
1253
|
var _slot_product;
|
|
964
1254
|
const orientation = aiStockOrientation(slot.aspect);
|
|
965
1255
|
const size = orientation === 'vertical' ? {
|
|
@@ -968,62 +1258,165 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
968
1258
|
minWidth: 900
|
|
969
1259
|
};
|
|
970
1260
|
const craftStems = stemList(craft);
|
|
971
|
-
const
|
|
1261
|
+
const world = new Set([
|
|
1262
|
+
...domain,
|
|
1263
|
+
...craftStems
|
|
1264
|
+
]);
|
|
1265
|
+
const subjects = ((_ref = (_slot_product = slot.product) == null ? void 0 : _slot_product.subjects) != null ? _ref : []).map((raw)=>aiStockSubjectWords(raw.replace(PRODUCT_SIZES, ' '), '')).filter(Boolean);
|
|
972
1266
|
// The product's head noun is its name's: "candle" of "Hand-Poured Soy Candle".
|
|
973
|
-
const
|
|
1267
|
+
const own = (_subjects_ = subjects[0]) != null ? _subjects_ : '';
|
|
1268
|
+
const object = aiStockObjectWord(own);
|
|
974
1269
|
const kin = object ? objectKin(object, [
|
|
975
1270
|
...craftStems,
|
|
976
1271
|
...domain,
|
|
977
1272
|
...stemList(subjects.join(' '))
|
|
978
1273
|
]) : null;
|
|
979
1274
|
const kinPhrases = kinStems(kin);
|
|
1275
|
+
// A product of the shop's world beside its category is held to either:
|
|
1276
|
+
// a soap shop's body lotion names "lotion", never "soap".
|
|
1277
|
+
const ownWorld = stemList(own).filter((stem)=>world.has(stem) && !WRAPPERS.has(stem) && !craftStems.includes(stem));
|
|
1278
|
+
const category = craft && ownWorld.length ? [
|
|
1279
|
+
...craftStems,
|
|
1280
|
+
...ownWorld
|
|
1281
|
+
] : [];
|
|
1282
|
+
const core = [
|
|
1283
|
+
...new Set([
|
|
1284
|
+
...craftStems,
|
|
1285
|
+
...(_fallback_core = fallback.core) != null ? _fallback_core : []
|
|
1286
|
+
])
|
|
1287
|
+
];
|
|
1288
|
+
const shun = ((_ref1 = kin == null ? void 0 : kin.shun) != null ? _ref1 : []).map(aiStockStem);
|
|
980
1289
|
const queries = [];
|
|
981
1290
|
for (const subject of subjects){
|
|
1291
|
+
var _ref2;
|
|
982
1292
|
const stems = stemList(subject);
|
|
983
|
-
const
|
|
1293
|
+
const qualifier = (_ref2 = kin == null ? void 0 : kin.qualifier) != null ? _ref2 : '';
|
|
1294
|
+
const query = kin ? !qualifier || stems.includes(aiStockStem(qualifier)) ? subject : kin.after ? `${subject} ${qualifier}` : `${qualifier} ${subject}` : craft && !stems.some((stem)=>world.has(stem)) ? `${craft} ${subject}` : subject;
|
|
984
1295
|
queries.push({
|
|
985
1296
|
query,
|
|
986
1297
|
subject,
|
|
987
|
-
object
|
|
1298
|
+
object,
|
|
1299
|
+
lead: true
|
|
988
1300
|
});
|
|
989
1301
|
}
|
|
990
|
-
// The category alone stands in only for a thing that is no more than of
|
|
991
|
-
//
|
|
992
|
-
|
|
1302
|
+
// The category alone stands in only for a thing that is no more than of
|
|
1303
|
+
// it; a thing that is its own word of the shop's world is searched by that
|
|
1304
|
+
// word ("lotion"). Its hits are still scored by the product's own words: a
|
|
1305
|
+
// soy candle over a taper.
|
|
993
1306
|
if (kin) for (const query of kin.queries)queries.push({
|
|
994
1307
|
query,
|
|
995
1308
|
subject: own || query,
|
|
996
|
-
object
|
|
1309
|
+
object,
|
|
1310
|
+
lead: true
|
|
1311
|
+
});
|
|
1312
|
+
else if (object && ownWorld.includes(object)) queries.push({
|
|
1313
|
+
query: object,
|
|
1314
|
+
subject: own,
|
|
1315
|
+
object,
|
|
1316
|
+
lead: true
|
|
997
1317
|
});
|
|
998
1318
|
else if (craft) queries.push({
|
|
999
1319
|
query: craft,
|
|
1000
1320
|
subject: own || craft,
|
|
1001
|
-
object: craft
|
|
1321
|
+
object: craft,
|
|
1322
|
+
lead: true,
|
|
1323
|
+
tier: 'borrow'
|
|
1002
1324
|
});
|
|
1325
|
+
const held = [
|
|
1326
|
+
...queries
|
|
1327
|
+
];
|
|
1328
|
+
// 2. A photo OF its category, which may be one the job already shows.
|
|
1329
|
+
for (const query of (_fallback_broad = fallback.broad) != null ? _fallback_broad : [])queries.push({
|
|
1330
|
+
query,
|
|
1331
|
+
subject: own,
|
|
1332
|
+
object: '',
|
|
1333
|
+
lead: true,
|
|
1334
|
+
broad: true,
|
|
1335
|
+
tier: 'borrow'
|
|
1336
|
+
});
|
|
1337
|
+
// 3. The head noun anywhere among the tags, by the same searches.
|
|
1338
|
+
for (const entry of held)queries.push(_extends({}, entry, {
|
|
1339
|
+
lead: false,
|
|
1340
|
+
tier: 'weak'
|
|
1341
|
+
}));
|
|
1342
|
+
// A gift is its craft's: "bath gift box" asks for a "soap gift box".
|
|
1343
|
+
const wrapped = own.split(/\s+/).filter((word)=>WRAPPERS.has(word) && aiStockStem(word) !== object);
|
|
1344
|
+
const gift = wrapped.find((word)=>GIFT_WRAPPERS.has(word));
|
|
1345
|
+
if (craft && gift && !kin) queries.push({
|
|
1346
|
+
query: `${craft} ${wrapped.join(' ')}`,
|
|
1347
|
+
subject: own,
|
|
1348
|
+
object: aiStockStem(gift),
|
|
1349
|
+
lead: false,
|
|
1350
|
+
tier: 'weak'
|
|
1351
|
+
});
|
|
1352
|
+
// The craft's own broad searches, any hit naming the craft.
|
|
1353
|
+
for (const query of (_fallback_broad1 = fallback.broad) != null ? _fallback_broad1 : [])queries.push({
|
|
1354
|
+
query,
|
|
1355
|
+
subject: own,
|
|
1356
|
+
object: '',
|
|
1357
|
+
lead: false,
|
|
1358
|
+
broad: true,
|
|
1359
|
+
tier: 'weak'
|
|
1360
|
+
});
|
|
1361
|
+
// What a borrowed photo is OF: the product's kin, its own word of the shop's world, else the craft.
|
|
1362
|
+
const borrowed = kinPhrases.length ? kinPhrases : ownWorld.length ? ownWorld : core;
|
|
1003
1363
|
const seen = new Set();
|
|
1004
1364
|
const searches = [];
|
|
1005
1365
|
for (const entry of queries){
|
|
1366
|
+
var _entry_tier;
|
|
1006
1367
|
const query = clip(entry.query);
|
|
1007
|
-
|
|
1008
|
-
seen.
|
|
1009
|
-
|
|
1368
|
+
const key = `${query}|${entry.lead}|${entry.object}|${entry.broad === true}|${(_entry_tier = entry.tier) != null ? _entry_tier : ''}`;
|
|
1369
|
+
if (!query || seen.has(key)) continue;
|
|
1370
|
+
seen.add(key);
|
|
1371
|
+
const common = _extends({
|
|
1010
1372
|
query,
|
|
1011
1373
|
orientation
|
|
1012
1374
|
}, size, {
|
|
1013
1375
|
subject: entry.subject,
|
|
1376
|
+
product: true
|
|
1377
|
+
}, entry.tier ? {
|
|
1378
|
+
tier: entry.tier
|
|
1379
|
+
} : {}, shun.length ? {
|
|
1380
|
+
shun
|
|
1381
|
+
} : {});
|
|
1382
|
+
if (entry.broad) {
|
|
1383
|
+
const heldTo = [
|
|
1384
|
+
...new Set([
|
|
1385
|
+
...core,
|
|
1386
|
+
...ownWorld
|
|
1387
|
+
])
|
|
1388
|
+
];
|
|
1389
|
+
searches.push(_extends({}, common, {
|
|
1390
|
+
broad: true
|
|
1391
|
+
}, entry.lead ? _extends({
|
|
1392
|
+
leadAny: borrowed
|
|
1393
|
+
}, kinPhrases.length ? {
|
|
1394
|
+
kin: kinPhrases
|
|
1395
|
+
} : {}, heldTo.length ? {
|
|
1396
|
+
category: heldTo
|
|
1397
|
+
} : {}) : core.length ? {
|
|
1398
|
+
category: core
|
|
1399
|
+
} : {}));
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
searches.push(_extends({}, common, entry.lead ? {
|
|
1014
1403
|
lead: true
|
|
1404
|
+
} : {
|
|
1405
|
+
leadWorld: true
|
|
1015
1406
|
}, entry.object ? {
|
|
1016
1407
|
object: entry.object
|
|
1017
|
-
} : {}, kinPhrases.length ? {
|
|
1408
|
+
} : {}, kinPhrases.length && entry.object === object ? {
|
|
1018
1409
|
kin: kinPhrases
|
|
1019
1410
|
} : {}, craft ? {
|
|
1020
1411
|
requires: craft
|
|
1412
|
+
} : {}, category.length ? {
|
|
1413
|
+
category
|
|
1021
1414
|
} : {}));
|
|
1022
1415
|
}
|
|
1023
1416
|
return searches;
|
|
1024
1417
|
}
|
|
1025
1418
|
/** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, terms, subject, part) {
|
|
1026
|
-
var _terms_making;
|
|
1419
|
+
var _terms_core, _terms_making;
|
|
1027
1420
|
const orientation = aiStockOrientation(slot.aspect);
|
|
1028
1421
|
const size = slot.role === 'hero' ? {
|
|
1029
1422
|
minWidth: 1600
|
|
@@ -1042,6 +1435,12 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1042
1435
|
const kinPhrases = kinStems(kin);
|
|
1043
1436
|
// The craft qualifies a subject that does not already name the site's world.
|
|
1044
1437
|
const namesWorld = subjectStems.some((stem)=>domain.has(stem));
|
|
1438
|
+
// A tile of the shop's world beside its category is held to either: a soap
|
|
1439
|
+
// shop's "Body care" tile names "body", never "soap" (AGL-3660).
|
|
1440
|
+
const ownWorld = subjectStems.filter((stem)=>domain.has(stem) && !WRAPPERS.has(stem) && stem !== terms.craft);
|
|
1441
|
+
// A hero of a craft names the craft itself: the Saltmarsh Soap Co hero
|
|
1442
|
+
// (2026-10-10) was a florist's bouquet tagged "natural" (AGL-3660).
|
|
1443
|
+
const core = (_terms_core = terms.core) != null ? _terms_core : [];
|
|
1045
1444
|
const qualify = (phrase)=>terms.craft && !namesWorld && !stemList(phrase).includes(terms.craft) ? `${terms.craft} ${phrase}` : phrase;
|
|
1046
1445
|
// A collection tile is product-like: held to the shop's category and its
|
|
1047
1446
|
// own object among the first tags, and searched by its object's kin, never
|
|
@@ -1117,8 +1516,15 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1117
1516
|
kin: kinPhrases
|
|
1118
1517
|
} : {}, held && collection ? {
|
|
1119
1518
|
lead: true
|
|
1120
|
-
} : {}, terms.craft && (raw.category || raw.held && collection) ? {
|
|
1519
|
+
} : {}, terms.craft && (raw.category || raw.held && collection) ? _extends({
|
|
1121
1520
|
requires: terms.craft
|
|
1521
|
+
}, ownWorld.length ? {
|
|
1522
|
+
category: [
|
|
1523
|
+
terms.craft,
|
|
1524
|
+
...ownWorld
|
|
1525
|
+
]
|
|
1526
|
+
} : {}) : {}, slot.role === 'hero' && core.length ? {
|
|
1527
|
+
category: core
|
|
1122
1528
|
} : {}));
|
|
1123
1529
|
}
|
|
1124
1530
|
return searches;
|
|
@@ -1133,7 +1539,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1133
1539
|
}
|
|
1134
1540
|
}
|
|
1135
1541
|
/** A hit's words: the first few (what it is OF), every one, and each tag's phrase. */ function hitWords(photo) {
|
|
1136
|
-
var _photo_tags, _photo_alt, _photo_alt1, _photo_alt2;
|
|
1542
|
+
var _photo_tags, _photo_alt, _photo_alt1, _tags_, _photo_alt2;
|
|
1137
1543
|
const slug = pageSlug(photo.pageUrl);
|
|
1138
1544
|
const tags = ((_photo_tags = photo.tags) != null ? _photo_tags : []).map((tag)=>tag.toLowerCase().trim()).filter(Boolean);
|
|
1139
1545
|
const lead = new Set(stemList([
|
|
@@ -1154,6 +1560,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1154
1560
|
...tags.slice(0, 3).map((tag)=>stemList(tag).join(' ')),
|
|
1155
1561
|
stemList(slug).join(' ')
|
|
1156
1562
|
];
|
|
1563
|
+
/** The first tag: what the photo is most of all. */ const firstPhrase = stemList((_tags_ = tags[0]) != null ? _tags_ : '').join(' ');
|
|
1157
1564
|
const rejected = tags.some((tag)=>REJECTED_TAGS.has(tag) || tag.split(/\s+/).some((word)=>REJECTED_TAGS.has(word)));
|
|
1158
1565
|
// Whole words only, a hyphenated one kept whole: "cross-stitch" is not a cross.
|
|
1159
1566
|
const words = new Set([
|
|
@@ -1165,6 +1572,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1165
1572
|
all,
|
|
1166
1573
|
phrases,
|
|
1167
1574
|
leadPhrases,
|
|
1575
|
+
firstPhrase,
|
|
1168
1576
|
rejected,
|
|
1169
1577
|
words
|
|
1170
1578
|
};
|
|
@@ -1193,25 +1601,59 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1193
1601
|
* the subject's two-word phrase 3, each of its other words 1, and the
|
|
1194
1602
|
* site's world 1, plus a quarter when it is among the first tags.
|
|
1195
1603
|
*/ export function aiStockRelevance(photo, judgement) {
|
|
1196
|
-
var _judgement_rivals, _judgement_kin, _judgement_requires,
|
|
1604
|
+
var _judgement_rivals, _judgement_subject, _judgement_named, _judgement_shun, _judgement_leadAny, _judgement_kin, _judgement_requires, _judgement_category, _judgement_object, _judgement_domain;
|
|
1197
1605
|
const hit = hitWords(photo);
|
|
1198
1606
|
if (hit.rejected) return 0;
|
|
1199
1607
|
if (aiStockSensitive(photo, judgement.named)) return 0;
|
|
1200
1608
|
// Glass art is not pottery: a rival craft's photo names this craft's own words.
|
|
1201
1609
|
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;
|
|
1610
|
+
const subject = stemList((_judgement_subject = judgement.subject) != null ? _judgement_subject : '');
|
|
1611
|
+
// What the slot or the brief names lets a shunned word, a room or a fragrance in.
|
|
1612
|
+
const owned = new Set([
|
|
1613
|
+
...subject,
|
|
1614
|
+
...(_judgement_named = judgement.named) != null ? _judgement_named : []
|
|
1615
|
+
]);
|
|
1616
|
+
// A dispenser is not a soapmaker's, nor a pump a bar's (AGL-3660).
|
|
1617
|
+
if (((_judgement_shun = judgement.shun) != null ? _judgement_shun : []).some((word)=>hit.all.has(word) && !owned.has(word))) return 0;
|
|
1618
|
+
if (judgement.product) {
|
|
1619
|
+
// A sauna, a bathroom shelf: a room is never the product.
|
|
1620
|
+
for (const word of hit.lead)if (PRODUCT_SCENES.has(word) && !owned.has(word)) return 0;
|
|
1621
|
+
// Colognes are not a lotion: a fragrance's bottle only for a fragrance.
|
|
1622
|
+
if (![
|
|
1623
|
+
...FRAGRANCE_BOTTLES
|
|
1624
|
+
].some((word)=>owned.has(word)) && [
|
|
1625
|
+
...hit.lead
|
|
1626
|
+
].some((word)=>FRAGRANCE_BOTTLES.has(word))) return 0;
|
|
1627
|
+
}
|
|
1628
|
+
// A borrowed photo is OF the product's category.
|
|
1629
|
+
const leadAny = (_judgement_leadAny = judgement.leadAny) != null ? _judgement_leadAny : [];
|
|
1630
|
+
if (leadAny.length && !leadAny.some((phrase)=>among(hit.leadPhrases, phrase))) return 0;
|
|
1202
1631
|
const kin = (_judgement_kin = judgement.kin) != null ? _judgement_kin : [];
|
|
1203
1632
|
const kinNamed = kin.some((phrase)=>among(hit.phrases, phrase));
|
|
1204
1633
|
const kinLead = kin.some((phrase)=>among(hit.leadPhrases, phrase));
|
|
1205
1634
|
const required = stemList((_judgement_requires = judgement.requires) != null ? _judgement_requires : '');
|
|
1635
|
+
const category = ((_judgement_category = judgement.category) != null ? _judgement_category : []).flatMap(stemList);
|
|
1636
|
+
const anyOf = category.length ? [
|
|
1637
|
+
...new Set([
|
|
1638
|
+
...required,
|
|
1639
|
+
...category
|
|
1640
|
+
])
|
|
1641
|
+
] : [];
|
|
1642
|
+
const demanded = required.length > 0 || anyOf.length > 0;
|
|
1643
|
+
const meets = anyOf.length ? anyOf.some((stem)=>hit.all.has(stem)) : required.every((stem)=>hit.all.has(stem));
|
|
1206
1644
|
// An object's kin stands for the category: a wax melt is the candle shop's.
|
|
1207
|
-
if (!kinNamed &&
|
|
1208
|
-
const subject = stemList((_judgement_subject = judgement.subject) != null ? _judgement_subject : '');
|
|
1645
|
+
if (demanded && !kinNamed && !meets) return 0;
|
|
1209
1646
|
const object = (_judgement_object = judgement.object) != null ? _judgement_object : judgement.subject ? aiStockObjectWord(judgement.subject) : '';
|
|
1210
1647
|
const domain = ((_judgement_domain = judgement.domain) != null ? _judgement_domain : []).filter((stem)=>stem !== object);
|
|
1211
1648
|
// A hit naming the word it must name is of the site's world: a product's
|
|
1212
1649
|
// category is that world even where it is the product's own object.
|
|
1213
|
-
const world =
|
|
1214
|
-
const worldLead = required.length > 0 && required.every((stem)=>hit.lead.has(stem)) || kinLead || domain.some((stem)=>hit.lead.has(stem));
|
|
1650
|
+
const world = demanded || kinNamed || domain.some((stem)=>hit.all.has(stem));
|
|
1651
|
+
const worldLead = (anyOf.length ? anyOf.some((stem)=>hit.lead.has(stem)) : required.length > 0 && required.every((stem)=>hit.lead.has(stem))) || kinLead || domain.some((stem)=>hit.lead.has(stem));
|
|
1652
|
+
if (judgement.leadWorld && !worldLead) return 0;
|
|
1653
|
+
// What the photo is most of all: its first tag names the object, its kin or what it must be OF.
|
|
1654
|
+
const firstNamed = (phrases)=>phrases.some((phrase)=>Boolean(phrase) && among([
|
|
1655
|
+
hit.firstPhrase
|
|
1656
|
+
], phrase));
|
|
1215
1657
|
const pair = subject.length >= 2 ? subject.slice(-2).join(' ') : '';
|
|
1216
1658
|
const phrased = Boolean(pair) && among(hit.phrases, pair);
|
|
1217
1659
|
let score = 0;
|
|
@@ -1225,9 +1667,13 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1225
1667
|
if (domain.length && !world && !phrased) return 0;
|
|
1226
1668
|
score += leading ? 4 : 2;
|
|
1227
1669
|
if (kinNamed) score += 3;
|
|
1670
|
+
if (firstNamed(kin.length ? kin : [
|
|
1671
|
+
object
|
|
1672
|
+
])) score += 1;
|
|
1228
1673
|
} else if (domain.length) {
|
|
1229
1674
|
if (!world) return 0;
|
|
1230
1675
|
}
|
|
1676
|
+
if (leadAny.length && firstNamed(leadAny)) score += 1;
|
|
1231
1677
|
if (phrased) score += 3;
|
|
1232
1678
|
for (const word of new Set(subject))if (word !== object && hit.all.has(word)) score += 1;
|
|
1233
1679
|
if (world) score += worldLead ? 1.25 : 1;
|
|
@@ -1345,6 +1791,8 @@ const EXTENSIONS = {
|
|
|
1345
1791
|
delete request.object;
|
|
1346
1792
|
delete request.kin;
|
|
1347
1793
|
delete request.lead;
|
|
1794
|
+
delete request.leadWorld;
|
|
1795
|
+
delete request.category;
|
|
1348
1796
|
const key = JSON.stringify([
|
|
1349
1797
|
request.query,
|
|
1350
1798
|
request.orientation,
|
|
@@ -1363,7 +1811,7 @@ const EXTENSIONS = {
|
|
|
1363
1811
|
answers.set(key, asked);
|
|
1364
1812
|
return asked;
|
|
1365
1813
|
};
|
|
1366
|
-
/** The photo placed in the site's library, a copy it holds or a new one; `null` when it cannot be placed. */ const keep = async (photo, slot, query)=>{
|
|
1814
|
+
/** The photo placed in the site's library, a copy it holds or a new one; `null` when it cannot be placed. */ const keep = async (photo, slot, query, repeat = false)=>{
|
|
1367
1815
|
const sourceKey = stockPhotoSourceKey(photo);
|
|
1368
1816
|
try {
|
|
1369
1817
|
var _EXTENSIONS_bytes_contentType;
|
|
@@ -1374,7 +1822,7 @@ const EXTENSIONS = {
|
|
|
1374
1822
|
if (kept) {
|
|
1375
1823
|
var _kept_width, _kept_height;
|
|
1376
1824
|
// Held because a slot of this page or another page of the job shows it.
|
|
1377
|
-
if (placedSrcs.has(kept.src) || elsewhere(kept.src)) return null;
|
|
1825
|
+
if (!repeat && (placedSrcs.has(kept.src) || elsewhere(kept.src))) return null;
|
|
1378
1826
|
remember(sourceKey, kept.src);
|
|
1379
1827
|
return {
|
|
1380
1828
|
src: kept.src,
|
|
@@ -1437,6 +1885,65 @@ const EXTENSIONS = {
|
|
|
1437
1885
|
}
|
|
1438
1886
|
return null;
|
|
1439
1887
|
};
|
|
1888
|
+
/** How a search's hits are judged for a slot. */ const judgementFor = (slot, request, subject)=>{
|
|
1889
|
+
var _request_subject, _request_shun;
|
|
1890
|
+
var _request_shun1, _request_leadAny;
|
|
1891
|
+
return _extends({
|
|
1892
|
+
subject: (_request_subject = request.subject) != null ? _request_subject : request.broad ? '' : subject,
|
|
1893
|
+
domain: terms.domain,
|
|
1894
|
+
strict: slot.role === 'gallery' && !request.broad || !!request.requires,
|
|
1895
|
+
named: terms.named,
|
|
1896
|
+
rivals: terms.rivals
|
|
1897
|
+
}, terms.shun.length || ((_request_shun1 = request.shun) == null ? void 0 : _request_shun1.length) ? {
|
|
1898
|
+
shun: [
|
|
1899
|
+
...new Set([
|
|
1900
|
+
...terms.shun,
|
|
1901
|
+
...(_request_shun = request.shun) != null ? _request_shun : []
|
|
1902
|
+
])
|
|
1903
|
+
]
|
|
1904
|
+
} : {}, request.product ? {
|
|
1905
|
+
product: true
|
|
1906
|
+
} : {}, ((_request_leadAny = request.leadAny) == null ? void 0 : _request_leadAny.length) ? {
|
|
1907
|
+
leadAny: request.leadAny
|
|
1908
|
+
} : {}, request.requires ? {
|
|
1909
|
+
requires: request.requires
|
|
1910
|
+
} : {}, request.category ? {
|
|
1911
|
+
category: request.category
|
|
1912
|
+
} : {}, request.object ? {
|
|
1913
|
+
object: request.object
|
|
1914
|
+
} : {}, request.kin ? {
|
|
1915
|
+
kin: request.kin
|
|
1916
|
+
} : {}, request.lead ? {
|
|
1917
|
+
lead: true
|
|
1918
|
+
} : {}, request.leadWorld ? {
|
|
1919
|
+
leadWorld: true
|
|
1920
|
+
} : {});
|
|
1921
|
+
};
|
|
1922
|
+
/**
|
|
1923
|
+
* The craft's own photos, the last thing a slot of a craft's site is
|
|
1924
|
+
* filled from (AGL-3660): its broad searches, any hit naming the craft
|
|
1925
|
+
* itself. Never a desk, a laptop or a living room for a soap shop.
|
|
1926
|
+
*/ const craftSearches = (slot)=>{
|
|
1927
|
+
if (!terms.core.length) return [];
|
|
1928
|
+
const orientation = aiStockOrientation(slot.aspect);
|
|
1929
|
+
const size = slot.role === 'hero' ? {
|
|
1930
|
+
minWidth: 1600
|
|
1931
|
+
} : orientation === 'vertical' ? {
|
|
1932
|
+
minHeight: 900
|
|
1933
|
+
} : {
|
|
1934
|
+
minWidth: 900
|
|
1935
|
+
};
|
|
1936
|
+
return terms.broad.map((query)=>_extends({
|
|
1937
|
+
query: clip(query),
|
|
1938
|
+
orientation
|
|
1939
|
+
}, size, {
|
|
1940
|
+
broad: true,
|
|
1941
|
+
category: terms.core
|
|
1942
|
+
}, slot.product ? {
|
|
1943
|
+
product: true,
|
|
1944
|
+
tier: 'weak'
|
|
1945
|
+
} : {}));
|
|
1946
|
+
};
|
|
1440
1947
|
// 1. Choose, slot by slot, so the page's choices never collide: each
|
|
1441
1948
|
// slot claims its best accepted hit before the next slot looks.
|
|
1442
1949
|
const choices = [];
|
|
@@ -1449,45 +1956,85 @@ const EXTENSIONS = {
|
|
|
1449
1956
|
const sectionName = (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '';
|
|
1450
1957
|
const subject = aiStockSubjectWords(slot.alt, sectionName);
|
|
1451
1958
|
let choice = null;
|
|
1452
|
-
const requests =
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1959
|
+
const requests = [
|
|
1960
|
+
...slot.product ? aiStockProductSearches(slot, terms.craft, terms.domain, {
|
|
1961
|
+
broad: terms.broad,
|
|
1962
|
+
core: terms.core
|
|
1963
|
+
}) : aiStockSearchesFor(slot, terms, subject, aiStockSlotPart(slot, sectionName)),
|
|
1964
|
+
...craftSearches(slot)
|
|
1965
|
+
];
|
|
1966
|
+
/** The slot's accepted hits for a search; `null` once the page's searches are spent. */ const accepted = async (request, repeat)=>{
|
|
1456
1967
|
const found = await search(request);
|
|
1457
|
-
if (found === null)
|
|
1458
|
-
const blocked = new Set(
|
|
1459
|
-
|
|
1460
|
-
const key
|
|
1461
|
-
|
|
1968
|
+
if (found === null) return null;
|
|
1969
|
+
const blocked = new Set();
|
|
1970
|
+
if (!repeat) {
|
|
1971
|
+
for (const key of claimed)blocked.add(key);
|
|
1972
|
+
for (const hit of found){
|
|
1973
|
+
const key = stockPhotoSourceKey(hit);
|
|
1974
|
+
if (elsewhere(key)) blocked.add(key);
|
|
1975
|
+
}
|
|
1462
1976
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1977
|
+
return aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, judgementFor(slot, request, subject));
|
|
1978
|
+
};
|
|
1979
|
+
/** The first of these searches with a hit to take: a new one, or one the job already shows. */ const choose = async (tried, repeat)=>{
|
|
1980
|
+
for (const request of tried){
|
|
1981
|
+
if (signal.aborted) return null;
|
|
1982
|
+
// Spent searches answer nothing; those already asked still answer from memory.
|
|
1983
|
+
const ranked = await accepted(request, repeat);
|
|
1984
|
+
if (!(ranked == null ? void 0 : ranked.length)) continue;
|
|
1985
|
+
if (!repeat) claimed.add(stockPhotoSourceKey(ranked[0]));
|
|
1986
|
+
return _extends({
|
|
1987
|
+
candidates: ranked.slice(0, AI_LAYOUT_STOCK_TRIES_PER_SLOT),
|
|
1988
|
+
query: request.query
|
|
1989
|
+
}, repeat ? {
|
|
1990
|
+
repeat: true
|
|
1991
|
+
} : {});
|
|
1992
|
+
}
|
|
1993
|
+
return null;
|
|
1994
|
+
};
|
|
1995
|
+
// 2. Nothing new of its own: a product or a hero reuses an on-topic
|
|
1996
|
+
// photo another slot of the job already shows (a product may repeat
|
|
1997
|
+
// a collection tile's photo, or another product's) before the
|
|
1998
|
+
// starters' desk or living room fills it (AGL-3660). Any other
|
|
1999
|
+
// picture keeps "never twice". A product goes by how sure each
|
|
2000
|
+
// search is: its own and its category's photos, new and then
|
|
2001
|
+
// repeated, before a weak hit of its own (the Tidewater sauna).
|
|
2002
|
+
const repeats = Boolean(slot.product) || slot.role === 'hero';
|
|
2003
|
+
const sure = slot.product ? requests.filter((request)=>request.tier !== 'weak') : requests;
|
|
2004
|
+
const unsure = slot.product ? requests.filter((request)=>request.tier === 'weak') : [];
|
|
2005
|
+
const phases = [
|
|
2006
|
+
[
|
|
2007
|
+
sure,
|
|
2008
|
+
false
|
|
2009
|
+
],
|
|
2010
|
+
...repeats ? [
|
|
2011
|
+
[
|
|
2012
|
+
sure,
|
|
2013
|
+
true
|
|
2014
|
+
]
|
|
2015
|
+
] : [],
|
|
2016
|
+
...unsure.length ? [
|
|
2017
|
+
[
|
|
2018
|
+
unsure,
|
|
2019
|
+
false
|
|
2020
|
+
],
|
|
2021
|
+
[
|
|
2022
|
+
unsure,
|
|
2023
|
+
true
|
|
2024
|
+
]
|
|
2025
|
+
] : []
|
|
2026
|
+
];
|
|
2027
|
+
for (const [tried, repeat] of phases){
|
|
2028
|
+
if (choice || signal.aborted) break;
|
|
2029
|
+
choice = await choose(tried, repeat);
|
|
1485
2030
|
}
|
|
1486
2031
|
choices.push(choice);
|
|
1487
2032
|
}
|
|
1488
|
-
//
|
|
1489
|
-
// accepted hit no other slot has claimed.
|
|
2033
|
+
// 3. Keep, a few at a time; a copy that fails tries the slot's next
|
|
2034
|
+
// accepted hit no other slot has claimed. A repeat waits for the
|
|
2035
|
+
// slot it repeats, so the library never holds a photo twice.
|
|
1490
2036
|
const photos = slots.map(()=>null);
|
|
2037
|
+
/** What this pass placed, by source key. */ const placedByKey = new Map();
|
|
1491
2038
|
const fill = async (index)=>{
|
|
1492
2039
|
const choice = choices[index];
|
|
1493
2040
|
const slot = slots[index];
|
|
@@ -1495,28 +2042,43 @@ const EXTENSIONS = {
|
|
|
1495
2042
|
for (const [rank, candidate] of choice.candidates.entries()){
|
|
1496
2043
|
if (signal.aborted) return;
|
|
1497
2044
|
const key = stockPhotoSourceKey(candidate);
|
|
2045
|
+
if (choice.repeat) {
|
|
2046
|
+
var _placedByKey_get;
|
|
2047
|
+
const placed = (_placedByKey_get = placedByKey.get(key)) != null ? _placedByKey_get : await keep(candidate, slot, choice.query, true);
|
|
2048
|
+
if (placed) {
|
|
2049
|
+
photos[index] = placed;
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
1498
2054
|
if (rank > 0) {
|
|
1499
2055
|
if (claimed.has(key)) continue;
|
|
1500
2056
|
claimed.add(key);
|
|
1501
2057
|
}
|
|
1502
2058
|
const placed = await keep(candidate, slot, choice.query);
|
|
1503
2059
|
if (placed) {
|
|
2060
|
+
placedByKey.set(key, placed);
|
|
1504
2061
|
photos[index] = placed;
|
|
1505
2062
|
return;
|
|
1506
2063
|
}
|
|
1507
2064
|
}
|
|
1508
2065
|
};
|
|
2066
|
+
const firsts = slots.map((_slot, index)=>index).filter((index)=>{
|
|
2067
|
+
var _choices_index;
|
|
2068
|
+
return !((_choices_index = choices[index]) == null ? void 0 : _choices_index.repeat);
|
|
2069
|
+
});
|
|
1509
2070
|
let next = 0;
|
|
1510
2071
|
const worker = async ()=>{
|
|
1511
|
-
while(next <
|
|
1512
|
-
const index = next;
|
|
2072
|
+
while(next < firsts.length){
|
|
2073
|
+
const index = firsts[next];
|
|
1513
2074
|
next += 1;
|
|
1514
2075
|
await fill(index);
|
|
1515
2076
|
}
|
|
1516
2077
|
};
|
|
1517
2078
|
await Promise.all(Array.from({
|
|
1518
|
-
length: Math.min(AI_LAYOUT_STOCK_COPIES_AT_ONCE,
|
|
2079
|
+
length: Math.min(AI_LAYOUT_STOCK_COPIES_AT_ONCE, firsts.length)
|
|
1519
2080
|
}, worker));
|
|
2081
|
+
for (const [index, choice] of choices.entries())if (choice == null ? void 0 : choice.repeat) await fill(index);
|
|
1520
2082
|
return photos;
|
|
1521
2083
|
};
|
|
1522
2084
|
}
|