@aglyn/plugins-ai 1.0.0-beta.272 → 1.0.0-beta.273

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.
Files changed (52) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-site-start-card.component.js +237 -234
  3. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  4. package/src/lib/jobs/ai-empty-site-sweep.d.ts +12 -1
  5. package/src/lib/jobs/ai-empty-site-sweep.js +15 -3
  6. package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -1
  7. package/src/lib/jobs/ai-job-page-step.js +21 -2
  8. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  9. package/src/lib/jobs/ai-job-seo-step.js +8 -4
  10. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  11. package/src/lib/jobs/ai-job-site-datasets.d.ts +39 -1
  12. package/src/lib/jobs/ai-job-site-datasets.js +111 -15
  13. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  14. package/src/lib/jobs/ai-job-site-step.d.ts +12 -3
  15. package/src/lib/jobs/ai-job-site-step.js +85 -11
  16. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  17. package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
  18. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  19. package/src/lib/jobs/ai-jobs.d.ts +3 -0
  20. package/src/lib/jobs/ai-jobs.js +4 -0
  21. package/src/lib/jobs/ai-jobs.js.map +1 -1
  22. package/src/lib/jobs/ai-layout-owner-photos.js +4 -4
  23. package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
  24. package/src/lib/jobs/ai-layout-stock-photos.d.ts +35 -0
  25. package/src/lib/jobs/ai-layout-stock-photos.js +114 -43
  26. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  27. package/src/lib/jobs/ai-site-late-photos.d.ts +143 -0
  28. package/src/lib/jobs/ai-site-late-photos.js +486 -0
  29. package/src/lib/jobs/ai-site-late-photos.js.map +1 -0
  30. package/src/lib/jobs/ai-site-publish.d.ts +18 -0
  31. package/src/lib/jobs/ai-site-publish.js +123 -5
  32. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  33. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.d.ts +83 -0
  34. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js +1553 -0
  35. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js.map +1 -0
  36. package/src/lib/layout-language/ai-layout-pictures.d.ts +50 -13
  37. package/src/lib/layout-language/ai-layout-pictures.js +81 -19
  38. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  39. package/src/lib/model/ai-jobs.types.d.ts +10 -0
  40. package/src/lib/model/ai-jobs.types.js.map +1 -1
  41. package/src/lib/model/ai-site-photos.d.ts +119 -0
  42. package/src/lib/model/ai-site-photos.js +128 -0
  43. package/src/lib/model/ai-site-photos.js.map +1 -0
  44. package/src/lib/runtime/ai-doctrine-validators.d.ts +3 -1
  45. package/src/lib/runtime/ai-doctrine-validators.js +3 -1
  46. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  47. package/src/lib/runtime/seo-fields.d.ts +29 -4
  48. package/src/lib/runtime/seo-fields.js +58 -11
  49. package/src/lib/runtime/seo-fields.js.map +1 -1
  50. package/src/lib/tools/ai-seo-tool.d.ts +25 -1
  51. package/src/lib/tools/ai-seo-tool.js +61 -0
  52. package/src/lib/tools/ai-seo-tool.js.map +1 -1
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginMediaIngest } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest';
18
+ import { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
19
+ import { type StockPhotoProvider } from '@aglyn/aglyn/plugin-manager/stock-photo-provider';
20
+ import type { NodesMap } from '@aglyn/aglyn/types/nodes';
21
+ import type { Firestore } from 'firebase-admin/firestore';
22
+ import { type AiLayoutPicturePhoto, type AiLayoutPictureSlot } from '../layout-language/ai-layout-pictures';
23
+ import type { AiJob } from '../model/ai-jobs.types';
24
+ import { type AiSitePhotos, type AiSiteStarterList } from '../model/ai-site-photos';
25
+ import { readAiDraftNodes, updateAiDraftNodes } from './ai-job-drafts';
26
+ /**
27
+ * THE LATE PHOTO PASS OF A SITE START (AGL-3988).
28
+ *
29
+ * Every page and every dataset of a guided start is built in a pass of its
30
+ * own, and each copies its own stock photos as it goes. Pixabay's image hosts
31
+ * (behind Cloudflare) answer a burst of downloads with 429 for minutes at a
32
+ * time, whatever the client waits inside one pass. The v1.0.0-beta.271 Laurel
33
+ * Fork Photography start (job yZUqIPmjIZ, 2026-10-19 23:05Z) had 22 cover
34
+ * downloads refused in the beat that built its datasets, 11 on its home page
35
+ * and 20 later. Photo 1846114, refused at 23:08, was copied at 23:10:42. The
36
+ * "Gallery collections" covers were written before the job had copied any
37
+ * photo, and the home page was built first, so each fell back to the office
38
+ * team and the desk, which also became the site's share image.
39
+ *
40
+ * So once every page is built, and before the site is published, the site
41
+ * step runs this pass over what the job holds in `sitePhotos`:
42
+ *
43
+ * 1. **The refused picks again, after the burst.** It waits until
44
+ * {@link AI_SITE_LATE_PHOTOS_WAIT_MS} after the last refusal, then asks
45
+ * each picture's own picks for again from the search that found them,
46
+ * which the provider's 24-hour cache answers. No model, no credits. A
47
+ * pick a later page copied is taken from the library; two downloads
48
+ * refused in a row say the burst is not over, and nothing more is
49
+ * downloaded.
50
+ * 2. **Then the job's own photos, everywhere.** A picture still on a starter
51
+ * takes (a) its own pick where the page does not show it yet, else (b)
52
+ * one of the photos the job copied that the page does not show, by the
53
+ * matchers its pages and covers already use: a page's picture by role,
54
+ * shape and words, a hero a wide photo (`aiLayoutJobPhotoPicks`); and
55
+ * only where none is left, (c) one the page shows elsewhere, and last
56
+ * the photo beside it, since the other choice is a starter that shows
57
+ * another business. A record goes by its category, then the people its
58
+ * moment shows, its neighbor's photo only where nothing else is left,
59
+ * from the end that repeats least (`aiSiteDatasetRecordsFromOwn`). A
60
+ * team's records take only their own picks, never a photo of something
61
+ * else.
62
+ *
63
+ * The pages' drafts are written in place, only where a picture still holds
64
+ * a starter; the records through the data plugin's writer, only where a
65
+ * record still holds the starter it was written with. A job that copied no
66
+ * photo at all keeps its starters. Nothing here throws: a picture is
67
+ * decoration, and a site is never held back by one.
68
+ */
69
+ /** How long after the image hosts refused a pick it is asked for again: their bursts clear within minutes. */
70
+ export declare const AI_SITE_LATE_PHOTOS_WAIT_MS = 60000;
71
+ /** The wall clock the refused picks may take to copy, inside the pass's signal. */
72
+ export declare const AI_SITE_LATE_PHOTOS_COPY_MS = 30000;
73
+ /**
74
+ * Downloads refused in a row after which the pass downloads nothing more: the
75
+ * burst is not over, and each refusal costs the image hosts' own backoff.
76
+ */
77
+ export declare const AI_SITE_LATE_PHOTOS_REFUSALS_IN_A_ROW = 2;
78
+ /** ASSUMED: the pages' drafts, the records, and the publish that follows in the same pass. */
79
+ export declare const AI_SITE_LATE_PHOTOS_WRITES_MS = 20000;
80
+ /** The least time the pass needs: the wait, the copies, and the writes after them. */
81
+ export declare const AI_SITE_LATE_PHOTOS_MINIMUM_MS: number;
82
+ export interface AiSiteLatePhotosInput {
83
+ firestore: Firestore;
84
+ job: Pick<AiJob, '$id' | 'orgId' | 'hostId' | 'createdBy' | 'inputs' | 'plan' | 'outputs'>;
85
+ photos: AiSitePhotos;
86
+ /** The pages the start built: only their pictures are filled, and only their photos are the job's. */
87
+ pageIds: readonly string[];
88
+ org: Readonly<Record<string, unknown>> | null;
89
+ now: Date;
90
+ signal?: AbortSignal;
91
+ }
92
+ export interface AiSiteLatePhotosResult {
93
+ /** What is left for another pass, where this one ran out of time; `null` once it is done. */
94
+ photos: AiSitePhotos | null;
95
+ /** Pictures whose refused picks were asked for again, and of them copied. */
96
+ retried: number;
97
+ copied: number;
98
+ /** Pictures given one of the job's own photos. */
99
+ filled: number;
100
+ /** Pictures still on a starter. */
101
+ left: number;
102
+ }
103
+ export interface AiSiteLatePhotosDeps {
104
+ provider?: () => StockPhotoProvider | null;
105
+ ingest?: () => PluginMediaIngest | null;
106
+ readNodes?: typeof readAiDraftNodes;
107
+ updateNodes?: typeof updateAiDraftNodes;
108
+ writerFor?: typeof pluginResourceDraftWriter;
109
+ now?: () => number;
110
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
111
+ }
112
+ /** One image of a stored page, in document order. */
113
+ interface PageImage {
114
+ imageId: string;
115
+ src: string;
116
+ width: number;
117
+ height: number;
118
+ alt: string;
119
+ }
120
+ /**
121
+ * A stored page's pictures in document order, as its picture slots were:
122
+ * every `image` showing a photo, never one bound to a record's field.
123
+ */
124
+ export declare function aiSitePageImages(nodes: NodesMap, rootId?: string): PageImage[];
125
+ /**
126
+ * The page with each starter its plan names given its photo, re-read on the
127
+ * stored page: an image that no longer holds a starter is left as it is.
128
+ */
129
+ export declare function aiSitePageWithPhotos(nodes: NodesMap, plan: ReadonlyArray<{
130
+ slot: AiLayoutPictureSlot;
131
+ photo: AiLayoutPicturePhoto;
132
+ }>): NodesMap | null;
133
+ /** Runs the late photo pass (see the module note). Never throws. */
134
+ export declare function aiSiteLatePhotos(input: AiSiteLatePhotosInput, deps?: AiSiteLatePhotosDeps): Promise<AiSiteLatePhotosResult>;
135
+ /**
136
+ * A list's records' photos once the late pass is done (AGL-3988): each
137
+ * record's own refused pick where it was copied, then, for a list of things,
138
+ * one of the photos it or the job shows (`aiSiteDatasetRecordsFromOwn`). A
139
+ * team's records take only their own picks: no photo of the job's is of
140
+ * one of the business's people. Pure.
141
+ */
142
+ export declare function aiSiteListPhotos(list: AiSiteStarterList, retried: ReadonlyMap<string, AiLayoutPicturePhoto>, pool: readonly AiLayoutPicturePhoto[], seed: string): Array<AiLayoutPicturePhoto | null>;
143
+ export {};
@@ -0,0 +1,486 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { CANVAS_ROOT_ELEMENT_ID } from "@aglyn/aglyn/foundation/constants/canvas";
18
+ import { pluginMediaIngest } from "@aglyn/aglyn/plugin-manager/plugin-media-ingest";
19
+ import { pluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
20
+ import { stockPhotoProvider, stockPhotoSourceKey } from "@aglyn/aglyn/plugin-manager/stock-photo-provider";
21
+ import { aiLayoutJobPhotoPicks, aiLayoutPlacePictures } from "../layout-language/ai-layout-pictures.js";
22
+ import { aiSiteWords } from "../model/ai-site-job.js";
23
+ import { aiSiteIsStarterSrc, aiSitePhotosLastRefusalMs } from "../model/ai-site-photos.js";
24
+ import { AI_DATASET_DRAFT_RESOURCE } from "./ai-build-unit-outcome.js";
25
+ import { aiOriginJobId } from "./ai-job-draft-ids.js";
26
+ import { readAiDraftNodes, updateAiDraftNodes } from "./ai-job-drafts.js";
27
+ import { aiSiteProductPhotoSrc } from "./ai-job-site-content.js";
28
+ import { aiSiteDatasetPlacedPhotoDetails, aiSiteDatasetRecordsFromOwn } from "./ai-job-site-datasets.js";
29
+ import { AI_LAYOUT_STOCK_MAX_BYTES, AI_STOCK_PHOTO_OWN_REFUSALS, aiStockIngestRequest, aiStockPhotoAlt, aiStockPhotoWordsOf, aiStockPlacedPhotos, aiStockProperWords } from "./ai-layout-stock-photos.js";
30
+ /**
31
+ * THE LATE PHOTO PASS OF A SITE START (AGL-3988).
32
+ *
33
+ * Every page and every dataset of a guided start is built in a pass of its
34
+ * own, and each copies its own stock photos as it goes. Pixabay's image hosts
35
+ * (behind Cloudflare) answer a burst of downloads with 429 for minutes at a
36
+ * time, whatever the client waits inside one pass. The v1.0.0-beta.271 Laurel
37
+ * Fork Photography start (job yZUqIPmjIZ, 2026-10-19 23:05Z) had 22 cover
38
+ * downloads refused in the beat that built its datasets, 11 on its home page
39
+ * and 20 later. Photo 1846114, refused at 23:08, was copied at 23:10:42. The
40
+ * "Gallery collections" covers were written before the job had copied any
41
+ * photo, and the home page was built first, so each fell back to the office
42
+ * team and the desk, which also became the site's share image.
43
+ *
44
+ * So once every page is built, and before the site is published, the site
45
+ * step runs this pass over what the job holds in `sitePhotos`:
46
+ *
47
+ * 1. **The refused picks again, after the burst.** It waits until
48
+ * {@link AI_SITE_LATE_PHOTOS_WAIT_MS} after the last refusal, then asks
49
+ * each picture's own picks for again from the search that found them,
50
+ * which the provider's 24-hour cache answers. No model, no credits. A
51
+ * pick a later page copied is taken from the library; two downloads
52
+ * refused in a row say the burst is not over, and nothing more is
53
+ * downloaded.
54
+ * 2. **Then the job's own photos, everywhere.** A picture still on a starter
55
+ * takes (a) its own pick where the page does not show it yet, else (b)
56
+ * one of the photos the job copied that the page does not show, by the
57
+ * matchers its pages and covers already use: a page's picture by role,
58
+ * shape and words, a hero a wide photo (`aiLayoutJobPhotoPicks`); and
59
+ * only where none is left, (c) one the page shows elsewhere, and last
60
+ * the photo beside it, since the other choice is a starter that shows
61
+ * another business. A record goes by its category, then the people its
62
+ * moment shows, its neighbor's photo only where nothing else is left,
63
+ * from the end that repeats least (`aiSiteDatasetRecordsFromOwn`). A
64
+ * team's records take only their own picks, never a photo of something
65
+ * else.
66
+ *
67
+ * The pages' drafts are written in place, only where a picture still holds
68
+ * a starter; the records through the data plugin's writer, only where a
69
+ * record still holds the starter it was written with. A job that copied no
70
+ * photo at all keeps its starters. Nothing here throws: a picture is
71
+ * decoration, and a site is never held back by one.
72
+ */ /** How long after the image hosts refused a pick it is asked for again: their bursts clear within minutes. */ export const AI_SITE_LATE_PHOTOS_WAIT_MS = 60000;
73
+ /** The wall clock the refused picks may take to copy, inside the pass's signal. */ export const AI_SITE_LATE_PHOTOS_COPY_MS = 30000;
74
+ /**
75
+ * Downloads refused in a row after which the pass downloads nothing more: the
76
+ * burst is not over, and each refusal costs the image hosts' own backoff.
77
+ */ export const AI_SITE_LATE_PHOTOS_REFUSALS_IN_A_ROW = 2;
78
+ /** ASSUMED: the pages' drafts, the records, and the publish that follows in the same pass. */ export const AI_SITE_LATE_PHOTOS_WRITES_MS = 20000;
79
+ /** The least time the pass needs: the wait, the copies, and the writes after them. */ export const AI_SITE_LATE_PHOTOS_MINIMUM_MS = AI_SITE_LATE_PHOTOS_WAIT_MS + AI_SITE_LATE_PHOTOS_COPY_MS + AI_SITE_LATE_PHOTOS_WRITES_MS;
80
+ /** Waits `ms`, or less when `signal` aborts first; resolves either way. */ function sleepFor(ms, signal) {
81
+ return new Promise((resolve)=>{
82
+ if ((signal == null ? void 0 : signal.aborted) || ms <= 0) return resolve();
83
+ const timer = setTimeout(done, ms);
84
+ function done() {
85
+ clearTimeout(timer);
86
+ signal == null ? void 0 : signal.removeEventListener('abort', done);
87
+ resolve();
88
+ }
89
+ signal == null ? void 0 : signal.addEventListener('abort', done, {
90
+ once: true
91
+ });
92
+ });
93
+ }
94
+ /**
95
+ * A stored page's pictures in document order, as its picture slots were:
96
+ * every `image` showing a photo, never one bound to a record's field.
97
+ */ export function aiSitePageImages(nodes, rootId = CANVAS_ROOT_ELEMENT_ID) {
98
+ const map = nodes;
99
+ const images = [];
100
+ const seen = new Set();
101
+ const visit = (id)=>{
102
+ var _node_nodes;
103
+ if (seen.has(id)) return;
104
+ seen.add(id);
105
+ const node = map[id];
106
+ if (!node) return;
107
+ if (node.componentId === 'image') {
108
+ var _node_props;
109
+ const src = (_node_props = node.props) == null ? void 0 : _node_props['src'];
110
+ if (typeof src === 'string' && src.trim() && !src.includes('{{')) {
111
+ var _node_props1, _node_props2, _node_props3;
112
+ images.push({
113
+ imageId: id,
114
+ src,
115
+ width: Number((_node_props1 = node.props) == null ? void 0 : _node_props1['intrinsicWidth']) || 0,
116
+ height: Number((_node_props2 = node.props) == null ? void 0 : _node_props2['intrinsicHeight']) || 0,
117
+ alt: typeof ((_node_props3 = node.props) == null ? void 0 : _node_props3['alt']) === 'string' ? node.props['alt'] : ''
118
+ });
119
+ }
120
+ }
121
+ for (const child of (_node_nodes = node.nodes) != null ? _node_nodes : [])visit(child);
122
+ };
123
+ visit(rootId);
124
+ return images;
125
+ }
126
+ /** A starter slot as the matchers read it: its words, role and shape, and nothing it is not. */ function slotOf(imageId, entry) {
127
+ var _ref, _ref1;
128
+ return {
129
+ imageId,
130
+ frameId: null,
131
+ iconId: null,
132
+ alt: (_ref = entry == null ? void 0 : entry.words) != null ? _ref : '',
133
+ aspect: entry && entry.aspect > 0 ? entry.aspect : 1,
134
+ sectionIndex: (entry == null ? void 0 : entry.role) === 'hero' ? 0 : 1,
135
+ role: (_ref1 = entry == null ? void 0 : entry.role) != null ? _ref1 : 'gallery'
136
+ };
137
+ }
138
+ /**
139
+ * The page with each starter its plan names given its photo, re-read on the
140
+ * stored page: an image that no longer holds a starter is left as it is.
141
+ */ export function aiSitePageWithPhotos(nodes, plan) {
142
+ const map = nodes;
143
+ const live = plan.filter(({ slot })=>{
144
+ var _map_slot_imageId_props, _map_slot_imageId;
145
+ return aiSiteIsStarterSrc((_map_slot_imageId = map[slot.imageId]) == null ? void 0 : (_map_slot_imageId_props = _map_slot_imageId.props) == null ? void 0 : _map_slot_imageId_props['src']);
146
+ });
147
+ if (!live.length) return null;
148
+ return aiLayoutPlacePictures(nodes, live.map(({ slot })=>slot), // A photo that brings no words of its own takes the slot's, never the starter's (AGL-3831).
149
+ live.map(({ slot, photo })=>{
150
+ var _photo_alt;
151
+ return ((_photo_alt = photo.alt) == null ? void 0 : _photo_alt.trim()) || !slot.alt ? photo : _extends({}, photo, {
152
+ alt: slot.alt
153
+ });
154
+ }));
155
+ }
156
+ /** Runs the late photo pass (see the module note). Never throws. */ export async function aiSiteLatePhotos(input, deps = {}) {
157
+ var _deps_now, _deps_sleep, _deps_readNodes, _deps_updateNodes, _deps_writerFor, _job_hostId, _deps_provider, _deps_ingest;
158
+ const { job, signal } = input;
159
+ const now = (_deps_now = deps.now) != null ? _deps_now : Date.now;
160
+ const sleep = (_deps_sleep = deps.sleep) != null ? _deps_sleep : sleepFor;
161
+ const readNodes = (_deps_readNodes = deps.readNodes) != null ? _deps_readNodes : readAiDraftNodes;
162
+ const updateNodes = (_deps_updateNodes = deps.updateNodes) != null ? _deps_updateNodes : updateAiDraftNodes;
163
+ const writerFor = (_deps_writerFor = deps.writerFor) != null ? _deps_writerFor : pluginResourceDraftWriter;
164
+ const hostId = (_job_hostId = job.hostId) != null ? _job_hostId : '';
165
+ const built = new Set(input.pageIds);
166
+ const pages = input.photos.pages.filter((page)=>built.has(page.screen) && page.slots.length);
167
+ const lists = input.photos.lists;
168
+ const counts = {
169
+ retried: 0,
170
+ copied: 0,
171
+ filled: 0,
172
+ left: 0
173
+ };
174
+ const done = ()=>_extends({
175
+ photos: null
176
+ }, counts);
177
+ if (!hostId || !pages.length && !lists.length) return done();
178
+ const provider = ((_deps_provider = deps.provider) != null ? _deps_provider : stockPhotoProvider)();
179
+ const ingest = ((_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest)();
180
+ try {
181
+ var _job_outputs;
182
+ // ── 1. The refused picks, asked for again once the burst is over ──
183
+ const retried = new Map();
184
+ const asked = [
185
+ // The pages first, each one's hero first: what a visitor sees first.
186
+ ...pages.flatMap((page)=>[
187
+ ...page.slots
188
+ ].sort((a, b)=>Number(b.role === 'hero') - Number(a.role === 'hero')).flatMap((slot)=>slot.refused ? [
189
+ {
190
+ key: `${page.screen}:${slot.image}`,
191
+ picks: slot.refused,
192
+ slot: slotOf(slot.image, slot)
193
+ }
194
+ ] : [])),
195
+ ...lists.flatMap((list)=>list.records.flatMap((record, index)=>record.refused && aiSiteIsStarterSrc(record.value) ? [
196
+ {
197
+ key: `${list.dataset}:${index}`,
198
+ picks: record.refused,
199
+ slot: _extends({}, slotOf(`record${index}`, {
200
+ words: record.name,
201
+ role: list.people ? 'about' : 'gallery',
202
+ aspect: list.people ? 4 / 5 : 4 / 3
203
+ }), {
204
+ // A record's name is a search's words, never what its photo shows (AGL-3856).
205
+ standIn: true
206
+ })
207
+ }
208
+ ] : []))
209
+ ];
210
+ if (asked.length && provider && ingest) {
211
+ const last = aiSitePhotosLastRefusalMs({
212
+ pages,
213
+ lists
214
+ });
215
+ const wait = last === null ? 0 : Math.min(AI_SITE_LATE_PHOTOS_WAIT_MS, Math.max(0, last + AI_SITE_LATE_PHOTOS_WAIT_MS - now()));
216
+ if (wait > 0) await sleep(wait, signal);
217
+ // The pass's own time ran out while it waited: the next pass picks it up.
218
+ if (signal == null ? void 0 : signal.aborted) return _extends({
219
+ photos: input.photos
220
+ }, counts);
221
+ const timeout = AbortSignal.timeout(AI_SITE_LATE_PHOTOS_COPY_MS);
222
+ const copying = signal ? AbortSignal.any([
223
+ signal,
224
+ timeout
225
+ ]) : timeout;
226
+ const words = aiStockPhotoWordsOf(job);
227
+ const proper = aiStockProperWords(aiSiteWords(job.inputs).about);
228
+ /**
229
+ * Whether a pick not in the site's library yet is still downloaded:
230
+ * not once the library refused to store one, nor once two downloads
231
+ * in a row were refused, which says the burst is not over. The picks
232
+ * a later page copied are still looked up in the library.
233
+ */ let downloading = true;
234
+ let refusedInARow = 0;
235
+ for (const entry of asked){
236
+ if (copying.aborted) break;
237
+ counts.retried += 1;
238
+ const found = await provider.search(entry.picks.request, {
239
+ signal: copying
240
+ }).catch(()=>null);
241
+ for (const sourceKey of entry.picks.keys){
242
+ var _aiStockPhotoAlt;
243
+ if (copying.aborted) break;
244
+ const photo = found == null ? void 0 : found.photos.find((hit)=>stockPhotoSourceKey(hit) === sourceKey);
245
+ if (!photo) continue;
246
+ const alt = (_aiStockPhotoAlt = aiStockPhotoAlt(photo, entry.slot, _extends({}, words.language ? {
247
+ language: words.language
248
+ } : {}, {
249
+ proper
250
+ }))) != null ? _aiStockPhotoAlt : entry.slot.alt;
251
+ const kept = await ingest.findStockPhoto({
252
+ hostId,
253
+ sourceKey
254
+ }).catch(()=>null);
255
+ if (kept) {
256
+ var _kept_width, _kept_height;
257
+ retried.set(entry.key, {
258
+ src: kept.src,
259
+ width: (_kept_width = kept.width) != null ? _kept_width : photo.width,
260
+ height: (_kept_height = kept.height) != null ? _kept_height : photo.height,
261
+ alt
262
+ });
263
+ break;
264
+ }
265
+ if (!downloading) continue;
266
+ const bytes = await provider.download(photo, {
267
+ maxBytes: AI_LAYOUT_STOCK_MAX_BYTES,
268
+ signal: copying
269
+ }).catch(()=>null);
270
+ if (copying.aborted) break;
271
+ if (!bytes) {
272
+ refusedInARow += 1;
273
+ if (refusedInARow >= AI_SITE_LATE_PHOTOS_REFUSALS_IN_A_ROW) downloading = false;
274
+ continue;
275
+ }
276
+ refusedInARow = 0;
277
+ const stored = await ingest.ingest(aiStockIngestRequest(provider, {
278
+ hostId,
279
+ uid: job.createdBy,
280
+ photo,
281
+ bytes,
282
+ alt,
283
+ query: entry.picks.request.query
284
+ })).catch(()=>null);
285
+ if (stored == null ? void 0 : stored.ok) {
286
+ var _stored_width, _stored_height;
287
+ retried.set(entry.key, {
288
+ src: stored.src,
289
+ width: (_stored_width = stored.width) != null ? _stored_width : photo.width,
290
+ height: (_stored_height = stored.height) != null ? _stored_height : photo.height,
291
+ alt
292
+ });
293
+ counts.copied += 1;
294
+ break;
295
+ }
296
+ // A refusal of this photo's own bytes tries the next pick; any other holds for the pass (AGL-3923).
297
+ if (!stored || !AI_STOCK_PHOTO_OWN_REFUSALS.has(stored.status)) downloading = false;
298
+ }
299
+ }
300
+ }
301
+ // The pass's own time ran out while it copied: the next pass finds what it copied in the library.
302
+ if (signal == null ? void 0 : signal.aborted) return _extends({
303
+ photos: input.photos
304
+ }, counts);
305
+ // ── 2. The job's own photos, for every picture still on a starter ──
306
+ const read = new Map();
307
+ for (const id of built){
308
+ var _ref;
309
+ const stored = await readNodes(input.firestore, {
310
+ kind: 'screen',
311
+ hostId,
312
+ id
313
+ }).catch(()=>null);
314
+ read.set(id, (_ref = stored == null ? void 0 : stored.nodes) != null ? _ref : null);
315
+ }
316
+ const seen = new Set();
317
+ const pool = [
318
+ ...[
319
+ ...read.values()
320
+ ].flatMap((nodes)=>aiStockPlacedPhotos(nodes)),
321
+ ...lists.flatMap((list)=>list.records.flatMap((record)=>record.photo ? [
322
+ record.photo
323
+ ] : [])),
324
+ ...aiSiteDatasetPlacedPhotoDetails((_job_outputs = job.outputs) != null ? _job_outputs : []),
325
+ ...retried.values()
326
+ ].filter((photo)=>photo.src.startsWith('media:') && !seen.has(photo.src) && Boolean(seen.add(photo.src)));
327
+ const origin = aiOriginJobId(job);
328
+ for (const page of pages){
329
+ const nodes = read.get(page.screen);
330
+ if (!nodes) continue;
331
+ const images = aiSitePageImages(nodes);
332
+ const entries = new Map(page.slots.map((slot)=>[
333
+ slot.image,
334
+ slot
335
+ ]));
336
+ const slots = images.map((image)=>{
337
+ var _entries_get;
338
+ return slotOf(image.imageId, (_entries_get = entries.get(image.imageId)) != null ? _entries_get : null);
339
+ });
340
+ const placed = images.map((image)=>aiSiteIsStarterSrc(image.src) ? null : {
341
+ src: image.src,
342
+ width: image.width,
343
+ height: image.height,
344
+ alt: image.alt
345
+ });
346
+ const need = images.map((image)=>entries.has(image.imageId) && aiSiteIsStarterSrc(image.src));
347
+ const photos = images.map(()=>null);
348
+ // (a) A picture's own pick first, where the page does not show it yet;
349
+ // else it is one of the job's photos like any other.
350
+ images.forEach((image, index)=>{
351
+ const own = need[index] ? retried.get(`${page.screen}:${image.imageId}`) : undefined;
352
+ if (!own || placed.some((photo)=>(photo == null ? void 0 : photo.src) === own.src)) return;
353
+ photos[index] = own;
354
+ placed[index] = own;
355
+ need[index] = false;
356
+ });
357
+ // (b) Then the job's photos, by the matcher the page's own pass used: one
358
+ // the page does not show first; (c) a repeat only where nothing else is
359
+ // left, since the other choice is a starter that shows another business.
360
+ if (pool.length) {
361
+ aiLayoutJobPhotoPicks(slots, need, pool, placed, `${origin}:${page.screen}:late`, {
362
+ repeats: 'last'
363
+ }).forEach((pick, index)=>{
364
+ if (pick && need[index]) photos[index] = pick;
365
+ });
366
+ }
367
+ const starters = images.filter((image)=>entries.has(image.imageId) && aiSiteIsStarterSrc(image.src)).length;
368
+ const plan = images.flatMap((image, index)=>{
369
+ const photo = photos[index];
370
+ return photo ? [
371
+ {
372
+ slot: slots[index],
373
+ photo
374
+ }
375
+ ] : [];
376
+ });
377
+ const update = plan.length ? await updateNodes(input.firestore, {
378
+ kind: 'screen',
379
+ hostId,
380
+ id: page.screen,
381
+ now: input.now,
382
+ update: (current)=>aiSitePageWithPhotos(current, plan)
383
+ }) : null;
384
+ const filled = (update == null ? void 0 : update.ok) ? plan.length : 0;
385
+ counts.filled += filled;
386
+ counts.left += Math.max(0, starters - filled);
387
+ }
388
+ for (const list of lists){
389
+ const filled = aiSiteListPhotos(list, retried, pool, `${origin}:${list.dataset}:late`);
390
+ const records = filled.flatMap((photo, index)=>{
391
+ var _ref;
392
+ var _photo_alt;
393
+ const record = list.records[index];
394
+ const value = photo ? aiSiteProductPhotoSrc(photo.src, hostId) : null;
395
+ if (!record || !photo || !value || !aiSiteIsStarterSrc(record.value)) return [];
396
+ return [
397
+ {
398
+ order: index,
399
+ // The starter's words go with it: a photo that brings none leaves the description empty (AGL-3856).
400
+ values: _extends({
401
+ [list.imageField]: value
402
+ }, list.altField ? {
403
+ [list.altField]: (_ref = (_photo_alt = photo.alt) == null ? void 0 : _photo_alt.trim()) != null ? _ref : ''
404
+ } : {}),
405
+ where: {
406
+ [list.imageField]: record.value
407
+ }
408
+ }
409
+ ];
410
+ });
411
+ const starters = list.records.filter((record)=>aiSiteIsStarterSrc(record.value)).length;
412
+ if (!records.length) {
413
+ counts.left += starters;
414
+ continue;
415
+ }
416
+ const keeper = writerFor(AI_DATASET_DRAFT_RESOURCE);
417
+ if (!(keeper == null ? void 0 : keeper.writer.reviseRecords)) {
418
+ console.warn('ai site late photos: the dataset writer cannot change records here', {
419
+ jobId: job.$id,
420
+ dataset: list.dataset
421
+ });
422
+ counts.left += starters;
423
+ continue;
424
+ }
425
+ const revised = await keeper.writer.reviseRecords({
426
+ orgId: job.orgId,
427
+ hostId,
428
+ uid: job.createdBy,
429
+ org: input.org,
430
+ now: input.now,
431
+ id: list.dataset,
432
+ records
433
+ });
434
+ const changed = revised.ok ? revised.changed : 0;
435
+ if (revised.ok === false) {
436
+ console.warn('ai site late photos: the dataset writer refused the covers', {
437
+ jobId: job.$id,
438
+ dataset: list.dataset,
439
+ status: revised.status
440
+ });
441
+ }
442
+ counts.filled += changed;
443
+ counts.left += Math.max(0, starters - changed);
444
+ }
445
+ console.info('ai site late photos', _extends({
446
+ jobId: job.$id,
447
+ hostId
448
+ }, counts));
449
+ return done();
450
+ } catch (error) {
451
+ // The site is built either way; its pictures keep what they hold.
452
+ if (signal == null ? void 0 : signal.aborted) return _extends({
453
+ photos: input.photos
454
+ }, counts);
455
+ console.error('ai site late photos failed; the site keeps its pictures as they are', {
456
+ jobId: job.$id,
457
+ error: String(error)
458
+ });
459
+ return done();
460
+ }
461
+ }
462
+ /**
463
+ * A list's records' photos once the late pass is done (AGL-3988): each
464
+ * record's own refused pick where it was copied, then, for a list of things,
465
+ * one of the photos it or the job shows (`aiSiteDatasetRecordsFromOwn`). A
466
+ * team's records take only their own picks: no photo of the job's is of
467
+ * one of the business's people. Pure.
468
+ */ export function aiSiteListPhotos(list, retried, pool, seed) {
469
+ const current = list.records.map((record, index)=>{
470
+ var _retried_get, _record_photo;
471
+ return aiSiteIsStarterSrc(record.value) ? (_retried_get = retried.get(`${list.dataset}:${index}`)) != null ? _retried_get : null : (_record_photo = record.photo) != null ? _record_photo : null;
472
+ });
473
+ if (list.people) return current;
474
+ return aiSiteDatasetRecordsFromOwn(list.records.map((record)=>{
475
+ var _record_category;
476
+ return {
477
+ name: record.name,
478
+ category: (_record_category = record.category) != null ? _record_category : null
479
+ };
480
+ }), current, seed, pool, // A starter is the other choice: a record every photo sits beside takes one anyway.
481
+ {
482
+ besideLast: true
483
+ });
484
+ }
485
+
486
+ //# sourceMappingURL=ai-site-late-photos.js.map