@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.
- package/package.json +17 -17
- package/src/lib/components/ai-site-start-card.component.js +237 -234
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/jobs/ai-empty-site-sweep.d.ts +12 -1
- package/src/lib/jobs/ai-empty-site-sweep.js +15 -3
- package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +21 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.js +8 -4
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +39 -1
- package/src/lib/jobs/ai-job-site-datasets.js +111 -15
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +12 -3
- package/src/lib/jobs/ai-job-site-step.js +85 -11
- 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 +3 -0
- package/src/lib/jobs/ai-jobs.js +4 -0
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-owner-photos.js +4 -4
- package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +35 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +114 -43
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-late-photos.d.ts +143 -0
- package/src/lib/jobs/ai-site-late-photos.js +486 -0
- package/src/lib/jobs/ai-site-late-photos.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +18 -0
- package/src/lib/jobs/ai-site-publish.js +123 -5
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.d.ts +83 -0
- package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js +1553 -0
- package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js.map +1 -0
- package/src/lib/layout-language/ai-layout-pictures.d.ts +50 -13
- package/src/lib/layout-language/ai-layout-pictures.js +81 -19
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +10 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-photos.d.ts +119 -0
- package/src/lib/model/ai-site-photos.js +128 -0
- package/src/lib/model/ai-site-photos.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +3 -1
- package/src/lib/runtime/ai-doctrine-validators.js +3 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/seo-fields.d.ts +29 -4
- package/src/lib/runtime/seo-fields.js +58 -11
- package/src/lib/runtime/seo-fields.js.map +1 -1
- package/src/lib/tools/ai-seo-tool.d.ts +25 -1
- package/src/lib/tools/ai-seo-tool.js +61 -0
- 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
|