@aglyn/plugins-ai 1.0.0-beta.271 → 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 (132) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +4 -3
  3. package/src/lib/activity/ai-activity-actions.js +3 -0
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-site-build-page.component.js +18 -0
  6. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  7. package/src/lib/components/ai-site-import-job.component.d.ts +41 -0
  8. package/src/lib/components/ai-site-import-job.component.js +707 -0
  9. package/src/lib/components/ai-site-import-job.component.js.map +1 -0
  10. package/src/lib/components/ai-site-import-start.component.d.ts +64 -0
  11. package/src/lib/components/ai-site-import-start.component.js +335 -0
  12. package/src/lib/components/ai-site-import-start.component.js.map +1 -0
  13. package/src/lib/components/ai-site-ownership-proof.component.d.ts +42 -0
  14. package/src/lib/components/ai-site-ownership-proof.component.js +353 -0
  15. package/src/lib/components/ai-site-ownership-proof.component.js.map +1 -0
  16. package/src/lib/components/ai-site-start-card.component.js +329 -252
  17. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  18. package/src/lib/jobs/ai-empty-site-sweep.d.ts +12 -1
  19. package/src/lib/jobs/ai-empty-site-sweep.js +15 -3
  20. package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -1
  21. package/src/lib/jobs/ai-job-drafts.d.ts +14 -0
  22. package/src/lib/jobs/ai-job-drafts.js +24 -0
  23. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  24. package/src/lib/jobs/ai-job-import-step.d.ts +136 -0
  25. package/src/lib/jobs/ai-job-import-step.js +809 -0
  26. package/src/lib/jobs/ai-job-import-step.js.map +1 -0
  27. package/src/lib/jobs/ai-job-page-language.d.ts +39 -1
  28. package/src/lib/jobs/ai-job-page-language.js +62 -4
  29. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  30. package/src/lib/jobs/ai-job-page-step.d.ts +2 -2
  31. package/src/lib/jobs/ai-job-page-step.js +26 -4
  32. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  33. package/src/lib/jobs/ai-job-seo-step.js +8 -4
  34. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-site-datasets.d.ts +39 -1
  36. package/src/lib/jobs/ai-job-site-datasets.js +111 -15
  37. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  38. package/src/lib/jobs/ai-job-site-step.d.ts +12 -3
  39. package/src/lib/jobs/ai-job-site-step.js +85 -11
  40. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  41. package/src/lib/jobs/ai-job-text-step.d.ts +15 -0
  42. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  43. package/src/lib/jobs/ai-jobs.d.ts +8 -0
  44. package/src/lib/jobs/ai-jobs.js +19 -3
  45. package/src/lib/jobs/ai-jobs.js.map +1 -1
  46. package/src/lib/jobs/ai-layout-owner-photos.d.ts +16 -1
  47. package/src/lib/jobs/ai-layout-owner-photos.js +49 -2
  48. package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
  49. package/src/lib/jobs/ai-layout-stock-photos.d.ts +35 -0
  50. package/src/lib/jobs/ai-layout-stock-photos.js +114 -43
  51. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  52. package/src/lib/jobs/ai-site-late-photos.d.ts +143 -0
  53. package/src/lib/jobs/ai-site-late-photos.js +486 -0
  54. package/src/lib/jobs/ai-site-late-photos.js.map +1 -0
  55. package/src/lib/jobs/ai-site-publish.d.ts +18 -0
  56. package/src/lib/jobs/ai-site-publish.js +123 -5
  57. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  58. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +61 -3
  59. package/src/lib/jobs/ai-theme-brand-inputs.js +117 -27
  60. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  61. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.d.ts +83 -0
  62. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js +1553 -0
  63. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js.map +1 -0
  64. package/src/lib/layout-language/ai-layout-pictures.d.ts +50 -13
  65. package/src/lib/layout-language/ai-layout-pictures.js +81 -19
  66. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  67. package/src/lib/model/ai-credit-estimate.d.ts +20 -0
  68. package/src/lib/model/ai-credit-estimate.js +13 -0
  69. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  70. package/src/lib/model/ai-job-activity.d.ts +2 -2
  71. package/src/lib/model/ai-job-activity.js +6 -4
  72. package/src/lib/model/ai-job-activity.js.map +1 -1
  73. package/src/lib/model/ai-job-notice.js +18 -4
  74. package/src/lib/model/ai-job-notice.js.map +1 -1
  75. package/src/lib/model/ai-jobs.types.d.ts +39 -2
  76. package/src/lib/model/ai-jobs.types.js +2 -1
  77. package/src/lib/model/ai-jobs.types.js.map +1 -1
  78. package/src/lib/model/ai-site-import-view.d.ts +130 -0
  79. package/src/lib/model/ai-site-import-view.js +182 -0
  80. package/src/lib/model/ai-site-import-view.js.map +1 -0
  81. package/src/lib/model/ai-site-import.d.ts +302 -0
  82. package/src/lib/model/ai-site-import.js +526 -0
  83. package/src/lib/model/ai-site-import.js.map +1 -0
  84. package/src/lib/model/ai-site-photos.d.ts +119 -0
  85. package/src/lib/model/ai-site-photos.js +128 -0
  86. package/src/lib/model/ai-site-photos.js.map +1 -0
  87. package/src/lib/model/ai-site-start.d.ts +22 -9
  88. package/src/lib/model/ai-site-start.js +64 -23
  89. package/src/lib/model/ai-site-start.js.map +1 -1
  90. package/src/lib/runtime/ai-doctrine-validators.d.ts +3 -1
  91. package/src/lib/runtime/ai-doctrine-validators.js +3 -1
  92. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  93. package/src/lib/runtime/seo-fields.d.ts +29 -4
  94. package/src/lib/runtime/seo-fields.js +58 -11
  95. package/src/lib/runtime/seo-fields.js.map +1 -1
  96. package/src/lib/server/ai-jobs-route.js +11 -0
  97. package/src/lib/server/ai-jobs-route.js.map +1 -1
  98. package/src/lib/server/ai-site-import-route.d.ts +89 -0
  99. package/src/lib/server/ai-site-import-route.js +356 -0
  100. package/src/lib/server/ai-site-import-route.js.map +1 -0
  101. package/src/lib/server.js +14 -0
  102. package/src/lib/server.js.map +1 -1
  103. package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +45 -0
  104. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +313 -0
  105. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -0
  106. package/src/lib/site-import/fixtures/site-import-wave-recordings.d.ts +50 -0
  107. package/src/lib/site-import/fixtures/site-import-wave-recordings.js +217 -0
  108. package/src/lib/site-import/fixtures/site-import-wave-recordings.js.map +1 -0
  109. package/src/lib/site-import/site-import-crawl.d.ts +163 -0
  110. package/src/lib/site-import/site-import-crawl.js +467 -0
  111. package/src/lib/site-import/site-import-crawl.js.map +1 -0
  112. package/src/lib/site-import/site-import-flag.d.ts +23 -0
  113. package/src/lib/site-import/site-import-flag.js +23 -0
  114. package/src/lib/site-import/site-import-flag.js.map +1 -0
  115. package/src/lib/site-import/site-import-html.d.ts +94 -0
  116. package/src/lib/site-import/site-import-html.js +462 -0
  117. package/src/lib/site-import/site-import-html.js.map +1 -0
  118. package/src/lib/site-import/site-import-images.d.ts +54 -0
  119. package/src/lib/site-import/site-import-images.js +204 -0
  120. package/src/lib/site-import/site-import-images.js.map +1 -0
  121. package/src/lib/site-import/site-import-page.d.ts +48 -0
  122. package/src/lib/site-import/site-import-page.js +150 -0
  123. package/src/lib/site-import/site-import-page.js.map +1 -0
  124. package/src/lib/site-import/site-import-review.d.ts +40 -0
  125. package/src/lib/site-import/site-import-review.js +149 -0
  126. package/src/lib/site-import/site-import-review.js.map +1 -0
  127. package/src/lib/site-import/site-import-robots.d.ts +52 -0
  128. package/src/lib/site-import/site-import-robots.js +126 -0
  129. package/src/lib/site-import/site-import-robots.js.map +1 -0
  130. package/src/lib/tools/ai-seo-tool.d.ts +25 -1
  131. package/src/lib/tools/ai-seo-tool.js +61 -0
  132. package/src/lib/tools/ai-seo-tool.js.map +1 -1
