@jhb.software/payload-alt-text-plugin 0.10.0 → 0.12.0
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/README.md +225 -36
- package/dist/components/BulkGenerateAltTextsButton.js +3 -16
- package/dist/components/BulkGenerateAltTextsButton.js.map +1 -1
- package/dist/components/summarizeBulkGenerate.d.ts +26 -0
- package/dist/components/summarizeBulkGenerate.js +56 -0
- package/dist/components/summarizeBulkGenerate.js.map +1 -0
- package/dist/endpoints/bulkGenerateAltTexts.d.ts +18 -1
- package/dist/endpoints/bulkGenerateAltTexts.js +39 -16
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +17 -9
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +20 -4
- package/dist/plugin.js.map +1 -1
- package/dist/resolvers/anthropic.d.ts +64 -0
- package/dist/resolvers/anthropic.js +140 -0
- package/dist/resolvers/anthropic.js.map +1 -0
- package/dist/resolvers/createVisionResolver.d.ts +148 -0
- package/dist/resolvers/createVisionResolver.js +300 -0
- package/dist/resolvers/createVisionResolver.js.map +1 -0
- package/dist/resolvers/mistral.d.ts +15 -1
- package/dist/resolvers/mistral.js +85 -250
- package/dist/resolvers/mistral.js.map +1 -1
- package/dist/resolvers/openAI.d.ts +22 -3
- package/dist/resolvers/openAI.js +57 -138
- package/dist/resolvers/openAI.js.map +1 -1
- package/dist/translations/de.js +12 -4
- package/dist/translations/de.js.map +1 -1
- package/dist/translations/en.js +12 -4
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/translation-schema.json +24 -8
- package/dist/types/AltTextPluginConfig.d.ts +71 -12
- package/dist/types/AltTextPluginConfig.js.map +1 -1
- package/dist/utilities/altTextHealth.d.ts +3 -1
- package/dist/utilities/altTextHealth.js +103 -12
- package/dist/utilities/altTextHealth.js.map +1 -1
- package/dist/utilities/resolveLocales.d.ts +15 -0
- package/dist/utilities/resolveLocales.js +38 -0
- package/dist/utilities/resolveLocales.js.map +1 -0
- package/dist/utilities/stableStringify.d.ts +9 -0
- package/dist/utilities/stableStringify.js +19 -0
- package/dist/utilities/stableStringify.js.map +1 -0
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# Image Alt Text Generation Plugin for Payload CMS
|
|
2
2
|
|
|
3
|
-
A [Payload CMS](https://payloadcms.com/) plugin that adds AI-powered alt text generation for images.
|
|
3
|
+
A [Payload CMS](https://payloadcms.com/) plugin that adds AI-powered alt text generation for images. It automatically adds an alt text field with a button to generate the alt text to specified upload collections, and includes a bulk generation feature in the list view for processing multiple images at once.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Generate alt text for images using AI in the Payload Admin UI
|
|
8
8
|
- Supports any AI provider using a resolver pattern (e.g., OpenAI, Anthropic, etc.)
|
|
9
|
-
- Comes with ready-to-use OpenAI and Mistral resolvers out of the box
|
|
9
|
+
- Comes with ready-to-use OpenAI, Anthropic and Mistral resolvers out of the box
|
|
10
10
|
- Automatic keyword extraction for improved admin search
|
|
11
|
-
- Bulk generation
|
|
11
|
+
- Bulk generation from the collection list view
|
|
12
12
|
- Full localization support
|
|
13
13
|
- Dashboard health widget with cached coverage insights across all configured upload collections
|
|
14
|
+
- Multi-tenant aware: the health report can be scoped to the tenant the request is for
|
|
14
15
|
|
|
15
16
|
When the plugin is enabled for an upload collection, it will:
|
|
16
17
|
|
|
@@ -21,7 +22,6 @@ When the plugin is enabled for an upload collection, it will:
|
|
|
21
22
|
- This field will be automatically filled when generating the alt text
|
|
22
23
|
- It will be used for improving the search of images in the admin panel
|
|
23
24
|
3. Add a bulk generate button to the collection list view
|
|
24
|
-
- This button will allow you to generate alt text for multiple images at once
|
|
25
25
|
4. Register an `Alt text health` dashboard widget
|
|
26
26
|
- Results are cached and revalidated when documents in the configured upload collections change
|
|
27
27
|
|
|
@@ -77,19 +77,20 @@ This is also the recommended escape hatch if you hit Payload's Postgres SQL-buil
|
|
|
77
77
|
|
|
78
78
|
### Plugin Options
|
|
79
79
|
|
|
80
|
-
| Option | Type
|
|
81
|
-
| ---------------------------- |
|
|
82
|
-
| `collections` | `(CollectionSlug \| CollectionObj)[]`
|
|
83
|
-
| `resolver` | `AltTextResolver`
|
|
84
|
-
| `getImageThumbnail` | `Function`
|
|
85
|
-
| `enabled` | `boolean`
|
|
86
|
-
| `access` | `({ req }) => boolean \| Promise<boolean>`
|
|
87
|
-
| `locale` | `string`
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
80
|
+
| Option | Type | Required | Description |
|
|
81
|
+
| ---------------------------- | ----------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `collections` | `(CollectionSlug \| CollectionObj)[]` | Yes | Collections to enable alt text generation for (see [Per-collection options](#per-collection-options)) |
|
|
83
|
+
| `resolver` | `AltTextResolver` | Yes | Alt text resolver to use (e.g., `openAIResolver`) |
|
|
84
|
+
| `getImageThumbnail` | `Function` | Yes | Function to get the thumbnail URL from an image document |
|
|
85
|
+
| `enabled` | `boolean` | No | Disables the plugin entirely when `false` (default: `true`) |
|
|
86
|
+
| `access` | `({ req }) => boolean \| Promise<boolean>` | No | Access control for the plugin's REST endpoints. Defaults to `({ req }) => !!req.user` (any authenticated user) — see [Authentication](#authentication) |
|
|
87
|
+
| `locale` | `string` | No | Locale for alt text generation (required when localization is disabled) |
|
|
88
|
+
| `filterLocales` | `({ locales, req }) => string[] \| Promise<string[]>` | No | Narrows the locales a request generates for and is measured against — in a multi-tenant CMS, to the locales the selected tenant serves (see [Per-request locales](#per-request-locales)) |
|
|
89
|
+
| `maxBulkGenerateConcurrency` | `number` | No | Maximum concurrent API requests for bulk operations (default: 16) |
|
|
90
|
+
| `maxBulkGenerateIds` | `number` | No | Maximum number of image IDs accepted per bulk generate request; larger requests are rejected with `400`. Duplicate IDs are collapsed before the limit is applied (default: 100) |
|
|
91
|
+
| `fieldsOverride` | `Function` | No | Override the default fields inserted by the plugin |
|
|
92
|
+
| `healthCheck` | `boolean \| AltTextHealthCheckConfig` | No | Alt text health tracking (REST endpoint, cache revalidation hooks, dashboard widget). `false` disables it; `true` enables it for every document, gated by `access`; an object enables it and configures its `access` gate and `baseFilter` (see [Health report](#dashboard-widget)) (default: `true`) |
|
|
93
|
+
| `imageThumbnailMimeType` | `string` | No | The MIME type `getImageThumbnail` delivers. Set it when your thumbnail URL transcodes the image, so the stored format no longer decides whether generation is possible (see [Transcoding thumbnails](#transcoding-thumbnails)) |
|
|
93
94
|
|
|
94
95
|
`getImageThumbnail` receives the document and `{ collection, req }`, so a single function can build different URLs per collection:
|
|
95
96
|
|
|
@@ -104,9 +105,48 @@ It may also be async, so the URL can be signed on demand:
|
|
|
104
105
|
getImageThumbnail: async (doc, { req }) => await presignThumbnailUrl(String(doc.url), req)
|
|
105
106
|
```
|
|
106
107
|
|
|
108
|
+
### Per-request locales
|
|
109
|
+
|
|
110
|
+
Payload's locale list is config-wide. A multi-tenant project therefore configures the union of every tenant's locales — and without narrowing, every tenant is served all of them: a bulk generation writes (and pays for) locales the tenant does not serve, and the health report counts its images as incomplete until locales it will never publish are filled.
|
|
111
|
+
|
|
112
|
+
`filterLocales` narrows that list per request:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
|
|
116
|
+
|
|
117
|
+
filterLocales: async ({ locales, req }) => {
|
|
118
|
+
const tenantId = getTenantFromCookie(req.headers, req.payload.db.defaultIDType)
|
|
119
|
+
|
|
120
|
+
// No tenant selected: every configured locale, matching the tenant selector.
|
|
121
|
+
if (!tenantId) {
|
|
122
|
+
return locales
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const tenant = await req.payload.findByID({
|
|
126
|
+
id: tenantId,
|
|
127
|
+
collection: 'tenants',
|
|
128
|
+
depth: 0,
|
|
129
|
+
req,
|
|
130
|
+
select: { locales: true },
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return tenant?.locales?.length ? tenant.locales : locales
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
It governs all three locale-dependent behaviors:
|
|
138
|
+
|
|
139
|
+
| Behavior | Effect |
|
|
140
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------- |
|
|
141
|
+
| `POST /alt-text/generate/bulk` | Generates and writes only the admitted locales |
|
|
142
|
+
| `POST /alt-text/generate` | Rejects a request `locale` outside them with `400`, before the resolver runs |
|
|
143
|
+
| Health report and widget | Counts a document complete once the admitted locales are filled, and scopes its cache entry |
|
|
144
|
+
|
|
145
|
+
The returned codes must be a non-empty subset of the configured locales; anything else fails the request rather than writing into a locale the project does not define. Omit the option and every request targets the full configured list — no call is made, so a project that does not scope locales pays nothing for it.
|
|
146
|
+
|
|
107
147
|
### Per-collection options
|
|
108
148
|
|
|
109
|
-
Each entry in `collections` may be either a bare collection slug
|
|
149
|
+
Each entry in `collections` may be either a bare collection slug or an object with the following fields:
|
|
110
150
|
|
|
111
151
|
| Option | Type | Required | Description |
|
|
112
152
|
| ------------------------ | ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -118,7 +158,7 @@ Each entry in `collections` may be either a bare collection slug (shorthand, def
|
|
|
118
158
|
```ts
|
|
119
159
|
payloadAltTextPlugin({
|
|
120
160
|
collections: [
|
|
121
|
-
'images', // shorthand
|
|
161
|
+
'images', // shorthand
|
|
122
162
|
{ slug: 'media', mimeTypes: ['image/*', 'application/pdf'] },
|
|
123
163
|
],
|
|
124
164
|
// ...
|
|
@@ -141,7 +181,7 @@ payloadAltTextPlugin({
|
|
|
141
181
|
})
|
|
142
182
|
```
|
|
143
183
|
|
|
144
|
-
|
|
184
|
+
Which source formats get alt text at all is still governed by each collection's `mimeTypes`. The declaration is validated against the resolver's `supportedMimeTypes` once at config load, so transcoding into a format your resolver cannot handle fails at boot instead of once per image.
|
|
145
185
|
|
|
146
186
|
Only declare a format your transformation **always** produces. A `f_auto`-style transformation negotiates the format from the fetching client's `Accept` header and may serve the source format back, so leave it unset there and let the conservative source check apply. If you want AVIF sources to work, transcode explicitly.
|
|
147
187
|
|
|
@@ -205,6 +245,34 @@ buildConfig({
|
|
|
205
245
|
|
|
206
246
|
Set `healthCheck: false` in the plugin config to disable the REST endpoint, cache revalidation hooks, and dashboard widget. If your project replaces the default dashboard via `admin.components.views.dashboard`, you need to integrate the widget into your custom dashboard yourself.
|
|
207
247
|
|
|
248
|
+
#### Gating and scoping the report
|
|
249
|
+
|
|
250
|
+
`healthCheck` also takes an object. The main use of `baseFilter` is multi-tenancy: scope the report to the tenant selected in the admin panel, whose id [@payloadcms/plugin-multi-tenant](https://payloadcms.com/docs/plugins/multi-tenant) keeps in the `payload-tenant` cookie.
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
|
|
254
|
+
|
|
255
|
+
healthCheck: {
|
|
256
|
+
// Restrict the collection-wide report more strictly than the per-document
|
|
257
|
+
// generate endpoints. Gates the REST endpoint and hides the widget.
|
|
258
|
+
access: ({ req }) => req.user?.role === 'admin',
|
|
259
|
+
// Narrow what the report counts, e.g. to the tenant selected in the admin panel.
|
|
260
|
+
baseFilter: ({ collection, req }) => {
|
|
261
|
+
const tenant = getTenantFromCookie(req.headers, req.payload.db.defaultIDType)
|
|
262
|
+
|
|
263
|
+
return tenant ? { tenant: { equals: tenant } } : {}
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`baseFilter` returns a `Where` that is ANDed onto the scan's MIME type filter. It is resolved once per configured collection, so a collection that does not carry the constraining field — a media library shared across tenants, say — can return `{}` and be scanned whole. Omitting `baseFilter` entirely does the same for every collection.
|
|
269
|
+
|
|
270
|
+
The scan is cached across requests, and its cache key is derived from the resolved filters: a narrowed scan always gets its own cache entry, so one tenant's counts can never be served to another. Cache invalidation stays per collection, so a write in one tenant refreshes the report for all of them.
|
|
271
|
+
|
|
272
|
+
This scopes what the report counts, not who may see it — use `access` for that. Independently of both, the report always omits the collections the requesting user cannot read.
|
|
273
|
+
|
|
274
|
+
Which locales a document is measured against is scoped separately, by [`filterLocales`](#per-request-locales).
|
|
275
|
+
|
|
208
276
|
#### Skipping cache revalidation for individual writes
|
|
209
277
|
|
|
210
278
|
The plugin invalidates the cached health scan via `afterChange` and `afterDelete` hooks. For writes that don't need to invalidate the cache — typically seed data created from `payload.onInit`, batch imports, or migrations — pass `context: { disableRevalidate: true }` to skip the revalidation:
|
|
@@ -219,9 +287,13 @@ await payload.create({
|
|
|
219
287
|
|
|
220
288
|
### Resolvers
|
|
221
289
|
|
|
222
|
-
|
|
290
|
+
Three resolvers ship with the plugin — [OpenAI](#openai-resolver),
|
|
291
|
+
[Mistral](#mistral-resolver) and [Anthropic](#anthropic-resolver) — and any other
|
|
292
|
+
provider can be used by writing your own.
|
|
223
293
|
|
|
224
|
-
|
|
294
|
+
They differ in how the image reaches the provider: the OpenAI resolver passes the
|
|
295
|
+
thumbnail URL, the Mistral and Anthropic resolvers download the image and send
|
|
296
|
+
the bytes.
|
|
225
297
|
|
|
226
298
|
#### OpenAI Resolver
|
|
227
299
|
|
|
@@ -234,12 +306,18 @@ openAIResolver({
|
|
|
234
306
|
})
|
|
235
307
|
```
|
|
236
308
|
|
|
309
|
+
OpenAI fetches the thumbnail URL itself, so it must be reachable from the public
|
|
310
|
+
internet. If it is not, use [`mistralResolver`](#mistral-resolver) or
|
|
311
|
+
[`anthropicResolver`](#anthropic-resolver), which send the bytes instead.
|
|
312
|
+
|
|
237
313
|
| Option | Type | Required | Description |
|
|
238
314
|
| -------------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
239
315
|
| `apiKey` | `string` | Yes | API key for authentication |
|
|
240
316
|
| `model` | `string` | No | Model to use (default: `gpt-4.1-nano`) |
|
|
241
|
-
| `baseUrl` | `string` | No | Base URL for an OpenAI-compatible provider (e.g. Nebius, Azure)
|
|
317
|
+
| `baseUrl` | `string` | No | Base URL for an OpenAI-compatible provider, version segment included (default: `https://api.openai.com/v1`; e.g. Nebius, Azure) |
|
|
242
318
|
| `supportedMimeTypes` | `string[]` | No | Image formats the provider accepts (default: `['image/jpeg', 'image/png', 'image/gif', 'image/webp']`, per OpenAI's vision docs). Override it when using a `baseUrl` whose provider differs |
|
|
319
|
+
| `timeoutMs` | `number` | No | Abort after this many milliseconds, retries included (default: `30000`) |
|
|
320
|
+
| `instructions` | `function` | No | Customizes the prompt, see [Customizing the instructions](#customizing-the-instructions) |
|
|
243
321
|
|
|
244
322
|
#### Mistral Resolver
|
|
245
323
|
|
|
@@ -252,22 +330,125 @@ mistralResolver({
|
|
|
252
330
|
})
|
|
253
331
|
```
|
|
254
332
|
|
|
255
|
-
Unlike the OpenAI resolver, this one downloads the image and sends the bytes
|
|
256
|
-
rather than handing Mistral the thumbnail URL. Mistral's own fetcher needs the
|
|
257
|
-
file to be reachable from the public internet, which is never the case in local
|
|
258
|
-
development and not the case for private buckets; some hosts also refuse it
|
|
259
|
-
outright (`File could not be fetched from url`, error 3310). Sending the bytes
|
|
260
|
-
costs one extra download and removes that whole class of failure.
|
|
261
|
-
|
|
262
333
|
Because there is no image conversion step, `supportedMimeTypes` is limited to
|
|
263
334
|
what the Mistral API accepts directly: JPEG, PNG, GIF and WebP. Documents in
|
|
264
335
|
other formats — SVG or AVIF, for instance — keep their generate button disabled.
|
|
265
336
|
|
|
337
|
+
| Option | Type | Required | Description |
|
|
338
|
+
| -------------- | ---------- | -------- | ---------------------------------------------------------------------------------------- |
|
|
339
|
+
| `apiKey` | `string` | Yes | API key for authentication |
|
|
340
|
+
| `model` | `string` | No | Model to use (default: `mistral-medium-latest`) |
|
|
341
|
+
| `baseUrl` | `string` | No | Base URL of the Mistral API (default: `https://api.mistral.ai/v1`) |
|
|
342
|
+
| `timeoutMs` | `number` | No | Abort after this many milliseconds, image download included (default: `30000`) |
|
|
343
|
+
| `instructions` | `function` | No | Customizes the prompt, see [Customizing the instructions](#customizing-the-instructions) |
|
|
344
|
+
|
|
345
|
+
#### Anthropic Resolver
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
import { anthropicResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
349
|
+
|
|
350
|
+
anthropicResolver({
|
|
351
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
352
|
+
model: 'claude-opus-5', // default; `claude-sonnet-5` is cheaper for a large library
|
|
353
|
+
effort: 'low', // optional; describing an image needs little thinking
|
|
354
|
+
})
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`supportedMimeTypes` is limited to what the Messages API accepts: JPEG, PNG, GIF
|
|
358
|
+
and WebP. Documents in other formats keep their generate button disabled.
|
|
359
|
+
|
|
360
|
+
| Option | Type | Required | Description |
|
|
361
|
+
| -------------- | ------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
362
|
+
| `apiKey` | `string` | Yes | API key for authentication |
|
|
363
|
+
| `model` | `string` | No | Model to use (default: `claude-opus-5`). `claude-sonnet-5` is cheaper; `claude-haiku-4-5` works too, but only without `effort` |
|
|
364
|
+
| `effort` | `'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'` | No | How long Claude thinks before answering. `low` is plenty for describing an image and keeps the spend down. Omitted, the field is not sent and Claude uses its default (`high`), so models without effort support stay usable |
|
|
365
|
+
| `baseUrl` | `string` | No | Base URL of the Anthropic API (default: `https://api.anthropic.com`) |
|
|
366
|
+
| `timeoutMs` | `number` | No | Abort after this many milliseconds, image download included (default: `30000`) |
|
|
367
|
+
| `instructions` | `function` | No | Customizes the prompt, see [Customizing the instructions](#customizing-the-instructions) |
|
|
368
|
+
|
|
369
|
+
### Customizing the instructions
|
|
370
|
+
|
|
371
|
+
Every bundled resolver accepts an `instructions` function that receives the
|
|
372
|
+
instructions the resolver would send on its own, so a house style rule can be
|
|
373
|
+
appended without restating the rules the plugin depends on:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
openAIResolver({
|
|
377
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
378
|
+
instructions: ({ defaultInstructions }) =>
|
|
379
|
+
`${defaultInstructions}\n\nName the product line when its packaging is legible. Never guess at a person's role.`,
|
|
380
|
+
})
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
It is called once per generation and receives `{ defaultInstructions, locales, filename }`.
|
|
384
|
+
Returning something entirely different is allowed — the image and the required
|
|
385
|
+
response shape travel separately from the instructions, so a replacement cannot
|
|
386
|
+
break the resolver's contract with its provider.
|
|
387
|
+
|
|
266
388
|
## Custom Resolver
|
|
267
389
|
|
|
268
|
-
|
|
390
|
+
For another LLM provider, `createVisionResolver` is usually the shortest path: it
|
|
391
|
+
owns the prompt, the per-locale response schema, the optional image download and
|
|
392
|
+
the strict reading of the response, leaving only the provider call to `generate`.
|
|
393
|
+
Every bundled resolver is built on it.
|
|
269
394
|
|
|
270
|
-
|
|
395
|
+
```ts
|
|
396
|
+
import { createVisionResolver, VisionProviderError } from '@jhb.software/payload-alt-text-plugin'
|
|
397
|
+
|
|
398
|
+
export const myResolver = ({ apiKey }: { apiKey: string }) =>
|
|
399
|
+
createVisionResolver({
|
|
400
|
+
apiKey,
|
|
401
|
+
// `image` is only present when `inlineImage` is set; otherwise pass
|
|
402
|
+
// `imageThumbnailUrl` to the provider and let it fetch the file.
|
|
403
|
+
generate: async ({ image, instructions, maxTokens, responseSchema, signal }) => {
|
|
404
|
+
if (!image) {
|
|
405
|
+
throw new Error('The image was not downloaded')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const response = await fetch('https://api.example.com/v1/vision', {
|
|
409
|
+
body: JSON.stringify({ instructions, image: image.dataUri, schema: responseSchema }),
|
|
410
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
411
|
+
method: 'POST',
|
|
412
|
+
signal,
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// A rate limit or an outage is worth another attempt: throwing
|
|
416
|
+
// `VisionProviderError` lets the factory retry it. Any other error fails
|
|
417
|
+
// the generation immediately, with its message shown in the admin panel.
|
|
418
|
+
if (!response.ok) {
|
|
419
|
+
throw new VisionProviderError({
|
|
420
|
+
body: await response.text(),
|
|
421
|
+
label: 'My Provider',
|
|
422
|
+
status: response.status,
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return await response.json()
|
|
427
|
+
},
|
|
428
|
+
inlineImage: true,
|
|
429
|
+
key: 'my-provider',
|
|
430
|
+
label: 'My Provider',
|
|
431
|
+
supportedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
432
|
+
})
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
A provider call that fails with `VisionProviderError` is retried twice, with a
|
|
436
|
+
short backoff, when the status is a rate limit (`429`) or a server-side failure
|
|
437
|
+
(`5xx`) — a bulk generation trips those routinely, and giving up on the first one
|
|
438
|
+
leaves documents without an alt text. Any other status fails immediately: a `4xx`
|
|
439
|
+
would fail identically on every attempt. The resolver's `timeoutMs` covers the
|
|
440
|
+
attempts together.
|
|
441
|
+
|
|
442
|
+
Pass the provider's response as `body` and it is written to the server log, never
|
|
443
|
+
to the error the admin panel shows — that message reaches everyone allowed to
|
|
444
|
+
generate an alt text, and a provider's error text is not written with them in
|
|
445
|
+
mind: OpenAI quotes the rejected API key back in a 401. The panel gets the
|
|
446
|
+
provider name and the status.
|
|
447
|
+
|
|
448
|
+
For a provider that does not fit that shape at all, implement the
|
|
449
|
+
`AltTextResolver` interface directly.
|
|
450
|
+
|
|
451
|
+
Alongside `imageThumbnailUrl`, the resolver receives `imageThumbnailMimeType` — the format served at that URL, when the collection declares one via [`imageThumbnailMimeType`](#transcoding-thumbnails). Resolvers that hand the URL to the provider can ignore it. Resolvers that inline the bytes need it, because an explicit media type cannot be sniffed from a URL: Anthropic image blocks require `media_type` and Gemini's `inline_data` requires `mime_type`. It is `undefined` when nothing was declared. Resolvers built on `createVisionResolver` get this for free: `image.mediaType` is the type the host actually served, with the declaration standing in when the host sent none or a generic `application/octet-stream`.
|
|
271
452
|
|
|
272
453
|
```ts
|
|
273
454
|
import type { AltTextResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
@@ -314,7 +495,7 @@ That default fits a setup where every Payload user is trusted staff. Generating
|
|
|
314
495
|
access: ({ req }) => req.user?.role === 'editor'
|
|
315
496
|
```
|
|
316
497
|
|
|
317
|
-
Beyond that gate, the generate endpoints enforce each collection's own access control on the documents they read and write, and the health endpoint reports only the collections the requesting user can read (and can be gated separately via
|
|
498
|
+
Beyond that gate, the generate endpoints enforce each collection's own access control on the documents they read and write, and the health endpoint reports only the collections the requesting user can read (and can be gated separately via `healthCheck.access`).
|
|
318
499
|
|
|
319
500
|
### `POST /api/alt-text/generate`
|
|
320
501
|
|
|
@@ -342,7 +523,7 @@ Generates alt text for a single image. By default, returns the result without sa
|
|
|
342
523
|
|
|
343
524
|
### `POST /api/alt-text/generate/bulk`
|
|
344
525
|
|
|
345
|
-
Generates and persists alt text for multiple images across
|
|
526
|
+
Generates and persists alt text for multiple images across every target locale (see [Per-request locales](#per-request-locales)).
|
|
346
527
|
|
|
347
528
|
**Request body:**
|
|
348
529
|
|
|
@@ -355,12 +536,20 @@ Generates and persists alt text for multiple images across all configured locale
|
|
|
355
536
|
|
|
356
537
|
```json
|
|
357
538
|
{
|
|
358
|
-
"updatedDocs":
|
|
539
|
+
"updatedDocs": 4,
|
|
359
540
|
"totalDocs": 6,
|
|
360
|
-
"erroredDocs": ["abc789"]
|
|
541
|
+
"erroredDocs": ["abc789"],
|
|
542
|
+
"skippedDocs": [{ "id": "def456", "reason": "notTracked" }]
|
|
361
543
|
}
|
|
362
544
|
```
|
|
363
545
|
|
|
546
|
+
`skippedDocs` holds the documents no provider call was made for. They are reported separately from `erroredDocs` because they are not failures of the run — a selection covering a whole list view routinely contains a few — and each carries the reason, which decides what the editor has to do:
|
|
547
|
+
|
|
548
|
+
| Reason | Meaning |
|
|
549
|
+
| ------------------- | ----------------------------------------------------------------------------------------------- |
|
|
550
|
+
| `notTracked` | The collection does not track this file type, so it needs no alt text at all |
|
|
551
|
+
| `unsupportedFormat` | A tracked file whose format the resolver cannot read — it still needs alt text, written by hand |
|
|
552
|
+
|
|
364
553
|
### `GET /api/alt-text/health`
|
|
365
554
|
|
|
366
555
|
Returns alt text coverage statistics across all configured collections. Only available when `healthCheck` is enabled.
|
|
@@ -6,6 +6,7 @@ import { useTransition } from 'react';
|
|
|
6
6
|
import { PLUGIN_SLUG } from '../constants.js';
|
|
7
7
|
import { Lightning } from './icons/Lightning.js';
|
|
8
8
|
import { Spinner } from './icons/Spinner.js';
|
|
9
|
+
import { summarizeBulkGenerate } from './summarizeBulkGenerate.js';
|
|
9
10
|
export function BulkGenerateAltTextsButton({ collectionSlug }) {
|
|
10
11
|
const { t } = useTranslation();
|
|
11
12
|
const [isPending, startTransition] = useTransition();
|
|
@@ -33,22 +34,8 @@ export function BulkGenerateAltTextsButton({ collectionSlug }) {
|
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
const data = await response.json();
|
|
36
|
-
|
|
37
|
-
toast
|
|
38
|
-
count: data.erroredDocs.length
|
|
39
|
-
}));
|
|
40
|
-
}
|
|
41
|
-
// in case not all images were updated, show a warning instead of a success message:
|
|
42
|
-
if (data.updatedDocs === data.totalDocs) {
|
|
43
|
-
toast.success(t('@jhb.software/payload-alt-text-plugin:xOfYImagesUpdated', {
|
|
44
|
-
total: data.totalDocs,
|
|
45
|
-
updated: data.updatedDocs
|
|
46
|
-
}));
|
|
47
|
-
} else {
|
|
48
|
-
toast.warning(t('@jhb.software/payload-alt-text-plugin:xOfYImagesUpdated', {
|
|
49
|
-
total: data.totalDocs,
|
|
50
|
-
updated: data.updatedDocs
|
|
51
|
-
}));
|
|
37
|
+
for (const { severity, translationKey, variables } of summarizeBulkGenerate(data)){
|
|
38
|
+
toast[severity](t(`@jhb.software/payload-alt-text-plugin:${translationKey}`, variables));
|
|
52
39
|
}
|
|
53
40
|
// deselect all previously selected images
|
|
54
41
|
for (const id of selectedIds){
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useAuth, useConfig, useSelection, useTranslation } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation.js'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\n\nimport { PLUGIN_SLUG } from '../constants.js'\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function BulkGenerateAltTextsButton({ collectionSlug }: { collectionSlug: string }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const [isPending, startTransition] = useTransition()\n const { permissions } = useAuth()\n const { selected, setSelection } = useSelection()\n\n const canUpdateCollection = Boolean(permissions?.collections?.[collectionSlug]?.update)\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const selectedIds = Array.from(selected.entries())\n .filter(([, isSelected]) => isSelected)\n .map(([id]) => id) as string[]\n\n const router = useRouter()\n\n const handleGenerateAltTexts = () => {\n startTransition(async () => {\n if (!collectionSlug) {\n throw new Error('Collection slug is required')\n }\n\n try {\n const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate/bulk`, {\n body: JSON.stringify({\n collection: collectionSlug,\n ids: selectedIds,\n }),\n method: 'POST',\n })\n\n if (!response.ok) {\n toast.error(t('@jhb.software/payload-alt-text-plugin:failedToGenerate'))\n return\n }\n\n const data = (await response.json()) as
|
|
1
|
+
{"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useAuth, useConfig, useSelection, useTranslation } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation.js'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\nimport type { BulkGenerateResult } from './summarizeBulkGenerate.js'\n\nimport { PLUGIN_SLUG } from '../constants.js'\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\nimport { summarizeBulkGenerate } from './summarizeBulkGenerate.js'\n\nexport function BulkGenerateAltTextsButton({ collectionSlug }: { collectionSlug: string }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const [isPending, startTransition] = useTransition()\n const { permissions } = useAuth()\n const { selected, setSelection } = useSelection()\n\n const canUpdateCollection = Boolean(permissions?.collections?.[collectionSlug]?.update)\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const selectedIds = Array.from(selected.entries())\n .filter(([, isSelected]) => isSelected)\n .map(([id]) => id) as string[]\n\n const router = useRouter()\n\n const handleGenerateAltTexts = () => {\n startTransition(async () => {\n if (!collectionSlug) {\n throw new Error('Collection slug is required')\n }\n\n try {\n const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate/bulk`, {\n body: JSON.stringify({\n collection: collectionSlug,\n ids: selectedIds,\n }),\n method: 'POST',\n })\n\n if (!response.ok) {\n toast.error(t('@jhb.software/payload-alt-text-plugin:failedToGenerate'))\n return\n }\n\n const data = (await response.json()) as BulkGenerateResult\n\n for (const { severity, translationKey, variables } of summarizeBulkGenerate(data)) {\n toast[severity](t(`@jhb.software/payload-alt-text-plugin:${translationKey}`, variables))\n }\n\n // deselect all previously selected images\n for (const id of selectedIds) {\n setSelection(id)\n }\n\n router.refresh()\n } catch (error) {\n console.error('Error generating alt text:', error)\n toast.error(t('@jhb.software/payload-alt-text-plugin:errorGeneratingAltText'))\n }\n })\n }\n\n return (\n canUpdateCollection &&\n selectedIds.length > 0 && (\n <div className=\"m-0\" style={{ display: 'flex', justifyContent: 'right' }}>\n <Button\n className=\"m-0\"\n disabled={isPending || selectedIds.length === 0}\n icon={isPending ? <Spinner /> : <Lightning />}\n onClick={handleGenerateAltTexts}\n >\n {t('@jhb.software/payload-alt-text-plugin:generateAltTextFor', {\n count: selectedIds.length,\n })}\n </Button>\n </div>\n )\n )\n}\n"],"names":["Button","toast","useAuth","useConfig","useSelection","useTranslation","useRouter","useTransition","PLUGIN_SLUG","Lightning","Spinner","summarizeBulkGenerate","BulkGenerateAltTextsButton","collectionSlug","t","isPending","startTransition","permissions","selected","setSelection","canUpdateCollection","Boolean","collections","update","config","routes","api","apiRoute","serverURL","selectedIds","Array","from","entries","filter","isSelected","map","id","router","handleGenerateAltTexts","Error","response","fetch","body","JSON","stringify","collection","ids","method","ok","error","data","json","severity","translationKey","variables","refresh","console","length","div","className","style","display","justifyContent","disabled","icon","onClick","count"],"mappings":"AAAA;;AAEA,SAASA,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEC,SAAS,EAAEC,YAAY,EAAEC,cAAc,QAAQ,iBAAgB;AAChG,SAASC,SAAS,QAAQ,qBAAoB;AAC9C,SAASC,aAAa,QAAQ,QAAO;AAQrC,SAASC,WAAW,QAAQ,kBAAiB;AAC7C,SAASC,SAAS,QAAQ,uBAAsB;AAChD,SAASC,OAAO,QAAQ,qBAAoB;AAC5C,SAASC,qBAAqB,QAAQ,6BAA4B;AAElE,OAAO,SAASC,2BAA2B,EAAEC,cAAc,EAA8B;IACvF,MAAM,EAAEC,CAAC,EAAE,GAAGT;IACd,MAAM,CAACU,WAAWC,gBAAgB,GAAGT;IACrC,MAAM,EAAEU,WAAW,EAAE,GAAGf;IACxB,MAAM,EAAEgB,QAAQ,EAAEC,YAAY,EAAE,GAAGf;IAEnC,MAAMgB,sBAAsBC,QAAQJ,aAAaK,aAAa,CAACT,eAAe,EAAEU;IAChF,MAAM,EACJC,QAAQ,EACNC,QAAQ,EAAEC,KAAKC,QAAQ,EAAE,EACzBC,SAAS,EACV,EACF,GAAGzB;IAEJ,MAAM0B,cAAcC,MAAMC,IAAI,CAACb,SAASc,OAAO,IAC5CC,MAAM,CAAC,CAAC,GAAGC,WAAW,GAAKA,YAC3BC,GAAG,CAAC,CAAC,CAACC,GAAG,GAAKA;IAEjB,MAAMC,SAAS/B;IAEf,MAAMgC,yBAAyB;QAC7BtB,gBAAgB;YACd,IAAI,CAACH,gBAAgB;gBACnB,MAAM,IAAI0B,MAAM;YAClB;YAEA,IAAI;gBACF,MAAMC,WAAW,MAAMC,MAAM,GAAGb,aAAa,KAAKD,SAAS,CAAC,EAAEnB,YAAY,cAAc,CAAC,EAAE;oBACzFkC,MAAMC,KAAKC,SAAS,CAAC;wBACnBC,YAAYhC;wBACZiC,KAAKjB;oBACP;oBACAkB,QAAQ;gBACV;gBAEA,IAAI,CAACP,SAASQ,EAAE,EAAE;oBAChB/C,MAAMgD,KAAK,CAACnC,EAAE;oBACd;gBACF;gBAEA,MAAMoC,OAAQ,MAAMV,SAASW,IAAI;gBAEjC,KAAK,MAAM,EAAEC,QAAQ,EAAEC,cAAc,EAAEC,SAAS,EAAE,IAAI3C,sBAAsBuC,MAAO;oBACjFjD,KAAK,CAACmD,SAAS,CAACtC,EAAE,CAAC,sCAAsC,EAAEuC,gBAAgB,EAAEC;gBAC/E;gBAEA,0CAA0C;gBAC1C,KAAK,MAAMlB,MAAMP,YAAa;oBAC5BV,aAAaiB;gBACf;gBAEAC,OAAOkB,OAAO;YAChB,EAAE,OAAON,OAAO;gBACdO,QAAQP,KAAK,CAAC,8BAA8BA;gBAC5ChD,MAAMgD,KAAK,CAACnC,EAAE;YAChB;QACF;IACF;IAEA,OACEM,uBACAS,YAAY4B,MAAM,GAAG,mBACnB,KAACC;QAAIC,WAAU;QAAMC,OAAO;YAAEC,SAAS;YAAQC,gBAAgB;QAAQ;kBACrE,cAAA,KAAC9D;YACC2D,WAAU;YACVI,UAAUhD,aAAac,YAAY4B,MAAM,KAAK;YAC9CO,MAAMjD,0BAAY,KAACL,6BAAa,KAACD;YACjCwD,SAAS3B;sBAERxB,EAAE,4DAA4D;gBAC7DoD,OAAOrC,YAAY4B,MAAM;YAC3B;;;AAKV"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SkippedDoc } from '../endpoints/bulkGenerateAltTexts.js';
|
|
2
|
+
export type BulkGenerateResult = {
|
|
3
|
+
erroredDocs: (number | string)[];
|
|
4
|
+
skippedDocs: SkippedDoc[];
|
|
5
|
+
totalDocs: number;
|
|
6
|
+
updatedDocs: number;
|
|
7
|
+
};
|
|
8
|
+
export type BulkGenerateToast = {
|
|
9
|
+
severity: 'error' | 'info' | 'success' | 'warning';
|
|
10
|
+
translationKey: 'failedToGenerateForXImages' | 'skippedNoAltTextNeeded' | 'skippedUnsupportedFormat' | 'xOfYImagesUpdated';
|
|
11
|
+
variables: {
|
|
12
|
+
count: number;
|
|
13
|
+
total?: number;
|
|
14
|
+
updated?: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* What a finished bulk run tells the editor, in the order it is told.
|
|
19
|
+
*
|
|
20
|
+
* Skipped files were never candidates, so they are reported on their own terms
|
|
21
|
+
* — by what the editor has to do about each — and left out of the count the run
|
|
22
|
+
* is measured against. A run whose selection skipped away entirely has no
|
|
23
|
+
* result to report: "0 of 0 images updated" reads as a failure of work that
|
|
24
|
+
* never existed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function summarizeBulkGenerate({ erroredDocs, skippedDocs, totalDocs, updatedDocs, }: BulkGenerateResult): BulkGenerateToast[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a finished bulk run tells the editor, in the order it is told.
|
|
3
|
+
*
|
|
4
|
+
* Skipped files were never candidates, so they are reported on their own terms
|
|
5
|
+
* — by what the editor has to do about each — and left out of the count the run
|
|
6
|
+
* is measured against. A run whose selection skipped away entirely has no
|
|
7
|
+
* result to report: "0 of 0 images updated" reads as a failure of work that
|
|
8
|
+
* never existed.
|
|
9
|
+
*/ export function summarizeBulkGenerate({ erroredDocs, skippedDocs, totalDocs, updatedDocs }) {
|
|
10
|
+
const toasts = [];
|
|
11
|
+
if (erroredDocs.length > 0) {
|
|
12
|
+
toasts.push({
|
|
13
|
+
severity: 'error',
|
|
14
|
+
translationKey: 'failedToGenerateForXImages',
|
|
15
|
+
variables: {
|
|
16
|
+
count: erroredDocs.length
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const notTracked = skippedDocs.filter((doc)=>doc.reason === 'notTracked').length;
|
|
21
|
+
const unsupported = skippedDocs.length - notTracked;
|
|
22
|
+
if (notTracked > 0) {
|
|
23
|
+
toasts.push({
|
|
24
|
+
severity: 'info',
|
|
25
|
+
translationKey: 'skippedNoAltTextNeeded',
|
|
26
|
+
variables: {
|
|
27
|
+
count: notTracked
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Needs alt text, just not from a model — so it is the editor's to write.
|
|
32
|
+
if (unsupported > 0) {
|
|
33
|
+
toasts.push({
|
|
34
|
+
severity: 'warning',
|
|
35
|
+
translationKey: 'skippedUnsupportedFormat',
|
|
36
|
+
variables: {
|
|
37
|
+
count: unsupported
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const attempted = totalDocs - skippedDocs.length;
|
|
42
|
+
if (attempted > 0) {
|
|
43
|
+
toasts.push({
|
|
44
|
+
severity: updatedDocs === attempted ? 'success' : 'warning',
|
|
45
|
+
translationKey: 'xOfYImagesUpdated',
|
|
46
|
+
variables: {
|
|
47
|
+
count: attempted,
|
|
48
|
+
total: attempted,
|
|
49
|
+
updated: updatedDocs
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return toasts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=summarizeBulkGenerate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/summarizeBulkGenerate.ts"],"sourcesContent":["import type { SkippedDoc } from '../endpoints/bulkGenerateAltTexts.js'\n\nexport type BulkGenerateResult = {\n erroredDocs: (number | string)[]\n skippedDocs: SkippedDoc[]\n totalDocs: number\n updatedDocs: number\n}\n\nexport type BulkGenerateToast = {\n severity: 'error' | 'info' | 'success' | 'warning'\n translationKey:\n | 'failedToGenerateForXImages'\n | 'skippedNoAltTextNeeded'\n | 'skippedUnsupportedFormat'\n | 'xOfYImagesUpdated'\n variables: { count: number; total?: number; updated?: number }\n}\n\n/**\n * What a finished bulk run tells the editor, in the order it is told.\n *\n * Skipped files were never candidates, so they are reported on their own terms\n * — by what the editor has to do about each — and left out of the count the run\n * is measured against. A run whose selection skipped away entirely has no\n * result to report: \"0 of 0 images updated\" reads as a failure of work that\n * never existed.\n */\nexport function summarizeBulkGenerate({\n erroredDocs,\n skippedDocs,\n totalDocs,\n updatedDocs,\n}: BulkGenerateResult): BulkGenerateToast[] {\n const toasts: BulkGenerateToast[] = []\n\n if (erroredDocs.length > 0) {\n toasts.push({\n severity: 'error',\n translationKey: 'failedToGenerateForXImages',\n variables: { count: erroredDocs.length },\n })\n }\n\n const notTracked = skippedDocs.filter((doc) => doc.reason === 'notTracked').length\n const unsupported = skippedDocs.length - notTracked\n\n if (notTracked > 0) {\n toasts.push({\n severity: 'info',\n translationKey: 'skippedNoAltTextNeeded',\n variables: { count: notTracked },\n })\n }\n\n // Needs alt text, just not from a model — so it is the editor's to write.\n if (unsupported > 0) {\n toasts.push({\n severity: 'warning',\n translationKey: 'skippedUnsupportedFormat',\n variables: { count: unsupported },\n })\n }\n\n const attempted = totalDocs - skippedDocs.length\n\n if (attempted > 0) {\n toasts.push({\n severity: updatedDocs === attempted ? 'success' : 'warning',\n translationKey: 'xOfYImagesUpdated',\n variables: { count: attempted, total: attempted, updated: updatedDocs },\n })\n }\n\n return toasts\n}\n"],"names":["summarizeBulkGenerate","erroredDocs","skippedDocs","totalDocs","updatedDocs","toasts","length","push","severity","translationKey","variables","count","notTracked","filter","doc","reason","unsupported","attempted","total","updated"],"mappings":"AAmBA;;;;;;;;CAQC,GACD,OAAO,SAASA,sBAAsB,EACpCC,WAAW,EACXC,WAAW,EACXC,SAAS,EACTC,WAAW,EACQ;IACnB,MAAMC,SAA8B,EAAE;IAEtC,IAAIJ,YAAYK,MAAM,GAAG,GAAG;QAC1BD,OAAOE,IAAI,CAAC;YACVC,UAAU;YACVC,gBAAgB;YAChBC,WAAW;gBAAEC,OAAOV,YAAYK,MAAM;YAAC;QACzC;IACF;IAEA,MAAMM,aAAaV,YAAYW,MAAM,CAAC,CAACC,MAAQA,IAAIC,MAAM,KAAK,cAAcT,MAAM;IAClF,MAAMU,cAAcd,YAAYI,MAAM,GAAGM;IAEzC,IAAIA,aAAa,GAAG;QAClBP,OAAOE,IAAI,CAAC;YACVC,UAAU;YACVC,gBAAgB;YAChBC,WAAW;gBAAEC,OAAOC;YAAW;QACjC;IACF;IAEA,0EAA0E;IAC1E,IAAII,cAAc,GAAG;QACnBX,OAAOE,IAAI,CAAC;YACVC,UAAU;YACVC,gBAAgB;YAChBC,WAAW;gBAAEC,OAAOK;YAAY;QAClC;IACF;IAEA,MAAMC,YAAYd,YAAYD,YAAYI,MAAM;IAEhD,IAAIW,YAAY,GAAG;QACjBZ,OAAOE,IAAI,CAAC;YACVC,UAAUJ,gBAAgBa,YAAY,YAAY;YAClDR,gBAAgB;YAChBC,WAAW;gBAAEC,OAAOM;gBAAWC,OAAOD;gBAAWE,SAASf;YAAY;QACxE;IACF;IAEA,OAAOC;AACT"}
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import type { PayloadHandler } from 'payload';
|
|
2
2
|
import type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js';
|
|
3
3
|
/**
|
|
4
|
-
* Generates and updates alt text for multiple images in all locales.
|
|
4
|
+
* Generates and updates alt text for multiple images in all target locales.
|
|
5
|
+
*
|
|
6
|
+
* Files nothing can be generated for are reported as `skippedDocs` rather than
|
|
7
|
+
* `erroredDocs` — burying them among real failures hides those — each with the
|
|
8
|
+
* reason that decides what the editor has to do next. See {@link SkipReason}.
|
|
5
9
|
*/
|
|
6
10
|
export declare const bulkGenerateAltTextsEndpoint: (access: AltTextPluginConfig["access"]) => PayloadHandler;
|
|
11
|
+
/**
|
|
12
|
+
* Why a document was left alone.
|
|
13
|
+
*
|
|
14
|
+
* - `notTracked` — the collection does not track this file type, so it needs no
|
|
15
|
+
* alt text at all.
|
|
16
|
+
* - `unsupportedFormat` — a tracked file whose format the resolver cannot read.
|
|
17
|
+
* It still needs alt text; an editor has to write it.
|
|
18
|
+
*/
|
|
19
|
+
export type SkipReason = 'notTracked' | 'unsupportedFormat';
|
|
20
|
+
export type SkippedDoc = {
|
|
21
|
+
id: number | string;
|
|
22
|
+
reason: SkipReason;
|
|
23
|
+
};
|