@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.
Files changed (45) hide show
  1. package/README.md +225 -36
  2. package/dist/components/BulkGenerateAltTextsButton.js +3 -16
  3. package/dist/components/BulkGenerateAltTextsButton.js.map +1 -1
  4. package/dist/components/summarizeBulkGenerate.d.ts +26 -0
  5. package/dist/components/summarizeBulkGenerate.js +56 -0
  6. package/dist/components/summarizeBulkGenerate.js.map +1 -0
  7. package/dist/endpoints/bulkGenerateAltTexts.d.ts +18 -1
  8. package/dist/endpoints/bulkGenerateAltTexts.js +39 -16
  9. package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
  10. package/dist/endpoints/generateAltText.js +17 -9
  11. package/dist/endpoints/generateAltText.js.map +1 -1
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugin.js +20 -4
  16. package/dist/plugin.js.map +1 -1
  17. package/dist/resolvers/anthropic.d.ts +64 -0
  18. package/dist/resolvers/anthropic.js +140 -0
  19. package/dist/resolvers/anthropic.js.map +1 -0
  20. package/dist/resolvers/createVisionResolver.d.ts +148 -0
  21. package/dist/resolvers/createVisionResolver.js +300 -0
  22. package/dist/resolvers/createVisionResolver.js.map +1 -0
  23. package/dist/resolvers/mistral.d.ts +15 -1
  24. package/dist/resolvers/mistral.js +85 -250
  25. package/dist/resolvers/mistral.js.map +1 -1
  26. package/dist/resolvers/openAI.d.ts +22 -3
  27. package/dist/resolvers/openAI.js +57 -138
  28. package/dist/resolvers/openAI.js.map +1 -1
  29. package/dist/translations/de.js +12 -4
  30. package/dist/translations/de.js.map +1 -1
  31. package/dist/translations/en.js +12 -4
  32. package/dist/translations/en.js.map +1 -1
  33. package/dist/translations/translation-schema.json +24 -8
  34. package/dist/types/AltTextPluginConfig.d.ts +71 -12
  35. package/dist/types/AltTextPluginConfig.js.map +1 -1
  36. package/dist/utilities/altTextHealth.d.ts +3 -1
  37. package/dist/utilities/altTextHealth.js +103 -12
  38. package/dist/utilities/altTextHealth.js.map +1 -1
  39. package/dist/utilities/resolveLocales.d.ts +15 -0
  40. package/dist/utilities/resolveLocales.js +38 -0
  41. package/dist/utilities/resolveLocales.js.map +1 -0
  42. package/dist/utilities/stableStringify.d.ts +9 -0
  43. package/dist/utilities/stableStringify.js +19 -0
  44. package/dist/utilities/stableStringify.js.map +1 -0
  45. 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
- // determine target locales based on config
66
- const locales = localesFromConfig(req.payload.config);
67
- const targetLocales = locales ?? [
68
- pluginConfig.locale
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
- console.log(`${updatedDocs}/${uniqueIds.length} updated (${Math.round(updatedDocs / uniqueIds.length * 100)}%)`);
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
- console.error(`Error generating alt text for ${id}:`, error);
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
- console.error(`Failed for: ${erroredDocs.join(', ')}`);
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
- console.error('Error in bulk generation:', error);
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
- throw new Error(`Alt text is not tracked for files of type "${mimeType}" in the "${collection}" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`);
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
- throw new Error(unsupportedSourceError);
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":"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"}
@@ -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
- // When localization is enabled, the requested locale must be one of the
96
- // configured locales. Reject anything else before it can be written to an
97
- // unconfigured locale or interpolated into the resolver's prompt.
98
- if (locale != null && pluginConfig.locales.length > 0 && !pluginConfig.locales.includes(locale)) {
99
- return Response.json({
100
- error: `Locale "${locale}" is not configured. Configured locales: ${pluginConfig.locales.join(', ')}.`
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
- console.error('Error generating alt text:', error);
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;AACtD,cAAc,uBAAsB;AAMpC,SAASC,gBAAgB,QAAQ,+BAA8B;AAO/D,SAASC,eAAe,EAAEC,eAAe,QAAQ,2BAA0B"}
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
- // A function form of `healthCheck` doubles as the health report's access
54
- // gate; otherwise it falls back to the shared `access`.
55
- const healthCheckAccess = typeof incomingPluginConfig.healthCheck === 'function' ? incomingPluginConfig.healthCheck : access;
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
- console.warn(`AI Alt Text Plugin: Collection "${collectionConfig.slug}" is not an upload collection. Skipping field injection.`);
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
  };
@@ -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;