@aglyn/plugins-ai 1.0.0-beta.242 → 1.0.0-beta.244
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-layout-language.js +11 -6
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +3 -1
- package/src/lib/jobs/ai-job-page-language.js +17 -5
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +11 -4
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +22 -8
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +52 -8
- package/src/lib/jobs/ai-layout-stock-photos.js +351 -72
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +14 -0
- package/src/lib/jobs/ai-site-publish.js +28 -3
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- 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
- package/src/lib/layout-language/ai-layout-links.js +5 -2
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +42 -1
- package/src/lib/runtime/ai-doctrine-validators.js +98 -11
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/usage/ai-quota-checks.d.ts +13 -3
- package/src/lib/usage/ai-quota-checks.js +46 -8
- package/src/lib/usage/ai-quota-checks.js.map +1 -1
|
@@ -106,6 +106,16 @@ import { aiStorefrontSays } from "../layout-language/ai-layout-storefront.js";
|
|
|
106
106
|
* searches, and then — a product or a hero only — reuses an on-topic photo
|
|
107
107
|
* the job already shows rather than take a starter. Every slot of a
|
|
108
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.
|
|
109
119
|
*
|
|
110
120
|
* A slot nothing acceptable answered — the library down, refusing, or out
|
|
111
121
|
* of time — is left `null` for the starter photos. A named object is never
|
|
@@ -149,6 +159,47 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
149
159
|
* an empty frame on the page (the Juniper Clay hero), a template or a
|
|
150
160
|
* graphic is not a photo of anything.
|
|
151
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));
|
|
152
203
|
/**
|
|
153
204
|
* Topics a business's photos never carry unless its own words name them
|
|
154
205
|
* (AGL-3660, the beta.239 Ember & Oak start, whose "Gifts for melt fans" tile
|
|
@@ -316,6 +367,7 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
316
367
|
},
|
|
317
368
|
{
|
|
318
369
|
// Tobacco and drugs.
|
|
370
|
+
// "cbd" and "cannabinoid": a hemp cream tube was the next "body lotion" answer (Tidewater, 2026-10-11).
|
|
319
371
|
words: [
|
|
320
372
|
'cigarette',
|
|
321
373
|
'smoking',
|
|
@@ -323,7 +375,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
323
375
|
'cannabis',
|
|
324
376
|
'marijuana',
|
|
325
377
|
'drug',
|
|
326
|
-
'drugs'
|
|
378
|
+
'drugs',
|
|
379
|
+
'cbd',
|
|
380
|
+
'cannabinoid',
|
|
381
|
+
'thc'
|
|
327
382
|
],
|
|
328
383
|
allow: [
|
|
329
384
|
'dispensary',
|
|
@@ -510,6 +565,100 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
510
565
|
queries: [
|
|
511
566
|
'soap bars',
|
|
512
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'
|
|
513
662
|
]
|
|
514
663
|
}
|
|
515
664
|
];
|
|
@@ -683,7 +832,11 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
683
832
|
'spa',
|
|
684
833
|
'artisan'
|
|
685
834
|
]
|
|
686
|
-
}
|
|
835
|
+
},
|
|
836
|
+
shun: [
|
|
837
|
+
'dispenser',
|
|
838
|
+
'dispensers'
|
|
839
|
+
]
|
|
687
840
|
},
|
|
688
841
|
{
|
|
689
842
|
match: [
|
|
@@ -958,7 +1111,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
958
1111
|
const core = new Set(craft ? [
|
|
959
1112
|
craft
|
|
960
1113
|
] : []);
|
|
1114
|
+
const shun = new Set();
|
|
961
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));
|
|
962
1118
|
for (const word of group.match)core.add(aiStockStem(word));
|
|
963
1119
|
for (const word of group.kin)domain.add(aiStockStem(word));
|
|
964
1120
|
broad.push(...group.broad);
|
|
@@ -987,6 +1143,9 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
987
1143
|
rivals,
|
|
988
1144
|
core: [
|
|
989
1145
|
...core
|
|
1146
|
+
],
|
|
1147
|
+
shun: [
|
|
1148
|
+
...shun
|
|
990
1149
|
]
|
|
991
1150
|
};
|
|
992
1151
|
}
|
|
@@ -1038,6 +1197,11 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1038
1197
|
/** An object's kin phrases as stems, or none. */ const kinStems = (entry)=>entry ? [
|
|
1039
1198
|
...new Set(entry.kin.map((phrase)=>stemList(phrase).join(' ')))
|
|
1040
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;
|
|
1041
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');
|
|
1042
1206
|
/**
|
|
1043
1207
|
* A product's searches (AGL-3676), most specific first: each of its subjects
|
|
@@ -1061,18 +1225,31 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1061
1225
|
* back to its own craft (AGL-3660). The Saltmarsh Soap Co start (2026-10-10)
|
|
1062
1226
|
* found no hit naming "bar" or "box" among the first tags, and four of its
|
|
1063
1227
|
* five products took the starter photos — a laptop and roses, a living room
|
|
1064
|
-
* of plants.
|
|
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}):
|
|
1065
1234
|
*
|
|
1066
|
-
* 1.
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
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}).
|
|
1071
1246
|
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
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.
|
|
1074
1251
|
*/ export function aiStockProductSearches(slot, craft, domain = [], fallback = {}) {
|
|
1075
|
-
var _ref, _subjects_, _fallback_broad,
|
|
1252
|
+
var _ref, _subjects_, _fallback_core, _ref1, _fallback_broad, _fallback_broad1;
|
|
1076
1253
|
var _slot_product;
|
|
1077
1254
|
const orientation = aiStockOrientation(slot.aspect);
|
|
1078
1255
|
const size = orientation === 'vertical' ? {
|
|
@@ -1085,7 +1262,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1085
1262
|
...domain,
|
|
1086
1263
|
...craftStems
|
|
1087
1264
|
]);
|
|
1088
|
-
const subjects = ((_ref = (_slot_product = slot.product) == null ? void 0 : _slot_product.subjects) != null ? _ref : []).map((raw)=>aiStockSubjectWords(raw, '')).filter(Boolean);
|
|
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);
|
|
1089
1266
|
// The product's head noun is its name's: "candle" of "Hand-Poured Soy Candle".
|
|
1090
1267
|
const own = (_subjects_ = subjects[0]) != null ? _subjects_ : '';
|
|
1091
1268
|
const object = aiStockObjectWord(own);
|
|
@@ -1102,10 +1279,19 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1102
1279
|
...craftStems,
|
|
1103
1280
|
...ownWorld
|
|
1104
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);
|
|
1105
1289
|
const queries = [];
|
|
1106
1290
|
for (const subject of subjects){
|
|
1291
|
+
var _ref2;
|
|
1107
1292
|
const stems = stemList(subject);
|
|
1108
|
-
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;
|
|
1109
1295
|
queries.push({
|
|
1110
1296
|
query,
|
|
1111
1297
|
subject,
|
|
@@ -1133,13 +1319,25 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1133
1319
|
query: craft,
|
|
1134
1320
|
subject: own || craft,
|
|
1135
1321
|
object: craft,
|
|
1136
|
-
lead: true
|
|
1322
|
+
lead: true,
|
|
1323
|
+
tier: 'borrow'
|
|
1137
1324
|
});
|
|
1138
|
-
|
|
1139
|
-
for (const entry of [
|
|
1325
|
+
const held = [
|
|
1140
1326
|
...queries
|
|
1141
|
-
]
|
|
1142
|
-
|
|
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'
|
|
1143
1341
|
}));
|
|
1144
1342
|
// A gift is its craft's: "bath gift box" asks for a "soap gift box".
|
|
1145
1343
|
const wrapped = own.split(/\s+/).filter((word)=>WRAPPERS.has(word) && aiStockStem(word) !== object);
|
|
@@ -1148,47 +1346,60 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1148
1346
|
query: `${craft} ${wrapped.join(' ')}`,
|
|
1149
1347
|
subject: own,
|
|
1150
1348
|
object: aiStockStem(gift),
|
|
1151
|
-
lead: false
|
|
1349
|
+
lead: false,
|
|
1350
|
+
tier: 'weak'
|
|
1152
1351
|
});
|
|
1153
|
-
//
|
|
1154
|
-
for (const query of (
|
|
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({
|
|
1155
1354
|
query,
|
|
1156
1355
|
subject: own,
|
|
1157
1356
|
object: '',
|
|
1158
1357
|
lead: false,
|
|
1159
|
-
broad: true
|
|
1358
|
+
broad: true,
|
|
1359
|
+
tier: 'weak'
|
|
1160
1360
|
});
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
...craftStems,
|
|
1164
|
-
...(_fallback_core = fallback.core) != null ? _fallback_core : []
|
|
1165
|
-
])
|
|
1166
|
-
];
|
|
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;
|
|
1167
1363
|
const seen = new Set();
|
|
1168
1364
|
const searches = [];
|
|
1169
1365
|
for (const entry of queries){
|
|
1366
|
+
var _entry_tier;
|
|
1170
1367
|
const query = clip(entry.query);
|
|
1171
|
-
const key = `${query}|${entry.lead}|${entry.object}|${entry.broad === true}`;
|
|
1368
|
+
const key = `${query}|${entry.lead}|${entry.object}|${entry.broad === true}|${(_entry_tier = entry.tier) != null ? _entry_tier : ''}`;
|
|
1172
1369
|
if (!query || seen.has(key)) continue;
|
|
1173
1370
|
seen.add(key);
|
|
1371
|
+
const common = _extends({
|
|
1372
|
+
query,
|
|
1373
|
+
orientation
|
|
1374
|
+
}, size, {
|
|
1375
|
+
subject: entry.subject,
|
|
1376
|
+
product: true
|
|
1377
|
+
}, entry.tier ? {
|
|
1378
|
+
tier: entry.tier
|
|
1379
|
+
} : {}, shun.length ? {
|
|
1380
|
+
shun
|
|
1381
|
+
} : {});
|
|
1174
1382
|
if (entry.broad) {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
},
|
|
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 ? {
|
|
1182
1398
|
category: core
|
|
1183
1399
|
} : {}));
|
|
1184
1400
|
continue;
|
|
1185
1401
|
}
|
|
1186
|
-
searches.push(_extends({
|
|
1187
|
-
query,
|
|
1188
|
-
orientation
|
|
1189
|
-
}, size, {
|
|
1190
|
-
subject: entry.subject
|
|
1191
|
-
}, entry.lead ? {
|
|
1402
|
+
searches.push(_extends({}, common, entry.lead ? {
|
|
1192
1403
|
lead: true
|
|
1193
1404
|
} : {
|
|
1194
1405
|
leadWorld: true
|
|
@@ -1328,7 +1539,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1328
1539
|
}
|
|
1329
1540
|
}
|
|
1330
1541
|
/** A hit's words: the first few (what it is OF), every one, and each tag's phrase. */ function hitWords(photo) {
|
|
1331
|
-
var _photo_tags, _photo_alt, _photo_alt1, _photo_alt2;
|
|
1542
|
+
var _photo_tags, _photo_alt, _photo_alt1, _tags_, _photo_alt2;
|
|
1332
1543
|
const slug = pageSlug(photo.pageUrl);
|
|
1333
1544
|
const tags = ((_photo_tags = photo.tags) != null ? _photo_tags : []).map((tag)=>tag.toLowerCase().trim()).filter(Boolean);
|
|
1334
1545
|
const lead = new Set(stemList([
|
|
@@ -1349,6 +1560,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1349
1560
|
...tags.slice(0, 3).map((tag)=>stemList(tag).join(' ')),
|
|
1350
1561
|
stemList(slug).join(' ')
|
|
1351
1562
|
];
|
|
1563
|
+
/** The first tag: what the photo is most of all. */ const firstPhrase = stemList((_tags_ = tags[0]) != null ? _tags_ : '').join(' ');
|
|
1352
1564
|
const rejected = tags.some((tag)=>REJECTED_TAGS.has(tag) || tag.split(/\s+/).some((word)=>REJECTED_TAGS.has(word)));
|
|
1353
1565
|
// Whole words only, a hyphenated one kept whole: "cross-stitch" is not a cross.
|
|
1354
1566
|
const words = new Set([
|
|
@@ -1360,6 +1572,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1360
1572
|
all,
|
|
1361
1573
|
phrases,
|
|
1362
1574
|
leadPhrases,
|
|
1575
|
+
firstPhrase,
|
|
1363
1576
|
rejected,
|
|
1364
1577
|
words
|
|
1365
1578
|
};
|
|
@@ -1388,12 +1601,33 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1388
1601
|
* the subject's two-word phrase 3, each of its other words 1, and the
|
|
1389
1602
|
* site's world 1, plus a quarter when it is among the first tags.
|
|
1390
1603
|
*/ export function aiStockRelevance(photo, judgement) {
|
|
1391
|
-
var _judgement_rivals, _judgement_kin, _judgement_requires, _judgement_category,
|
|
1604
|
+
var _judgement_rivals, _judgement_subject, _judgement_named, _judgement_shun, _judgement_leadAny, _judgement_kin, _judgement_requires, _judgement_category, _judgement_object, _judgement_domain;
|
|
1392
1605
|
const hit = hitWords(photo);
|
|
1393
1606
|
if (hit.rejected) return 0;
|
|
1394
1607
|
if (aiStockSensitive(photo, judgement.named)) return 0;
|
|
1395
1608
|
// Glass art is not pottery: a rival craft's photo names this craft's own words.
|
|
1396
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;
|
|
1397
1631
|
const kin = (_judgement_kin = judgement.kin) != null ? _judgement_kin : [];
|
|
1398
1632
|
const kinNamed = kin.some((phrase)=>among(hit.phrases, phrase));
|
|
1399
1633
|
const kinLead = kin.some((phrase)=>among(hit.leadPhrases, phrase));
|
|
@@ -1409,7 +1643,6 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1409
1643
|
const meets = anyOf.length ? anyOf.some((stem)=>hit.all.has(stem)) : required.every((stem)=>hit.all.has(stem));
|
|
1410
1644
|
// An object's kin stands for the category: a wax melt is the candle shop's.
|
|
1411
1645
|
if (demanded && !kinNamed && !meets) return 0;
|
|
1412
|
-
const subject = stemList((_judgement_subject = judgement.subject) != null ? _judgement_subject : '');
|
|
1413
1646
|
const object = (_judgement_object = judgement.object) != null ? _judgement_object : judgement.subject ? aiStockObjectWord(judgement.subject) : '';
|
|
1414
1647
|
const domain = ((_judgement_domain = judgement.domain) != null ? _judgement_domain : []).filter((stem)=>stem !== object);
|
|
1415
1648
|
// A hit naming the word it must name is of the site's world: a product's
|
|
@@ -1417,6 +1650,10 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1417
1650
|
const world = demanded || kinNamed || domain.some((stem)=>hit.all.has(stem));
|
|
1418
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));
|
|
1419
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));
|
|
1420
1657
|
const pair = subject.length >= 2 ? subject.slice(-2).join(' ') : '';
|
|
1421
1658
|
const phrased = Boolean(pair) && among(hit.phrases, pair);
|
|
1422
1659
|
let score = 0;
|
|
@@ -1430,9 +1667,13 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1430
1667
|
if (domain.length && !world && !phrased) return 0;
|
|
1431
1668
|
score += leading ? 4 : 2;
|
|
1432
1669
|
if (kinNamed) score += 3;
|
|
1670
|
+
if (firstNamed(kin.length ? kin : [
|
|
1671
|
+
object
|
|
1672
|
+
])) score += 1;
|
|
1433
1673
|
} else if (domain.length) {
|
|
1434
1674
|
if (!world) return 0;
|
|
1435
1675
|
}
|
|
1676
|
+
if (leadAny.length && firstNamed(leadAny)) score += 1;
|
|
1436
1677
|
if (phrased) score += 3;
|
|
1437
1678
|
for (const word of new Set(subject))if (word !== object && hit.all.has(word)) score += 1;
|
|
1438
1679
|
if (world) score += worldLead ? 1.25 : 1;
|
|
@@ -1645,14 +1886,26 @@ const EXTENSIONS = {
|
|
|
1645
1886
|
return null;
|
|
1646
1887
|
};
|
|
1647
1888
|
/** How a search's hits are judged for a slot. */ const judgementFor = (slot, request, subject)=>{
|
|
1648
|
-
var _request_subject;
|
|
1889
|
+
var _request_subject, _request_shun;
|
|
1890
|
+
var _request_shun1, _request_leadAny;
|
|
1649
1891
|
return _extends({
|
|
1650
1892
|
subject: (_request_subject = request.subject) != null ? _request_subject : request.broad ? '' : subject,
|
|
1651
1893
|
domain: terms.domain,
|
|
1652
1894
|
strict: slot.role === 'gallery' && !request.broad || !!request.requires,
|
|
1653
1895
|
named: terms.named,
|
|
1654
1896
|
rivals: terms.rivals
|
|
1655
|
-
}, request.
|
|
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 ? {
|
|
1656
1909
|
requires: request.requires
|
|
1657
1910
|
} : {}, request.category ? {
|
|
1658
1911
|
category: request.category
|
|
@@ -1686,7 +1939,10 @@ const EXTENSIONS = {
|
|
|
1686
1939
|
}, size, {
|
|
1687
1940
|
broad: true,
|
|
1688
1941
|
category: terms.core
|
|
1689
|
-
}
|
|
1942
|
+
}, slot.product ? {
|
|
1943
|
+
product: true,
|
|
1944
|
+
tier: 'weak'
|
|
1945
|
+
} : {}));
|
|
1690
1946
|
};
|
|
1691
1947
|
// 1. Choose, slot by slot, so the page's choices never collide: each
|
|
1692
1948
|
// slot claims its best accepted hit before the next slot looks.
|
|
@@ -1720,34 +1976,57 @@ const EXTENSIONS = {
|
|
|
1720
1976
|
}
|
|
1721
1977
|
return aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, judgementFor(slot, request, subject));
|
|
1722
1978
|
};
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
const first = ranked[0];
|
|
1729
|
-
claimed.add(stockPhotoSourceKey(first));
|
|
1730
|
-
choice = {
|
|
1731
|
-
candidates: ranked.slice(0, AI_LAYOUT_STOCK_TRIES_PER_SLOT),
|
|
1732
|
-
query: request.query
|
|
1733
|
-
};
|
|
1734
|
-
}
|
|
1735
|
-
// 2. Nothing new of its own: a product or a hero reuses an on-topic
|
|
1736
|
-
// photo another slot of the job already shows (a product may repeat
|
|
1737
|
-
// a collection tile's photo) before the starters' desk or living
|
|
1738
|
-
// room fills it (AGL-3660). Only the searches already answered are
|
|
1739
|
-
// asked again, from memory. Any other picture keeps "never twice".
|
|
1740
|
-
if (!choice && (slot.product || slot.role === 'hero')) {
|
|
1741
|
-
for (const request of requests){
|
|
1742
|
-
if (choice || signal.aborted) break;
|
|
1743
|
-
const ranked = await accepted(request, true);
|
|
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);
|
|
1744
1984
|
if (!(ranked == null ? void 0 : ranked.length)) continue;
|
|
1745
|
-
|
|
1985
|
+
if (!repeat) claimed.add(stockPhotoSourceKey(ranked[0]));
|
|
1986
|
+
return _extends({
|
|
1746
1987
|
candidates: ranked.slice(0, AI_LAYOUT_STOCK_TRIES_PER_SLOT),
|
|
1747
|
-
query: request.query
|
|
1988
|
+
query: request.query
|
|
1989
|
+
}, repeat ? {
|
|
1748
1990
|
repeat: true
|
|
1749
|
-
};
|
|
1991
|
+
} : {});
|
|
1750
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);
|
|
1751
2030
|
}
|
|
1752
2031
|
choices.push(choice);
|
|
1753
2032
|
}
|