@@ -49,9 +49,24 @@ export declare function aiOwnerPhotoFits(slot: AiLayoutPictureSlot): boolean;
49
49
  * search, in slot order.
50
50
  */
51
51
  export declare function aiAssignOwnerPhotos(slots: readonly AiLayoutPictureSlot[], photos: readonly PluginMediaAsset[], avoid?: ReadonlySet<string>): Array<AiLayoutPicturePhoto | null>;
52
+ /** The unit input carrying the photos a page brought in from the owner's own site (AGL-3976). */
53
+ export declare const AI_LAYOUT_PAGE_PHOTOS_INPUT = "pagePhotos";
54
+ /** The page's own photos a job's inputs carry, read defensively: library assets with their sizes only. */
55
+ export declare function aiLayoutPagePhotosOf(inputs: Readonly<Record<string, unknown>> | null | undefined): PluginMediaAsset[];
56
+ /**
57
+ * The photos a page brought in from the owner's own site placed first
58
+ * (AGL-3976): the page's own pictures, already in the site's library, fill
59
+ * the slots they fit, in the same order an owner's photos do and with no
60
+ * job-wide repeat rule, since a team photo the old site showed twice is the
61
+ * owner's to show twice. The slots left go to `next`.
62
+ */
63
+ export declare function aiLayoutPagePhotoSource(photos: readonly PluginMediaAsset[], next: AiLayoutPictureSource | null): AiLayoutPictureSource | null;
52
64
  /**
53
65
  * The source for one page: the owner's photos for the slots they fit, the
54
66
  * stock library for the rest. `null` only when neither can answer — no
55
67
  * media door and no stock library — which leaves every slot to the starters.
68
+ * A page a site import brings in (AGL-3976) places its own pictures first.
56
69
  */
57
- export declare function aiLayoutSitePhotoSource(input: AiLayoutStockPhotoSourceInput, deps?: AiLayoutOwnerPhotoDeps): AiLayoutPictureSource | null;
70
+ export declare function aiLayoutSitePhotoSource(input: AiLayoutStockPhotoSourceInput & {
71
+ pagePhotos?: readonly PluginMediaAsset[];
72
+ }, deps?: AiLayoutOwnerPhotoDeps): AiLayoutPictureSource | null;
@@ -67,18 +67,65 @@ function area(photo) {
67
67
  }
68
68
  return answers;
69
69
  }
