@aglyn/plugins-ai 1.0.0-beta.237 → 1.0.0-beta.238
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-credits-prompt.component.d.ts +11 -4
- package/src/lib/components/ai-credits-prompt.component.js +84 -34
- package/src/lib/components/ai-credits-prompt.component.js.map +1 -1
- package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
- package/src/lib/components/ai-job-cancel.component.js +156 -0
- package/src/lib/components/ai-job-cancel.component.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.js +12 -1
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-list-page.component.js +141 -72
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +22 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
- package/src/lib/components/ai-usage-strip.component.js +40 -9
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +12 -16
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +87 -51
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +4 -1
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
- package/src/lib/jobs/ai-job-cancel.js +102 -0
- package/src/lib/jobs/ai-job-cancel.js.map +1 -0
- package/src/lib/jobs/ai-job-component-step.js +4 -1
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.js +2 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.js +4 -1
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-language.js +13 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +6 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +8 -0
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +12 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +20 -1
- package/src/lib/jobs/ai-job-plan-step.js +49 -9
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +15 -0
- package/src/lib/jobs/ai-job-site-content.js +66 -7
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +4 -1
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +4 -1
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +37 -12
- package/src/lib/jobs/ai-jobs.js +184 -28
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -70
- package/src/lib/jobs/ai-layout-stock-photos.js +649 -176
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-compiler.js +304 -8
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
- package/src/lib/layout-language/ai-layout-design.js +5 -3
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -3
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +13 -1
- package/src/lib/layout-language/ai-layout-listings.js +61 -46
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
- package/src/lib/layout-language/ai-layout-storefront.js +223 -0
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
- package/src/lib/model/ai-build-job.d.ts +9 -1
- package/src/lib/model/ai-build-job.js +19 -5
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-progress.d.ts +2 -2
- package/src/lib/model/ai-build-progress.js +6 -4
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +69 -2
- package/src/lib/model/ai-credit-estimate.js +91 -3
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
- package/src/lib/model/ai-job-cancel-copy.js +53 -0
- package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
- package/src/lib/model/ai-job-failure-copy.js +8 -3
- package/src/lib/model/ai-job-failure-copy.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +20 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.js +7 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +19 -2
- package/src/lib/model/ai-site-job.js +29 -8
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +4 -1
- package/src/lib/model/ai-site-kinds.js +34 -6
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +37 -1
- package/src/lib/runtime/ai-doctrine-validators.js +151 -6
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +4 -3
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +53 -7
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
- package/src/lib/server/ai-jobs-cancel.js +27 -9
- package/src/lib/server/ai-jobs-cancel.js.map +1 -1
- package/src/lib/server/ai-jobs-gate.d.ts +12 -2
- package/src/lib/server/ai-jobs-gate.js +20 -9
- package/src/lib/server/ai-jobs-gate.js.map +1 -1
- package/src/lib/usage/ai-usage-meter.d.ts +2 -1
- package/src/lib/usage/ai-usage-meter.js +17 -1
- package/src/lib/usage/ai-usage-meter.js.map +1 -1
- package/src/lib/usage/ai-usage-wire.d.ts +6 -0
- package/src/lib/usage/ai-usage-wire.js.map +1 -1
- package/src/lib/usage/assist-usage.d.ts +7 -0
- package/src/lib/usage/assist-usage.js +14 -4
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
- package/src/lib/usage/free-ai-credits-left.js +4 -2
- package/src/lib/usage/free-ai-credits-left.js.map +1 -1
|
@@ -26,59 +26,337 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
26
26
|
* server door (`core.media-ingest`), and builds no source at all when either
|
|
27
27
|
* is missing, so a deployment without a key behaves exactly as before.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* What the live starts of 2026-10-10 taught (Juniper Clay Works, a ceramic
|
|
30
|
+
* artist's portfolio, and Willow Wick Candles, a candle store), each photo
|
|
31
|
+
* traced through the library's cached answers:
|
|
30
32
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
33
|
+
* - The site's words were read as "portfolio" and "online" — the kind of
|
|
34
|
+
* SITE, not the craft — so every search said "serving bowl portfolio" and
|
|
35
|
+
* the hero asked for "portfolio": a wallet, fashion models, and a blank
|
|
36
|
+
* paper mockup that read as an empty panel beside the brushes.
|
|
37
|
+
* - A hit naming the object anywhere won: a robin tagged "food bowl" filled
|
|
38
|
+
* "Speckled serving bowl", a lilac branch tagged "bud, vase" filled "Tall
|
|
39
|
+
* bud vase", letterpress type tagged "set" filled "Nesting bowl set" (its
|
|
40
|
+
* head noun was read as "set"), and an antique tea set tagged "brown
|
|
41
|
+
* candle" filled "Candle Gift Set".
|
|
42
|
+
* - The page's 20 seconds ran out after four photos (each copy took ~5s, one
|
|
43
|
+
* after another), and the slots left took the starter photos: the laptop
|
|
44
|
+
* desk with pink roses under "Candle Wick Trimmer" and a work card, and the
|
|
45
|
+
* windowsill plants under another.
|
|
46
|
+
*
|
|
47
|
+
* So, for each slot:
|
|
48
|
+
*
|
|
49
|
+
* 1. **The site's terms** ({@link aiStockSiteTerms}): the business is the
|
|
50
|
+
* first clause of its type that says more than what kind of site it is
|
|
51
|
+
* ("ceramic artist" from "a portfolio for a ceramic artist who makes
|
|
52
|
+
* stoneware bowls"), its craft one word ("ceramic"), and its DOMAIN the
|
|
53
|
+
* words a photo of its world names: the craft, the business type's own
|
|
54
|
+
* nouns ("stoneware", "soy") and, for common crafts, their kin ("pottery",
|
|
55
|
+
* "clay", "wax"). Its broad searches ask for the world itself ("handmade
|
|
56
|
+
* ceramics", "pottery").
|
|
57
|
+
* 2. **Search.** A picture of a thing asks for its subject — the noun phrase
|
|
58
|
+
* of the caption, filler dropped — qualified by the craft where it does
|
|
59
|
+
* not already name the domain ("ceramic serving bowl"), then the craft and
|
|
60
|
+
* the object alone ("ceramic bowl"), then the broad searches. A hero asks
|
|
61
|
+
* for the business, then broad; an about picture for the business's
|
|
62
|
+
* people, then broad. The orientation and a minimum size come from the
|
|
63
|
+
* slot's frame.
|
|
64
|
+
* 3. **Accept, or reject.** A hit tagged as a mockup, a blank or a template
|
|
65
|
+
* never fills anything. A picture of a thing takes only a hit naming its
|
|
66
|
+
* OBJECT (the head noun, past "set", "gift", "kit": "bowl" in "nesting
|
|
67
|
+
* bowl set", "candle" in "candle gift set") AND either the site's domain
|
|
68
|
+
* or the subject's own two-word phrase ("bud vase"). A broad, hero or
|
|
69
|
+
* about search takes only a hit naming the domain. The accepted are
|
|
70
|
+
* ranked: the object in the hit's first tags (what the photo is OF) over
|
|
71
|
+
* a mention in its last, the subject's other words, the domain. Among
|
|
72
|
+
* equals the job's seed picks, so two sites differ and one job repeats.
|
|
73
|
+
* 4. **Never twice.** A photo is placed once per job: not twice on a page,
|
|
50
74
|
* and not on a page when another page (or another pass, in this process)
|
|
51
|
-
* of the same job already placed it
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* never the library's address.
|
|
75
|
+
* of the same job already placed it. The caller hands in what the job's
|
|
76
|
+
* other pages hold (`avoid`); a pass repeated over its own page keeps its
|
|
77
|
+
* own.
|
|
78
|
+
* 5. **Keep, all at once.** The choosing is done first, slot by slot; the
|
|
79
|
+
* copies then run side by side, a few at a time, so a page of eight
|
|
80
|
+
* pictures takes about as long as two. A photo the site's library already
|
|
81
|
+
* holds (by its source key) is reused; otherwise its bytes are copied
|
|
82
|
+
* into the site's own library as the member who started the job, credited
|
|
83
|
+
* to its photographer. A copy that fails tries the slot's next accepted
|
|
84
|
+
* hit.
|
|
62
85
|
*
|
|
63
|
-
* A slot
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
/** The
|
|
86
|
+
* A slot nothing acceptable answered — the library down, refusing, or out
|
|
87
|
+
* of time — is left `null` for the starter photos. A named object is never
|
|
88
|
+
* given one while the library answers anything of the site's world: the
|
|
89
|
+
* broad searches come first. Nothing here throws, and none of it is an AI
|
|
90
|
+
* model call: photos cost no AI credits.
|
|
91
|
+
*/ /** Wall clock the whole page's photos may take, inside the step's own signal. */ export const AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS = 30000;
|
|
92
|
+
/** Searches one page may send to the provider; a repeated query is answered from the page's own memory. */ export const AI_LAYOUT_STOCK_SEARCHES_PER_PAGE = 18;
|
|
93
|
+
/** The hits a pick is made among. */ export const AI_LAYOUT_STOCK_PICK_WINDOW = 40;
|
|
94
|
+
/** The accepted hits a slot keeps in hand, in case a copy fails. */ const AI_LAYOUT_STOCK_TRIES_PER_SLOT = 3;
|
|
95
|
+
/** Copies made side by side. */ const AI_LAYOUT_STOCK_COPIES_AT_ONCE = 4;
|
|
71
96
|
/** The largest photo copied: inside one direct upload. */ export const AI_LAYOUT_STOCK_MAX_BYTES = 3 * 1024 * 1024;
|
|
72
97
|
/**
|
|
73
98
|
* The unit job input naming the screen drafts the job's other pages were
|
|
74
99
|
* written to, whose photos a page does not place again (AGL-3660).
|
|
75
100
|
*/ export const AI_STOCK_PHOTO_PAGES_INPUT = 'stockPhotoPages';
|
|
76
|
-
|
|
101
|
+
const words = (list)=>new Set(list.trim().split(/\s+/));
|
|
102
|
+
/** Words that carry no subject. */ const STOPWORDS = words('a an the and or of in on at to for with from by into onto over under near our your their my his her its ' + 'this that these those is are be being was were who whom which where while as about than then very just ' + 'small local best new top quality premium family-owned family owned independent professional ' + 'business company services service page photo picture image showing shows show ' + 'someone people person one two three some many several');
|
|
77
103
|
/**
|
|
78
104
|
* Words that describe a picture's look, not what it shows, dropped from a
|
|
79
105
|
* subject. A word that is as often a noun ("light", "set") is not one.
|
|
80
|
-
*/ const FILLER =
|
|
81
|
-
/** The nouns that name a place of business, not its craft: "studio" in "ceramics studio". */ const VENUES =
|
|
106
|
+
*/ const FILLER = words('beautiful beautifully stunning gorgeous lovely pretty elegant modern contemporary rustic cozy cosy warm cool ' + 'soft bright natural simple minimal minimalist delicate unique special artisan artisanal handmade ' + 'hand-made hand-thrown handthrown hand-crafted handcrafted crafted hand-built handbuilt hand-poured ' + 'hand-painted hand-dyed hand-stitched poured finished styled perfect ' + 'fresh clean calm serene quiet peaceful vibrant colorful colourful sunlit sunny golden glossy matte textured ' + 'close-up closeup up view views detail details detailed shot shots overhead flat lay flatlay flat-lay ' + 'top-down wide angle featuring featured displayed arranged sitting resting placed standing lined ' + 'single pair collection assortment selection group few various array range large tiny big little ' + 'tall short wide narrow everyday classic signature original favorite favourite bestselling best-selling ' + 'small-batch batch limited edition custom bespoke ' + 'white black grey gray beige cream ivory brown tan blue green red pink yellow orange purple speckled ' + 'gentle airy inviting welcoming cheerful happy smiling friendly');
|
|
107
|
+
/** The nouns that name a place of business, not its craft: "studio" in "ceramics studio". */ const VENUES = words('studio studios shop shops store stores boutique company co workshop gallery atelier agency brand practice ' + 'salon house collective lab market services service firm clinic center centre parlor parlour bar ' + 'class classes lessons school academy');
|
|
108
|
+
/** The person who does the craft, not the craft: "artist" in "ceramic artist". */ const ROLES = words('artist artists maker makers artisan artisans owner owners founder founders creator creators');
|
|
109
|
+
/** 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');
|
|
110
|
+
/** Verbs a business type uses to say what it does; what follows them is its domain, never its business. */ const DOING = /(?<![\w-])(?:makes?|making|made|sells?|selling|sold|offers?|offering|creates?|creating|specializ(?:es|ing)|specialis(?:es|ing)|provides?|providing|featuring|showcasing)(?![\w-])/g;
|
|
111
|
+
/** Where a business type's own clause ends. */ const BUSINESS_BREAK = /[,;:.()]|\s[-–—]\s|\b(?:in|for|near|serving|based|located|that|which|who|with|from|since|and|&)\b/;
|
|
112
|
+
/** A clause after one of these says where or for whom, never what: "in Austin", "for busy parents". */ const PLACE_OR_AUDIENCE = /^(?:in|near|serving|based|located|across|around|throughout|for)$/;
|
|
113
|
+
/**
|
|
114
|
+
* Words naming a set or a wrapper of a thing, never the thing: the OBJECT
|
|
115
|
+
* of "nesting bowl set" is the bowl, of "candle gift set" the candle.
|
|
116
|
+
*/ const WRAPPERS = words('set sets kit kits gift gifts bundle bundles pack packs box boxes trio duo pair collection sampler assortment ' + 'series range line group lot bunch');
|
|
117
|
+
/**
|
|
118
|
+
* A hit tagged with any of these is never placed: a mockup's blank panel is
|
|
119
|
+
* an empty frame on the page (the Juniper Clay hero), a template or a
|
|
120
|
+
* graphic is not a photo of anything.
|
|
121
|
+
*/ const REJECTED_TAGS = words('mockup mock-up mock blank template templates clipart clip-art illustration vector logo icon ' + 'ai-generated generated render rendering 3d');
|
|
122
|
+
/**
|
|
123
|
+
* The kin of a few common crafts: the words a photo of that world is
|
|
124
|
+
* tagged with, and the broad searches that find it. A craft not listed
|
|
125
|
+
* still has its own words; this only widens what counts as its world.
|
|
126
|
+
*/ const CRAFT_KIN = [
|
|
127
|
+
{
|
|
128
|
+
match: [
|
|
129
|
+
'ceramic',
|
|
130
|
+
'ceramics',
|
|
131
|
+
'pottery',
|
|
132
|
+
'potter',
|
|
133
|
+
'stoneware',
|
|
134
|
+
'porcelain',
|
|
135
|
+
'earthenware',
|
|
136
|
+
'clay'
|
|
137
|
+
],
|
|
138
|
+
kin: [
|
|
139
|
+
'ceramic',
|
|
140
|
+
'ceramics',
|
|
141
|
+
'pottery',
|
|
142
|
+
'potter',
|
|
143
|
+
'stoneware',
|
|
144
|
+
'porcelain',
|
|
145
|
+
'earthenware',
|
|
146
|
+
'clay',
|
|
147
|
+
'glaze',
|
|
148
|
+
'kiln'
|
|
149
|
+
],
|
|
150
|
+
broad: [
|
|
151
|
+
'handmade ceramics',
|
|
152
|
+
'pottery'
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
match: [
|
|
157
|
+
'candle',
|
|
158
|
+
'candlemaker',
|
|
159
|
+
'candlemaking'
|
|
160
|
+
],
|
|
161
|
+
kin: [
|
|
162
|
+
'candle',
|
|
163
|
+
'candlelight',
|
|
164
|
+
'wax',
|
|
165
|
+
'soy',
|
|
166
|
+
'wick',
|
|
167
|
+
'scented',
|
|
168
|
+
'votive',
|
|
169
|
+
'candlestick'
|
|
170
|
+
],
|
|
171
|
+
broad: [
|
|
172
|
+
'handmade candles',
|
|
173
|
+
'scented candles'
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
match: [
|
|
178
|
+
'jewelry',
|
|
179
|
+
'jewellery',
|
|
180
|
+
'jeweler',
|
|
181
|
+
'jeweller',
|
|
182
|
+
'silversmith',
|
|
183
|
+
'goldsmith'
|
|
184
|
+
],
|
|
185
|
+
kin: [
|
|
186
|
+
'jewelry',
|
|
187
|
+
'jewellery',
|
|
188
|
+
'ring',
|
|
189
|
+
'necklace',
|
|
190
|
+
'earring',
|
|
191
|
+
'bracelet',
|
|
192
|
+
'silver',
|
|
193
|
+
'gold',
|
|
194
|
+
'gemstone'
|
|
195
|
+
],
|
|
196
|
+
broad: [
|
|
197
|
+
'handmade jewelry',
|
|
198
|
+
'jewelry'
|
|
199
|
+
]
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
match: [
|
|
203
|
+
'soap',
|
|
204
|
+
'soapmaker',
|
|
205
|
+
'skincare'
|
|
206
|
+
],
|
|
207
|
+
kin: [
|
|
208
|
+
'soap',
|
|
209
|
+
'skincare',
|
|
210
|
+
'lotion',
|
|
211
|
+
'bath',
|
|
212
|
+
'spa',
|
|
213
|
+
'natural',
|
|
214
|
+
'cosmetic'
|
|
215
|
+
],
|
|
216
|
+
broad: [
|
|
217
|
+
'handmade soap',
|
|
218
|
+
'natural skincare'
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
match: [
|
|
223
|
+
'wood',
|
|
224
|
+
'woodwork',
|
|
225
|
+
'woodworking',
|
|
226
|
+
'woodworker',
|
|
227
|
+
'carpenter',
|
|
228
|
+
'carpentry',
|
|
229
|
+
'furniture',
|
|
230
|
+
'joinery'
|
|
231
|
+
],
|
|
232
|
+
kin: [
|
|
233
|
+
'wood',
|
|
234
|
+
'wooden',
|
|
235
|
+
'woodwork',
|
|
236
|
+
'timber',
|
|
237
|
+
'furniture',
|
|
238
|
+
'carpentry',
|
|
239
|
+
'oak',
|
|
240
|
+
'walnut'
|
|
241
|
+
],
|
|
242
|
+
broad: [
|
|
243
|
+
'woodworking',
|
|
244
|
+
'wooden furniture'
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
match: [
|
|
249
|
+
'bakery',
|
|
250
|
+
'baker',
|
|
251
|
+
'bread',
|
|
252
|
+
'pastry',
|
|
253
|
+
'cake',
|
|
254
|
+
'patisserie'
|
|
255
|
+
],
|
|
256
|
+
kin: [
|
|
257
|
+
'bakery',
|
|
258
|
+
'bread',
|
|
259
|
+
'pastry',
|
|
260
|
+
'cake',
|
|
261
|
+
'baking',
|
|
262
|
+
'baked',
|
|
263
|
+
'dough',
|
|
264
|
+
'croissant'
|
|
265
|
+
],
|
|
266
|
+
broad: [
|
|
267
|
+
'bakery',
|
|
268
|
+
'fresh bread'
|
|
269
|
+
]
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
match: [
|
|
273
|
+
'coffee',
|
|
274
|
+
'cafe',
|
|
275
|
+
'café',
|
|
276
|
+
'roaster',
|
|
277
|
+
'espresso'
|
|
278
|
+
],
|
|
279
|
+
kin: [
|
|
280
|
+
'coffee',
|
|
281
|
+
'cafe',
|
|
282
|
+
'espresso',
|
|
283
|
+
'latte',
|
|
284
|
+
'cappuccino',
|
|
285
|
+
'barista',
|
|
286
|
+
'beans'
|
|
287
|
+
],
|
|
288
|
+
broad: [
|
|
289
|
+
'coffee shop',
|
|
290
|
+
'coffee'
|
|
291
|
+
]
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
match: [
|
|
295
|
+
'florist',
|
|
296
|
+
'flower',
|
|
297
|
+
'floral'
|
|
298
|
+
],
|
|
299
|
+
kin: [
|
|
300
|
+
'flower',
|
|
301
|
+
'floral',
|
|
302
|
+
'bouquet',
|
|
303
|
+
'florist',
|
|
304
|
+
'bloom',
|
|
305
|
+
'blossom'
|
|
306
|
+
],
|
|
307
|
+
broad: [
|
|
308
|
+
'florist',
|
|
309
|
+
'flower bouquet'
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
match: [
|
|
314
|
+
'knit',
|
|
315
|
+
'knitting',
|
|
316
|
+
'yarn',
|
|
317
|
+
'weaving',
|
|
318
|
+
'weaver',
|
|
319
|
+
'textile',
|
|
320
|
+
'textiles',
|
|
321
|
+
'quilt',
|
|
322
|
+
'quilting',
|
|
323
|
+
'sewing'
|
|
324
|
+
],
|
|
325
|
+
kin: [
|
|
326
|
+
'knit',
|
|
327
|
+
'knitting',
|
|
328
|
+
'yarn',
|
|
329
|
+
'wool',
|
|
330
|
+
'weaving',
|
|
331
|
+
'textile',
|
|
332
|
+
'fabric',
|
|
333
|
+
'quilt',
|
|
334
|
+
'sewing'
|
|
335
|
+
],
|
|
336
|
+
broad: [
|
|
337
|
+
'handmade textiles',
|
|
338
|
+
'yarn'
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
match: [
|
|
343
|
+
'leather',
|
|
344
|
+
'leatherwork',
|
|
345
|
+
'leathercraft'
|
|
346
|
+
],
|
|
347
|
+
kin: [
|
|
348
|
+
'leather',
|
|
349
|
+
'wallet',
|
|
350
|
+
'bag',
|
|
351
|
+
'belt',
|
|
352
|
+
'stitching'
|
|
353
|
+
],
|
|
354
|
+
broad: [
|
|
355
|
+
'leather craft',
|
|
356
|
+
'leather goods'
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
];
|
|
82
360
|
/** Lowercase words, letters only, without stopwords. */ function contentWords(text) {
|
|
83
361
|
return text.toLowerCase().replace(/https?:\/\/\S+|\S+@\S+|\+?\d[\d\s().-]{5,}\d/g, ' ').replace(/[^a-zÀ-ɏ\s-]/g, ' ').split(/\s+/).map((word)=>word.replace(/^-+|-+$/g, '')).filter((word)=>word.length > 1 && !STOPWORDS.has(word));
|
|
84
362
|
}
|
|
@@ -93,35 +371,102 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
93
371
|
if (lower.length > 3 && lower.endsWith('s') && !/(?:ss|us|is)$/.test(lower)) return lower.slice(0, -1);
|
|
94
372
|
return lower;
|
|
95
373
|
}
|
|
96
|
-
/** A text's words as stems; a hyphenated word counts as its parts. */ function
|
|
97
|
-
|
|
98
|
-
for (const word of text.toLowerCase().split(/[^a-zÀ-ɏ]+/)){
|
|
99
|
-
if (word.length > 1) out.add(aiStockStem(word));
|
|
100
|
-
}
|
|
101
|
-
return out;
|
|
374
|
+
/** A text's words as stems, in order; a hyphenated word counts as its parts. */ function stemList(text) {
|
|
375
|
+
return text.toLowerCase().split(/[^a-zÀ-ɏ]+/).filter((word)=>word.length > 1).map(aiStockStem);
|
|
102
376
|
}
|
|
103
377
|
/** Where a noun phrase ends: punctuation, a preposition, a conjunction. */ const CLAUSE_BREAK = /[,;:.()]|\s[-–—]\s|\b(?:with|on|in|at|against|beside|next|near|under|over|by|from|for|during|while|atop|inside|across|beneath|through|into|onto|amid|among|and|of|that|which|who)\b|&/;
|
|
378
|
+
/** A business type's clauses, each with the word that opened it. */ function businessClauses(businessType) {
|
|
379
|
+
const text = businessType.toLowerCase().replace(DOING, ' | ');
|
|
380
|
+
const clauses = [];
|
|
381
|
+
const breaker = new RegExp(`${BUSINESS_BREAK.source}|\\|`, 'g');
|
|
382
|
+
let at = 0;
|
|
383
|
+
let opener = '';
|
|
384
|
+
for (const match of text.matchAll(breaker)){
|
|
385
|
+
var _match_index;
|
|
386
|
+
clauses.push({
|
|
387
|
+
opener,
|
|
388
|
+
words: contentWords(text.slice(at, match.index))
|
|
389
|
+
});
|
|
390
|
+
opener = match[0].trim();
|
|
391
|
+
at = ((_match_index = match.index) != null ? _match_index : 0) + match[0].length;
|
|
392
|
+
}
|
|
393
|
+
clauses.push({
|
|
394
|
+
opener,
|
|
395
|
+
words: contentWords(text.slice(at))
|
|
396
|
+
});
|
|
397
|
+
return clauses;
|
|
398
|
+
}
|
|
399
|
+
/** A clause's words that name the business: no site kind, no filler. */ const businessOf = (clause)=>clause.filter((word)=>!SITE_KINDS.has(word) && !FILLER.has(word));
|
|
104
400
|
/**
|
|
105
401
|
* The kind of business in a few words: the first clause of the business
|
|
106
|
-
* type
|
|
107
|
-
*
|
|
402
|
+
* type that says more than what kind of site it is, before a place, an
|
|
403
|
+
* audience or what it makes, filler dropped, its last three words (the head
|
|
404
|
+
* of an English noun phrase is at its end). "ceramic artist" from "a
|
|
405
|
+
* portfolio for a ceramic artist who makes stoneware bowls"; "candle shop"
|
|
406
|
+
* from "a small-batch candle shop selling hand-poured soy candles online".
|
|
108
407
|
*/ export function aiStockBusinessWords(businessType) {
|
|
109
|
-
const clause
|
|
110
|
-
|
|
111
|
-
|
|
408
|
+
for (const { words: clause } of businessClauses(businessType)){
|
|
409
|
+
const named = businessOf(clause);
|
|
410
|
+
if (named.length) return named.slice(-3).join(' ');
|
|
411
|
+
}
|
|
412
|
+
return '';
|
|
112
413
|
}
|
|
113
414
|
/**
|
|
114
415
|
* The business's craft in one word: the last of its words that is not the
|
|
115
|
-
* place it is done in, singular — "ceramic" from
|
|
116
|
-
* studio", "yoga" from "yoga studio". Empty
|
|
117
|
-
* place ("a shop").
|
|
416
|
+
* place it is done in nor the person doing it, singular — "ceramic" from
|
|
417
|
+
* "ceramics studio" or "ceramic artist", "yoga" from "yoga studio". Empty
|
|
418
|
+
* when the business is only a place ("a shop").
|
|
118
419
|
*/ export function aiStockCraftWords(business) {
|
|
119
|
-
const
|
|
120
|
-
const craft =
|
|
420
|
+
const named = business.split(/\s+/).filter((word)=>word && !VENUES.has(word) && !ROLES.has(word) && !FILLER.has(word) && !SITE_KINDS.has(word));
|
|
421
|
+
const craft = named[named.length - 1];
|
|
121
422
|
return craft ? aiStockStem(craft) : '';
|
|
122
423
|
}
|
|
123
|
-
/**
|
|
124
|
-
|
|
424
|
+
/** The site's terms, read from its business type. */ export function aiStockSiteTerms(businessType) {
|
|
425
|
+
const business = aiStockBusinessWords(businessType);
|
|
426
|
+
const craft = aiStockCraftWords(business);
|
|
427
|
+
const domain = new Set();
|
|
428
|
+
/** The things it makes, by their head nouns. */ const made = new Set();
|
|
429
|
+
let named = false;
|
|
430
|
+
let making = false;
|
|
431
|
+
for (const { opener, words: clause } of businessClauses(businessType)){
|
|
432
|
+
if (opener === '|') making = true;
|
|
433
|
+
// Where it is, and who it is for, are not its world — once something has named it.
|
|
434
|
+
if (named && PLACE_OR_AUDIENCE.test(opener)) {
|
|
435
|
+
making = false;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const kept = businessOf(clause).filter((word)=>!VENUES.has(word) && !ROLES.has(word));
|
|
439
|
+
if (kept.length) named = true;
|
|
440
|
+
// What it makes names its world by what the things are made of, never
|
|
441
|
+
// the things: "stoneware" of "stoneware bowls", "soy" of "soy candles".
|
|
442
|
+
// A bowl is not a ceramic world; a robin's food bowl is not pottery.
|
|
443
|
+
const world = making ? kept.slice(0, -1) : kept;
|
|
444
|
+
for (const word of world)for (const stem of stemList(word))domain.add(stem);
|
|
445
|
+
if (making) for (const word of kept.slice(-1))for (const stem of stemList(word))made.add(stem);
|
|
446
|
+
}
|
|
447
|
+
if (craft) domain.add(craft);
|
|
448
|
+
// A business named only by what it sells ("a shop selling pottery") is of that world.
|
|
449
|
+
if (!domain.size) for (const stem of made)domain.add(stem);
|
|
450
|
+
const broad = [];
|
|
451
|
+
for (const group of CRAFT_KIN){
|
|
452
|
+
if (!group.match.some((word)=>domain.has(aiStockStem(word)) || made.has(aiStockStem(word)))) continue;
|
|
453
|
+
for (const word of group.kin)domain.add(aiStockStem(word));
|
|
454
|
+
broad.push(...group.broad);
|
|
455
|
+
}
|
|
456
|
+
if (!broad.length) broad.push(business, craft);
|
|
457
|
+
return {
|
|
458
|
+
business,
|
|
459
|
+
craft,
|
|
460
|
+
domain: [
|
|
461
|
+
...domain
|
|
462
|
+
].filter((stem)=>stem.length > 1),
|
|
463
|
+
broad: [
|
|
464
|
+
...new Set(broad.map((query)=>query.trim()).filter(Boolean))
|
|
465
|
+
]
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/** A phrase's content words without its filler; all filler, and nothing is left. */ function subjectOf(list) {
|
|
469
|
+
return list.filter((word)=>!FILLER.has(word));
|
|
125
470
|
}
|
|
126
471
|
/**
|
|
127
472
|
* A picture's subject in a few words (AGL-3660): the noun phrase its alt
|
|
@@ -130,19 +475,80 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
130
475
|
* head noun is the last) — else its section's name.
|
|
131
476
|
*/ export function aiStockSubjectWords(alt, sectionName) {
|
|
132
477
|
for (const clause of alt.toLowerCase().split(CLAUSE_BREAK)){
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
478
|
+
const found = subjectOf(contentWords(clause != null ? clause : ''));
|
|
479
|
+
if (found.length) return found.slice(-3).join(' ');
|
|
135
480
|
}
|
|
136
481
|
return subjectOf(contentWords(sectionName)).slice(0, 2).join(' ');
|
|
137
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* The thing a subject shows, one singular word: its last word that is not
|
|
485
|
+
* a set or a wrapper of it — "bowl" from "nesting bowl set", "candle" from
|
|
486
|
+
* "candle gift set", "vase" from "bud vase". Its last word when every word
|
|
487
|
+
* is one ("gift set").
|
|
488
|
+
*/ export function aiStockObjectWord(subject) {
|
|
489
|
+
var _ref, _reverse_find;
|
|
490
|
+
const list = subject.split(/\s+/).filter(Boolean);
|
|
491
|
+
const object = (_ref = (_reverse_find = [
|
|
492
|
+
...list
|
|
493
|
+
].reverse().find((word)=>!WRAPPERS.has(word))) != null ? _reverse_find : list[list.length - 1]) != null ? _ref : '';
|
|
494
|
+
return object ? aiStockStem(object) : '';
|
|
495
|
+
}
|
|
138
496
|
const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUERY_MAX_CHARS).trim();
|
|
139
497
|
/** The frame's orientation: a wide frame wants a landscape photo, a tall one a portrait. */ export function aiStockOrientation(aspect) {
|
|
140
498
|
if (aspect > 1.15) return 'horizontal';
|
|
141
499
|
if (aspect < 0.87) return 'vertical';
|
|
142
500
|
return 'any';
|
|
143
501
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
502
|
+
/**
|
|
503
|
+
* A product's searches (AGL-3676), most specific first: each of its subjects
|
|
504
|
+
* — its name's noun phrase, then its photo's — with the shop's category
|
|
505
|
+
* where the phrase does not already say it ("candle gift box"), then the
|
|
506
|
+
* category alone, a plain photo of what the shop sells. Every one must find a
|
|
507
|
+
* hit naming the category, so no product is ever filled with a lifestyle shot
|
|
508
|
+
* of something else: the beta.237 Willow Wick start put a laptop and roses
|
|
509
|
+
* under "Candle Wick Trimmer" and an antique tea set under "Candle Gift Set".
|
|
510
|
+
*/ export function aiStockProductSearches(slot, craft) {
|
|
511
|
+
var _ref;
|
|
512
|
+
var _slot_product;
|
|
513
|
+
const orientation = aiStockOrientation(slot.aspect);
|
|
514
|
+
const size = orientation === 'vertical' ? {
|
|
515
|
+
minHeight: 900
|
|
516
|
+
} : {
|
|
517
|
+
minWidth: 900
|
|
518
|
+
};
|
|
519
|
+
const craftStems = stemList(craft);
|
|
520
|
+
const queries = [];
|
|
521
|
+
for (const raw of (_ref = (_slot_product = slot.product) == null ? void 0 : _slot_product.subjects) != null ? _ref : []){
|
|
522
|
+
const subject = aiStockSubjectWords(raw, '');
|
|
523
|
+
if (!subject) continue;
|
|
524
|
+
const says = craftStems.every((stem)=>stemList(subject).includes(stem));
|
|
525
|
+
queries.push({
|
|
526
|
+
query: craft && !says ? `${craft} ${subject}` : subject,
|
|
527
|
+
subject
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
if (craft) queries.push({
|
|
531
|
+
query: craft,
|
|
532
|
+
subject: craft
|
|
533
|
+
});
|
|
534
|
+
const seen = new Set();
|
|
535
|
+
const searches = [];
|
|
536
|
+
for (const entry of queries){
|
|
537
|
+
const query = clip(entry.query);
|
|
538
|
+
if (!query || seen.has(query)) continue;
|
|
539
|
+
seen.add(query);
|
|
540
|
+
searches.push(_extends({
|
|
541
|
+
query,
|
|
542
|
+
orientation
|
|
543
|
+
}, size, {
|
|
544
|
+
subject: entry.subject
|
|
545
|
+
}, craft ? {
|
|
546
|
+
requires: craft
|
|
547
|
+
} : {}));
|
|
548
|
+
}
|
|
549
|
+
return searches;
|
|
550
|
+
}
|
|
551
|
+
/** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, terms, subject) {
|
|
146
552
|
const orientation = aiStockOrientation(slot.aspect);
|
|
147
553
|
const size = slot.role === 'hero' ? {
|
|
148
554
|
minWidth: 1600
|
|
@@ -151,107 +557,143 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
151
557
|
} : {
|
|
152
558
|
minWidth: 900
|
|
153
559
|
};
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
560
|
+
const domain = new Set(terms.domain);
|
|
561
|
+
const object = aiStockObjectWord(subject);
|
|
562
|
+
const subjectStems = stemList(subject);
|
|
563
|
+
// The craft qualifies a subject that does not already name the site's world.
|
|
564
|
+
const namesWorld = subjectStems.some((stem)=>domain.has(stem));
|
|
565
|
+
const qualify = (phrase)=>terms.craft && !namesWorld && !stemList(phrase).includes(terms.craft) ? `${terms.craft} ${phrase}` : phrase;
|
|
566
|
+
const broad = terms.broad.map((query)=>({
|
|
567
|
+
query,
|
|
568
|
+
broad: true
|
|
569
|
+
}));
|
|
161
570
|
const queries = slot.role === 'hero' ? [
|
|
162
571
|
{
|
|
163
|
-
query:
|
|
572
|
+
query: terms.business,
|
|
573
|
+
broad: true
|
|
164
574
|
},
|
|
165
|
-
|
|
166
|
-
query: `${words.business} ${words.subject}`
|
|
167
|
-
}
|
|
575
|
+
...broad
|
|
168
576
|
] : slot.role === 'about' ? [
|
|
169
577
|
{
|
|
170
|
-
query: `${
|
|
578
|
+
query: `${terms.business} ${subject}`,
|
|
579
|
+
broad: true,
|
|
580
|
+
people: true
|
|
171
581
|
},
|
|
172
582
|
{
|
|
173
|
-
query:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
{
|
|
177
|
-
query: qualified
|
|
583
|
+
query: terms.business,
|
|
584
|
+
broad: true,
|
|
585
|
+
people: true
|
|
178
586
|
},
|
|
587
|
+
...broad
|
|
588
|
+
] : subject ? [
|
|
179
589
|
{
|
|
180
|
-
query:
|
|
590
|
+
query: qualify(subject)
|
|
181
591
|
},
|
|
182
592
|
{
|
|
183
|
-
query:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
] :
|
|
187
|
-
{
|
|
188
|
-
query: place,
|
|
189
|
-
neutral: true
|
|
190
|
-
}
|
|
191
|
-
];
|
|
593
|
+
query: qualify(object)
|
|
594
|
+
},
|
|
595
|
+
...broad
|
|
596
|
+
] : broad;
|
|
192
597
|
const seen = new Set();
|
|
193
598
|
const searches = [];
|
|
194
599
|
for (const raw of queries){
|
|
195
600
|
const query = clip(raw.query);
|
|
196
|
-
|
|
197
|
-
seen.
|
|
601
|
+
const key = `${query}|${raw.people === true}`;
|
|
602
|
+
if (!query || seen.has(key)) continue;
|
|
603
|
+
seen.add(key);
|
|
198
604
|
searches.push(_extends({
|
|
199
605
|
query,
|
|
200
606
|
orientation
|
|
201
|
-
}, size,
|
|
607
|
+
}, size, raw.people ? {
|
|
202
608
|
people: true
|
|
203
|
-
} : {}, raw.
|
|
204
|
-
|
|
609
|
+
} : {}, raw.broad ? {
|
|
610
|
+
broad: true
|
|
205
611
|
} : {}));
|
|
206
612
|
}
|
|
207
613
|
return searches;
|
|
208
614
|
}
|
|
209
|
-
/**
|
|
210
|
-
* How well a hit names a subject (AGL-3660): each subject word its tags, its
|
|
211
|
-
* description or its page's address names counts once, the head noun (the
|
|
212
|
-
* subject's last word) twice, and a craft word a quarter, as a tiebreak.
|
|
213
|
-
* Zero when it names none of the subject.
|
|
214
|
-
*/ export function aiStockRelevance(photo, terms) {
|
|
215
|
-
var _photo_tags, _photo_alt, _terms_craft;
|
|
216
|
-
const subject = terms.subject.split(/\s+/).filter(Boolean).map(aiStockStem);
|
|
217
|
-
if (!subject.length) return 0;
|
|
218
|
-
let slug;
|
|
615
|
+
/** The words of a photo's page address at its library, its id dropped: "robin-bird-songbird-garden-winter". */ function pageSlug(pageUrl) {
|
|
219
616
|
try {
|
|
220
|
-
|
|
617
|
+
var _ref;
|
|
618
|
+
const last = decodeURIComponent(new URL(pageUrl).pathname).split('/').filter(Boolean).pop();
|
|
619
|
+
return (_ref = last == null ? void 0 : last.replace(/-?\d+$/, '')) != null ? _ref : '';
|
|
221
620
|
} catch (unused) {
|
|
222
|
-
|
|
621
|
+
return '';
|
|
223
622
|
}
|
|
224
|
-
|
|
225
|
-
|
|
623
|
+
}
|
|
624
|
+
/** A hit's words: the first few (what it is OF), every one, and each tag's phrase. */ function hitWords(photo) {
|
|
625
|
+
var _photo_tags, _photo_alt, _photo_alt1;
|
|
626
|
+
const slug = pageSlug(photo.pageUrl);
|
|
627
|
+
const tags = ((_photo_tags = photo.tags) != null ? _photo_tags : []).map((tag)=>tag.toLowerCase().trim()).filter(Boolean);
|
|
628
|
+
const lead = new Set(stemList([
|
|
629
|
+
...tags.slice(0, 3),
|
|
630
|
+
slug
|
|
631
|
+
].join(' ')));
|
|
632
|
+
const all = new Set(stemList([
|
|
633
|
+
...tags,
|
|
226
634
|
(_photo_alt = photo.alt) != null ? _photo_alt : '',
|
|
227
635
|
slug
|
|
228
|
-
].join(' '));
|
|
229
|
-
const
|
|
636
|
+
].join(' ')));
|
|
637
|
+
const phrases = [
|
|
638
|
+
...tags.map((tag)=>stemList(tag).join(' ')),
|
|
639
|
+
stemList((_photo_alt1 = photo.alt) != null ? _photo_alt1 : '').join(' '),
|
|
640
|
+
stemList(slug).join(' ')
|
|
641
|
+
];
|
|
642
|
+
const rejected = tags.some((tag)=>REJECTED_TAGS.has(tag) || tag.split(/\s+/).some((word)=>REJECTED_TAGS.has(word)));
|
|
643
|
+
return {
|
|
644
|
+
lead,
|
|
645
|
+
all,
|
|
646
|
+
phrases,
|
|
647
|
+
rejected
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* How well a hit shows what a slot needs (AGL-3660), or 0 when it must not
|
|
652
|
+
* fill it: a mockup or a blank never; a picture of a thing only when the hit
|
|
653
|
+
* names its object AND either the site's world or the subject's own
|
|
654
|
+
* two-word phrase ("bud vase", "wick trimmer"); anything else only when it
|
|
655
|
+
* names the site's world (when the site names one). Among the accepted, the
|
|
656
|
+
* object among the hit's first tags counts 4 (a mention among its last, 2),
|
|
657
|
+
* the subject's two-word phrase 3, each of its other words 1, and the
|
|
658
|
+
* site's world 1, plus a quarter when it is among the first tags.
|
|
659
|
+
*/ export function aiStockRelevance(photo, judgement) {
|
|
660
|
+
var _judgement_requires, _judgement_subject, _judgement_domain;
|
|
661
|
+
const hit = hitWords(photo);
|
|
662
|
+
if (hit.rejected) return 0;
|
|
663
|
+
const required = stemList((_judgement_requires = judgement.requires) != null ? _judgement_requires : '');
|
|
664
|
+
if (required.some((stem)=>!hit.all.has(stem))) return 0;
|
|
665
|
+
const subject = stemList((_judgement_subject = judgement.subject) != null ? _judgement_subject : '');
|
|
666
|
+
const object = judgement.subject ? aiStockObjectWord(judgement.subject) : '';
|
|
667
|
+
const domain = ((_judgement_domain = judgement.domain) != null ? _judgement_domain : []).filter((stem)=>stem !== object);
|
|
668
|
+
// A hit naming the word it must name is of the site's world: a product's
|
|
669
|
+
// category is that world even where it is the product's own object.
|
|
670
|
+
const world = required.length > 0 || domain.some((stem)=>hit.all.has(stem));
|
|
671
|
+
const worldLead = required.length > 0 && required.every((stem)=>hit.lead.has(stem)) || domain.some((stem)=>hit.lead.has(stem));
|
|
672
|
+
const pair = subject.length >= 2 ? subject.slice(-2).join(' ') : '';
|
|
673
|
+
const phrased = Boolean(pair) && hit.phrases.some((phrase)=>` ${phrase} `.includes(` ${pair} `));
|
|
230
674
|
let score = 0;
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (
|
|
675
|
+
if (judgement.strict && object) {
|
|
676
|
+
if (!hit.all.has(object)) return 0;
|
|
677
|
+
if (domain.length && !world && !phrased) return 0;
|
|
678
|
+
score += hit.lead.has(object) ? 4 : 2;
|
|
679
|
+
} else if (domain.length) {
|
|
680
|
+
if (!world) return 0;
|
|
237
681
|
}
|
|
238
|
-
|
|
682
|
+
if (phrased) score += 3;
|
|
683
|
+
for (const word of new Set(subject))if (word !== object && hit.all.has(word)) score += 1;
|
|
684
|
+
if (world) score += worldLead ? 1.25 : 1;
|
|
685
|
+
return Math.max(score, 0.5);
|
|
239
686
|
}
|
|
240
687
|
/**
|
|
241
|
-
* A search's hits in the order they are tried (AGL-3660): those
|
|
242
|
-
* yet, among the first few
|
|
243
|
-
*
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const subject = (_options_subject = options.subject) != null ? _options_subject : '';
|
|
688
|
+
* A search's acceptable hits in the order they are tried (AGL-3660): those
|
|
689
|
+
* not placed yet, among the first few the library returned, those it must
|
|
690
|
+
* not take dropped ({@link aiStockRelevance}), best first; the seed turns
|
|
691
|
+
* the hits that score alike, so two jobs differ and one job repeats itself.
|
|
692
|
+
*/ export function aiStockRank(photos, used, seed, judgement = {}) {
|
|
247
693
|
const scored = photos.filter((photo)=>!used.has(stockPhotoSourceKey(photo))).slice(0, AI_LAYOUT_STOCK_PICK_WINDOW).map((photo)=>({
|
|
248
694
|
photo,
|
|
249
|
-
score:
|
|
250
|
-
|
|
251
|
-
}, options.craft ? {
|
|
252
|
-
craft: options.craft
|
|
253
|
-
} : {})) : 0
|
|
254
|
-
})).filter((entry)=>!options.strict || !subject || entry.score > 0);
|
|
695
|
+
score: aiStockRelevance(photo, judgement)
|
|
696
|
+
})).filter((entry)=>entry.score > 0);
|
|
255
697
|
if (!scored.length) return [];
|
|
256
698
|
const turn = aiLayoutSeedNumber(seed);
|
|
257
699
|
const ranked = [];
|
|
@@ -267,9 +709,9 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
267
709
|
}
|
|
268
710
|
return ranked;
|
|
269
711
|
}
|
|
270
|
-
/** The pick among a search's hits: the first {@link aiStockRank} would try, or `null`. */ export function aiStockPick(photos, used, seed,
|
|
712
|
+
/** The pick among a search's hits: the first {@link aiStockRank} would try, or `null`. */ export function aiStockPick(photos, used, seed, judgement = {}) {
|
|
271
713
|
var _aiStockRank_;
|
|
272
|
-
return (_aiStockRank_ = aiStockRank(photos, used, seed,
|
|
714
|
+
return (_aiStockRank_ = aiStockRank(photos, used, seed, judgement)[0]) != null ? _aiStockRank_ : null;
|
|
273
715
|
}
|
|
274
716
|
/**
|
|
275
717
|
* The library photos a stored page already shows: every `image` whose
|
|
@@ -323,8 +765,7 @@ const EXTENSIONS = {
|
|
|
323
765
|
const provider = ((_deps_provider = deps.provider) != null ? _deps_provider : stockPhotoProvider)();
|
|
324
766
|
const ingest = ((_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest)();
|
|
325
767
|
if (!provider || !ingest || !input.hostId || !input.uid) return null;
|
|
326
|
-
const
|
|
327
|
-
const craft = aiStockCraftWords(business);
|
|
768
|
+
const terms = aiStockSiteTerms(input.business);
|
|
328
769
|
return async (slots)=>{
|
|
329
770
|
var _deps_budgetMs, _input_avoid;
|
|
330
771
|
const timeout = AbortSignal.timeout((_deps_budgetMs = deps.budgetMs) != null ? _deps_budgetMs : AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS);
|
|
@@ -336,7 +777,7 @@ const EXTENSIONS = {
|
|
|
336
777
|
const job = jobPhotos(input.jobId ? `${input.hostId}:${input.jobId}` : null);
|
|
337
778
|
const avoid = new Set((_input_avoid = input.avoid) != null ? _input_avoid : []);
|
|
338
779
|
/** Placed elsewhere in the job: by another page in this process, or one the caller read. */ const elsewhere = (key)=>avoid.has(key) || (job == null ? void 0 : job.has(key)) === true && job.get(key) !== scope;
|
|
339
|
-
/** Source keys this page has
|
|
780
|
+
/** Source keys this page has claimed for a slot, or turned down. */ const claimed = new Set();
|
|
340
781
|
/** Asset srcs this page has placed. */ const placedSrcs = new Set();
|
|
341
782
|
const answers = new Map();
|
|
342
783
|
let searches = 0;
|
|
@@ -346,9 +787,12 @@ const EXTENSIONS = {
|
|
|
346
787
|
job == null ? void 0 : job.set(sourceKey, scope);
|
|
347
788
|
job == null ? void 0 : job.set(src, scope);
|
|
348
789
|
};
|
|
349
|
-
/** A search's hits; the same words twice are answered from memory, and `null` once the page's searches are spent. */ const search =
|
|
790
|
+
/** A search's hits; the same words twice are answered from memory, and `null` once the page's searches are spent. */ const search = (wanted)=>{
|
|
791
|
+
var _input_searches;
|
|
350
792
|
const request = _extends({}, wanted);
|
|
351
|
-
delete request.
|
|
793
|
+
delete request.broad;
|
|
794
|
+
delete request.subject;
|
|
795
|
+
delete request.requires;
|
|
352
796
|
const key = JSON.stringify([
|
|
353
797
|
request.query,
|
|
354
798
|
request.orientation,
|
|
@@ -356,24 +800,19 @@ const EXTENSIONS = {
|
|
|
356
800
|
]);
|
|
357
801
|
const known = answers.get(key);
|
|
358
802
|
if (known) return known;
|
|
359
|
-
if (searches >= AI_LAYOUT_STOCK_SEARCHES_PER_PAGE) return null;
|
|
803
|
+
if (searches >= ((_input_searches = input.searches) != null ? _input_searches : AI_LAYOUT_STOCK_SEARCHES_PER_PAGE)) return Promise.resolve(null);
|
|
360
804
|
searches += 1;
|
|
361
|
-
|
|
362
|
-
|
|
805
|
+
const asked = provider.search(request, {
|
|
806
|
+
signal
|
|
807
|
+
}).then((found)=>{
|
|
363
808
|
var _ref;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
} catch (unused) {
|
|
369
|
-
found = [];
|
|
370
|
-
}
|
|
371
|
-
answers.set(key, found);
|
|
372
|
-
return found;
|
|
809
|
+
return (_ref = found == null ? void 0 : found.photos) != null ? _ref : [];
|
|
810
|
+
}).catch(()=>[]);
|
|
811
|
+
answers.set(key, asked);
|
|
812
|
+
return asked;
|
|
373
813
|
};
|
|
374
814
|
/** 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)=>{
|
|
375
815
|
const sourceKey = stockPhotoSourceKey(photo);
|
|
376
|
-
used.add(sourceKey);
|
|
377
816
|
try {
|
|
378
817
|
var _EXTENSIONS_bytes_contentType;
|
|
379
818
|
const kept = await ingest.findStockPhoto({
|
|
@@ -393,12 +832,12 @@ const EXTENSIONS = {
|
|
|
393
832
|
}
|
|
394
833
|
// A refusal to store (the storage band, a lockdown) holds for the
|
|
395
834
|
// whole page, so the rest of it reuses what it can and copies nothing.
|
|
396
|
-
if (!storing) return null;
|
|
835
|
+
if (!storing || signal.aborted) return null;
|
|
397
836
|
const bytes = await provider.download(photo, {
|
|
398
837
|
maxBytes: AI_LAYOUT_STOCK_MAX_BYTES,
|
|
399
838
|
signal
|
|
400
839
|
});
|
|
401
|
-
if (!bytes || signal.aborted) return null;
|
|
840
|
+
if (!bytes || signal.aborted || !storing) return null;
|
|
402
841
|
const credit = provider.credit(photo);
|
|
403
842
|
const stored = await ingest.ingest({
|
|
404
843
|
hostId: input.hostId,
|
|
@@ -446,43 +885,77 @@ const EXTENSIONS = {
|
|
|
446
885
|
}
|
|
447
886
|
return null;
|
|
448
887
|
};
|
|
449
|
-
|
|
888
|
+
// 1. Choose, slot by slot, so the page's choices never collide: each
|
|
889
|
+
// slot claims its best accepted hit before the next slot looks.
|
|
890
|
+
const choices = [];
|
|
450
891
|
for (const [index, slot] of slots.entries()){
|
|
451
892
|
var _input_sectionNames_slot_sectionIndex;
|
|
452
893
|
if (signal.aborted) {
|
|
453
|
-
|
|
894
|
+
choices.push(null);
|
|
454
895
|
continue;
|
|
455
896
|
}
|
|
456
897
|
const subject = aiStockSubjectWords(slot.alt, (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '');
|
|
457
|
-
let
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
})){
|
|
463
|
-
if (placed || signal.aborted) break;
|
|
898
|
+
let choice = null;
|
|
899
|
+
const requests = slot.product ? aiStockProductSearches(slot, terms.craft) : aiStockSearchesFor(slot, terms, subject);
|
|
900
|
+
for (const request of requests){
|
|
901
|
+
var _request_subject;
|
|
902
|
+
if (choice || signal.aborted) break;
|
|
464
903
|
const found = await search(request);
|
|
465
904
|
if (found === null) break;
|
|
466
|
-
|
|
467
|
-
// a hero, an about picture and the neutral search rank, unfiltered.
|
|
468
|
-
const blocked = new Set(used);
|
|
905
|
+
const blocked = new Set(claimed);
|
|
469
906
|
for (const hit of found){
|
|
470
907
|
const key = stockPhotoSourceKey(hit);
|
|
471
908
|
if (elsewhere(key)) blocked.add(key);
|
|
472
909
|
}
|
|
473
|
-
const ranked = aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, {
|
|
474
|
-
subject: request.
|
|
475
|
-
|
|
476
|
-
strict: slot.role === 'gallery' && !request.
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
910
|
+
const ranked = aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, _extends({
|
|
911
|
+
subject: (_request_subject = request.subject) != null ? _request_subject : request.broad ? '' : subject,
|
|
912
|
+
domain: terms.domain,
|
|
913
|
+
strict: slot.role === 'gallery' && !request.broad || !!request.requires
|
|
914
|
+
}, request.requires ? {
|
|
915
|
+
requires: request.requires
|
|
916
|
+
} : {}));
|
|
917
|
+
if (!ranked.length) continue;
|
|
918
|
+
const first = ranked[0];
|
|
919
|
+
claimed.add(stockPhotoSourceKey(first));
|
|
920
|
+
choice = {
|
|
921
|
+
candidates: ranked.slice(0, AI_LAYOUT_STOCK_TRIES_PER_SLOT),
|
|
922
|
+
query: request.query
|
|
923
|
+
};
|
|
483
924
|
}
|
|
484
|
-
|
|
925
|
+
choices.push(choice);
|
|
485
926
|
}
|
|
927
|
+
// 2. Keep, a few at a time; a copy that fails tries the slot's next
|
|
928
|
+
// accepted hit no other slot has claimed.
|
|
929
|
+
const photos = slots.map(()=>null);
|
|
930
|
+
const fill = async (index)=>{
|
|
931
|
+
const choice = choices[index];
|
|
932
|
+
const slot = slots[index];
|
|
933
|
+
if (!choice || !slot) return;
|
|
934
|
+
for (const [rank, candidate] of choice.candidates.entries()){
|
|
935
|
+
if (signal.aborted) return;
|
|
936
|
+
const key = stockPhotoSourceKey(candidate);
|
|
937
|
+
if (rank > 0) {
|
|
938
|
+
if (claimed.has(key)) continue;
|
|
939
|
+
claimed.add(key);
|
|
940
|
+
}
|
|
941
|
+
const placed = await keep(candidate, slot, choice.query);
|
|
942
|
+
if (placed) {
|
|
943
|
+
photos[index] = placed;
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
let next = 0;
|
|
949
|
+
const worker = async ()=>{
|
|
950
|
+
while(next < slots.length){
|
|
951
|
+
const index = next;
|
|
952
|
+
next += 1;
|
|
953
|
+
await fill(index);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
await Promise.all(Array.from({
|
|
957
|
+
length: Math.min(AI_LAYOUT_STOCK_COPIES_AT_ONCE, slots.length)
|
|
958
|
+
}, worker));
|
|
486
959
|
return photos;
|
|
487
960
|
};
|
|
488
961
|
}
|