@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
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import pMap from 'p-map';
|
|
2
2
|
import { APIError, Forbidden } from 'payload';
|
|
3
3
|
import { ZodError } from 'zod';
|
|
4
|
-
import { localesFromConfig } from '../utilities/localesFromConfig.js';
|
|
5
4
|
import { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js';
|
|
5
|
+
import { resolveLocales } from '../utilities/resolveLocales.js';
|
|
6
6
|
import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js';
|
|
7
7
|
/**
|
|
8
|
-
* Generates and updates alt text for multiple images in all locales.
|
|
8
|
+
* Generates and updates alt text for multiple images in all target locales.
|
|
9
|
+
*
|
|
10
|
+
* Files nothing can be generated for are reported as `skippedDocs` rather than
|
|
11
|
+
* `erroredDocs` — burying them among real failures hides those — each with the
|
|
12
|
+
* reason that decides what the editor has to do next. See {@link SkipReason}.
|
|
9
13
|
*/ export const bulkGenerateAltTextsEndpoint = (access)=>async (req)=>{
|
|
10
14
|
try {
|
|
11
15
|
if (!await access({
|
|
@@ -21,6 +25,7 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
21
25
|
const { collection, ids } = bulkGenerateAltTextsRequestSchema.parse(data);
|
|
22
26
|
let updatedDocs = 0;
|
|
23
27
|
const erroredDocs = [];
|
|
28
|
+
const skippedDocs = [];
|
|
24
29
|
// Get plugin config from payload config
|
|
25
30
|
const pluginConfig = req.payload.config.custom?.altTextPluginConfig;
|
|
26
31
|
if (!pluginConfig) {
|
|
@@ -62,12 +67,11 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
62
67
|
status: 400
|
|
63
68
|
});
|
|
64
69
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!targetLocales) {
|
|
70
|
+
const targetLocales = await resolveLocales({
|
|
71
|
+
pluginConfig,
|
|
72
|
+
req
|
|
73
|
+
});
|
|
74
|
+
if (targetLocales.length === 0) {
|
|
71
75
|
return Response.json({
|
|
72
76
|
error: 'Could not determine target locales for alt text generation. Please check your plugin configuration.'
|
|
73
77
|
}, {
|
|
@@ -76,7 +80,7 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
76
80
|
}
|
|
77
81
|
await pMap(uniqueIds, async (id)=>{
|
|
78
82
|
try {
|
|
79
|
-
await generateAndUpdateAltText({
|
|
83
|
+
const skipReason = await generateAndUpdateAltText({
|
|
80
84
|
id,
|
|
81
85
|
collection,
|
|
82
86
|
locales: targetLocales,
|
|
@@ -84,8 +88,16 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
84
88
|
pluginConfig,
|
|
85
89
|
req
|
|
86
90
|
});
|
|
91
|
+
if (skipReason) {
|
|
92
|
+
skippedDocs.push({
|
|
93
|
+
id,
|
|
94
|
+
reason: skipReason.reason
|
|
95
|
+
});
|
|
96
|
+
req.payload.logger.info(`Skipped ${id}: ${skipReason.detail}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
87
99
|
updatedDocs++;
|
|
88
|
-
|
|
100
|
+
req.payload.logger.info(`${updatedDocs}/${uniqueIds.length} updated (${Math.round(updatedDocs / uniqueIds.length * 100)}%)`);
|
|
89
101
|
} catch (error) {
|
|
90
102
|
// A Forbidden means the user has no read/update access to the
|
|
91
103
|
// collection at all — it applies to every id, so fail the whole
|
|
@@ -94,17 +106,20 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
94
106
|
if (error instanceof Forbidden) {
|
|
95
107
|
throw error;
|
|
96
108
|
}
|
|
97
|
-
|
|
109
|
+
req.payload.logger.error({
|
|
110
|
+
err: error
|
|
111
|
+
}, `Error generating alt text for ${id}`);
|
|
98
112
|
erroredDocs.push(id);
|
|
99
113
|
}
|
|
100
114
|
}, {
|
|
101
115
|
concurrency
|
|
102
116
|
});
|
|
103
117
|
if (erroredDocs.length > 0) {
|
|
104
|
-
|
|
118
|
+
req.payload.logger.error(`Failed for: ${erroredDocs.join(', ')}`);
|
|
105
119
|
}
|
|
106
120
|
return Response.json({
|
|
107
121
|
erroredDocs,
|
|
122
|
+
skippedDocs,
|
|
108
123
|
totalDocs: uniqueIds.length,
|
|
109
124
|
updatedDocs
|
|
110
125
|
});
|
|
@@ -123,7 +138,9 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
123
138
|
status: error.status
|
|
124
139
|
});
|
|
125
140
|
}
|
|
126
|
-
|
|
141
|
+
req.payload.logger.error({
|
|
142
|
+
err: error
|
|
143
|
+
}, 'Error in bulk generation');
|
|
127
144
|
return Response.json({
|
|
128
145
|
error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
129
146
|
}, {
|
|
@@ -131,7 +148,7 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
131
148
|
});
|
|
132
149
|
}
|
|
133
150
|
};
|
|
134
|
-
async function generateAndUpdateAltText({ id, collection, locales, payload, pluginConfig, req }) {
|
|
151
|
+
/** Returns why the document was skipped, or `undefined` once it has been written. */ async function generateAndUpdateAltText({ id, collection, locales, payload, pluginConfig, req }) {
|
|
135
152
|
const imageDoc = await payload.findByID({
|
|
136
153
|
id,
|
|
137
154
|
collection,
|
|
@@ -149,7 +166,10 @@ async function generateAndUpdateAltText({ id, collection, locales, payload, plug
|
|
|
149
166
|
// reaching this helper, so a matching entry is guaranteed.
|
|
150
167
|
const collectionConfig = pluginConfig.collections.find((entry)=>entry.slug === collection);
|
|
151
168
|
if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {
|
|
152
|
-
|
|
169
|
+
return {
|
|
170
|
+
detail: `alt text is not tracked for files of type "${mimeType}" in the "${collection}" collection`,
|
|
171
|
+
reason: 'notTracked'
|
|
172
|
+
};
|
|
153
173
|
}
|
|
154
174
|
const unsupportedSourceError = getUnsupportedSourceMimeTypeError({
|
|
155
175
|
declaredThumbnailMimeType: collectionConfig.imageThumbnailMimeType,
|
|
@@ -157,7 +177,10 @@ async function generateAndUpdateAltText({ id, collection, locales, payload, plug
|
|
|
157
177
|
supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes
|
|
158
178
|
});
|
|
159
179
|
if (unsupportedSourceError) {
|
|
160
|
-
|
|
180
|
+
return {
|
|
181
|
+
detail: unsupportedSourceError,
|
|
182
|
+
reason: 'unsupportedFormat'
|
|
183
|
+
};
|
|
161
184
|
}
|
|
162
185
|
const imageThumbnailUrl = await pluginConfig.getImageThumbnail(imageDoc, {
|
|
163
186
|
collection,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { APIError, Forbidden } from 'payload'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\nimport { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js'\nimport { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { collection, ids } = bulkGenerateAltTextsRequestSchema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n // Treat the configured collections as an allowlist. Reject any other\n // collection before touching the Local API, so the endpoint can only ever\n // operate on the upload collections the plugin manages.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (!collectionConfig) {\n return Response.json(\n { error: `Collection \"${collection}\" is not managed by the alt text plugin.` },\n { status: 403 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // De-duplicate so the same image is never generated (and billed) twice,\n // then bound the batch so a single request cannot fan out into an\n // unbounded number of paid resolver calls.\n const uniqueIds = [...new Set(ids)]\n\n if (uniqueIds.length > pluginConfig.maxBulkGenerateIds) {\n return Response.json(\n {\n error: `Too many ids: ${uniqueIds.length} exceeds the maximum of ${pluginConfig.maxBulkGenerateIds} per request.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n uniqueIds,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${uniqueIds.length} updated (${Math.round((updatedDocs / uniqueIds.length) * 100)}%)`,\n )\n } catch (error) {\n // A Forbidden means the user has no read/update access to the\n // collection at all — it applies to every id, so fail the whole\n // request with a real 403 instead of silently listing all ids as\n // errored. Row-level NotFound stays a per-doc error (partial success).\n if (error instanceof Forbidden) {\n throw error\n }\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: uniqueIds.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n // Surface Payload access errors (Forbidden 403) with their real status so\n // an agent gets an accurate, non-retryable signal instead of a 500.\n if (error instanceof APIError) {\n return Response.json({ error: error.message }, { status: error.status })\n }\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n // The handler validates `collection` against the configured collections before\n // reaching this helper, so a matching entry is guaranteed.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)!\n\n if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n throw new Error(\n `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n )\n }\n\n const unsupportedSourceError = getUnsupportedSourceMimeTypeError({\n declaredThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n mimeType,\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n })\n if (unsupportedSourceError) {\n throw new Error(unsupportedSourceError)\n }\n\n const imageThumbnailUrl = await pluginConfig.getImageThumbnail(imageDoc, { collection, req })\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n }\n }\n}\n"],"names":["pMap","APIError","Forbidden","ZodError","localesFromConfig","getUnsupportedSourceMimeTypeError","matchesMimeType","bulkGenerateAltTextsRequestSchema","formatZodError","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","collection","ids","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","collectionConfig","collections","find","entry","slug","resolver","concurrency","maxBulkGenerateConcurrency","uniqueIds","Set","length","maxBulkGenerateIds","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","Math","round","push","join","totalDocs","message","Error","imageDoc","findByID","depth","overrideAccess","user","mimeType","undefined","mimeTypes","unsupportedSourceError","declaredThumbnailMimeType","imageThumbnailMimeType","supportedMimeTypes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { APIError, Forbidden } from 'payload'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js'\nimport { resolveLocales } from '../utilities/resolveLocales.js'\nimport { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'\n\n/**\n * Generates and updates alt text for multiple images in all target locales.\n *\n * Files nothing can be generated for are reported as `skippedDocs` rather than\n * `erroredDocs` — burying them among real failures hides those — each with the\n * reason that decides what the editor has to do next. See {@link SkipReason}.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { collection, ids } = bulkGenerateAltTextsRequestSchema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n const skippedDocs: SkippedDoc[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n // Treat the configured collections as an allowlist. Reject any other\n // collection before touching the Local API, so the endpoint can only ever\n // operate on the upload collections the plugin manages.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (!collectionConfig) {\n return Response.json(\n { error: `Collection \"${collection}\" is not managed by the alt text plugin.` },\n { status: 403 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // De-duplicate so the same image is never generated (and billed) twice,\n // then bound the batch so a single request cannot fan out into an\n // unbounded number of paid resolver calls.\n const uniqueIds = [...new Set(ids)]\n\n if (uniqueIds.length > pluginConfig.maxBulkGenerateIds) {\n return Response.json(\n {\n error: `Too many ids: ${uniqueIds.length} exceeds the maximum of ${pluginConfig.maxBulkGenerateIds} per request.`,\n },\n { status: 400 },\n )\n }\n\n const targetLocales = await resolveLocales({ pluginConfig, req })\n\n if (targetLocales.length === 0) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n uniqueIds,\n async (id) => {\n try {\n const skipReason = await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n\n if (skipReason) {\n skippedDocs.push({ id, reason: skipReason.reason })\n req.payload.logger.info(`Skipped ${id}: ${skipReason.detail}`)\n return\n }\n\n updatedDocs++\n req.payload.logger.info(\n `${updatedDocs}/${uniqueIds.length} updated (${Math.round((updatedDocs / uniqueIds.length) * 100)}%)`,\n )\n } catch (error) {\n // A Forbidden means the user has no read/update access to the\n // collection at all — it applies to every id, so fail the whole\n // request with a real 403 instead of silently listing all ids as\n // errored. Row-level NotFound stays a per-doc error (partial success).\n if (error instanceof Forbidden) {\n throw error\n }\n req.payload.logger.error({ err: error }, `Error generating alt text for ${id}`)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n req.payload.logger.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n skippedDocs,\n totalDocs: uniqueIds.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n // Surface Payload access errors (Forbidden 403) with their real status so\n // an agent gets an accurate, non-retryable signal instead of a 500.\n if (error instanceof APIError) {\n return Response.json({ error: error.message }, { status: error.status })\n }\n req.payload.logger.error({ err: error }, 'Error in bulk generation')\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\n/**\n * Why a document was left alone.\n *\n * - `notTracked` — the collection does not track this file type, so it needs no\n * alt text at all.\n * - `unsupportedFormat` — a tracked file whose format the resolver cannot read.\n * It still needs alt text; an editor has to write it.\n */\nexport type SkipReason = 'notTracked' | 'unsupportedFormat'\n\nexport type SkippedDoc = { id: number | string; reason: SkipReason }\n\n/** Returns why the document was skipped, or `undefined` once it has been written. */\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}): Promise<{ detail: string; reason: SkipReason } | undefined> {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n // The handler validates `collection` against the configured collections before\n // reaching this helper, so a matching entry is guaranteed.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)!\n\n if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n return {\n detail: `alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection`,\n reason: 'notTracked',\n }\n }\n\n const unsupportedSourceError = getUnsupportedSourceMimeTypeError({\n declaredThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n mimeType,\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n })\n if (unsupportedSourceError) {\n return { detail: unsupportedSourceError, reason: 'unsupportedFormat' }\n }\n\n const imageThumbnailUrl = await pluginConfig.getImageThumbnail(imageDoc, { collection, req })\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n }\n }\n}\n"],"names":["pMap","APIError","Forbidden","ZodError","getUnsupportedSourceMimeTypeError","matchesMimeType","resolveLocales","bulkGenerateAltTextsRequestSchema","formatZodError","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","collection","ids","parse","updatedDocs","erroredDocs","skippedDocs","pluginConfig","payload","config","custom","altTextPluginConfig","collectionConfig","collections","find","entry","slug","resolver","concurrency","maxBulkGenerateConcurrency","uniqueIds","Set","length","maxBulkGenerateIds","targetLocales","id","skipReason","generateAndUpdateAltText","locales","push","reason","logger","info","detail","Math","round","err","join","totalDocs","message","Error","imageDoc","findByID","depth","overrideAccess","user","mimeType","undefined","mimeTypes","unsupportedSourceError","declaredThumbnailMimeType","imageThumbnailMimeType","supportedMimeTypes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","locale","localeResult","results","update","alt","altText","keywords"],"mappings":"AAEA,OAAOA,UAAU,QAAO;AACxB,SAASC,QAAQ,EAAEC,SAAS,QAAQ,UAAS;AAC7C,SAASC,QAAQ,QAAQ,MAAK;AAI9B,SAASC,iCAAiC,EAAEC,eAAe,QAAQ,4BAA2B;AAC9F,SAASC,cAAc,QAAQ,iCAAgC;AAC/D,SAASC,iCAAiC,EAAEC,cAAc,QAAQ,eAAc;AAEhF;;;;;;CAMC,GACD,OAAO,MAAMC,+BACX,CAACC,SACD,OAAOC;QACL,IAAI;YACF,IAAI,CAAE,MAAMD,OAAO;gBAAEC;YAAI,IAAK;gBAC5B,OAAOC,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAChE;YAEA,MAAMC,OAAO,UAAUL,OAAO,OAAOA,IAAIE,IAAI,KAAK,aAAa,MAAMF,IAAIE,IAAI,KAAK;YAElF,MAAM,EAAEI,UAAU,EAAEC,GAAG,EAAE,GAAGX,kCAAkCY,KAAK,CAACH;YAEpE,IAAII,cAAc;YAClB,MAAMC,cAAmC,EAAE;YAC3C,MAAMC,cAA4B,EAAE;YAEpC,wCAAwC;YACxC,MAAMC,eAAeZ,IAAIa,OAAO,CAACC,MAAM,CAACC,MAAM,EAAEC;YAGhD,IAAI,CAACJ,cAAc;gBACjB,OAAOX,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAA0B,GAAG;oBAAEC,QAAQ;gBAAI;YAC3E;YAEA,qEAAqE;YACrE,0EAA0E;YAC1E,wDAAwD;YACxD,MAAMa,mBAAmBL,aAAaM,WAAW,CAACC,IAAI,CAAC,CAACC,QAAUA,MAAMC,IAAI,KAAKf;YAEjF,IAAI,CAACW,kBAAkB;gBACrB,OAAOhB,SAASC,IAAI,CAClB;oBAAEC,OAAO,CAAC,YAAY,EAAEG,WAAW,wCAAwC,CAAC;gBAAC,GAC7E;oBAAEF,QAAQ;gBAAI;YAElB;YAEA,IAAI,CAACQ,aAAaU,QAAQ,EAAE;gBAC1B,OAAOrB,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAkC,GAAG;oBAAEC,QAAQ;gBAAI;YACnF;YAEA,MAAMmB,cAAcX,aAAaY,0BAA0B;YAE3D,wEAAwE;YACxE,kEAAkE;YAClE,2CAA2C;YAC3C,MAAMC,YAAY;mBAAI,IAAIC,IAAInB;aAAK;YAEnC,IAAIkB,UAAUE,MAAM,GAAGf,aAAagB,kBAAkB,EAAE;gBACtD,OAAO3B,SAASC,IAAI,CAClB;oBACEC,OAAO,CAAC,cAAc,EAAEsB,UAAUE,MAAM,CAAC,wBAAwB,EAAEf,aAAagB,kBAAkB,CAAC,aAAa,CAAC;gBACnH,GACA;oBAAExB,QAAQ;gBAAI;YAElB;YAEA,MAAMyB,gBAAgB,MAAMlC,eAAe;gBAAEiB;gBAAcZ;YAAI;YAE/D,IAAI6B,cAAcF,MAAM,KAAK,GAAG;gBAC9B,OAAO1B,SAASC,IAAI,CAClB;oBACEC,OACE;gBACJ,GACA;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMf,KACJoC,WACA,OAAOK;gBACL,IAAI;oBACF,MAAMC,aAAa,MAAMC,yBAAyB;wBAChDF;wBACAxB;wBACA2B,SAASJ;wBACThB,SAASb,IAAIa,OAAO;wBACpBD;wBACAZ;oBACF;oBAEA,IAAI+B,YAAY;wBACdpB,YAAYuB,IAAI,CAAC;4BAAEJ;4BAAIK,QAAQJ,WAAWI,MAAM;wBAAC;wBACjDnC,IAAIa,OAAO,CAACuB,MAAM,CAACC,IAAI,CAAC,CAAC,QAAQ,EAAEP,GAAG,EAAE,EAAEC,WAAWO,MAAM,EAAE;wBAC7D;oBACF;oBAEA7B;oBACAT,IAAIa,OAAO,CAACuB,MAAM,CAACC,IAAI,CACrB,GAAG5B,YAAY,CAAC,EAAEgB,UAAUE,MAAM,CAAC,UAAU,EAAEY,KAAKC,KAAK,CAAC,AAAC/B,cAAcgB,UAAUE,MAAM,GAAI,KAAK,EAAE,CAAC;gBAEzG,EAAE,OAAOxB,OAAO;oBACd,8DAA8D;oBAC9D,gEAAgE;oBAChE,iEAAiE;oBACjE,uEAAuE;oBACvE,IAAIA,iBAAiBZ,WAAW;wBAC9B,MAAMY;oBACR;oBACAH,IAAIa,OAAO,CAACuB,MAAM,CAACjC,KAAK,CAAC;wBAAEsC,KAAKtC;oBAAM,GAAG,CAAC,8BAA8B,EAAE2B,IAAI;oBAC9EpB,YAAYwB,IAAI,CAACJ;gBACnB;YACF,GACA;gBAAEP;YAAY;YAGhB,IAAIb,YAAYiB,MAAM,GAAG,GAAG;gBAC1B3B,IAAIa,OAAO,CAACuB,MAAM,CAACjC,KAAK,CAAC,CAAC,YAAY,EAAEO,YAAYgC,IAAI,CAAC,OAAO;YAClE;YAEA,OAAOzC,SAASC,IAAI,CAAC;gBACnBQ;gBACAC;gBACAgC,WAAWlB,UAAUE,MAAM;gBAC3BlB;YACF;QACF,EAAE,OAAON,OAAO;YACd,IAAIA,iBAAiBX,UAAU;gBAC7B,OAAOS,SAASC,IAAI,CAACL,eAAeM,QAAQ;oBAAEC,QAAQ;gBAAI;YAC5D;YACA,0EAA0E;YAC1E,oEAAoE;YACpE,IAAID,iBAAiBb,UAAU;gBAC7B,OAAOW,SAASC,IAAI,CAAC;oBAAEC,OAAOA,MAAMyC,OAAO;gBAAC,GAAG;oBAAExC,QAAQD,MAAMC,MAAM;gBAAC;YACxE;YACAJ,IAAIa,OAAO,CAACuB,MAAM,CAACjC,KAAK,CAAC;gBAAEsC,KAAKtC;YAAM,GAAG;YACzC,OAAOF,SAASC,IAAI,CAClB;gBACEC,OAAO,CAAC,2BAA2B,EAAEA,iBAAiB0C,QAAQ1C,MAAMyC,OAAO,GAAG,iBAAiB;YACjG,GACA;gBAAExC,QAAQ;YAAI;QAElB;IACF,EAAC;AAcH,mFAAmF,GACnF,eAAe4B,yBAAyB,EACtCF,EAAE,EACFxB,UAAU,EACV2B,OAAO,EACPpB,OAAO,EACPD,YAAY,EACZZ,GAAG,EAQJ;IACC,MAAM8C,WAAW,MAAMjC,QAAQkC,QAAQ,CAAC;QACtCjB;QACAxB;QACA0C,OAAO;QACP,gEAAgE;QAChE,sEAAsE;QACtEC,gBAAgB;QAChBC,MAAMlD,IAAIkD,IAAI;IAChB;IAEA,IAAI,CAACJ,UAAU;QACb,MAAM,IAAID,MAAM;IAClB;IAEA,MAAMM,WACJ,cAAcL,YAAY,OAAOA,SAASK,QAAQ,KAAK,WAAWL,SAASK,QAAQ,GAAGC;IAExF,+EAA+E;IAC/E,2DAA2D;IAC3D,MAAMnC,mBAAmBL,aAAaM,WAAW,CAACC,IAAI,CAAC,CAACC,QAAUA,MAAMC,IAAI,KAAKf;IAEjF,IAAI6C,YAAY,CAACzD,gBAAgByD,UAAUlC,iBAAiBoC,SAAS,GAAG;QACtE,OAAO;YACLf,QAAQ,CAAC,2CAA2C,EAAEa,SAAS,UAAU,EAAE7C,WAAW,YAAY,CAAC;YACnG6B,QAAQ;QACV;IACF;IAEA,MAAMmB,yBAAyB7D,kCAAkC;QAC/D8D,2BAA2BtC,iBAAiBuC,sBAAsB;QAClEL;QACAM,oBAAoB7C,aAAaU,QAAQ,CAACmC,kBAAkB;IAC9D;IACA,IAAIH,wBAAwB;QAC1B,OAAO;YAAEhB,QAAQgB;YAAwBnB,QAAQ;QAAoB;IACvE;IAEA,MAAMuB,oBAAoB,MAAM9C,aAAa+C,iBAAiB,CAACb,UAAU;QAAExC;QAAYN;IAAI;IAE3F,MAAM4D,SAAS,MAAMhD,aAAaU,QAAQ,CAACuC,WAAW,CAAC;QACrDC,UACE,cAAchB,YAAY,OAAOA,SAASgB,QAAQ,KAAK,WACnDhB,SAASgB,QAAQ,GACjBV;QACNI,wBAAwBvC,iBAAiBuC,sBAAsB;QAC/DE;QACAzB;QACAjC;IACF;IAEA,IAAI,CAAC4D,OAAOG,OAAO,EAAE;QACnB,MAAM,IAAIlB,MAAMe,OAAOzD,KAAK,IAAI;IAClC;IAEA,KAAK,MAAM6D,UAAU/B,QAAS;QAC5B,MAAMgC,eAAeL,OAAOM,OAAO,CAACF,OAAO;QAC3C,IAAIC,cAAc;YAChB,MAAMpD,QAAQsD,MAAM,CAAC;gBACnBrC;gBACAxB;gBACAD,MAAM;oBACJ+D,KAAKH,aAAaI,OAAO;oBACzBC,UAAUL,aAAaK,QAAQ;gBACjC;gBACAN;gBACA,gEAAgE;gBAChE,sEAAsE;gBACtEf,gBAAgB;gBAChBC,MAAMlD,IAAIkD,IAAI;YAChB;QACF;IACF;AACF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { APIError } from 'payload';
|
|
2
2
|
import { ZodError } from 'zod';
|
|
3
3
|
import { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js';
|
|
4
|
+
import { resolveLocales } from '../utilities/resolveLocales.js';
|
|
4
5
|
import { formatZodError, generateAltTextRequestSchema } from './schemas.js';
|
|
5
6
|
/**
|
|
6
7
|
* Generates alt text for a single image using the configured resolver.
|
|
@@ -92,15 +93,20 @@ import { formatZodError, generateAltTextRequestSchema } from './schemas.js';
|
|
|
92
93
|
status: 400
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}, {
|
|
102
|
-
status: 400
|
|
96
|
+
// Reject a locale this request may not write before it reaches the
|
|
97
|
+
// document or the resolver's prompt, which interpolates it verbatim.
|
|
98
|
+
if (locale != null && pluginConfig.locales.length > 0) {
|
|
99
|
+
const availableLocales = await resolveLocales({
|
|
100
|
+
pluginConfig,
|
|
101
|
+
req
|
|
103
102
|
});
|
|
103
|
+
if (!availableLocales.includes(locale)) {
|
|
104
|
+
return Response.json({
|
|
105
|
+
error: `Locale "${locale}" is not available. Available locales: ${availableLocales.join(', ')}.`
|
|
106
|
+
}, {
|
|
107
|
+
status: 400
|
|
108
|
+
});
|
|
109
|
+
}
|
|
104
110
|
}
|
|
105
111
|
// determine target locale
|
|
106
112
|
const targetLocale = locale ?? pluginConfig.locale;
|
|
@@ -165,7 +171,9 @@ import { formatZodError, generateAltTextRequestSchema } from './schemas.js';
|
|
|
165
171
|
status: error.status
|
|
166
172
|
});
|
|
167
173
|
}
|
|
168
|
-
|
|
174
|
+
req.payload.logger.error({
|
|
175
|
+
err: error
|
|
176
|
+
}, 'Error generating alt text');
|
|
169
177
|
return Response.json({
|
|
170
178
|
error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
171
179
|
}, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { APIError } from 'payload'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js'\nimport { formatZodError, generateAltTextRequestSchema } from './schemas.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\n *\n * By default, returns the result without updating the document (preview mode).\n * Pass `update: true` in the request body to also persist the generated alt text\n * and keywords to the document — useful for programmatic/agent workflows.\n *\n * The response always includes the `id` and `collection` for easy correlation.\n */\nexport const generateAltTextEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { id, collection, locale, update } = generateAltTextRequestSchema.parse(data)\n\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n // Treat the configured collections as an allowlist. Reject any other\n // collection before touching the Local API, so the endpoint can only ever\n // operate on the upload collections the plugin manages.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (!collectionConfig) {\n return Response.json(\n { error: `Collection \"${collection}\" is not managed by the alt text plugin.` },\n { status: 403 },\n )\n }\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\n if (!pluginConfig.getImageThumbnail) {\n return Response.json(\n { error: 'getImageThumbnail function not configured' },\n { status: 500 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string'\n ? imageDoc.mimeType\n : undefined\n\n if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n return Response.json(\n {\n error: `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n const unsupportedSourceError = getUnsupportedSourceMimeTypeError({\n declaredThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n mimeType,\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n })\n if (unsupportedSourceError) {\n return Response.json({ error: unsupportedSourceError }, { status: 400 })\n }\n\n // When localization is enabled, the requested locale must be one of the\n // configured locales. Reject anything else before it can be written to an\n // unconfigured locale or interpolated into the resolver's prompt.\n if (\n locale != null &&\n pluginConfig.locales.length > 0 &&\n !pluginConfig.locales.includes(locale)\n ) {\n return Response.json(\n {\n error: `Locale \"${locale}\" is not configured. Configured locales: ${pluginConfig.locales.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locale\n const targetLocale = locale ?? pluginConfig.locale\n if (!targetLocale) {\n return Response.json(\n {\n error:\n 'Could not determine target locale for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n const imageThumbnailUrl = await pluginConfig.getImageThumbnail(imageDoc, { collection, req })\n\n const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n if (update) {\n await req.payload.update({\n id,\n collection,\n data: {\n alt: result.result.altText,\n keywords: result.result.keywords,\n },\n locale: targetLocale,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n }\n\n return Response.json({ id, collection, ...result.result })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n // Surface Payload access errors (Forbidden 403 / NotFound 404) with their\n // real status so an agent gets an accurate, non-retryable signal instead\n // of a misleading 500.\n if (error instanceof APIError) {\n return Response.json({ error: error.message }, { status: error.status })\n }\n console.error('Error generating alt text:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n"],"names":["APIError","ZodError","getUnsupportedSourceMimeTypeError","matchesMimeType","formatZodError","generateAltTextRequestSchema","generateAltTextEndpoint","access","req","Response","json","error","status","data","id","collection","locale","update","parse","pluginConfig","payload","config","custom","altTextPluginConfig","collectionConfig","collections","find","entry","slug","imageDoc","findByID","depth","overrideAccess","user","getImageThumbnail","resolver","mimeType","undefined","mimeTypes","join","unsupportedSourceError","declaredThumbnailMimeType","imageThumbnailMimeType","supportedMimeTypes","locales","length","includes","targetLocale","imageThumbnailUrl","result","resolve","filename","success","alt","altText","keywords","message","console","Error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { APIError } from 'payload'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { getUnsupportedSourceMimeTypeError, matchesMimeType } from '../utilities/mimeTypes.js'\nimport { resolveLocales } from '../utilities/resolveLocales.js'\nimport { formatZodError, generateAltTextRequestSchema } from './schemas.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\n *\n * By default, returns the result without updating the document (preview mode).\n * Pass `update: true` in the request body to also persist the generated alt text\n * and keywords to the document — useful for programmatic/agent workflows.\n *\n * The response always includes the `id` and `collection` for easy correlation.\n */\nexport const generateAltTextEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { id, collection, locale, update } = generateAltTextRequestSchema.parse(data)\n\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n // Treat the configured collections as an allowlist. Reject any other\n // collection before touching the Local API, so the endpoint can only ever\n // operate on the upload collections the plugin manages.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (!collectionConfig) {\n return Response.json(\n { error: `Collection \"${collection}\" is not managed by the alt text plugin.` },\n { status: 403 },\n )\n }\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\n if (!pluginConfig.getImageThumbnail) {\n return Response.json(\n { error: 'getImageThumbnail function not configured' },\n { status: 500 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string'\n ? imageDoc.mimeType\n : undefined\n\n if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n return Response.json(\n {\n error: `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n const unsupportedSourceError = getUnsupportedSourceMimeTypeError({\n declaredThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n mimeType,\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n })\n if (unsupportedSourceError) {\n return Response.json({ error: unsupportedSourceError }, { status: 400 })\n }\n\n // Reject a locale this request may not write before it reaches the\n // document or the resolver's prompt, which interpolates it verbatim.\n if (locale != null && pluginConfig.locales.length > 0) {\n const availableLocales = await resolveLocales({ pluginConfig, req })\n\n if (!availableLocales.includes(locale)) {\n return Response.json(\n {\n error: `Locale \"${locale}\" is not available. Available locales: ${availableLocales.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n }\n\n // determine target locale\n const targetLocale = locale ?? pluginConfig.locale\n if (!targetLocale) {\n return Response.json(\n {\n error:\n 'Could not determine target locale for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n const imageThumbnailUrl = await pluginConfig.getImageThumbnail(imageDoc, { collection, req })\n\n const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailMimeType: collectionConfig.imageThumbnailMimeType,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n if (update) {\n await req.payload.update({\n id,\n collection,\n data: {\n alt: result.result.altText,\n keywords: result.result.keywords,\n },\n locale: targetLocale,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n }\n\n return Response.json({ id, collection, ...result.result })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n // Surface Payload access errors (Forbidden 403 / NotFound 404) with their\n // real status so an agent gets an accurate, non-retryable signal instead\n // of a misleading 500.\n if (error instanceof APIError) {\n return Response.json({ error: error.message }, { status: error.status })\n }\n req.payload.logger.error({ err: error }, 'Error generating alt text')\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n"],"names":["APIError","ZodError","getUnsupportedSourceMimeTypeError","matchesMimeType","resolveLocales","formatZodError","generateAltTextRequestSchema","generateAltTextEndpoint","access","req","Response","json","error","status","data","id","collection","locale","update","parse","pluginConfig","payload","config","custom","altTextPluginConfig","collectionConfig","collections","find","entry","slug","imageDoc","findByID","depth","overrideAccess","user","getImageThumbnail","resolver","mimeType","undefined","mimeTypes","join","unsupportedSourceError","declaredThumbnailMimeType","imageThumbnailMimeType","supportedMimeTypes","locales","length","availableLocales","includes","targetLocale","imageThumbnailUrl","result","resolve","filename","success","alt","altText","keywords","message","logger","err","Error"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export { payloadAltTextPlugin } from './plugin.js';
|
|
2
|
+
export { anthropicResolver } from './resolvers/anthropic.js';
|
|
3
|
+
export type { AnthropicResolverConfig } from './resolvers/anthropic.js';
|
|
4
|
+
export { createVisionResolver, VisionProviderError } from './resolvers/createVisionResolver.js';
|
|
5
|
+
export type { VisionGenerateArgs, VisionImage, VisionInstructions, VisionInstructionsArgs, VisionResolverConfig, } from './resolvers/createVisionResolver.js';
|
|
2
6
|
export { mistralResolver } from './resolvers/mistral.js';
|
|
3
7
|
export type { MistralResolverConfig } from './resolvers/mistral.js';
|
|
4
8
|
export { openAIResolver } from './resolvers/openAI.js';
|
|
9
|
+
export type { OpenAIResolverConfig } from './resolvers/openAI.js';
|
|
5
10
|
export * from './resolvers/types.js';
|
|
6
|
-
export type { AltTextCollectionConfig, GetImageThumbnail, IncomingAltTextPluginConfig as AltTextPluginConfig, } from './types/AltTextPluginConfig.js';
|
|
11
|
+
export type { AltTextCollectionConfig, AltTextHealthBaseFilter, AltTextHealthCheckConfig, FilterLocales, GetImageThumbnail, IncomingAltTextPluginConfig as AltTextPluginConfig, } from './types/AltTextPluginConfig.js';
|
|
7
12
|
export { getAltTextHealth } from './utilities/altTextHealth.js';
|
|
8
13
|
export type { AltTextHealthError, AltTextHealthErrorCode, AltTextHealthScan, AltTextHealthScanCollection, } from './utilities/altTextHealth.js';
|
|
9
14
|
export { matchesMimeType, validateAltText } from './utilities/mimeTypes.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { payloadAltTextPlugin } from './plugin.js';
|
|
2
|
+
export { anthropicResolver } from './resolvers/anthropic.js';
|
|
3
|
+
export { createVisionResolver, VisionProviderError } from './resolvers/createVisionResolver.js';
|
|
2
4
|
export { mistralResolver } from './resolvers/mistral.js';
|
|
3
5
|
export { openAIResolver } from './resolvers/openAI.js';
|
|
4
6
|
export * from './resolvers/types.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadAltTextPlugin } from './plugin.js'\nexport { mistralResolver } from './resolvers/mistral.js'\nexport type { MistralResolverConfig } from './resolvers/mistral.js'\nexport { openAIResolver } from './resolvers/openAI.js'\nexport * from './resolvers/types.js'\nexport type {\n AltTextCollectionConfig,\n GetImageThumbnail,\n IncomingAltTextPluginConfig as AltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\nexport { getAltTextHealth } from './utilities/altTextHealth.js'\nexport type {\n AltTextHealthError,\n AltTextHealthErrorCode,\n AltTextHealthScan,\n AltTextHealthScanCollection,\n} from './utilities/altTextHealth.js'\nexport { matchesMimeType, validateAltText } from './utilities/mimeTypes.js'\n"],"names":["payloadAltTextPlugin","mistralResolver","openAIResolver","getAltTextHealth","matchesMimeType","validateAltText"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,cAAa;AAClD,SAASC,eAAe,QAAQ,yBAAwB;AAExD,SAASC,cAAc,QAAQ,wBAAuB;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadAltTextPlugin } from './plugin.js'\nexport { anthropicResolver } from './resolvers/anthropic.js'\nexport type { AnthropicResolverConfig } from './resolvers/anthropic.js'\nexport { createVisionResolver, VisionProviderError } from './resolvers/createVisionResolver.js'\nexport type {\n VisionGenerateArgs,\n VisionImage,\n VisionInstructions,\n VisionInstructionsArgs,\n VisionResolverConfig,\n} from './resolvers/createVisionResolver.js'\nexport { mistralResolver } from './resolvers/mistral.js'\nexport type { MistralResolverConfig } from './resolvers/mistral.js'\nexport { openAIResolver } from './resolvers/openAI.js'\nexport type { OpenAIResolverConfig } from './resolvers/openAI.js'\nexport * from './resolvers/types.js'\nexport type {\n AltTextCollectionConfig,\n AltTextHealthBaseFilter,\n AltTextHealthCheckConfig,\n FilterLocales,\n GetImageThumbnail,\n IncomingAltTextPluginConfig as AltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\nexport { getAltTextHealth } from './utilities/altTextHealth.js'\nexport type {\n AltTextHealthError,\n AltTextHealthErrorCode,\n AltTextHealthScan,\n AltTextHealthScanCollection,\n} from './utilities/altTextHealth.js'\nexport { matchesMimeType, validateAltText } from './utilities/mimeTypes.js'\n"],"names":["payloadAltTextPlugin","anthropicResolver","createVisionResolver","VisionProviderError","mistralResolver","openAIResolver","getAltTextHealth","matchesMimeType","validateAltText"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,cAAa;AAClD,SAASC,iBAAiB,QAAQ,2BAA0B;AAE5D,SAASC,oBAAoB,EAAEC,mBAAmB,QAAQ,sCAAqC;AAQ/F,SAASC,eAAe,QAAQ,yBAAwB;AAExD,SAASC,cAAc,QAAQ,wBAAuB;AAEtD,cAAc,uBAAsB;AASpC,SAASC,gBAAgB,QAAQ,+BAA8B;AAO/D,SAASC,eAAe,EAAEC,eAAe,QAAQ,2BAA0B"}
|
package/dist/plugin.js
CHANGED
|
@@ -50,17 +50,24 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
const access = incomingPluginConfig.access ?? (({ req })=>!!req.user);
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
53
|
+
// The former function form was the health report's access gate. Accepting it
|
|
54
|
+
// silently would widen that gate to the plugin's `access`, so it fails at boot.
|
|
55
|
+
if (typeof incomingPluginConfig.healthCheck === 'function') {
|
|
56
|
+
throw new Error('The alt-text plugin no longer accepts a function for `healthCheck`. ' + 'Move the access check to `healthCheck: { access: ({ req }) => ... }`.');
|
|
57
|
+
}
|
|
58
|
+
const healthCheckConfig = typeof incomingPluginConfig.healthCheck === 'object' ? incomingPluginConfig.healthCheck : {};
|
|
59
|
+
// The health report's own gate, falling back to the shared `access`.
|
|
60
|
+
const healthCheckAccess = healthCheckConfig.access ?? access;
|
|
56
61
|
const pluginConfig = {
|
|
57
62
|
access,
|
|
58
63
|
collections: normalizedCollections,
|
|
59
64
|
enabled: incomingPluginConfig.enabled ?? true,
|
|
60
65
|
fieldsOverride: incomingPluginConfig.fieldsOverride,
|
|
66
|
+
filterLocales: incomingPluginConfig.filterLocales,
|
|
61
67
|
getImageThumbnail: incomingPluginConfig.getImageThumbnail,
|
|
62
68
|
healthCheck: enableHealthCheck,
|
|
63
69
|
healthCheckAccess,
|
|
70
|
+
healthCheckBaseFilter: healthCheckConfig.baseFilter,
|
|
64
71
|
locale: incomingPluginConfig.locale,
|
|
65
72
|
locales,
|
|
66
73
|
maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,
|
|
@@ -75,6 +82,9 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
75
82
|
entry.slug,
|
|
76
83
|
entry
|
|
77
84
|
]));
|
|
85
|
+
// Collected while collections are mapped and flushed in `onInit`: no Payload instance —
|
|
86
|
+
// and therefore no logger — exists while the config is still being built.
|
|
87
|
+
const configWarnings = [];
|
|
78
88
|
// Ensure collections array exists
|
|
79
89
|
config.collections = config.collections || [];
|
|
80
90
|
// Map over collections and inject AI alt text fields into specified ones
|
|
@@ -82,7 +92,7 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
82
92
|
const altTextCollectionConfig = collectionConfigBySlug.get(collectionConfig.slug);
|
|
83
93
|
if (altTextCollectionConfig) {
|
|
84
94
|
if (!collectionConfig.upload) {
|
|
85
|
-
|
|
95
|
+
configWarnings.push(`AI Alt Text Plugin: Collection "${collectionConfig.slug}" is not an upload collection. Skipping field injection.`);
|
|
86
96
|
return collectionConfig;
|
|
87
97
|
}
|
|
88
98
|
const defaultFields = [
|
|
@@ -189,6 +199,12 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
189
199
|
i18n: {
|
|
190
200
|
...config.i18n,
|
|
191
201
|
translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {})
|
|
202
|
+
},
|
|
203
|
+
onInit: async (payload)=>{
|
|
204
|
+
for (const warning of configWarnings){
|
|
205
|
+
payload.logger.warn(warning);
|
|
206
|
+
}
|
|
207
|
+
await config.onInit?.(payload);
|
|
192
208
|
}
|
|
193
209
|
};
|
|
194
210
|
};
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, Widget } from 'payload'\n\nimport type {\n AltTextPluginConfig,\n IncomingAltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\n\nimport { PLUGIN_SLUG } from './constants.js'\nimport { altTextHealthEndpoint } from './endpoints/altTextHealth.js'\nimport { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js'\nimport { generateAltTextEndpoint } from './endpoints/generateAltText.js'\nimport { altTextField } from './fields/altTextField.js'\nimport { keywordsField } from './fields/keywordsField.js'\nimport {\n createRevalidateAltTextHealthAfterChangeHook,\n createRevalidateAltTextHealthAfterDeleteHook,\n} from './hooks/revalidateAltTextHealth.js'\nimport { translations } from './translations/index.js'\nimport { isValidMimeType, normalizeCollectionsConfig } from './utilities/mimeTypes.js'\nimport { deepMergeSimple } from './utils/deepMergeSimple.js'\n\nconst altTextHealthWidgetDefinition = {\n slug: 'alt-text-health',\n // `Component` was renamed from `ComponentPath` in Payload 3.79.0. Set both for backward compatibility.\n Component: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n ComponentPath: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n label: {\n de: 'Alternativtexte Zustand',\n en: 'Alt text health',\n },\n maxWidth: 'full',\n minWidth: 'medium',\n} satisfies { ComponentPath: string } & Widget\n\nexport const payloadAltTextPlugin =\n (incomingPluginConfig: IncomingAltTextPluginConfig) =>\n (incomingConfig: Config): Config => {\n const config = { ...incomingConfig }\n\n // If the plugin is disabled, return the config without modifying it\n if (incomingPluginConfig.enabled === false) {\n return config\n }\n\n const locales = config.localization\n ? config.localization.locales.map((localeConfig) =>\n typeof localeConfig === 'string' ? localeConfig : localeConfig.code,\n )\n : []\n\n const enableHealthCheck = incomingPluginConfig.healthCheck !== false\n\n const normalizedCollections = normalizeCollectionsConfig(incomingPluginConfig.collections, {\n imageThumbnailMimeType: incomingPluginConfig.imageThumbnailMimeType,\n })\n\n // A declared thumbnail MIME type replaces the per-document source check, so a\n // wrong one fails at boot rather than as a silently missing guard or a 500 per\n // image.\n const supportedMimeTypes = incomingPluginConfig.resolver.supportedMimeTypes\n for (const collection of normalizedCollections) {\n const declared = collection.imageThumbnailMimeType\n if (declared === undefined) {\n continue\n }\n\n if (!isValidMimeType(declared)) {\n throw new Error(\n `The alt-text plugin is configured with imageThumbnailMimeType \"${declared}\" for the \"${collection.slug}\" collection, ` +\n 'but that is not a valid MIME type. Expected something like \"image/webp\".',\n )\n }\n\n if (supportedMimeTypes && !supportedMimeTypes.includes(declared)) {\n throw new Error(\n `The alt-text plugin is configured with imageThumbnailMimeType \"${declared}\" for the \"${collection.slug}\" collection, ` +\n `but the \"${incomingPluginConfig.resolver.key}\" resolver does not support it. ` +\n `Supported types: ${supportedMimeTypes.join(', ')}. ` +\n \"Either change the transformation in getImageThumbnail, or remove the declaration to fall back to checking each document's own mime type.\",\n )\n }\n }\n\n const access = incomingPluginConfig.access ?? (({ req }) => !!req.user)\n\n // A function form of `healthCheck` doubles as the health report's access\n // gate; otherwise it falls back to the shared `access`.\n const healthCheckAccess =\n typeof incomingPluginConfig.healthCheck === 'function'\n ? incomingPluginConfig.healthCheck\n : access\n\n const pluginConfig: AltTextPluginConfig = {\n access,\n collections: normalizedCollections,\n enabled: incomingPluginConfig.enabled ?? true,\n fieldsOverride: incomingPluginConfig.fieldsOverride,\n getImageThumbnail: incomingPluginConfig.getImageThumbnail,\n healthCheck: enableHealthCheck,\n healthCheckAccess,\n locale: incomingPluginConfig.locale,\n locales,\n maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,\n maxBulkGenerateIds: incomingPluginConfig.maxBulkGenerateIds ?? 100,\n resolver: incomingPluginConfig.resolver,\n }\n\n // Validate locale requirement for non-localized mode\n if (locales.length === 0 && !incomingPluginConfig.locale) {\n throw new Error(\n 'The alt-text plugin requires a \"locale\" option when Payload localization is disabled. ' +\n 'Please add { locale: \"en\" } (or your preferred locale) to your plugin configuration.',\n )\n }\n\n const collectionConfigBySlug = new Map<string, (typeof normalizedCollections)[number]>(\n normalizedCollections.map((entry) => [entry.slug, entry]),\n )\n\n // Ensure collections array exists\n config.collections = config.collections || []\n\n // Map over collections and inject AI alt text fields into specified ones\n config.collections = config.collections.map((collectionConfig) => {\n const altTextCollectionConfig = collectionConfigBySlug.get(collectionConfig.slug)\n\n if (altTextCollectionConfig) {\n if (!collectionConfig.upload) {\n console.warn(\n `AI Alt Text Plugin: Collection \"${collectionConfig.slug}\" is not an upload collection. Skipping field injection.`,\n )\n return collectionConfig\n }\n\n const defaultFields = [\n altTextField({\n localized: Boolean(config.localization),\n // When the collection declares what getImageThumbnail delivers, the\n // document's own mime type says nothing about whether generation can\n // succeed — so don't let the admin UI disable the button on it.\n supportedMimeTypes: altTextCollectionConfig.imageThumbnailMimeType\n ? undefined\n : pluginConfig.resolver.supportedMimeTypes,\n trackedMimeTypes: altTextCollectionConfig.mimeTypes,\n validate: altTextCollectionConfig.validate,\n }),\n keywordsField({\n localized: Boolean(config.localization),\n }),\n ]\n\n const fields =\n incomingPluginConfig.fieldsOverride &&\n typeof incomingPluginConfig.fieldsOverride === 'function'\n ? incomingPluginConfig.fieldsOverride({ defaultFields })\n : defaultFields\n\n return {\n ...collectionConfig,\n admin: {\n ...collectionConfig.admin,\n components: {\n ...(collectionConfig.admin?.components ?? {}),\n // TODO: use the beforeBulkAction custom component slot once available: https://github.com/payloadcms/payload/pull/11719\n beforeListTable: [\n ...(collectionConfig.admin?.components?.beforeListTable ?? []),\n {\n path: '@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton',\n props: {\n collectionSlug: collectionConfig.slug,\n },\n },\n ],\n },\n // enhance the search by adding the filename, keywords and alt fields (if the user has not provided their own listSearchableFields)\n listSearchableFields: collectionConfig.admin?.listSearchableFields ?? [\n 'filename',\n 'keywords',\n 'alt',\n ],\n },\n fields: [...(collectionConfig.fields ?? []), ...fields],\n hooks: {\n ...collectionConfig.hooks,\n ...(enableHealthCheck && {\n afterChange: [\n ...(collectionConfig.hooks?.afterChange ?? []),\n createRevalidateAltTextHealthAfterChangeHook(collectionConfig.slug),\n ],\n afterDelete: [\n ...(collectionConfig.hooks?.afterDelete ?? []),\n createRevalidateAltTextHealthAfterDeleteHook(collectionConfig.slug),\n ],\n }),\n },\n }\n }\n\n return collectionConfig\n })\n\n const existingWidgets = config.admin?.dashboard?.widgets ?? []\n const widgets =\n !enableHealthCheck || existingWidgets.some((widget) => widget.slug === 'alt-text-health')\n ? existingWidgets\n : [...existingWidgets, altTextHealthWidgetDefinition]\n\n return {\n ...config,\n admin: {\n ...config.admin,\n dashboard: {\n ...config.admin?.dashboard,\n widgets,\n },\n },\n custom: {\n ...config.custom,\n // Make plugin config available in hooks/actions\n altTextPluginConfig: pluginConfig,\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n handler: generateAltTextEndpoint(pluginConfig.access),\n method: 'post',\n path: `/${PLUGIN_SLUG}/generate`,\n },\n {\n handler: bulkGenerateAltTextsEndpoint(pluginConfig.access),\n method: 'post',\n path: `/${PLUGIN_SLUG}/generate/bulk`,\n },\n ...(enableHealthCheck\n ? [\n {\n handler: altTextHealthEndpoint(pluginConfig.healthCheckAccess),\n method: 'get' as const,\n path: `/${PLUGIN_SLUG}/health`,\n },\n ]\n : []),\n ],\n i18n: {\n ...config.i18n,\n translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {}),\n },\n }\n }\n"],"names":["PLUGIN_SLUG","altTextHealthEndpoint","bulkGenerateAltTextsEndpoint","generateAltTextEndpoint","altTextField","keywordsField","createRevalidateAltTextHealthAfterChangeHook","createRevalidateAltTextHealthAfterDeleteHook","translations","isValidMimeType","normalizeCollectionsConfig","deepMergeSimple","altTextHealthWidgetDefinition","slug","Component","ComponentPath","label","de","en","maxWidth","minWidth","payloadAltTextPlugin","incomingPluginConfig","incomingConfig","config","enabled","locales","localization","map","localeConfig","code","enableHealthCheck","healthCheck","normalizedCollections","collections","imageThumbnailMimeType","supportedMimeTypes","resolver","collection","declared","undefined","Error","includes","key","join","access","req","user","healthCheckAccess","pluginConfig","fieldsOverride","getImageThumbnail","locale","maxBulkGenerateConcurrency","maxBulkGenerateIds","length","collectionConfigBySlug","Map","entry","collectionConfig","altTextCollectionConfig","get","upload","console","warn","defaultFields","localized","Boolean","trackedMimeTypes","mimeTypes","validate","fields","admin","components","beforeListTable","path","props","collectionSlug","listSearchableFields","hooks","afterChange","afterDelete","existingWidgets","dashboard","widgets","some","widget","custom","altTextPluginConfig","endpoints","handler","method","i18n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, Widget } from 'payload'\n\nimport type {\n AltTextPluginConfig,\n IncomingAltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\n\nimport { PLUGIN_SLUG } from './constants.js'\nimport { altTextHealthEndpoint } from './endpoints/altTextHealth.js'\nimport { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js'\nimport { generateAltTextEndpoint } from './endpoints/generateAltText.js'\nimport { altTextField } from './fields/altTextField.js'\nimport { keywordsField } from './fields/keywordsField.js'\nimport {\n createRevalidateAltTextHealthAfterChangeHook,\n createRevalidateAltTextHealthAfterDeleteHook,\n} from './hooks/revalidateAltTextHealth.js'\nimport { translations } from './translations/index.js'\nimport { isValidMimeType, normalizeCollectionsConfig } from './utilities/mimeTypes.js'\nimport { deepMergeSimple } from './utils/deepMergeSimple.js'\n\nconst altTextHealthWidgetDefinition = {\n slug: 'alt-text-health',\n // `Component` was renamed from `ComponentPath` in Payload 3.79.0. Set both for backward compatibility.\n Component: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n ComponentPath: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n label: {\n de: 'Alternativtexte Zustand',\n en: 'Alt text health',\n },\n maxWidth: 'full',\n minWidth: 'medium',\n} satisfies { ComponentPath: string } & Widget\n\nexport const payloadAltTextPlugin =\n (incomingPluginConfig: IncomingAltTextPluginConfig) =>\n (incomingConfig: Config): Config => {\n const config = { ...incomingConfig }\n\n // If the plugin is disabled, return the config without modifying it\n if (incomingPluginConfig.enabled === false) {\n return config\n }\n\n const locales = config.localization\n ? config.localization.locales.map((localeConfig) =>\n typeof localeConfig === 'string' ? localeConfig : localeConfig.code,\n )\n : []\n\n const enableHealthCheck = incomingPluginConfig.healthCheck !== false\n\n const normalizedCollections = normalizeCollectionsConfig(incomingPluginConfig.collections, {\n imageThumbnailMimeType: incomingPluginConfig.imageThumbnailMimeType,\n })\n\n // A declared thumbnail MIME type replaces the per-document source check, so a\n // wrong one fails at boot rather than as a silently missing guard or a 500 per\n // image.\n const supportedMimeTypes = incomingPluginConfig.resolver.supportedMimeTypes\n for (const collection of normalizedCollections) {\n const declared = collection.imageThumbnailMimeType\n if (declared === undefined) {\n continue\n }\n\n if (!isValidMimeType(declared)) {\n throw new Error(\n `The alt-text plugin is configured with imageThumbnailMimeType \"${declared}\" for the \"${collection.slug}\" collection, ` +\n 'but that is not a valid MIME type. Expected something like \"image/webp\".',\n )\n }\n\n if (supportedMimeTypes && !supportedMimeTypes.includes(declared)) {\n throw new Error(\n `The alt-text plugin is configured with imageThumbnailMimeType \"${declared}\" for the \"${collection.slug}\" collection, ` +\n `but the \"${incomingPluginConfig.resolver.key}\" resolver does not support it. ` +\n `Supported types: ${supportedMimeTypes.join(', ')}. ` +\n \"Either change the transformation in getImageThumbnail, or remove the declaration to fall back to checking each document's own mime type.\",\n )\n }\n }\n\n const access = incomingPluginConfig.access ?? (({ req }) => !!req.user)\n\n // The former function form was the health report's access gate. Accepting it\n // silently would widen that gate to the plugin's `access`, so it fails at boot.\n if (typeof incomingPluginConfig.healthCheck === 'function') {\n throw new Error(\n 'The alt-text plugin no longer accepts a function for `healthCheck`. ' +\n 'Move the access check to `healthCheck: { access: ({ req }) => ... }`.',\n )\n }\n\n const healthCheckConfig =\n typeof incomingPluginConfig.healthCheck === 'object' ? incomingPluginConfig.healthCheck : {}\n\n // The health report's own gate, falling back to the shared `access`.\n const healthCheckAccess = healthCheckConfig.access ?? access\n\n const pluginConfig: AltTextPluginConfig = {\n access,\n collections: normalizedCollections,\n enabled: incomingPluginConfig.enabled ?? true,\n fieldsOverride: incomingPluginConfig.fieldsOverride,\n filterLocales: incomingPluginConfig.filterLocales,\n getImageThumbnail: incomingPluginConfig.getImageThumbnail,\n healthCheck: enableHealthCheck,\n healthCheckAccess,\n healthCheckBaseFilter: healthCheckConfig.baseFilter,\n locale: incomingPluginConfig.locale,\n locales,\n maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,\n maxBulkGenerateIds: incomingPluginConfig.maxBulkGenerateIds ?? 100,\n resolver: incomingPluginConfig.resolver,\n }\n\n // Validate locale requirement for non-localized mode\n if (locales.length === 0 && !incomingPluginConfig.locale) {\n throw new Error(\n 'The alt-text plugin requires a \"locale\" option when Payload localization is disabled. ' +\n 'Please add { locale: \"en\" } (or your preferred locale) to your plugin configuration.',\n )\n }\n\n const collectionConfigBySlug = new Map<string, (typeof normalizedCollections)[number]>(\n normalizedCollections.map((entry) => [entry.slug, entry]),\n )\n\n // Collected while collections are mapped and flushed in `onInit`: no Payload instance —\n // and therefore no logger — exists while the config is still being built.\n const configWarnings: string[] = []\n\n // Ensure collections array exists\n config.collections = config.collections || []\n\n // Map over collections and inject AI alt text fields into specified ones\n config.collections = config.collections.map((collectionConfig) => {\n const altTextCollectionConfig = collectionConfigBySlug.get(collectionConfig.slug)\n\n if (altTextCollectionConfig) {\n if (!collectionConfig.upload) {\n configWarnings.push(\n `AI Alt Text Plugin: Collection \"${collectionConfig.slug}\" is not an upload collection. Skipping field injection.`,\n )\n return collectionConfig\n }\n\n const defaultFields = [\n altTextField({\n localized: Boolean(config.localization),\n // When the collection declares what getImageThumbnail delivers, the\n // document's own mime type says nothing about whether generation can\n // succeed — so don't let the admin UI disable the button on it.\n supportedMimeTypes: altTextCollectionConfig.imageThumbnailMimeType\n ? undefined\n : pluginConfig.resolver.supportedMimeTypes,\n trackedMimeTypes: altTextCollectionConfig.mimeTypes,\n validate: altTextCollectionConfig.validate,\n }),\n keywordsField({\n localized: Boolean(config.localization),\n }),\n ]\n\n const fields =\n incomingPluginConfig.fieldsOverride &&\n typeof incomingPluginConfig.fieldsOverride === 'function'\n ? incomingPluginConfig.fieldsOverride({ defaultFields })\n : defaultFields\n\n return {\n ...collectionConfig,\n admin: {\n ...collectionConfig.admin,\n components: {\n ...(collectionConfig.admin?.components ?? {}),\n // TODO: use the beforeBulkAction custom component slot once available: https://github.com/payloadcms/payload/pull/11719\n beforeListTable: [\n ...(collectionConfig.admin?.components?.beforeListTable ?? []),\n {\n path: '@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton',\n props: {\n collectionSlug: collectionConfig.slug,\n },\n },\n ],\n },\n // enhance the search by adding the filename, keywords and alt fields (if the user has not provided their own listSearchableFields)\n listSearchableFields: collectionConfig.admin?.listSearchableFields ?? [\n 'filename',\n 'keywords',\n 'alt',\n ],\n },\n fields: [...(collectionConfig.fields ?? []), ...fields],\n hooks: {\n ...collectionConfig.hooks,\n ...(enableHealthCheck && {\n afterChange: [\n ...(collectionConfig.hooks?.afterChange ?? []),\n createRevalidateAltTextHealthAfterChangeHook(collectionConfig.slug),\n ],\n afterDelete: [\n ...(collectionConfig.hooks?.afterDelete ?? []),\n createRevalidateAltTextHealthAfterDeleteHook(collectionConfig.slug),\n ],\n }),\n },\n }\n }\n\n return collectionConfig\n })\n\n const existingWidgets = config.admin?.dashboard?.widgets ?? []\n const widgets =\n !enableHealthCheck || existingWidgets.some((widget) => widget.slug === 'alt-text-health')\n ? existingWidgets\n : [...existingWidgets, altTextHealthWidgetDefinition]\n\n return {\n ...config,\n admin: {\n ...config.admin,\n dashboard: {\n ...config.admin?.dashboard,\n widgets,\n },\n },\n custom: {\n ...config.custom,\n // Make plugin config available in hooks/actions\n altTextPluginConfig: pluginConfig,\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n handler: generateAltTextEndpoint(pluginConfig.access),\n method: 'post',\n path: `/${PLUGIN_SLUG}/generate`,\n },\n {\n handler: bulkGenerateAltTextsEndpoint(pluginConfig.access),\n method: 'post',\n path: `/${PLUGIN_SLUG}/generate/bulk`,\n },\n ...(enableHealthCheck\n ? [\n {\n handler: altTextHealthEndpoint(pluginConfig.healthCheckAccess),\n method: 'get' as const,\n path: `/${PLUGIN_SLUG}/health`,\n },\n ]\n : []),\n ],\n i18n: {\n ...config.i18n,\n translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {}),\n },\n onInit: async (payload) => {\n for (const warning of configWarnings) {\n payload.logger.warn(warning)\n }\n\n await config.onInit?.(payload)\n },\n }\n }\n"],"names":["PLUGIN_SLUG","altTextHealthEndpoint","bulkGenerateAltTextsEndpoint","generateAltTextEndpoint","altTextField","keywordsField","createRevalidateAltTextHealthAfterChangeHook","createRevalidateAltTextHealthAfterDeleteHook","translations","isValidMimeType","normalizeCollectionsConfig","deepMergeSimple","altTextHealthWidgetDefinition","slug","Component","ComponentPath","label","de","en","maxWidth","minWidth","payloadAltTextPlugin","incomingPluginConfig","incomingConfig","config","enabled","locales","localization","map","localeConfig","code","enableHealthCheck","healthCheck","normalizedCollections","collections","imageThumbnailMimeType","supportedMimeTypes","resolver","collection","declared","undefined","Error","includes","key","join","access","req","user","healthCheckConfig","healthCheckAccess","pluginConfig","fieldsOverride","filterLocales","getImageThumbnail","healthCheckBaseFilter","baseFilter","locale","maxBulkGenerateConcurrency","maxBulkGenerateIds","length","collectionConfigBySlug","Map","entry","configWarnings","collectionConfig","altTextCollectionConfig","get","upload","push","defaultFields","localized","Boolean","trackedMimeTypes","mimeTypes","validate","fields","admin","components","beforeListTable","path","props","collectionSlug","listSearchableFields","hooks","afterChange","afterDelete","existingWidgets","dashboard","widgets","some","widget","custom","altTextPluginConfig","endpoints","handler","method","i18n","onInit","payload","warning","logger","warn"],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { VisionInstructions } from './createVisionResolver.js';
|
|
2
|
+
import type { AltTextResolver } from './types.js';
|
|
3
|
+
export type AnthropicResolverConfig = {
|
|
4
|
+
/** Anthropic API key for authentication */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/**
|
|
7
|
+
* Base URL of the Anthropic API.
|
|
8
|
+
* @default 'https://api.anthropic.com'
|
|
9
|
+
*/
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Caps how long Claude thinks before answering. Lower effort still thinks on a
|
|
13
|
+
* difficult image, just less than a higher setting would.
|
|
14
|
+
*
|
|
15
|
+
* Describing an image is not a reasoning-heavy task, so `'low'` keeps the
|
|
16
|
+
* spend down on the models that accept it. Omitted, the field is not sent and
|
|
17
|
+
* Claude uses its default (`'high'`) — which also keeps models without effort
|
|
18
|
+
* support, such as `claude-haiku-4-5`, usable.
|
|
19
|
+
*/
|
|
20
|
+
effort?: 'high' | 'low' | 'max' | 'medium' | 'xhigh';
|
|
21
|
+
/**
|
|
22
|
+
* Builds the instructions from the default ones, e.g. to append a house style
|
|
23
|
+
* rule. Sent as the system prompt, separately from the image.
|
|
24
|
+
*
|
|
25
|
+
* @default ({ defaultInstructions }) => defaultInstructions
|
|
26
|
+
*/
|
|
27
|
+
instructions?: VisionInstructions;
|
|
28
|
+
/**
|
|
29
|
+
* The Claude model to use for alt text generation.
|
|
30
|
+
*
|
|
31
|
+
* Must be able to read images. `claude-sonnet-5` is the cheaper choice for a
|
|
32
|
+
* large media library; `claude-haiku-4-5` works too, but only without
|
|
33
|
+
* `effort`.
|
|
34
|
+
*
|
|
35
|
+
* @default 'claude-opus-5'
|
|
36
|
+
*/
|
|
37
|
+
model?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Abort after this many milliseconds. Covers downloading the image and the
|
|
40
|
+
* message call together.
|
|
41
|
+
* @default 30000
|
|
42
|
+
*/
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Creates a Claude-based resolver for alt text generation.
|
|
47
|
+
*
|
|
48
|
+
* The image is downloaded and sent as bytes. Claude can fetch an image URL
|
|
49
|
+
* itself, but that path is not dependable for a CMS: it requires the file to be
|
|
50
|
+
* reachable from the public internet. Sending the bytes removes that whole class
|
|
51
|
+
* of failure for the price of one extra download. The `media_type` a base64 image block carries
|
|
52
|
+
* comes from the download, which reads it off what the thumbnail URL served.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import { anthropicResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
57
|
+
*
|
|
58
|
+
* anthropicResolver({
|
|
59
|
+
* apiKey: process.env.ANTHROPIC_API_KEY,
|
|
60
|
+
* model: 'claude-opus-5', // optional, this is the default
|
|
61
|
+
* })
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare const anthropicResolver: ({ apiKey, baseUrl, effort, instructions, model, timeoutMs, }: AnthropicResolverConfig) => AltTextResolver;
|