@aglyn/plugins-ai 1.0.0-beta.241 → 1.0.0-beta.242
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/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 +63 -8
- package/src/lib/jobs/ai-layout-stock-photos.js +350 -67
- 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
|
@@ -95,6 +95,18 @@ 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
|
+
*
|
|
98
110
|
* A slot nothing acceptable answered — the library down, refusing, or out
|
|
99
111
|
* of time — is left `null` for the starter photos. A named object is never
|
|
100
112
|
* given one while the library answers anything of the site's world: the
|
|
@@ -120,7 +132,8 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
120
132
|
/**
|
|
121
133
|
* Words that describe a picture's look, not what it shows, dropped from a
|
|
122
134
|
* 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 ' +
|
|
135
|
+
*/ const FILLER = words('beautiful beautifully stunning gorgeous lovely pretty elegant modern contemporary rustic cozy cosy warm cool ' + 'soft bright natural simple minimal minimalist delicate unique special artisan artisanal handmade ' + 'hand-made hand-thrown handthrown hand-crafted handcrafted crafted hand-built handbuilt hand-poured ' + 'hand-painted hand-dyed hand-stitched poured finished styled perfect wheel-thrown wheelthrown thrown ' + 'fresh clean calm serene quiet peaceful vibrant colorful colourful sunlit sunny golden glossy matte textured ' + 'close-up closeup up view views detail details detailed shot shots overhead flat lay flatlay flat-lay ' + 'top-down wide angle featuring featured displayed arranged sitting resting placed standing lined ' + 'single pair collection assortment selection group few various array range large tiny big little ' + 'tall short wide narrow everyday classic signature original favorite favourite bestselling best-selling ' + 'small-batch batch limited edition custom bespoke ' + // How a soap is made, never what it shows: "cold" and "process" named a supermarket fridge (Saltmarsh, 2026-10-10).
|
|
136
|
+
'cold-process cold-processed hot-process hot-processed melt-and-pour ' + 'white black grey gray beige cream ivory brown tan blue green red pink yellow orange purple speckled ' + 'gentle airy inviting welcoming cheerful happy smiling friendly');
|
|
124
137
|
/** The nouns that name a place of business, not its craft: "studio" in "ceramics studio". */ const VENUES = words('studio studios shop shops store stores boutique company co workshop gallery atelier agency brand practice ' + 'salon house collective lab market services service firm clinic center centre parlor parlour bar ' + 'class classes lessons school academy');
|
|
125
138
|
/** The person who does the craft, not the craft: "artist" in "ceramic artist". */ const ROLES = words('artist artists maker makers artisan artisans owner owners founder founders creator creators');
|
|
126
139
|
/** Words naming what kind of SITE it is, never what it shows: "portfolio" in "a portfolio for a ceramic artist". */ const SITE_KINDS = words('portfolio portfolios website websites site sites blog blogs online web webshop ecommerce e-commerce ' + 'landing personal homepage storefront');
|
|
@@ -469,6 +482,35 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
469
482
|
'wax melt warmer',
|
|
470
483
|
'scented wax cubes'
|
|
471
484
|
]
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
// A "bar" is a pub's before it is a soap's (the Saltmarsh "cold-process
|
|
488
|
+
// soap bar" search answered a pint of beer second): a soap bar is named
|
|
489
|
+
// by soap, and searched by it.
|
|
490
|
+
objects: [
|
|
491
|
+
'bar'
|
|
492
|
+
],
|
|
493
|
+
within: [
|
|
494
|
+
'soap',
|
|
495
|
+
'soapmaker',
|
|
496
|
+
'skincare'
|
|
497
|
+
],
|
|
498
|
+
qualifier: 'soap',
|
|
499
|
+
kin: [
|
|
500
|
+
'soap bar',
|
|
501
|
+
'soap bars',
|
|
502
|
+
'bar of soap',
|
|
503
|
+
'bars of soap',
|
|
504
|
+
'soap',
|
|
505
|
+
'soaps',
|
|
506
|
+
'handmade soap',
|
|
507
|
+
'soap block',
|
|
508
|
+
'soap cubes'
|
|
509
|
+
],
|
|
510
|
+
queries: [
|
|
511
|
+
'soap bars',
|
|
512
|
+
'handmade soap bar'
|
|
513
|
+
]
|
|
472
514
|
}
|
|
473
515
|
];
|
|
474
516
|
/** An object's kin where the site or the subject is of its world, else `null`. */ function objectKin(object, world) {
|
|
@@ -595,22 +637,53 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
595
637
|
'soapmaker',
|
|
596
638
|
'skincare'
|
|
597
639
|
],
|
|
640
|
+
// Never "natural": a florist's bouquet tagged "natural" was the
|
|
641
|
+
// Saltmarsh Soap Co hero (2026-10-10).
|
|
598
642
|
kin: [
|
|
599
643
|
'soap',
|
|
600
644
|
'skincare',
|
|
601
645
|
'lotion',
|
|
646
|
+
'cream',
|
|
602
647
|
'bath',
|
|
603
648
|
'spa',
|
|
604
|
-
'natural',
|
|
605
649
|
'cosmetic'
|
|
606
650
|
],
|
|
607
651
|
broad: [
|
|
608
652
|
'handmade soap',
|
|
609
653
|
'natural skincare'
|
|
610
654
|
],
|
|
655
|
+
// "Soap making" is answered with soap bubbles; a bar is what a soapmaker makes.
|
|
611
656
|
making: [
|
|
612
|
-
'soap making'
|
|
613
|
-
|
|
657
|
+
'soap making',
|
|
658
|
+
'handmade soap'
|
|
659
|
+
],
|
|
660
|
+
// Soap bubbles, a dispenser and detergent are soap, never a soapmaker's:
|
|
661
|
+
// the same start's story band showed bubbles in flight.
|
|
662
|
+
rivals: {
|
|
663
|
+
words: [
|
|
664
|
+
'bubble',
|
|
665
|
+
'bubbles',
|
|
666
|
+
'soapy',
|
|
667
|
+
'dispenser',
|
|
668
|
+
'detergent',
|
|
669
|
+
'dishwashing',
|
|
670
|
+
'laundry'
|
|
671
|
+
],
|
|
672
|
+
core: [
|
|
673
|
+
'bar',
|
|
674
|
+
'bars',
|
|
675
|
+
'handmade',
|
|
676
|
+
'homemade',
|
|
677
|
+
'loaf',
|
|
678
|
+
'lye',
|
|
679
|
+
'glycerin',
|
|
680
|
+
'skincare',
|
|
681
|
+
'lotion',
|
|
682
|
+
'cosmetic',
|
|
683
|
+
'spa',
|
|
684
|
+
'artisan'
|
|
685
|
+
]
|
|
686
|
+
}
|
|
614
687
|
},
|
|
615
688
|
{
|
|
616
689
|
match: [
|
|
@@ -837,10 +910,11 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
837
910
|
return craft ? aiStockStem(craft) : '';
|
|
838
911
|
}
|
|
839
912
|
/** The site's terms, read from its business type. */ export function aiStockSiteTerms(businessType) {
|
|
840
|
-
|
|
841
|
-
|
|
913
|
+
let business = aiStockBusinessWords(businessType);
|
|
914
|
+
let craft = aiStockCraftWords(business);
|
|
842
915
|
const domain = new Set();
|
|
843
916
|
/** The things it makes, by their head nouns. */ const made = new Set();
|
|
917
|
+
/** The last word of the first clause that names the world: "soap" of "for handmade cold-process soap". */ let firstWorld = '';
|
|
844
918
|
let named = false;
|
|
845
919
|
let making = false;
|
|
846
920
|
for (const { opener, words: clause } of businessClauses(businessType)){
|
|
@@ -858,16 +932,34 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
858
932
|
// A set or a gift is never a world: "gift" of "gift sets" is not one.
|
|
859
933
|
const world = (making ? kept.slice(0, -1) : kept).filter((word)=>!WRAPPERS.has(word));
|
|
860
934
|
for (const word of world)for (const stem of stemList(word))domain.add(stem);
|
|
935
|
+
const last = world[world.length - 1];
|
|
936
|
+
if (!firstWorld && last) firstWorld = aiStockStem(last);
|
|
861
937
|
if (making) for (const word of kept.slice(-1))for (const stem of stemList(word))made.add(stem);
|
|
862
938
|
}
|
|
863
|
-
if (craft) domain.add(craft);
|
|
864
939
|
// A business named only by what it sells ("a shop selling pottery") is of that world.
|
|
865
940
|
if (!domain.size) for (const stem of made)domain.add(stem);
|
|
941
|
+
const groups = CRAFT_KIN.filter((group)=>group.match.some((word)=>domain.has(aiStockStem(word)) || made.has(aiStockStem(word)) || aiStockStem(word) === craft));
|
|
942
|
+
// A business that is only a place ("an online shop for handmade
|
|
943
|
+
// cold-process soap") takes its craft from what it is for: the Saltmarsh
|
|
944
|
+
// Soap Co start (2026-10-10) read its craft as nothing, so its hero searched
|
|
945
|
+
// "shop" (a florist's bouquet) and no product was held to soap.
|
|
946
|
+
if (!craft) {
|
|
947
|
+
var _groups_;
|
|
948
|
+
const listed = (_groups_ = groups[0]) == null ? void 0 : _groups_.match.map(aiStockStem).find((stem)=>domain.has(stem) || made.has(stem));
|
|
949
|
+
craft = listed || firstWorld || [
|
|
950
|
+
...made
|
|
951
|
+
][0] || '';
|
|
952
|
+
if (craft && !stemList(business).includes(craft)) business = `${craft} ${business}`.trim();
|
|
953
|
+
}
|
|
954
|
+
if (craft) domain.add(craft);
|
|
866
955
|
const broad = [];
|
|
867
956
|
const makingSearches = [];
|
|
868
957
|
const rivals = [];
|
|
869
|
-
|
|
870
|
-
|
|
958
|
+
const core = new Set(craft ? [
|
|
959
|
+
craft
|
|
960
|
+
] : []);
|
|
961
|
+
for (const group of groups){
|
|
962
|
+
for (const word of group.match)core.add(aiStockStem(word));
|
|
871
963
|
for (const word of group.kin)domain.add(aiStockStem(word));
|
|
872
964
|
broad.push(...group.broad);
|
|
873
965
|
makingSearches.push(...group.making);
|
|
@@ -892,7 +984,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
|
892
984
|
named: [
|
|
893
985
|
...new Set(stemList(businessType))
|
|
894
986
|
],
|
|
895
|
-
rivals
|
|
987
|
+
rivals,
|
|
988
|
+
core: [
|
|
989
|
+
...core
|
|
990
|
+
]
|
|
896
991
|
};
|
|
897
992
|
}
|
|
898
993
|
/** A phrase's content words without its filler; all filler, and nothing is left. */ function subjectOf(list) {
|
|
@@ -943,23 +1038,41 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
943
1038
|
/** An object's kin phrases as stems, or none. */ const kinStems = (entry)=>entry ? [
|
|
944
1039
|
...new Set(entry.kin.map((phrase)=>stemList(phrase).join(' ')))
|
|
945
1040
|
] : [];
|
|
1041
|
+
/** The wrapper words a gift is named by, searched with the craft: "soap gift box". */ const GIFT_WRAPPERS = words('gift gifts box boxes basket baskets hamper hampers');
|
|
946
1042
|
/**
|
|
947
1043
|
* A product's searches (AGL-3676), most specific first: each of its subjects
|
|
948
1044
|
* — 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
|
-
*
|
|
1045
|
+
* where the phrase does not already name the shop's world ("candle gift
|
|
1046
|
+
* box"), then the category alone, a plain photo of what the shop sells. Every
|
|
1047
|
+
* one must find a hit naming the category — or the product's own word of the
|
|
1048
|
+
* shop's world, "lotion" in a soap shop — so no product is ever filled with a
|
|
1049
|
+
* lifestyle shot of something else: the beta.237 Willow Wick start put a
|
|
1050
|
+
* laptop and roses under "Candle Wick Trimmer" and an antique tea set under
|
|
1051
|
+
* "Candle Gift Set".
|
|
954
1052
|
*
|
|
955
|
-
* And
|
|
1053
|
+
* And each must find a hit naming the product's own head noun among its
|
|
956
1054
|
* first tags (AGL-3660): the beta.239 Ember & Oak start put a bed with a book,
|
|
957
1055
|
* a mug and a camera (its candle a last tag) under "Hand-Poured Soy Candle".
|
|
958
1056
|
* An object whose own word is not enough ({@link OBJECT_KIN}) is searched by
|
|
959
1057
|
* its kin instead of the category alone, and named by them: a single taper
|
|
960
1058
|
* candle filled "Wax Melt Gift Set".
|
|
961
|
-
|
|
962
|
-
|
|
1059
|
+
*
|
|
1060
|
+
* Then, before a product is ever given a photo of something else, it falls
|
|
1061
|
+
* back to its own craft (AGL-3660). The Saltmarsh Soap Co start (2026-10-10)
|
|
1062
|
+
* found no hit naming "bar" or "box" among the first tags, and four of its
|
|
1063
|
+
* five products took the starter photos — a laptop and roses, a living room
|
|
1064
|
+
* of plants. So, in order:
|
|
1065
|
+
*
|
|
1066
|
+
* 1. The same searches again, the head noun named anywhere among the tags
|
|
1067
|
+
* (no extra search: the page remembers its answers), and a gift's own
|
|
1068
|
+
* wrapper with the craft ("soap gift box").
|
|
1069
|
+
* 2. The site's broad searches ("handmade soap", "natural skincare"), any hit
|
|
1070
|
+
* naming the craft itself ({@link AiStockSiteTerms.core}).
|
|
1071
|
+
*
|
|
1072
|
+
* The source then reuses an on-topic photo another slot of the job already
|
|
1073
|
+
* shows before it gives up ({@link aiLayoutStockPhotoSource}).
|
|
1074
|
+
*/ export function aiStockProductSearches(slot, craft, domain = [], fallback = {}) {
|
|
1075
|
+
var _ref, _subjects_, _fallback_broad, _fallback_core;
|
|
963
1076
|
var _slot_product;
|
|
964
1077
|
const orientation = aiStockOrientation(slot.aspect);
|
|
965
1078
|
const size = orientation === 'vertical' ? {
|
|
@@ -968,62 +1081,131 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
968
1081
|
minWidth: 900
|
|
969
1082
|
};
|
|
970
1083
|
const craftStems = stemList(craft);
|
|
1084
|
+
const world = new Set([
|
|
1085
|
+
...domain,
|
|
1086
|
+
...craftStems
|
|
1087
|
+
]);
|
|
971
1088
|
const subjects = ((_ref = (_slot_product = slot.product) == null ? void 0 : _slot_product.subjects) != null ? _ref : []).map((raw)=>aiStockSubjectWords(raw, '')).filter(Boolean);
|
|
972
1089
|
// The product's head noun is its name's: "candle" of "Hand-Poured Soy Candle".
|
|
973
|
-
const
|
|
1090
|
+
const own = (_subjects_ = subjects[0]) != null ? _subjects_ : '';
|
|
1091
|
+
const object = aiStockObjectWord(own);
|
|
974
1092
|
const kin = object ? objectKin(object, [
|
|
975
1093
|
...craftStems,
|
|
976
1094
|
...domain,
|
|
977
1095
|
...stemList(subjects.join(' '))
|
|
978
1096
|
]) : null;
|
|
979
1097
|
const kinPhrases = kinStems(kin);
|
|
1098
|
+
// A product of the shop's world beside its category is held to either:
|
|
1099
|
+
// a soap shop's body lotion names "lotion", never "soap".
|
|
1100
|
+
const ownWorld = stemList(own).filter((stem)=>world.has(stem) && !WRAPPERS.has(stem) && !craftStems.includes(stem));
|
|
1101
|
+
const category = craft && ownWorld.length ? [
|
|
1102
|
+
...craftStems,
|
|
1103
|
+
...ownWorld
|
|
1104
|
+
] : [];
|
|
980
1105
|
const queries = [];
|
|
981
1106
|
for (const subject of subjects){
|
|
982
1107
|
const stems = stemList(subject);
|
|
983
|
-
const query = kin ? stems.includes(aiStockStem(kin.qualifier)) ? subject : `${kin.qualifier} ${subject}` : craft && !
|
|
1108
|
+
const query = kin ? stems.includes(aiStockStem(kin.qualifier)) ? subject : `${kin.qualifier} ${subject}` : craft && !stems.some((stem)=>world.has(stem)) ? `${craft} ${subject}` : subject;
|
|
984
1109
|
queries.push({
|
|
985
1110
|
query,
|
|
986
1111
|
subject,
|
|
987
|
-
object
|
|
1112
|
+
object,
|
|
1113
|
+
lead: true
|
|
988
1114
|
});
|
|
989
1115
|
}
|
|
990
|
-
// The category alone stands in only for a thing that is no more than of
|
|
991
|
-
//
|
|
992
|
-
|
|
1116
|
+
// The category alone stands in only for a thing that is no more than of
|
|
1117
|
+
// it; a thing that is its own word of the shop's world is searched by that
|
|
1118
|
+
// word ("lotion"). Its hits are still scored by the product's own words: a
|
|
1119
|
+
// soy candle over a taper.
|
|
993
1120
|
if (kin) for (const query of kin.queries)queries.push({
|
|
994
1121
|
query,
|
|
995
1122
|
subject: own || query,
|
|
996
|
-
object
|
|
1123
|
+
object,
|
|
1124
|
+
lead: true
|
|
1125
|
+
});
|
|
1126
|
+
else if (object && ownWorld.includes(object)) queries.push({
|
|
1127
|
+
query: object,
|
|
1128
|
+
subject: own,
|
|
1129
|
+
object,
|
|
1130
|
+
lead: true
|
|
997
1131
|
});
|
|
998
1132
|
else if (craft) queries.push({
|
|
999
1133
|
query: craft,
|
|
1000
1134
|
subject: own || craft,
|
|
1001
|
-
object: craft
|
|
1135
|
+
object: craft,
|
|
1136
|
+
lead: true
|
|
1137
|
+
});
|
|
1138
|
+
// 1. The head noun anywhere among the tags, by the same searches.
|
|
1139
|
+
for (const entry of [
|
|
1140
|
+
...queries
|
|
1141
|
+
])queries.push(_extends({}, entry, {
|
|
1142
|
+
lead: false
|
|
1143
|
+
}));
|
|
1144
|
+
// A gift is its craft's: "bath gift box" asks for a "soap gift box".
|
|
1145
|
+
const wrapped = own.split(/\s+/).filter((word)=>WRAPPERS.has(word) && aiStockStem(word) !== object);
|
|
1146
|
+
const gift = wrapped.find((word)=>GIFT_WRAPPERS.has(word));
|
|
1147
|
+
if (craft && gift && !kin) queries.push({
|
|
1148
|
+
query: `${craft} ${wrapped.join(' ')}`,
|
|
1149
|
+
subject: own,
|
|
1150
|
+
object: aiStockStem(gift),
|
|
1151
|
+
lead: false
|
|
1002
1152
|
});
|
|
1153
|
+
// 2. The craft's own broad searches, any hit naming the craft.
|
|
1154
|
+
for (const query of (_fallback_broad = fallback.broad) != null ? _fallback_broad : [])queries.push({
|
|
1155
|
+
query,
|
|
1156
|
+
subject: own,
|
|
1157
|
+
object: '',
|
|
1158
|
+
lead: false,
|
|
1159
|
+
broad: true
|
|
1160
|
+
});
|
|
1161
|
+
const core = [
|
|
1162
|
+
...new Set([
|
|
1163
|
+
...craftStems,
|
|
1164
|
+
...(_fallback_core = fallback.core) != null ? _fallback_core : []
|
|
1165
|
+
])
|
|
1166
|
+
];
|
|
1003
1167
|
const seen = new Set();
|
|
1004
1168
|
const searches = [];
|
|
1005
1169
|
for (const entry of queries){
|
|
1006
1170
|
const query = clip(entry.query);
|
|
1007
|
-
|
|
1008
|
-
seen.
|
|
1171
|
+
const key = `${query}|${entry.lead}|${entry.object}|${entry.broad === true}`;
|
|
1172
|
+
if (!query || seen.has(key)) continue;
|
|
1173
|
+
seen.add(key);
|
|
1174
|
+
if (entry.broad) {
|
|
1175
|
+
searches.push(_extends({
|
|
1176
|
+
query,
|
|
1177
|
+
orientation
|
|
1178
|
+
}, size, {
|
|
1179
|
+
broad: true,
|
|
1180
|
+
subject: entry.subject
|
|
1181
|
+
}, core.length ? {
|
|
1182
|
+
category: core
|
|
1183
|
+
} : {}));
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1009
1186
|
searches.push(_extends({
|
|
1010
1187
|
query,
|
|
1011
1188
|
orientation
|
|
1012
1189
|
}, size, {
|
|
1013
|
-
subject: entry.subject
|
|
1190
|
+
subject: entry.subject
|
|
1191
|
+
}, entry.lead ? {
|
|
1014
1192
|
lead: true
|
|
1193
|
+
} : {
|
|
1194
|
+
leadWorld: true
|
|
1015
1195
|
}, entry.object ? {
|
|
1016
1196
|
object: entry.object
|
|
1017
|
-
} : {}, kinPhrases.length ? {
|
|
1197
|
+
} : {}, kinPhrases.length && entry.object === object ? {
|
|
1018
1198
|
kin: kinPhrases
|
|
1019
1199
|
} : {}, craft ? {
|
|
1020
1200
|
requires: craft
|
|
1201
|
+
} : {}, category.length ? {
|
|
1202
|
+
category
|
|
1021
1203
|
} : {}));
|
|
1022
1204
|
}
|
|
1023
1205
|
return searches;
|
|
1024
1206
|
}
|
|
1025
1207
|
/** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, terms, subject, part) {
|
|
1026
|
-
var _terms_making;
|
|
1208
|
+
var _terms_core, _terms_making;
|
|
1027
1209
|
const orientation = aiStockOrientation(slot.aspect);
|
|
1028
1210
|
const size = slot.role === 'hero' ? {
|
|
1029
1211
|
minWidth: 1600
|
|
@@ -1042,6 +1224,12 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1042
1224
|
const kinPhrases = kinStems(kin);
|
|
1043
1225
|
// The craft qualifies a subject that does not already name the site's world.
|
|
1044
1226
|
const namesWorld = subjectStems.some((stem)=>domain.has(stem));
|
|
1227
|
+
// A tile of the shop's world beside its category is held to either: a soap
|
|
1228
|
+
// shop's "Body care" tile names "body", never "soap" (AGL-3660).
|
|
1229
|
+
const ownWorld = subjectStems.filter((stem)=>domain.has(stem) && !WRAPPERS.has(stem) && stem !== terms.craft);
|
|
1230
|
+
// A hero of a craft names the craft itself: the Saltmarsh Soap Co hero
|
|
1231
|
+
// (2026-10-10) was a florist's bouquet tagged "natural" (AGL-3660).
|
|
1232
|
+
const core = (_terms_core = terms.core) != null ? _terms_core : [];
|
|
1045
1233
|
const qualify = (phrase)=>terms.craft && !namesWorld && !stemList(phrase).includes(terms.craft) ? `${terms.craft} ${phrase}` : phrase;
|
|
1046
1234
|
// A collection tile is product-like: held to the shop's category and its
|
|
1047
1235
|
// own object among the first tags, and searched by its object's kin, never
|
|
@@ -1117,8 +1305,15 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1117
1305
|
kin: kinPhrases
|
|
1118
1306
|
} : {}, held && collection ? {
|
|
1119
1307
|
lead: true
|
|
1120
|
-
} : {}, terms.craft && (raw.category || raw.held && collection) ? {
|
|
1308
|
+
} : {}, terms.craft && (raw.category || raw.held && collection) ? _extends({
|
|
1121
1309
|
requires: terms.craft
|
|
1310
|
+
}, ownWorld.length ? {
|
|
1311
|
+
category: [
|
|
1312
|
+
terms.craft,
|
|
1313
|
+
...ownWorld
|
|
1314
|
+
]
|
|
1315
|
+
} : {}) : {}, slot.role === 'hero' && core.length ? {
|
|
1316
|
+
category: core
|
|
1122
1317
|
} : {}));
|
|
1123
1318
|
}
|
|
1124
1319
|
return searches;
|
|
@@ -1193,7 +1388,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1193
1388
|
* the subject's two-word phrase 3, each of its other words 1, and the
|
|
1194
1389
|
* site's world 1, plus a quarter when it is among the first tags.
|
|
1195
1390
|
*/ export function aiStockRelevance(photo, judgement) {
|
|
1196
|
-
var _judgement_rivals, _judgement_kin, _judgement_requires, _judgement_subject, _judgement_object, _judgement_domain;
|
|
1391
|
+
var _judgement_rivals, _judgement_kin, _judgement_requires, _judgement_category, _judgement_subject, _judgement_object, _judgement_domain;
|
|
1197
1392
|
const hit = hitWords(photo);
|
|
1198
1393
|
if (hit.rejected) return 0;
|
|
1199
1394
|
if (aiStockSensitive(photo, judgement.named)) return 0;
|
|
@@ -1203,15 +1398,25 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
1203
1398
|
const kinNamed = kin.some((phrase)=>among(hit.phrases, phrase));
|
|
1204
1399
|
const kinLead = kin.some((phrase)=>among(hit.leadPhrases, phrase));
|
|
1205
1400
|
const required = stemList((_judgement_requires = judgement.requires) != null ? _judgement_requires : '');
|
|
1401
|
+
const category = ((_judgement_category = judgement.category) != null ? _judgement_category : []).flatMap(stemList);
|
|
1402
|
+
const anyOf = category.length ? [
|
|
1403
|
+
...new Set([
|
|
1404
|
+
...required,
|
|
1405
|
+
...category
|
|
1406
|
+
])
|
|
1407
|
+
] : [];
|
|
1408
|
+
const demanded = required.length > 0 || anyOf.length > 0;
|
|
1409
|
+
const meets = anyOf.length ? anyOf.some((stem)=>hit.all.has(stem)) : required.every((stem)=>hit.all.has(stem));
|
|
1206
1410
|
// An object's kin stands for the category: a wax melt is the candle shop's.
|
|
1207
|
-
if (!kinNamed &&
|
|
1411
|
+
if (demanded && !kinNamed && !meets) return 0;
|
|
1208
1412
|
const subject = stemList((_judgement_subject = judgement.subject) != null ? _judgement_subject : '');
|
|
1209
1413
|
const object = (_judgement_object = judgement.object) != null ? _judgement_object : judgement.subject ? aiStockObjectWord(judgement.subject) : '';
|
|
1210
1414
|
const domain = ((_judgement_domain = judgement.domain) != null ? _judgement_domain : []).filter((stem)=>stem !== object);
|
|
1211
1415
|
// A hit naming the word it must name is of the site's world: a product's
|
|
1212
1416
|
// 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));
|
|
1417
|
+
const world = demanded || kinNamed || domain.some((stem)=>hit.all.has(stem));
|
|
1418
|
+
const worldLead = (anyOf.length ? anyOf.some((stem)=>hit.lead.has(stem)) : required.length > 0 && required.every((stem)=>hit.lead.has(stem))) || kinLead || domain.some((stem)=>hit.lead.has(stem));
|
|
1419
|
+
if (judgement.leadWorld && !worldLead) return 0;
|
|
1215
1420
|
const pair = subject.length >= 2 ? subject.slice(-2).join(' ') : '';
|
|
1216
1421
|
const phrased = Boolean(pair) && among(hit.phrases, pair);
|
|
1217
1422
|
let score = 0;
|
|
@@ -1345,6 +1550,8 @@ const EXTENSIONS = {
|
|
|
1345
1550
|
delete request.object;
|
|
1346
1551
|
delete request.kin;
|
|
1347
1552
|
delete request.lead;
|
|
1553
|
+
delete request.leadWorld;
|
|
1554
|
+
delete request.category;
|
|
1348
1555
|
const key = JSON.stringify([
|
|
1349
1556
|
request.query,
|
|
1350
1557
|
request.orientation,
|
|
@@ -1363,7 +1570,7 @@ const EXTENSIONS = {
|
|
|
1363
1570
|
answers.set(key, asked);
|
|
1364
1571
|
return asked;
|
|
1365
1572
|
};
|
|
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)=>{
|
|
1573
|
+
/** The photo placed in the site's library, a copy it holds or a new one; `null` when it cannot be placed. */ const keep = async (photo, slot, query, repeat = false)=>{
|
|
1367
1574
|
const sourceKey = stockPhotoSourceKey(photo);
|
|
1368
1575
|
try {
|
|
1369
1576
|
var _EXTENSIONS_bytes_contentType;
|
|
@@ -1374,7 +1581,7 @@ const EXTENSIONS = {
|
|
|
1374
1581
|
if (kept) {
|
|
1375
1582
|
var _kept_width, _kept_height;
|
|
1376
1583
|
// 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;
|
|
1584
|
+
if (!repeat && (placedSrcs.has(kept.src) || elsewhere(kept.src))) return null;
|
|
1378
1585
|
remember(sourceKey, kept.src);
|
|
1379
1586
|
return {
|
|
1380
1587
|
src: kept.src,
|
|
@@ -1437,6 +1644,50 @@ const EXTENSIONS = {
|
|
|
1437
1644
|
}
|
|
1438
1645
|
return null;
|
|
1439
1646
|
};
|
|
1647
|
+
/** How a search's hits are judged for a slot. */ const judgementFor = (slot, request, subject)=>{
|
|
1648
|
+
var _request_subject;
|
|
1649
|
+
return _extends({
|
|
1650
|
+
subject: (_request_subject = request.subject) != null ? _request_subject : request.broad ? '' : subject,
|
|
1651
|
+
domain: terms.domain,
|
|
1652
|
+
strict: slot.role === 'gallery' && !request.broad || !!request.requires,
|
|
1653
|
+
named: terms.named,
|
|
1654
|
+
rivals: terms.rivals
|
|
1655
|
+
}, request.requires ? {
|
|
1656
|
+
requires: request.requires
|
|
1657
|
+
} : {}, request.category ? {
|
|
1658
|
+
category: request.category
|
|
1659
|
+
} : {}, request.object ? {
|
|
1660
|
+
object: request.object
|
|
1661
|
+
} : {}, request.kin ? {
|
|
1662
|
+
kin: request.kin
|
|
1663
|
+
} : {}, request.lead ? {
|
|
1664
|
+
lead: true
|
|
1665
|
+
} : {}, request.leadWorld ? {
|
|
1666
|
+
leadWorld: true
|
|
1667
|
+
} : {});
|
|
1668
|
+
};
|
|
1669
|
+
/**
|
|
1670
|
+
* The craft's own photos, the last thing a slot of a craft's site is
|
|
1671
|
+
* filled from (AGL-3660): its broad searches, any hit naming the craft
|
|
1672
|
+
* itself. Never a desk, a laptop or a living room for a soap shop.
|
|
1673
|
+
*/ const craftSearches = (slot)=>{
|
|
1674
|
+
if (!terms.core.length) return [];
|
|
1675
|
+
const orientation = aiStockOrientation(slot.aspect);
|
|
1676
|
+
const size = slot.role === 'hero' ? {
|
|
1677
|
+
minWidth: 1600
|
|
1678
|
+
} : orientation === 'vertical' ? {
|
|
1679
|
+
minHeight: 900
|
|
1680
|
+
} : {
|
|
1681
|
+
minWidth: 900
|
|
1682
|
+
};
|
|
1683
|
+
return terms.broad.map((query)=>_extends({
|
|
1684
|
+
query: clip(query),
|
|
1685
|
+
orientation
|
|
1686
|
+
}, size, {
|
|
1687
|
+
broad: true,
|
|
1688
|
+
category: terms.core
|
|
1689
|
+
}));
|
|
1690
|
+
};
|
|
1440
1691
|
// 1. Choose, slot by slot, so the page's choices never collide: each
|
|
1441
1692
|
// slot claims its best accepted hit before the next slot looks.
|
|
1442
1693
|
const choices = [];
|
|
@@ -1449,32 +1700,30 @@ const EXTENSIONS = {
|
|
|
1449
1700
|
const sectionName = (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '';
|
|
1450
1701
|
const subject = aiStockSubjectWords(slot.alt, sectionName);
|
|
1451
1702
|
let choice = null;
|
|
1452
|
-
const requests =
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1703
|
+
const requests = [
|
|
1704
|
+
...slot.product ? aiStockProductSearches(slot, terms.craft, terms.domain, {
|
|
1705
|
+
broad: terms.broad,
|
|
1706
|
+
core: terms.core
|
|
1707
|
+
}) : aiStockSearchesFor(slot, terms, subject, aiStockSlotPart(slot, sectionName)),
|
|
1708
|
+
...craftSearches(slot)
|
|
1709
|
+
];
|
|
1710
|
+
/** The slot's accepted hits for a search; `null` once the page's searches are spent. */ const accepted = async (request, repeat)=>{
|
|
1456
1711
|
const found = await search(request);
|
|
1457
|
-
if (found === null)
|
|
1458
|
-
const blocked = new Set(
|
|
1459
|
-
|
|
1460
|
-
const key
|
|
1461
|
-
|
|
1712
|
+
if (found === null) return null;
|
|
1713
|
+
const blocked = new Set();
|
|
1714
|
+
if (!repeat) {
|
|
1715
|
+
for (const key of claimed)blocked.add(key);
|
|
1716
|
+
for (const hit of found){
|
|
1717
|
+
const key = stockPhotoSourceKey(hit);
|
|
1718
|
+
if (elsewhere(key)) blocked.add(key);
|
|
1719
|
+
}
|
|
1462
1720
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
}, request.requires ? {
|
|
1470
|
-
requires: request.requires
|
|
1471
|
-
} : {}, request.object ? {
|
|
1472
|
-
object: request.object
|
|
1473
|
-
} : {}, request.kin ? {
|
|
1474
|
-
kin: request.kin
|
|
1475
|
-
} : {}, request.lead ? {
|
|
1476
|
-
lead: true
|
|
1477
|
-
} : {}));
|
|
1721
|
+
return aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, judgementFor(slot, request, subject));
|
|
1722
|
+
};
|
|
1723
|
+
for (const request of requests){
|
|
1724
|
+
if (choice || signal.aborted) break;
|
|
1725
|
+
const ranked = await accepted(request, false);
|
|
1726
|
+
if (ranked === null) break;
|
|
1478
1727
|
if (!ranked.length) continue;
|
|
1479
1728
|
const first = ranked[0];
|
|
1480
1729
|
claimed.add(stockPhotoSourceKey(first));
|
|
@@ -1483,11 +1732,30 @@ const EXTENSIONS = {
|
|
|
1483
1732
|
query: request.query
|
|
1484
1733
|
};
|
|
1485
1734
|
}
|
|
1735
|
+
// 2. Nothing new of its own: a product or a hero reuses an on-topic
|
|
1736
|
+
// photo another slot of the job already shows (a product may repeat
|
|
1737
|
+
// a collection tile's photo) before the starters' desk or living
|
|
1738
|
+
// room fills it (AGL-3660). Only the searches already answered are
|
|
1739
|
+
// asked again, from memory. Any other picture keeps "never twice".
|
|
1740
|
+
if (!choice && (slot.product || slot.role === 'hero')) {
|
|
1741
|
+
for (const request of requests){
|
|
1742
|
+
if (choice || signal.aborted) break;
|
|
1743
|
+
const ranked = await accepted(request, true);
|
|
1744
|
+
if (!(ranked == null ? void 0 : ranked.length)) continue;
|
|
1745
|
+
choice = {
|
|
1746
|
+
candidates: ranked.slice(0, AI_LAYOUT_STOCK_TRIES_PER_SLOT),
|
|
1747
|
+
query: request.query,
|
|
1748
|
+
repeat: true
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1486
1752
|
choices.push(choice);
|
|
1487
1753
|
}
|
|
1488
|
-
//
|
|
1489
|
-
// accepted hit no other slot has claimed.
|
|
1754
|
+
// 3. Keep, a few at a time; a copy that fails tries the slot's next
|
|
1755
|
+
// accepted hit no other slot has claimed. A repeat waits for the
|
|
1756
|
+
// slot it repeats, so the library never holds a photo twice.
|
|
1490
1757
|
const photos = slots.map(()=>null);
|
|
1758
|
+
/** What this pass placed, by source key. */ const placedByKey = new Map();
|
|
1491
1759
|
const fill = async (index)=>{
|
|
1492
1760
|
const choice = choices[index];
|
|
1493
1761
|
const slot = slots[index];
|
|
@@ -1495,28 +1763,43 @@ const EXTENSIONS = {
|
|
|
1495
1763
|
for (const [rank, candidate] of choice.candidates.entries()){
|
|
1496
1764
|
if (signal.aborted) return;
|
|
1497
1765
|
const key = stockPhotoSourceKey(candidate);
|
|
1766
|
+
if (choice.repeat) {
|
|
1767
|
+
var _placedByKey_get;
|
|
1768
|
+
const placed = (_placedByKey_get = placedByKey.get(key)) != null ? _placedByKey_get : await keep(candidate, slot, choice.query, true);
|
|
1769
|
+
if (placed) {
|
|
1770
|
+
photos[index] = placed;
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1498
1775
|
if (rank > 0) {
|
|
1499
1776
|
if (claimed.has(key)) continue;
|
|
1500
1777
|
claimed.add(key);
|
|
1501
1778
|
}
|
|
1502
1779
|
const placed = await keep(candidate, slot, choice.query);
|
|
1503
1780
|
if (placed) {
|
|
1781
|
+
placedByKey.set(key, placed);
|
|
1504
1782
|
photos[index] = placed;
|
|
1505
1783
|
return;
|
|
1506
1784
|
}
|
|
1507
1785
|
}
|
|
1508
1786
|
};
|
|
1787
|
+
const firsts = slots.map((_slot, index)=>index).filter((index)=>{
|
|
1788
|
+
var _choices_index;
|
|
1789
|
+
return !((_choices_index = choices[index]) == null ? void 0 : _choices_index.repeat);
|
|
1790
|
+
});
|
|
1509
1791
|
let next = 0;
|
|
1510
1792
|
const worker = async ()=>{
|
|
1511
|
-
while(next <
|
|
1512
|
-
const index = next;
|
|
1793
|
+
while(next < firsts.length){
|
|
1794
|
+
const index = firsts[next];
|
|
1513
1795
|
next += 1;
|
|
1514
1796
|
await fill(index);
|
|
1515
1797
|
}
|
|
1516
1798
|
};
|
|
1517
1799
|
await Promise.all(Array.from({
|
|
1518
|
-
length: Math.min(AI_LAYOUT_STOCK_COPIES_AT_ONCE,
|
|
1800
|
+
length: Math.min(AI_LAYOUT_STOCK_COPIES_AT_ONCE, firsts.length)
|
|
1519
1801
|
}, worker));
|
|
1802
|
+
for (const [index, choice] of choices.entries())if (choice == null ? void 0 : choice.repeat) await fill(index);
|
|
1520
1803
|
return photos;
|
|
1521
1804
|
};
|
|
1522
1805
|
}
|