70
+ /** The unit input carrying the photos a page brought in from the owner's own site (AGL-3976). */ export const AI_LAYOUT_PAGE_PHOTOS_INPUT = 'pagePhotos';
71
+ /** The page's own photos a job's inputs carry, read defensively: library assets with their sizes only. */ export function aiLayoutPagePhotosOf(inputs) {
72
+ const raw = inputs == null ? void 0 : inputs[AI_LAYOUT_PAGE_PHOTOS_INPUT];
73
+ if (!Array.isArray(raw)) return [];
74
+ const photos = [];
75
+ for (const entry of raw){
76
+ if (!entry || typeof entry !== 'object') continue;
77
+ const { mediaId, src, width, height, alt } = entry;
78
+ if (typeof mediaId !== 'string' || typeof src !== 'string' || !src.startsWith('media:')) continue;
79
+ if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) continue;
80
+ photos.push(_extends({
81
+ mediaId,
82
+ src,
83
+ width,
84
+ height
85
+ }, typeof alt === 'string' && alt.trim() ? {
86
+ alt: alt.trim()
87
+ } : {}));
88
+ }
89
+ return photos;
90
+ }
91
+ /**
92
+ * The photos a page brought in from the owner's own site placed first
93
+ * (AGL-3976): the page's own pictures, already in the site's library, fill
94
+ * the slots they fit, in the same order an owner's photos do and with no
95
+ * job-wide repeat rule, since a team photo the old site showed twice is the
96
+ * owner's to show twice. The slots left go to `next`.
97
+ */ export function aiLayoutPagePhotoSource(photos, next) {
98
+ if (!photos.length) return next;
99
+ return async (slots, report)=>{
100
+ const answers = aiAssignOwnerPhotos(slots, photos);
101
+ if (!next) return answers;
102
+ const open = slots.filter((_slot, index)=>!answers[index]);
103
+ if (!open.length) return answers;
104
+ const found = await next(open, report);
105
+ let index = 0;
106
+ return answers.map((answer)=>{
107
+ var _ref;
108
+ return (_ref = answer != null ? answer : found[index++]) != null ? _ref : null;
109
+ });
110
+ };
111
+ }
70
112
  /**
71
113
  * The source for one page: the owner's photos for the slots they fit, the
72
114
  * stock library for the rest. `null` only when neither can answer — no
73
115
  * media door and no stock library — which leaves every slot to the starters.
116
+ * A page a site import brings in (AGL-3976) places its own pictures first.
74
117
  */ export function aiLayoutSitePhotoSource(input, deps = {}) {
118
+ var _input_pagePhotos;
119
+ return aiLayoutPagePhotoSource((_input_pagePhotos = input.pagePhotos) != null ? _input_pagePhotos : [], aiLayoutOwnerAndStockSource(input, deps));
120
+ }
121
+ function aiLayoutOwnerAndStockSource(input, deps) {
75
122
  var _deps_stock, _deps_ingest;
76
123
  var _ingest_listOwnerPhotos;
77
124
  const stock = ((_deps_stock = deps.stock) != null ? _deps_stock : aiLayoutStockPhotoSource)(input, deps);
78
125
  const ingest = ((_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest)();
79
126
  const list = ingest == null ? void 0 : (_ingest_listOwnerPhotos = ingest.listOwnerPhotos) == null ? void 0 : _ingest_listOwnerPhotos.bind(ingest);
80
127
  if (!list || !input.hostId) return stock;
81
- return async (slots)=>{
128
+ return async (slots, report)=>{
82
129
  var _input_avoid;
83
130
  let photos = [];
84
131
  try {
@@ -98,7 +145,7 @@ function area(photo) {
98
145
  if (!open.length) return answers;
99
146
  // The stock source sees only the slots the owner's photos left, so its
100
147
  // own no-repeat rules and search budget are spent on those alone.
101
- const found = await stock(open);
148
+ const found = await stock(open, report);
102
149
  let next = 0;
103
150
  return answers.map((answer)=>{
104
151
  var _ref;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-layout-owner-photos.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginMediaAsset } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport { pluginMediaIngest } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport type {\n AiLayoutPicturePhoto,\n AiLayoutPictureSlot,\n AiLayoutPictureSource,\n} from '../layout-language/ai-layout-pictures'\nimport {\n aiLayoutStockPhotoSource,\n type AiLayoutStockPhotoDeps,\n type AiLayoutStockPhotoSourceInput,\n} from './ai-layout-stock-photos'\n\n/**\n * THE BUSINESS'S OWN PHOTOS COME FIRST (AGL-3957).\n *\n * A site built for an owner at a booth or on a doorstep has, by the time its\n * pages are drawn, the photos they sent from their phone through a site\n * photo link: their van, their shop, their work. A photo of the owner's own\n * work beats any stock photo, so a page's picture slots are filled from\n * those first, and only the slots left over are searched for in the stock\n * library, the way they were before.\n *\n * Which photo goes where is decided by shape and size, not by subject: an\n * owner's photo says nothing a search could read. The hero takes the widest\n * photo among the largest; the about picture takes the next largest; the\n * gallery takes the rest, newest first, each photo once on the page and once\n * across the job (`avoid`). A product's photo, a person's portrait and a\n * record's cover are of one named thing, which a photo of the business in\n * general is not, so those slots go to the stock search untouched.\n */\n\nexport interface AiLayoutOwnerPhotoDeps extends AiLayoutStockPhotoDeps {\n /** The stock source the leftover slots go to; the real one otherwise. Specs hand in a fake. */\n stock?: typeof aiLayoutStockPhotoSource\n /** The most owner photos read for one page. */\n limit?: number\n}\n\nconst OWNER_PHOTOS_LIMIT = 60\n\n/** A slot an owner's photo may fill: one of the business in general, not of one named thing. */\nexport function aiOwnerPhotoFits(slot: AiLayoutPictureSlot): boolean {\n return !slot.product && !slot.people && !slot.record\n}\n\n/** Fill order: the hero first, so the biggest photo is the first one seen; then about; then the gallery in document order. */\nconst ROLE_RANK = { hero: 0, about: 1, gallery: 2 } as const\n\nfunction area(photo: PluginMediaAsset): number {\n return (photo.width ?? 0) * (photo.height ?? 0)\n}\n\n/**\n * Assigns the owner's photos to the slots, in the order above. Pure, so a\n * spec can hold the choice itself: `null` for a slot left to the stock\n * search, in slot order.\n */\nexport function aiAssignOwnerPhotos(\n slots: readonly AiLayoutPictureSlot[],\n photos: readonly PluginMediaAsset[],\n avoid: ReadonlySet<string> = new Set(),\n): Array<AiLayoutPicturePhoto | null> {\n const pool = photos\n .filter((photo) => photo.src && (photo.width ?? 0) > 0 && (photo.height ?? 0) > 0 && !avoid.has(photo.src))\n .sort((a, b) => area(b) - area(a))\n const answers: Array<AiLayoutPicturePhoto | null> = slots.map(() => null)\n const used = new Set<string>()\n const order = slots\n .map((slot, index) => ({ slot, index }))\n .filter(({ slot }) => aiOwnerPhotoFits(slot))\n .sort((a, b) => ROLE_RANK[a.slot.role] - ROLE_RANK[b.slot.role] || a.index - b.index)\n for (const { slot, index } of order) {\n const free = pool.filter((photo) => !used.has(photo.src))\n if (!free.length) break\n // The hero wants the widest of the three largest; a portrait phone photo\n // in a wide frame is the one mismatch a visitor sees first.\n const pick =\n slot.role === 'hero'\n ? [...free.slice(0, 3)].sort(\n (a, b) => (b.width ?? 0) / (b.height ?? 1) - (a.width ?? 0) / (a.height ?? 1),\n )[0]\n : free[0]\n if (!pick) break\n used.add(pick.src)\n answers[index] = {\n src: pick.src,\n width: pick.width as number,\n height: pick.height as number,\n ...(pick.alt ? { alt: pick.alt } : {}),\n }\n }\n return answers\n}\n\n/**\n * The source for one page: the owner's photos for the slots they fit, the\n * stock library for the rest. `null` only when neither can answer — no\n * media door and no stock library — which leaves every slot to the starters.\n */\nexport function aiLayoutSitePhotoSource(\n input: AiLayoutStockPhotoSourceInput,\n deps: AiLayoutOwnerPhotoDeps = {},\n): AiLayoutPictureSource | null {\n const stock = (deps.stock ?? aiLayoutStockPhotoSource)(input, deps)\n const ingest = (deps.ingest ?? pluginMediaIngest)()\n const list = ingest?.listOwnerPhotos?.bind(ingest)\n if (!list || !input.hostId) return stock\n return async (slots) => {\n let photos: PluginMediaAsset[] = []\n try {\n photos = await list({ hostId: input.hostId, limit: deps.limit ?? OWNER_PHOTOS_LIMIT })\n } catch (error) {\n console.warn('ai layout pictures: the owner photos could not be read; stock photos fill the page', { error })\n }\n const answers = aiAssignOwnerPhotos(slots, photos, new Set(input.avoid ?? []))\n if (!stock) return answers\n const open = slots.filter((_slot, index) => !answers[index])\n if (!open.length) return answers\n // The stock source sees only the slots the owner's photos left, so its\n // own no-repeat rules and search budget are spent on those alone.\n const found = await stock(open)\n let next = 0\n return answers.map((answer) => answer ?? found[next++] ?? null)\n }\n}\n"],"names":["pluginMediaIngest","aiLayoutStockPhotoSource","OWNER_PHOTOS_LIMIT","aiOwnerPhotoFits","slot","product","people","record","ROLE_RANK","hero","about","gallery","area","photo","width","height","aiAssignOwnerPhotos","slots","photos","avoid","Set","pool","filter","src","has","sort","a","b","answers","map","used","order","index","role","free","length","pick","slice","add","alt","aiLayoutSitePhotoSource","input","deps","ingest","stock","list","listOwnerPhotos","bind","hostId","limit","error","console","warn","open","_slot","found","next","answer"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-layout-owner-photos.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginMediaAsset } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport { pluginMediaIngest } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport type {\n AiLayoutPicturePhoto,\n AiLayoutPictureSlot,\n AiLayoutPictureSource,\n} from '../layout-language/ai-layout-pictures'\nimport {\n aiLayoutStockPhotoSource,\n type AiLayoutStockPhotoDeps,\n type AiLayoutStockPhotoSourceInput,\n} from './ai-layout-stock-photos'\n\n/**\n * THE BUSINESS'S OWN PHOTOS COME FIRST (AGL-3957).\n *\n * A site built for an owner at a booth or on a doorstep has, by the time its\n * pages are drawn, the photos they sent from their phone through a site\n * photo link: their van, their shop, their work. A photo of the owner's own\n * work beats any stock photo, so a page's picture slots are filled from\n * those first, and only the slots left over are searched for in the stock\n * library, the way they were before.\n *\n * Which photo goes where is decided by shape and size, not by subject: an\n * owner's photo says nothing a search could read. The hero takes the widest\n * photo among the largest; the about picture takes the next largest; the\n * gallery takes the rest, newest first, each photo once on the page and once\n * across the job (`avoid`). A product's photo, a person's portrait and a\n * record's cover are of one named thing, which a photo of the business in\n * general is not, so those slots go to the stock search untouched.\n */\n\nexport interface AiLayoutOwnerPhotoDeps extends AiLayoutStockPhotoDeps {\n /** The stock source the leftover slots go to; the real one otherwise. Specs hand in a fake. */\n stock?: typeof aiLayoutStockPhotoSource\n /** The most owner photos read for one page. */\n limit?: number\n}\n\nconst OWNER_PHOTOS_LIMIT = 60\n\n/** A slot an owner's photo may fill: one of the business in general, not of one named thing. */\nexport function aiOwnerPhotoFits(slot: AiLayoutPictureSlot): boolean {\n return !slot.product && !slot.people && !slot.record\n}\n\n/** Fill order: the hero first, so the biggest photo is the first one seen; then about; then the gallery in document order. */\nconst ROLE_RANK = { hero: 0, about: 1, gallery: 2 } as const\n\nfunction area(photo: PluginMediaAsset): number {\n return (photo.width ?? 0) * (photo.height ?? 0)\n}\n\n/**\n * Assigns the owner's photos to the slots, in the order above. Pure, so a\n * spec can hold the choice itself: `null` for a slot left to the stock\n * search, in slot order.\n */\nexport function aiAssignOwnerPhotos(\n slots: readonly AiLayoutPictureSlot[],\n photos: readonly PluginMediaAsset[],\n avoid: ReadonlySet<string> = new Set(),\n): Array<AiLayoutPicturePhoto | null> {\n const pool = photos\n .filter((photo) => photo.src && (photo.width ?? 0) > 0 && (photo.height ?? 0) > 0 && !avoid.has(photo.src))\n .sort((a, b) => area(b) - area(a))\n const answers: Array<AiLayoutPicturePhoto | null> = slots.map(() => null)\n const used = new Set<string>()\n const order = slots\n .map((slot, index) => ({ slot, index }))\n .filter(({ slot }) => aiOwnerPhotoFits(slot))\n .sort((a, b) => ROLE_RANK[a.slot.role] - ROLE_RANK[b.slot.role] || a.index - b.index)\n for (const { slot, index } of order) {\n const free = pool.filter((photo) => !used.has(photo.src))\n if (!free.length) break\n // The hero wants the widest of the three largest; a portrait phone photo\n // in a wide frame is the one mismatch a visitor sees first.\n const pick =\n slot.role === 'hero'\n ? [...free.slice(0, 3)].sort(\n (a, b) => (b.width ?? 0) / (b.height ?? 1) - (a.width ?? 0) / (a.height ?? 1),\n )[0]\n : free[0]\n if (!pick) break\n used.add(pick.src)\n answers[index] = {\n src: pick.src,\n width: pick.width as number,\n height: pick.height as number,\n ...(pick.alt ? { alt: pick.alt } : {}),\n }\n }\n return answers\n}\n\n/** The unit input carrying the photos a page brought in from the owner's own site (AGL-3976). */\nexport const AI_LAYOUT_PAGE_PHOTOS_INPUT = 'pagePhotos'\n\n/** The page's own photos a job's inputs carry, read defensively: library assets with their sizes only. */\nexport function aiLayoutPagePhotosOf(inputs: Readonly<Record<string, unknown>> | null | undefined): PluginMediaAsset[] {\n const raw = inputs?.[AI_LAYOUT_PAGE_PHOTOS_INPUT]\n if (!Array.isArray(raw)) return []\n const photos: PluginMediaAsset[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const { mediaId, src, width, height, alt } = entry as Record<string, unknown>\n if (typeof mediaId !== 'string' || typeof src !== 'string' || !src.startsWith('media:')) continue\n if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) continue\n photos.push({ mediaId, src, width, height, ...(typeof alt === 'string' && alt.trim() ? { alt: alt.trim() } : {}) })\n }\n return photos\n}\n\n/**\n * The photos a page brought in from the owner's own site placed first\n * (AGL-3976): the page's own pictures, already in the site's library, fill\n * the slots they fit, in the same order an owner's photos do and with no\n * job-wide repeat rule, since a team photo the old site showed twice is the\n * owner's to show twice. The slots left go to `next`.\n */\nexport function aiLayoutPagePhotoSource(\n photos: readonly PluginMediaAsset[],\n next: AiLayoutPictureSource | null,\n): AiLayoutPictureSource | null {\n if (!photos.length) return next\n return async (slots, report) => {\n const answers = aiAssignOwnerPhotos(slots, photos)\n if (!next) return answers\n const open = slots.filter((_slot, index) => !answers[index])\n if (!open.length) return answers\n const found = await next(open, report)\n let index = 0\n return answers.map((answer) => answer ?? found[index++] ?? null)\n }\n}\n\n/**\n * The source for one page: the owner's photos for the slots they fit, the\n * stock library for the rest. `null` only when neither can answer — no\n * media door and no stock library — which leaves every slot to the starters.\n * A page a site import brings in (AGL-3976) places its own pictures first.\n */\nexport function aiLayoutSitePhotoSource(\n input: AiLayoutStockPhotoSourceInput & { pagePhotos?: readonly PluginMediaAsset[] },\n deps: AiLayoutOwnerPhotoDeps = {},\n): AiLayoutPictureSource | null {\n return aiLayoutPagePhotoSource(input.pagePhotos ?? [], aiLayoutOwnerAndStockSource(input, deps))\n}\n\nfunction aiLayoutOwnerAndStockSource(\n input: AiLayoutStockPhotoSourceInput,\n deps: AiLayoutOwnerPhotoDeps,\n): AiLayoutPictureSource | null {\n const stock = (deps.stock ?? aiLayoutStockPhotoSource)(input, deps)\n const ingest = (deps.ingest ?? pluginMediaIngest)()\n const list = ingest?.listOwnerPhotos?.bind(ingest)\n if (!list || !input.hostId) return stock\n return async (slots, report) => {\n let photos: PluginMediaAsset[] = []\n try {\n photos = await list({ hostId: input.hostId, limit: deps.limit ?? OWNER_PHOTOS_LIMIT })\n } catch (error) {\n console.warn('ai layout pictures: the owner photos could not be read; stock photos fill the page', { error })\n }\n const answers = aiAssignOwnerPhotos(slots, photos, new Set(input.avoid ?? []))\n if (!stock) return answers\n const open = slots.filter((_slot, index) => !answers[index])\n if (!open.length) return answers\n // The stock source sees only the slots the owner's photos left, so its\n // own no-repeat rules and search budget are spent on those alone.\n const found = await stock(open, report)\n let next = 0\n return answers.map((answer) => answer ?? found[next++] ?? null)\n }\n}\n"],"names":["pluginMediaIngest","aiLayoutStockPhotoSource","OWNER_PHOTOS_LIMIT","aiOwnerPhotoFits","slot","product","people","record","ROLE_RANK","hero","about","gallery","area","photo","width","height","aiAssignOwnerPhotos","slots","photos","avoid","Set","pool","filter","src","has","sort","a","b","answers","map","used","order","index","role","free","length","pick","slice","add","alt","AI_LAYOUT_PAGE_PHOTOS_INPUT","aiLayoutPagePhotosOf","inputs","raw","Array","isArray","entry","mediaId","startsWith","push","trim","aiLayoutPagePhotoSource","next","report","open","_slot","found","answer","aiLayoutSitePhotoSource","input","deps","pagePhotos","aiLayoutOwnerAndStockSource","ingest","stock","list","listOwnerPhotos","bind","hostId","limit","error","console","warn"],"mappings":"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"}
@@ -184,6 +184,14 @@ export declare const AI_STOCK_PHOTO_AVOID_INPUT = "stockPhotoAvoid";
184
184
  * (AGL-3988): what a picture the library failed takes before any starter.
185
185
  */
186
186
  export declare const AI_STOCK_PHOTO_JOB_PHOTOS_INPUT = "stockPhotoJobPhotos";
187
+ /**
188
+ * The unit job input a guided start's page is built under (AGL-3988): its
189
+ * site step runs a late photo pass once every page is built, so a picture
190
+ * the library failed that none of the job's photos fits yet keeps a neutral
191
+ * starter for that pass to fill, rather than lose its frame, and the page
192
+ * reports it. A page built by any other job keeps #1831's rule.
193
+ */
194
+ export declare const AI_STOCK_PHOTO_LATE_PASS_INPUT = "stockPhotoLatePass";
187
195
  /** The job photos a unit input holds ({@link AI_STOCK_PHOTO_JOB_PHOTOS_INPUT}): library photos with a size; anything else is dropped. */
188
196
  export declare function aiStockJobPhotosOf(value: unknown): AiLayoutPicturePhoto[];
189
197
  /**
@@ -735,7 +743,34 @@ export interface AiLayoutStockPhotoDeps {
735
743
  provider?: () => StockPhotoProvider | null;
736
744
  ingest?: () => PluginMediaIngest | null;
737
745
  budgetMs?: number;
746
+ /** The clock a refusal is dated by (AGL-3988); specs hand in a fixed one. */
747
+ now?: () => number;
738
748
  }
749
+ /** A search as it is sent to the library: its words and size, none of how its hits are judged. */
750
+ export declare function aiStockSearchRequest(wanted: AiStockSearch): StockPhotoSearchRequest;
751
+ /**
752
+ * A search as a job keeps it to ask again (AGL-3988): only what a library's
753
+ * request is made of, so asked again it is the same request, which the
754
+ * provider's 24-hour cache answers.
755
+ */
756
+ export declare function aiStockKeptRequest(request: StockPhotoSearchRequest): StockPhotoSearchRequest;
757
+ /**
758
+ * The library's own record of a photo copied into a site (AGL-3923, AGL-3988):
759
+ * what the media library stores beside the bytes as `stockPhoto`, with the
760
+ * credit its license asks for. One shape for every copy a job makes, a
761
+ * page's and the late photo pass's.
762
+ */
763
+ export declare function aiStockIngestRequest(provider: Pick<StockPhotoProvider, 'credit'>, input: {
764
+ hostId: string;
765
+ uid: string;
766
+ photo: StockPhoto;
767
+ bytes: {
768
+ bytes: Uint8Array;
769
+ contentType: string;
770
+ };
771
+ alt: string;
772
+ query: string;
773
+ }): Parameters<PluginMediaIngest['ingest']>[0];
739
774
  /**
740
775
  * A job's language and English photo words (AGL-3760), the English name of
741
776
  * its kind of site (AGL-3771), and the business's name and audience as the
@@ -183,6 +183,13 @@ import { aiStockBusinessNoun } from "./ai-stock-business-nouns.js";
183
183
  * outside its pages — a dataset's records — with their size and words
184
184
  * (AGL-3988): what a picture the library failed takes before any starter.
185
185
  */ export const AI_STOCK_PHOTO_JOB_PHOTOS_INPUT = 'stockPhotoJobPhotos';
186
+ /**
187
+ * The unit job input a guided start's page is built under (AGL-3988): its
188
+ * site step runs a late photo pass once every page is built, so a picture
189
+ * the library failed that none of the job's photos fits yet keeps a neutral
190
+ * starter for that pass to fill, rather than lose its frame, and the page
191
+ * reports it. A page built by any other job keeps #1831's rule.
192
+ */ export const AI_STOCK_PHOTO_LATE_PASS_INPUT = 'stockPhotoLatePass';
186
193
  /** The job photos a unit input holds ({@link AI_STOCK_PHOTO_JOB_PHOTOS_INPUT}): library photos with a size; anything else is dropped. */ export function aiStockJobPhotosOf(value) {
187
194
  if (!Array.isArray(value)) return [];
188
195
  return value.flatMap((entry)=>{
@@ -3643,6 +3650,80 @@ const EXTENSIONS = {
3643
3650
  'image/png': 'png',
3644
3651
  'image/webp': 'webp'
3645
3652
  };
3653
+ /** The words of a search that only this module reads, never sent to a library. */ const SEARCH_JUDGMENT_KEYS = [
3654
+ 'broad',
3655
+ 'subject',
3656
+ 'requires',
3657
+ 'object',
3658
+ 'kin',
3659
+ 'lead',
3660
+ 'leadWorld',
3661
+ 'first',
3662
+ 'category',
3663
+ 'strict',
3664
+ 'refuse',
3665
+ 'needs',
3666
+ 'allow',
3667
+ 'refuseLead',
3668
+ 'worldless',
3669
+ 'away'
3670
+ ];
3671
+ /** A search as it is sent to the library: its words and size, none of how its hits are judged. */ export function aiStockSearchRequest(wanted) {
3672
+ const request = _extends({}, wanted);
3673
+ for (const key of SEARCH_JUDGMENT_KEYS)delete request[key];
3674
+ return request;
3675
+ }
3676
+ /**
3677
+ * A search as a job keeps it to ask again (AGL-3988): only what a library's
3678
+ * request is made of, so asked again it is the same request, which the
3679
+ * provider's 24-hour cache answers.
3680
+ */ export function aiStockKeptRequest(request) {
3681
+ return _extends({
3682
+ query: request.query
3683
+ }, request.orientation ? {
3684
+ orientation: request.orientation
3685
+ } : {}, request.minWidth ? {
3686
+ minWidth: request.minWidth
3687
+ } : {}, request.minHeight ? {
3688
+ minHeight: request.minHeight
3689
+ } : {}, request.people ? {
3690
+ people: true
3691
+ } : {});
3692
+ }
3693
+ /**
3694
+ * The library's own record of a photo copied into a site (AGL-3923, AGL-3988):
3695
+ * what the media library stores beside the bytes as `stockPhoto`, with the
3696
+ * credit its license asks for. One shape for every copy a job makes, a
3697
+ * page's and the late photo pass's.
3698
+ */ export function aiStockIngestRequest(provider, input) {
3699
+ var _EXTENSIONS_bytes_contentType;
3700
+ const { photo, bytes } = input;
3701
+ const credit = provider.credit(photo);
3702
+ return {
3703
+ hostId: input.hostId,
3704
+ uid: input.uid,
3705
+ fileName: `${photo.provider}-${photo.id}.${(_EXTENSIONS_bytes_contentType = EXTENSIONS[bytes.contentType]) != null ? _EXTENSIONS_bytes_contentType : 'jpg'}`,
3706
+ contentType: bytes.contentType,
3707
+ bytes: bytes.bytes,
3708
+ alt: input.alt,
3709
+ description: credit.text,
3710
+ stockPhoto: _extends({
3711
+ key: stockPhotoSourceKey(photo),
3712
+ provider: photo.provider,
3713
+ providerLabel: credit.providerLabel,
3714
+ id: photo.id,
3715
+ pageUrl: photo.pageUrl,
3716
+ photographer: photo.photographer
3717
+ }, photo.photographerUrl ? {
3718
+ photographerUrl: photo.photographerUrl
3719
+ } : {}, {
3720
+ license: credit.license,
3721
+ licenseUrl: credit.licenseUrl,
3722
+ attributionRequired: credit.attributionRequired,
3723
+ query: input.query
3724
+ })
3725
+ };
3726
+ }
3646
3727
  /**
3647
3728
  * A job's language and English photo words (AGL-3760), the English name of
3648
3729
  * its kind of site (AGL-3771), and the business's name and audience as the
@@ -4872,7 +4953,7 @@ const WORKSPACE = {
4872
4953
  * The source for one page, or `null` when this deployment has no stock
4873
4954
  * photo library or no media door, which leaves every slot to the starters.
4874
4955
  */ export function aiLayoutStockPhotoSource(input, deps = {}) {
4875
- var _deps_provider, _deps_ingest, _input_kind;
4956
+ var _deps_provider, _deps_ingest, _input_kind, _deps_now;
4876
4957
  const provider = ((_deps_provider = deps.provider) != null ? _deps_provider : stockPhotoProvider)();
4877
4958
  const ingest = ((_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest)();
4878
4959
  if (!provider || !ingest || !input.hostId || !input.uid) return null;
@@ -4888,7 +4969,8 @@ const WORKSPACE = {
4888
4969
  const wider = aiStockWiderTerms(terms, (_input_kind = input.kind) != null ? _input_kind : '');
4889
4970
  const unlisted = aiStockWorldIsUnlisted(terms);
4890
4971
  const proper = aiStockProperWords(input.business);
4891
- return async (slots)=>{
4972
+ const now = (_deps_now = deps.now) != null ? _deps_now : Date.now;
4973
+ return async (slots, report)=>{
4892
4974
  var _deps_budgetMs, _input_avoid;
4893
4975
  const timeout = AbortSignal.timeout((_deps_budgetMs = deps.budgetMs) != null ? _deps_budgetMs : AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS);
4894
4976
  const signal = input.signal ? AbortSignal.any([
@@ -4907,6 +4989,7 @@ const WORKSPACE = {
4907
4989
  let searches = 0;
4908
4990
  let storing = true;
4909
4991
  /** Slots a photo was chosen for and could not be copied into the library (AGL-3923). */ const uncopied = new Set();
4992
+ /** Each slot's picks the image hosts would not hand over, and when (AGL-3988). */ const refusedPicks = new Map();
4910
4993
  /** Whether the library answered any search of this page: a slot left empty then found nothing fitting. */ let answered = false;
4911
4994
  const remember = (sourceKey, src)=>{
4912
4995
  placedSrcs.add(src);
@@ -4915,23 +4998,7 @@ const WORKSPACE = {
4915
4998
  };
4916
4999
  /** A search's hits; the same words twice are answered from memory, and `null` once the page's searches are spent. */ const search = (wanted)=>{
4917
5000
  var _input_searches;
4918
- const request = _extends({}, wanted);
4919
- delete request.broad;
4920
- delete request.subject;
4921
- delete request.requires;
4922
- delete request.object;
4923
- delete request.kin;
4924
- delete request.lead;
4925
- delete request.leadWorld;
4926
- delete request.first;
4927
- delete request.category;
4928
- delete request.strict;
4929
- delete request.refuse;
4930
- delete request.needs;
4931
- delete request.allow;
4932
- delete request.refuseLead;
4933
- delete request.worldless;
4934
- delete request.away;
5001
+ const request = aiStockSearchRequest(wanted);
4935
5002
  const key = JSON.stringify([
4936
5003
  request.query,
4937
5004
  request.orientation,
@@ -4967,7 +5034,6 @@ const WORKSPACE = {
4967
5034
  alt
4968
5035
  };
4969
5036
  try {
4970
- var _EXTENSIONS_bytes_contentType;
4971
5037
  const kept = await ingest.findStockPhoto({
4972
5038
  hostId: input.hostId,
4973
5039
  sourceKey
@@ -4993,7 +5059,16 @@ const WORKSPACE = {
4993
5059
  if (!bytes || signal.aborted || !storing) {
4994
5060
  // A copy the library would not hand over is otherwise silent (AGL-3923).
4995
5061
  if (!bytes && !signal.aborted) {
5062
+ var _refusedPicks_get;
4996
5063
  uncopied.add(slot);
5064
+ // Asked for again by the late photo pass, once the burst is over (AGL-3988).
5065
+ const held = (_refusedPicks_get = refusedPicks.get(slot)) != null ? _refusedPicks_get : {
5066
+ keys: [],
5067
+ at: 0
5068
+ };
5069
+ if (!held.keys.includes(sourceKey)) held.keys.push(sourceKey);
5070
+ held.at = now();
5071
+ refusedPicks.set(slot, held);
4997
5072
  console.warn('ai layout pictures: a stock photo could not be downloaded', {
4998
5073
  provider: photo.provider,
4999
5074
  id: photo.id
@@ -5001,31 +5076,14 @@ const WORKSPACE = {
5001
5076
  }
5002
5077
  return null;
5003
5078
  }
5004
- const credit = provider.credit(photo);
5005
- const stored = await ingest.ingest({
5079
+ const stored = await ingest.ingest(aiStockIngestRequest(provider, {
5006
5080
  hostId: input.hostId,
5007
5081
  uid: input.uid,
5008
- fileName: `${photo.provider}-${photo.id}.${(_EXTENSIONS_bytes_contentType = EXTENSIONS[bytes.contentType]) != null ? _EXTENSIONS_bytes_contentType : 'jpg'}`,
5009
- contentType: bytes.contentType,
5010
- bytes: bytes.bytes,
5082
+ photo,
5083
+ bytes,
5011
5084
  alt: alt != null ? alt : slot.alt,
5012
- description: credit.text,
5013
- stockPhoto: _extends({
5014
- key: sourceKey,
5015
- provider: photo.provider,
5016
- providerLabel: credit.providerLabel,
5017
- id: photo.id,
5018
- pageUrl: photo.pageUrl,
5019
- photographer: photo.photographer
5020
- }, photo.photographerUrl ? {
5021
- photographerUrl: photo.photographerUrl
5022
- } : {}, {
5023
- license: credit.license,
5024
- licenseUrl: credit.licenseUrl,
5025
- attributionRequired: credit.attributionRequired,
5026
- query
5027
- })
5028
- });
5085
+ query
5086
+ }));
5029
5087
  if (stored.ok) {
5030
5088
  var _stored_width, _stored_height;
5031
5089
  remember(sourceKey, stored.src);
@@ -5306,7 +5364,8 @@ const WORKSPACE = {
5306
5364
  }
5307
5365
  return _extends({
5308
5366
  candidates: ranked.slice(0, AI_LAYOUT_STOCK_TRIES_PER_SLOT),
5309
- query: request.query
5367
+ query: request.query,
5368
+ request: aiStockKeptRequest(aiStockSearchRequest(request))
5310
5369
  }, repeat ? {
5311
5370
  repeat: true
5312
5371
  } : {});
@@ -5423,6 +5482,18 @@ const WORKSPACE = {
5423
5482
  length: Math.min(AI_LAYOUT_STOCK_COPIES_AT_ONCE, firsts.length)
5424
5483
  }, worker));
5425
5484
  for (const [index, choice] of choices.entries())if (choice == null ? void 0 : choice.repeat) await fill(index);
5485
+ // A slot left without a photo because its picks were refused: what they
5486
+ // were, for the late photo pass to ask for again (AGL-3988).
5487
+ for (const [index, slot] of slots.entries()){
5488
+ const held = refusedPicks.get(slot);
5489
+ const choice = choices[index];
5490
+ if (photos[index] || !(held == null ? void 0 : held.keys.length) || !choice) continue;
5491
+ report == null ? void 0 : report.refused.set(slot, {
5492
+ request: choice.request,
5493
+ keys: held.keys,
5494
+ at: new Date(held.at).toISOString()
5495
+ });
5496
+ }
5426
5497
  // The library answered and nothing in it fits: no photo rather than a
5427
5498
  // starter that shows another business (AGL-3831). A library that never
5428
5499
  // answered, or a site that refused to store, leaves the starters.