@aglyn/plugins-ai 1.0.0-beta.235 → 1.0.0-beta.237
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 +37 -0
- package/src/lib/components/ai-credits-prompt.component.js +67 -0
- package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.d.ts +5 -4
- package/src/lib/components/ai-job-plan.component.js +37 -12
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +10 -0
- package/src/lib/components/ai-job-requests.js +12 -4
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +26 -7
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +68 -22
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/assist-build-card.component.d.ts +3 -1
- package/src/lib/components/assist-build-card.component.js +58 -7
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +54 -6
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +2 -1
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-job-admission.d.ts +22 -2
- package/src/lib/jobs/ai-job-admission.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
- package/src/lib/jobs/ai-job-build-step.js +50 -34
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +1 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +4 -0
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
- package/src/lib/jobs/ai-job-page-step.js +34 -3
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +23 -0
- package/src/lib/jobs/ai-job-plan-step.js +75 -15
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +34 -3
- package/src/lib/jobs/ai-job-site-content.js +132 -32
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +45 -15
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +18 -1
- package/src/lib/jobs/ai-jobs.js +29 -13
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +105 -18
- package/src/lib/jobs/ai-layout-stock-photos.js +356 -114
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/layout-language/ai-layout-compiler.d.ts +27 -0
- package/src/lib/layout-language/ai-layout-compiler.js +78 -11
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +3 -1
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.js +1 -1
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +37 -1
- package/src/lib/layout-language/ai-layout-listings.js +66 -11
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +2 -1
- package/src/lib/layout-language/ai-layout-pictures.js +5 -2
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +31 -0
- package/src/lib/model/ai-build-job.js +86 -1
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-progress.d.ts +8 -0
- package/src/lib/model/ai-build-progress.js +33 -0
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +182 -0
- package/src/lib/model/ai-credit-estimate.js +190 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -0
- package/src/lib/model/ai-jobs.types.d.ts +15 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +59 -22
- package/src/lib/model/ai-site-job.js +104 -23
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.js +3 -3
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.js +10 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +11 -0
- package/src/lib/runtime/ai-node-tree.js +26 -0
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +539 -7
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js +93 -35
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +45 -4
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-chat.js +4 -1
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.js +3 -1
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/server/assist-music.d.ts +36 -0
- package/src/lib/server/assist-music.js +36 -0
- package/src/lib/server/assist-music.js.map +1 -0
- package/src/lib/server.js +6 -0
- package/src/lib/server.js.map +1 -1
|
@@ -28,17 +28,33 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
28
28
|
*
|
|
29
29
|
* For each slot, in document order:
|
|
30
30
|
*
|
|
31
|
-
* 1. **Search.** Plain words
|
|
32
|
-
* business type, "yoga studio" from "a family-owned
|
|
33
|
-
* Austin")
|
|
34
|
-
* the alt text the model wrote,
|
|
35
|
-
*
|
|
36
|
-
* a
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
31
|
+
* 1. **Search.** Plain words. The hero asks for the kind of business (the
|
|
32
|
+
* head of the site's business type, "yoga studio" from "a family-owned
|
|
33
|
+
* yoga studio in Austin"). Any other picture asks for its SUBJECT: the
|
|
34
|
+
* noun phrase that opens the alt text the model wrote, its filler
|
|
35
|
+
* adjectives dropped ("serving bowl" from "A beautiful hand-thrown serving
|
|
36
|
+
* bowl on a linen cloth"), qualified by the site's craft where the phrase
|
|
37
|
+
* does not already say it ("serving bowl ceramic" for a ceramics studio).
|
|
38
|
+
* An about picture asks for people. The orientation and a minimum size
|
|
39
|
+
* come from the slot's frame. The provider caches every search for a day
|
|
40
|
+
* and keeps its own rate limit; one page never asks the same words twice.
|
|
41
|
+
* 2. **Rank.** A picture of a thing must show the thing: every hit is scored
|
|
42
|
+
* by how many of the subject's words its tags, its description and its
|
|
43
|
+
* page's address name (singular and plural alike, the head noun counting
|
|
44
|
+
* double), and a hit naming none of them never fills a picture of a
|
|
45
|
+
* thing. The live Juniper Clay start put crackers under "Serving bowl"
|
|
46
|
+
* and plants under "Espresso cup". Among the best-scored hits the job's
|
|
47
|
+
* seed picks, so two sites built from the same words rarely open with the
|
|
48
|
+
* same photo while one job always picks the same.
|
|
49
|
+
* 3. **Never twice.** A photo is placed once per job: not twice on a page,
|
|
50
|
+
* and not on a page when another page (or another pass, in this process)
|
|
51
|
+
* of the same job already placed it — the maker's portrait in About is
|
|
52
|
+
* never a vase in Selected work. The caller hands in what the job's other
|
|
53
|
+
* pages hold (`avoid`); a pass repeated over its own page keeps its own.
|
|
54
|
+
* 4. **Fall back.** A picture of a thing nothing relevant answered asks once
|
|
55
|
+
* more for the place itself ("ceramics studio interior"), a neutral photo
|
|
56
|
+
* any caption sits under, before it is left to a starter photo.
|
|
57
|
+
* 5. **Keep.** A photo the site's library already holds (by its source key)
|
|
42
58
|
* is reused; otherwise its bytes are downloaded from the library and
|
|
43
59
|
* stored in the site's own media library as the member who started the
|
|
44
60
|
* job, credited to its photographer. The page names the site's asset,
|
|
@@ -49,13 +65,42 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
49
65
|
* photo. Nothing here throws, and none of it is an AI model call: photos cost
|
|
50
66
|
* no AI credits.
|
|
51
67
|
*/ /** Wall clock the whole page's photos may take, inside the step's own signal. */ export const AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS = 20000;
|
|
52
|
-
/** Searches one page may
|
|
68
|
+
/** 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 = 12;
|
|
53
69
|
/** The hits a pick is made among. */ export const AI_LAYOUT_STOCK_PICK_WINDOW = 8;
|
|
70
|
+
/** The ranked hits one search may try to place before the slot moves to its next search. */ const AI_LAYOUT_STOCK_TRIES_PER_SEARCH = 3;
|
|
54
71
|
/** The largest photo copied: inside one direct upload. */ export const AI_LAYOUT_STOCK_MAX_BYTES = 3 * 1024 * 1024;
|
|
72
|
+
/**
|
|
73
|
+
* The unit job input naming the screen drafts the job's other pages were
|
|
74
|
+
* written to, whose photos a page does not place again (AGL-3660).
|
|
75
|
+
*/ export const AI_STOCK_PHOTO_PAGES_INPUT = 'stockPhotoPages';
|
|
55
76
|
/** Words that carry no subject. */ const STOPWORDS = new Set(('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 shop services service site website page photo picture image showing shows show ' + 'someone people person one two three some many several').split(' '));
|
|
77
|
+
/**
|
|
78
|
+
* Words that describe a picture's look, not what it shows, dropped from a
|
|
79
|
+
* subject. A word that is as often a noun ("light", "set") is not one.
|
|
80
|
+
*/ const FILLER = new Set(('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 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 ' + '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').split(' '));
|
|
81
|
+
/** The nouns that name a place of business, not its craft: "studio" in "ceramics studio". */ const VENUES = new Set(('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').split(' '));
|
|
56
82
|
/** Lowercase words, letters only, without stopwords. */ function contentWords(text) {
|
|
57
83
|
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));
|
|
58
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* A word's singular, for matching only: "bowls" and "bowl", "vases" and
|
|
87
|
+
* "vase", "berries" and "berry", "dishes" and "dish" meet. Both sides of a
|
|
88
|
+
* comparison are reduced the same way, so an odd stem still matches itself.
|
|
89
|
+
*/ export function aiStockStem(word) {
|
|
90
|
+
const lower = word.toLowerCase();
|
|
91
|
+
if (lower.length > 4 && lower.endsWith('ies')) return `${lower.slice(0, -3)}y`;
|
|
92
|
+
if (lower.length > 4 && /(?:ches|shes|sses|xes|zes)$/.test(lower)) return lower.slice(0, -2);
|
|
93
|
+
if (lower.length > 3 && lower.endsWith('s') && !/(?:ss|us|is)$/.test(lower)) return lower.slice(0, -1);
|
|
94
|
+
return lower;
|
|
95
|
+
}
|
|
96
|
+
/** A text's words as stems; a hyphenated word counts as its parts. */ function stems(text) {
|
|
97
|
+
const out = new Set();
|
|
98
|
+
for (const word of text.toLowerCase().split(/[^a-zÀ-ɏ]+/)){
|
|
99
|
+
if (word.length > 1) out.add(aiStockStem(word));
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** 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|&/;
|
|
59
104
|
/**
|
|
60
105
|
* The kind of business in a few words: the first clause of the business
|
|
61
106
|
* type, before a place or an audience, its last three content words (the
|
|
@@ -65,10 +110,30 @@ import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
|
65
110
|
const words = contentWords(clause != null ? clause : '');
|
|
66
111
|
return words.slice(-3).join(' ');
|
|
67
112
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
/**
|
|
114
|
+
* 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 "small-batch ceramics
|
|
116
|
+
* studio", "yoga" from "yoga studio". Empty when the business is only a
|
|
117
|
+
* place ("a shop").
|
|
118
|
+
*/ export function aiStockCraftWords(business) {
|
|
119
|
+
const words = business.split(/\s+/).filter((word)=>word && !VENUES.has(word) && !FILLER.has(word));
|
|
120
|
+
const craft = words[words.length - 1];
|
|
121
|
+
return craft ? aiStockStem(craft) : '';
|
|
122
|
+
}
|
|
123
|
+
/** A phrase's content words without its filler; all filler, and nothing is left. */ function subjectOf(words) {
|
|
124
|
+
return words.filter((word)=>!FILLER.has(word));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A picture's subject in a few words (AGL-3660): the noun phrase its alt
|
|
128
|
+
* text opens with — its first clause holding more than filler, cut where a
|
|
129
|
+
* preposition starts the setting, filler dropped, its last three words (the
|
|
130
|
+
* head noun is the last) — else its section's name.
|
|
131
|
+
*/ export function aiStockSubjectWords(alt, sectionName) {
|
|
132
|
+
for (const clause of alt.toLowerCase().split(CLAUSE_BREAK)){
|
|
133
|
+
const words = subjectOf(contentWords(clause != null ? clause : ''));
|
|
134
|
+
if (words.length) return words.slice(-3).join(' ');
|
|
135
|
+
}
|
|
136
|
+
return subjectOf(contentWords(sectionName)).slice(0, 2).join(' ');
|
|
72
137
|
}
|
|
73
138
|
const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUERY_MAX_CHARS).trim();
|
|
74
139
|
/** The frame's orientation: a wide frame wants a landscape photo, a tall one a portrait. */ export function aiStockOrientation(aspect) {
|
|
@@ -77,6 +142,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
77
142
|
return 'any';
|
|
78
143
|
}
|
|
79
144
|
/** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, words) {
|
|
145
|
+
var _words_craft;
|
|
80
146
|
const orientation = aiStockOrientation(slot.aspect);
|
|
81
147
|
const size = slot.role === 'hero' ? {
|
|
82
148
|
minWidth: 1600
|
|
@@ -85,21 +151,48 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
85
151
|
} : {
|
|
86
152
|
minWidth: 900
|
|
87
153
|
};
|
|
154
|
+
const craft = (_words_craft = words.craft) != null ? _words_craft : aiStockCraftWords(words.business);
|
|
155
|
+
const subjectStems = stems(words.subject);
|
|
156
|
+
// The craft qualifies a subject that does not already say it.
|
|
157
|
+
const qualified = craft && [
|
|
158
|
+
...stems(craft)
|
|
159
|
+
].some((stem)=>!subjectStems.has(stem)) ? `${words.subject} ${craft}` : words.subject;
|
|
160
|
+
const place = words.business.split(/\s+/).some((word)=>VENUES.has(word)) ? `${words.business} interior` : words.business;
|
|
88
161
|
const queries = slot.role === 'hero' ? [
|
|
89
|
-
|
|
90
|
-
|
|
162
|
+
{
|
|
163
|
+
query: words.business
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
query: `${words.business} ${words.subject}`
|
|
167
|
+
}
|
|
91
168
|
] : slot.role === 'about' ? [
|
|
92
|
-
|
|
93
|
-
|
|
169
|
+
{
|
|
170
|
+
query: `${words.business} ${words.subject}`
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
query: words.business
|
|
174
|
+
}
|
|
175
|
+
] : words.subject ? [
|
|
176
|
+
{
|
|
177
|
+
query: qualified
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
query: words.subject
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
query: place,
|
|
184
|
+
neutral: true
|
|
185
|
+
}
|
|
94
186
|
] : [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
187
|
+
{
|
|
188
|
+
query: place,
|
|
189
|
+
neutral: true
|
|
190
|
+
}
|
|
98
191
|
];
|
|
99
192
|
const seen = new Set();
|
|
100
193
|
const searches = [];
|
|
101
194
|
for (const raw of queries){
|
|
102
|
-
const query = clip(raw);
|
|
195
|
+
const query = clip(raw.query);
|
|
103
196
|
if (!query || seen.has(query)) continue;
|
|
104
197
|
seen.add(query);
|
|
105
198
|
searches.push(_extends({
|
|
@@ -107,15 +200,115 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
|
|
|
107
200
|
orientation
|
|
108
201
|
}, size, slot.role === 'about' ? {
|
|
109
202
|
people: true
|
|
203
|
+
} : {}, raw.neutral ? {
|
|
204
|
+
neutral: true
|
|
110
205
|
} : {}));
|
|
111
206
|
}
|
|
112
207
|
return searches;
|
|
113
208
|
}
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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;
|
|
219
|
+
try {
|
|
220
|
+
slug = new URL(photo.pageUrl).pathname;
|
|
221
|
+
} catch (unused) {
|
|
222
|
+
slug = '';
|
|
223
|
+
}
|
|
224
|
+
const named = stems([
|
|
225
|
+
...(_photo_tags = photo.tags) != null ? _photo_tags : [],
|
|
226
|
+
(_photo_alt = photo.alt) != null ? _photo_alt : '',
|
|
227
|
+
slug
|
|
228
|
+
].join(' '));
|
|
229
|
+
const head = subject[subject.length - 1];
|
|
230
|
+
let score = 0;
|
|
231
|
+
for (const word of new Set(subject)){
|
|
232
|
+
if (named.has(word)) score += word === head ? 2 : 1;
|
|
233
|
+
}
|
|
234
|
+
if (score === 0) return 0;
|
|
235
|
+
for (const word of new Set(((_terms_craft = terms.craft) != null ? _terms_craft : '').split(/\s+/).filter(Boolean).map(aiStockStem))){
|
|
236
|
+
if (named.has(word)) score += 0.25;
|
|
237
|
+
}
|
|
238
|
+
return score;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* A search's hits in the order they are tried (AGL-3660): those not placed
|
|
242
|
+
* yet, among the first few, best-scored first; the seed turns the hits that
|
|
243
|
+
* score alike, so two jobs differ and one job repeats itself.
|
|
244
|
+
*/ export function aiStockRank(photos, used, seed, options = {}) {
|
|
245
|
+
var _options_subject;
|
|
246
|
+
const subject = (_options_subject = options.subject) != null ? _options_subject : '';
|
|
247
|
+
const scored = photos.filter((photo)=>!used.has(stockPhotoSourceKey(photo))).slice(0, AI_LAYOUT_STOCK_PICK_WINDOW).map((photo)=>({
|
|
248
|
+
photo,
|
|
249
|
+
score: subject ? aiStockRelevance(photo, _extends({
|
|
250
|
+
subject
|
|
251
|
+
}, options.craft ? {
|
|
252
|
+
craft: options.craft
|
|
253
|
+
} : {})) : 0
|
|
254
|
+
})).filter((entry)=>!options.strict || !subject || entry.score > 0);
|
|
255
|
+
if (!scored.length) return [];
|
|
256
|
+
const turn = aiLayoutSeedNumber(seed);
|
|
257
|
+
const ranked = [];
|
|
258
|
+
for (const score of [
|
|
259
|
+
...new Set(scored.map((entry)=>entry.score))
|
|
260
|
+
].sort((a, b)=>b - a)){
|
|
261
|
+
const tier = scored.filter((entry)=>entry.score === score);
|
|
262
|
+
const start = turn % tier.length;
|
|
263
|
+
for (const entry of [
|
|
264
|
+
...tier.slice(start),
|
|
265
|
+
...tier.slice(0, start)
|
|
266
|
+
])ranked.push(entry.photo);
|
|
267
|
+
}
|
|
268
|
+
return ranked;
|
|
269
|
+
}
|
|
270
|
+
/** The pick among a search's hits: the first {@link aiStockRank} would try, or `null`. */ export function aiStockPick(photos, used, seed, options = {}) {
|
|
271
|
+
var _aiStockRank_;
|
|
272
|
+
return (_aiStockRank_ = aiStockRank(photos, used, seed, options)[0]) != null ? _aiStockRank_ : null;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The library photos a stored page already shows: every `image` whose
|
|
276
|
+
* source is the site's media reference. Starter photos are the platform's,
|
|
277
|
+
* not the job's, and are not listed.
|
|
278
|
+
*/ export function aiStockPlacedSrcs(nodes) {
|
|
279
|
+
const map = nodes != null ? nodes : {};
|
|
280
|
+
const out = [];
|
|
281
|
+
for (const node of Object.values(map)){
|
|
282
|
+
var _node_props;
|
|
283
|
+
const src = (node == null ? void 0 : node.componentId) === 'image' ? (_node_props = node.props) == null ? void 0 : _node_props['src'] : null;
|
|
284
|
+
if (typeof src === 'string' && src.startsWith('media:')) out.push(src);
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* What this process has placed for each job, by source key and asset src,
|
|
290
|
+
* with the scope (the page's seed) that placed it (AGL-3660). It spans the
|
|
291
|
+
* passes of one job that run here, so a page, the products or a post built
|
|
292
|
+
* later does not place what an earlier one did, while a pass repeated over
|
|
293
|
+
* its own page keeps its own photos. Bounded; the oldest job goes first.
|
|
294
|
+
*/ const JOB_PHOTOS = new Map();
|
|
295
|
+
const JOB_PHOTOS_MAX = 200;
|
|
296
|
+
function jobPhotos(key) {
|
|
297
|
+
if (!key) return null;
|
|
298
|
+
let placed = JOB_PHOTOS.get(key);
|
|
299
|
+
if (!placed) {
|
|
300
|
+
placed = new Map();
|
|
301
|
+
JOB_PHOTOS.set(key, placed);
|
|
302
|
+
while(JOB_PHOTOS.size > JOB_PHOTOS_MAX){
|
|
303
|
+
const oldest = JOB_PHOTOS.keys().next().value;
|
|
304
|
+
if (oldest === undefined) break;
|
|
305
|
+
JOB_PHOTOS.delete(oldest);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return placed;
|
|
309
|
+
}
|
|
310
|
+
/** Forgets every job's placed photos; for specs. */ export function aiStockForgetJobPhotos() {
|
|
311
|
+
JOB_PHOTOS.clear();
|
|
119
312
|
}
|
|
120
313
|
const EXTENSIONS = {
|
|
121
314
|
'image/jpeg': 'jpg',
|
|
@@ -131,16 +324,128 @@ const EXTENSIONS = {
|
|
|
131
324
|
const ingest = ((_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest)();
|
|
132
325
|
if (!provider || !ingest || !input.hostId || !input.uid) return null;
|
|
133
326
|
const business = aiStockBusinessWords(input.business);
|
|
327
|
+
const craft = aiStockCraftWords(business);
|
|
134
328
|
return async (slots)=>{
|
|
135
|
-
var _deps_budgetMs;
|
|
329
|
+
var _deps_budgetMs, _input_avoid;
|
|
136
330
|
const timeout = AbortSignal.timeout((_deps_budgetMs = deps.budgetMs) != null ? _deps_budgetMs : AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS);
|
|
137
331
|
const signal = input.signal ? AbortSignal.any([
|
|
138
332
|
input.signal,
|
|
139
333
|
timeout
|
|
140
334
|
]) : timeout;
|
|
141
|
-
const
|
|
335
|
+
const scope = input.seed;
|
|
336
|
+
const job = jobPhotos(input.jobId ? `${input.hostId}:${input.jobId}` : null);
|
|
337
|
+
const avoid = new Set((_input_avoid = input.avoid) != null ? _input_avoid : []);
|
|
338
|
+
/** 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 placed or turned down. */ const used = new Set();
|
|
340
|
+
/** Asset srcs this page has placed. */ const placedSrcs = new Set();
|
|
341
|
+
const answers = new Map();
|
|
142
342
|
let searches = 0;
|
|
143
343
|
let storing = true;
|
|
344
|
+
const remember = (sourceKey, src)=>{
|
|
345
|
+
placedSrcs.add(src);
|
|
346
|
+
job == null ? void 0 : job.set(sourceKey, scope);
|
|
347
|
+
job == null ? void 0 : job.set(src, scope);
|
|
348
|
+
};
|
|
349
|
+
/** A search's hits; the same words twice are answered from memory, and `null` once the page's searches are spent. */ const search = async (wanted)=>{
|
|
350
|
+
const request = _extends({}, wanted);
|
|
351
|
+
delete request.neutral;
|
|
352
|
+
const key = JSON.stringify([
|
|
353
|
+
request.query,
|
|
354
|
+
request.orientation,
|
|
355
|
+
request.people === true
|
|
356
|
+
]);
|
|
357
|
+
const known = answers.get(key);
|
|
358
|
+
if (known) return known;
|
|
359
|
+
if (searches >= AI_LAYOUT_STOCK_SEARCHES_PER_PAGE) return null;
|
|
360
|
+
searches += 1;
|
|
361
|
+
let found;
|
|
362
|
+
try {
|
|
363
|
+
var _ref;
|
|
364
|
+
var _this;
|
|
365
|
+
found = (_ref = (_this = await provider.search(request, {
|
|
366
|
+
signal
|
|
367
|
+
})) == null ? void 0 : _this.photos) != null ? _ref : [];
|
|
368
|
+
} catch (unused) {
|
|
369
|
+
found = [];
|
|
370
|
+
}
|
|
371
|
+
answers.set(key, found);
|
|
372
|
+
return found;
|
|
373
|
+
};
|
|
374
|
+
/** 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
|
+
const sourceKey = stockPhotoSourceKey(photo);
|
|
376
|
+
used.add(sourceKey);
|
|
377
|
+
try {
|
|
378
|
+
var _EXTENSIONS_bytes_contentType;
|
|
379
|
+
const kept = await ingest.findStockPhoto({
|
|
380
|
+
hostId: input.hostId,
|
|
381
|
+
sourceKey
|
|
382
|
+
});
|
|
383
|
+
if (kept) {
|
|
384
|
+
var _kept_width, _kept_height;
|
|
385
|
+
// Held because a slot of this page or another page of the job shows it.
|
|
386
|
+
if (placedSrcs.has(kept.src) || elsewhere(kept.src)) return null;
|
|
387
|
+
remember(sourceKey, kept.src);
|
|
388
|
+
return {
|
|
389
|
+
src: kept.src,
|
|
390
|
+
width: (_kept_width = kept.width) != null ? _kept_width : photo.width,
|
|
391
|
+
height: (_kept_height = kept.height) != null ? _kept_height : photo.height
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// A refusal to store (the storage band, a lockdown) holds for the
|
|
395
|
+
// whole page, so the rest of it reuses what it can and copies nothing.
|
|
396
|
+
if (!storing) return null;
|
|
397
|
+
const bytes = await provider.download(photo, {
|
|
398
|
+
maxBytes: AI_LAYOUT_STOCK_MAX_BYTES,
|
|
399
|
+
signal
|
|
400
|
+
});
|
|
401
|
+
if (!bytes || signal.aborted) return null;
|
|
402
|
+
const credit = provider.credit(photo);
|
|
403
|
+
const stored = await ingest.ingest({
|
|
404
|
+
hostId: input.hostId,
|
|
405
|
+
uid: input.uid,
|
|
406
|
+
fileName: `${photo.provider}-${photo.id}.${(_EXTENSIONS_bytes_contentType = EXTENSIONS[bytes.contentType]) != null ? _EXTENSIONS_bytes_contentType : 'jpg'}`,
|
|
407
|
+
contentType: bytes.contentType,
|
|
408
|
+
bytes: bytes.bytes,
|
|
409
|
+
alt: slot.alt,
|
|
410
|
+
description: credit.text,
|
|
411
|
+
stockPhoto: _extends({
|
|
412
|
+
key: sourceKey,
|
|
413
|
+
provider: photo.provider,
|
|
414
|
+
providerLabel: credit.providerLabel,
|
|
415
|
+
id: photo.id,
|
|
416
|
+
pageUrl: photo.pageUrl,
|
|
417
|
+
photographer: photo.photographer
|
|
418
|
+
}, photo.photographerUrl ? {
|
|
419
|
+
photographerUrl: photo.photographerUrl
|
|
420
|
+
} : {}, {
|
|
421
|
+
license: credit.license,
|
|
422
|
+
licenseUrl: credit.licenseUrl,
|
|
423
|
+
attributionRequired: credit.attributionRequired,
|
|
424
|
+
query
|
|
425
|
+
})
|
|
426
|
+
});
|
|
427
|
+
if (stored.ok) {
|
|
428
|
+
var _stored_width, _stored_height;
|
|
429
|
+
remember(sourceKey, stored.src);
|
|
430
|
+
return {
|
|
431
|
+
src: stored.src,
|
|
432
|
+
width: (_stored_width = stored.width) != null ? _stored_width : photo.width,
|
|
433
|
+
height: (_stored_height = stored.height) != null ? _stored_height : photo.height
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
storing = false;
|
|
437
|
+
const refusal = stored;
|
|
438
|
+
console.warn('ai layout pictures: the media library refused a stock photo', {
|
|
439
|
+
status: refusal.status,
|
|
440
|
+
reason: refusal.reason
|
|
441
|
+
});
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.warn('ai layout pictures: a stock photo could not be kept', {
|
|
444
|
+
error: String(error)
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
};
|
|
144
449
|
const photos = [];
|
|
145
450
|
for (const [index, slot] of slots.entries()){
|
|
146
451
|
var _input_sectionNames_slot_sectionIndex;
|
|
@@ -152,91 +457,28 @@ const EXTENSIONS = {
|
|
|
152
457
|
let placed = null;
|
|
153
458
|
for (const request of aiStockSearchesFor(slot, {
|
|
154
459
|
business,
|
|
155
|
-
subject
|
|
460
|
+
subject,
|
|
461
|
+
craft
|
|
156
462
|
})){
|
|
157
|
-
if (placed || signal.aborted
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
} catch (unused) {
|
|
167
|
-
found = [];
|
|
463
|
+
if (placed || signal.aborted) break;
|
|
464
|
+
const found = await search(request);
|
|
465
|
+
if (found === null) break;
|
|
466
|
+
// A gallery picture names a thing, and only a hit naming it fills it;
|
|
467
|
+
// a hero, an about picture and the neutral search rank, unfiltered.
|
|
468
|
+
const blocked = new Set(used);
|
|
469
|
+
for (const hit of found){
|
|
470
|
+
const key = stockPhotoSourceKey(hit);
|
|
471
|
+
if (elsewhere(key)) blocked.add(key);
|
|
168
472
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
});
|
|
179
|
-
if (kept) {
|
|
180
|
-
var _kept_width, _kept_height;
|
|
181
|
-
placed = {
|
|
182
|
-
src: kept.src,
|
|
183
|
-
width: (_kept_width = kept.width) != null ? _kept_width : photo.width,
|
|
184
|
-
height: (_kept_height = kept.height) != null ? _kept_height : photo.height
|
|
185
|
-
};
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
// A refusal to store (the storage band, a lockdown) holds for the
|
|
189
|
-
// whole page, so the rest of it reuses what it can and copies nothing.
|
|
190
|
-
if (!storing) continue;
|
|
191
|
-
const bytes = await provider.download(photo, {
|
|
192
|
-
maxBytes: AI_LAYOUT_STOCK_MAX_BYTES,
|
|
193
|
-
signal
|
|
194
|
-
});
|
|
195
|
-
if (!bytes || signal.aborted) continue;
|
|
196
|
-
const credit = provider.credit(photo);
|
|
197
|
-
const stored = await ingest.ingest({
|
|
198
|
-
hostId: input.hostId,
|
|
199
|
-
uid: input.uid,
|
|
200
|
-
fileName: `${photo.provider}-${photo.id}.${(_EXTENSIONS_bytes_contentType = EXTENSIONS[bytes.contentType]) != null ? _EXTENSIONS_bytes_contentType : 'jpg'}`,
|
|
201
|
-
contentType: bytes.contentType,
|
|
202
|
-
bytes: bytes.bytes,
|
|
203
|
-
alt: slot.alt,
|
|
204
|
-
description: credit.text,
|
|
205
|
-
stockPhoto: _extends({
|
|
206
|
-
key: sourceKey,
|
|
207
|
-
provider: photo.provider,
|
|
208
|
-
providerLabel: credit.providerLabel,
|
|
209
|
-
id: photo.id,
|
|
210
|
-
pageUrl: photo.pageUrl,
|
|
211
|
-
photographer: photo.photographer
|
|
212
|
-
}, photo.photographerUrl ? {
|
|
213
|
-
photographerUrl: photo.photographerUrl
|
|
214
|
-
} : {}, {
|
|
215
|
-
license: credit.license,
|
|
216
|
-
licenseUrl: credit.licenseUrl,
|
|
217
|
-
attributionRequired: credit.attributionRequired,
|
|
218
|
-
query: request.query
|
|
219
|
-
})
|
|
220
|
-
});
|
|
221
|
-
if (stored.ok) {
|
|
222
|
-
var _stored_width, _stored_height;
|
|
223
|
-
placed = {
|
|
224
|
-
src: stored.src,
|
|
225
|
-
width: (_stored_width = stored.width) != null ? _stored_width : photo.width,
|
|
226
|
-
height: (_stored_height = stored.height) != null ? _stored_height : photo.height
|
|
227
|
-
};
|
|
228
|
-
} else {
|
|
229
|
-
storing = false;
|
|
230
|
-
const refusal = stored;
|
|
231
|
-
console.warn('ai layout pictures: the media library refused a stock photo', {
|
|
232
|
-
status: refusal.status,
|
|
233
|
-
reason: refusal.reason
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
} catch (error) {
|
|
237
|
-
console.warn('ai layout pictures: a stock photo could not be kept', {
|
|
238
|
-
error: String(error)
|
|
239
|
-
});
|
|
473
|
+
const ranked = aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, {
|
|
474
|
+
subject: request.neutral ? craft || business : subject,
|
|
475
|
+
craft,
|
|
476
|
+
strict: slot.role === 'gallery' && !request.neutral
|
|
477
|
+
});
|
|
478
|
+
for (const photo of ranked.slice(0, AI_LAYOUT_STOCK_TRIES_PER_SEARCH)){
|
|
479
|
+
if (signal.aborted) break;
|
|
480
|
+
placed = await keep(photo, slot, request.query);
|
|
481
|
+
if (placed) break;
|
|
240
482
|
}
|
|
241
483
|
}
|
|
242
484
|
photos.push(placed);
|