@jhb.software/payload-alt-text-plugin 0.9.1 → 0.11.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 +275 -20
- package/dist/endpoints/bulkGenerateAltTexts.js +21 -8
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +16 -5
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/hooks/revalidateAltTextHealth.js +31 -17
- package/dist/hooks/revalidateAltTextHealth.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +43 -7
- package/dist/plugin.js.map +1 -1
- package/dist/resolvers/anthropic.d.ts +65 -0
- package/dist/resolvers/anthropic.js +141 -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 +53 -0
- package/dist/resolvers/mistral.js +114 -0
- package/dist/resolvers/mistral.js.map +1 -0
- package/dist/resolvers/openAI.d.ts +32 -3
- package/dist/resolvers/openAI.js +63 -144
- package/dist/resolvers/openAI.js.map +1 -1
- package/dist/resolvers/types.d.ts +24 -1
- package/dist/resolvers/types.js.map +1 -1
- package/dist/translations/index.js.map +1 -1
- package/dist/types/AltTextPluginConfig.d.ts +86 -18
- package/dist/types/AltTextPluginConfig.js.map +1 -1
- package/dist/utilities/altTextHealth.d.ts +3 -1
- package/dist/utilities/altTextHealth.js +74 -8
- package/dist/utilities/altTextHealth.js.map +1 -1
- package/dist/utilities/mimeTypes.d.ts +54 -1
- package/dist/utilities/mimeTypes.js +41 -2
- package/dist/utilities/mimeTypes.js.map +1 -1
- 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 +14 -15
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
import { revalidateTag } from 'next/cache.js';
|
|
2
|
+
import { after } from 'next/server.js';
|
|
2
3
|
import { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealthCollectionTag } from '../utilities/altTextHealth.js';
|
|
3
4
|
function safeRevalidateTag(req, tag) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
5
|
+
const runRevalidate = ()=>{
|
|
6
|
+
try {
|
|
7
|
+
// Support both Next 15 and Next 16. Next 15 types `revalidateTag(tag)` as 1-arg; Next 16
|
|
8
|
+
// added a required second `profile` arg and logs a deprecation warning for 1-arg calls.
|
|
9
|
+
// Passing 'max' satisfies Next 16 and is ignored at runtime by Next 15. The cast lets the
|
|
10
|
+
// build succeed regardless of which Next types are resolved from the consuming project.
|
|
11
|
+
;
|
|
12
|
+
revalidateTag(tag, 'max');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
if (message.includes('static generation store missing')) {
|
|
16
|
+
req.payload.logger.warn({
|
|
17
|
+
msg: 'Skipping alt text health cache revalidation outside a Next.js request context.',
|
|
18
|
+
plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,
|
|
19
|
+
tag
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
20
24
|
}
|
|
21
|
-
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
// Defer via `after()` so the call escapes the current render scope.
|
|
28
|
+
// Next.js disallows synchronous `revalidateTag` from inside a server-component
|
|
29
|
+
// render — relevant when users seed via `payload.create` from `onInit`,
|
|
30
|
+
// which runs while the admin route is rendering.
|
|
31
|
+
after(runRevalidate);
|
|
32
|
+
} catch {
|
|
33
|
+
// No request scope (CLI / migrations / scripts). Run inline; the inner
|
|
34
|
+
// `try/catch` will warn-and-skip if Next.js itself has no context either.
|
|
35
|
+
runRevalidate();
|
|
22
36
|
}
|
|
23
37
|
}
|
|
24
38
|
export const createRevalidateAltTextHealthAfterChangeHook = (collectionSlug)=>({ doc, req })=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/revalidateAltTextHealth.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'\n\nimport { revalidateTag } from 'next/cache.js'\n\nimport {\n ALT_TEXT_HEALTH_PLUGIN_SLUG,\n getAltTextHealthCollectionTag,\n} from '../utilities/altTextHealth.js'\n\nfunction safeRevalidateTag(req: PayloadRequest, tag: string): void {\n try {\n
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/revalidateAltTextHealth.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'\n\nimport { revalidateTag } from 'next/cache.js'\nimport { after } from 'next/server.js'\n\nimport {\n ALT_TEXT_HEALTH_PLUGIN_SLUG,\n getAltTextHealthCollectionTag,\n} from '../utilities/altTextHealth.js'\n\nfunction safeRevalidateTag(req: PayloadRequest, tag: string): void {\n const runRevalidate = (): void => {\n try {\n // Support both Next 15 and Next 16. Next 15 types `revalidateTag(tag)` as 1-arg; Next 16\n // added a required second `profile` arg and logs a deprecation warning for 1-arg calls.\n // Passing 'max' satisfies Next 16 and is ignored at runtime by Next 15. The cast lets the\n // build succeed regardless of which Next types are resolved from the consuming project.\n ;(revalidateTag as (tag: string, profile?: string) => void)(tag, 'max')\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n if (message.includes('static generation store missing')) {\n req.payload.logger.warn({\n msg: 'Skipping alt text health cache revalidation outside a Next.js request context.',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n tag,\n })\n return\n }\n\n throw error\n }\n }\n\n try {\n // Defer via `after()` so the call escapes the current render scope.\n // Next.js disallows synchronous `revalidateTag` from inside a server-component\n // render — relevant when users seed via `payload.create` from `onInit`,\n // which runs while the admin route is rendering.\n after(runRevalidate)\n } catch {\n // No request scope (CLI / migrations / scripts). Run inline; the inner\n // `try/catch` will warn-and-skip if Next.js itself has no context either.\n runRevalidate()\n }\n}\n\nexport const createRevalidateAltTextHealthAfterChangeHook =\n (collectionSlug: string): CollectionAfterChangeHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n\nexport const createRevalidateAltTextHealthAfterDeleteHook =\n (collectionSlug: string): CollectionAfterDeleteHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n"],"names":["revalidateTag","after","ALT_TEXT_HEALTH_PLUGIN_SLUG","getAltTextHealthCollectionTag","safeRevalidateTag","req","tag","runRevalidate","error","message","Error","String","includes","payload","logger","warn","msg","plugin","createRevalidateAltTextHealthAfterChangeHook","collectionSlug","doc","context","disableRevalidate","createRevalidateAltTextHealthAfterDeleteHook"],"mappings":"AAEA,SAASA,aAAa,QAAQ,gBAAe;AAC7C,SAASC,KAAK,QAAQ,iBAAgB;AAEtC,SACEC,2BAA2B,EAC3BC,6BAA6B,QACxB,gCAA+B;AAEtC,SAASC,kBAAkBC,GAAmB,EAAEC,GAAW;IACzD,MAAMC,gBAAgB;QACpB,IAAI;YACF,yFAAyF;YACzF,wFAAwF;YACxF,0FAA0F;YAC1F,wFAAwF;;YACtFP,cAA0DM,KAAK;QACnE,EAAE,OAAOE,OAAO;YACd,MAAMC,UAAUD,iBAAiBE,QAAQF,MAAMC,OAAO,GAAGE,OAAOH;YAEhE,IAAIC,QAAQG,QAAQ,CAAC,oCAAoC;gBACvDP,IAAIQ,OAAO,CAACC,MAAM,CAACC,IAAI,CAAC;oBACtBC,KAAK;oBACLC,QAAQf;oBACRI;gBACF;gBACA;YACF;YAEA,MAAME;QACR;IACF;IAEA,IAAI;QACF,oEAAoE;QACpE,+EAA+E;QAC/E,wEAAwE;QACxE,iDAAiD;QACjDP,MAAMM;IACR,EAAE,OAAM;QACN,uEAAuE;QACvE,0EAA0E;QAC1EA;IACF;AACF;AAEA,OAAO,MAAMW,+CACX,CAACC,iBACD,CAAC,EAAEC,GAAG,EAAEf,GAAG,EAAE;QACX,IAAI,CAACA,IAAIgB,OAAO,EAAEC,mBAAmB;YACnClB,kBAAkBC,KAAKF,8BAA8BgB;QACvD;QAEA,OAAOC;IACT,EAAC;AAEH,OAAO,MAAMG,+CACX,CAACJ,iBACD,CAAC,EAAEC,GAAG,EAAEf,GAAG,EAAE;QACX,IAAI,CAACA,IAAIgB,OAAO,EAAEC,mBAAmB;YACnClB,kBAAkBC,KAAKF,8BAA8BgB;QACvD;QAEA,OAAOC;IACT,EAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +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';
|
|
6
|
+
export { mistralResolver } from './resolvers/mistral.js';
|
|
7
|
+
export type { MistralResolverConfig } from './resolvers/mistral.js';
|
|
2
8
|
export { openAIResolver } from './resolvers/openAI.js';
|
|
9
|
+
export type { OpenAIResolverConfig } from './resolvers/openAI.js';
|
|
3
10
|
export * from './resolvers/types.js';
|
|
4
|
-
export type { AltTextCollectionConfig, IncomingAltTextPluginConfig as AltTextPluginConfig, } from './types/AltTextPluginConfig.js';
|
|
11
|
+
export type { AltTextCollectionConfig, AltTextHealthBaseFilter, AltTextHealthCheckConfig, GetImageThumbnail, IncomingAltTextPluginConfig as AltTextPluginConfig, } from './types/AltTextPluginConfig.js';
|
|
5
12
|
export { getAltTextHealth } from './utilities/altTextHealth.js';
|
|
6
13
|
export type { AltTextHealthError, AltTextHealthErrorCode, AltTextHealthScan, AltTextHealthScanCollection, } from './utilities/altTextHealth.js';
|
|
7
14
|
export { matchesMimeType, validateAltText } from './utilities/mimeTypes.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { payloadAltTextPlugin } from './plugin.js';
|
|
2
|
+
export { anthropicResolver } from './resolvers/anthropic.js';
|
|
3
|
+
export { createVisionResolver, VisionProviderError } from './resolvers/createVisionResolver.js';
|
|
4
|
+
export { mistralResolver } from './resolvers/mistral.js';
|
|
2
5
|
export { openAIResolver } from './resolvers/openAI.js';
|
|
3
6
|
export * from './resolvers/types.js';
|
|
4
7
|
export { getAltTextHealth } from './utilities/altTextHealth.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadAltTextPlugin } from './plugin.js'\nexport { openAIResolver } from './resolvers/openAI.js'\nexport * from './resolvers/types.js'\nexport type {\n AltTextCollectionConfig,\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","openAIResolver","getAltTextHealth","matchesMimeType","validateAltText"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,cAAa;AAClD,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 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;AAQpC,SAASC,gBAAgB,QAAQ,+BAA8B;AAO/D,SAASC,eAAe,EAAEC,eAAe,QAAQ,2BAA0B"}
|
package/dist/plugin.js
CHANGED
|
@@ -6,7 +6,7 @@ import { altTextField } from './fields/altTextField.js';
|
|
|
6
6
|
import { keywordsField } from './fields/keywordsField.js';
|
|
7
7
|
import { createRevalidateAltTextHealthAfterChangeHook, createRevalidateAltTextHealthAfterDeleteHook } from './hooks/revalidateAltTextHealth.js';
|
|
8
8
|
import { translations } from './translations/index.js';
|
|
9
|
-
import { normalizeCollectionsConfig } from './utilities/mimeTypes.js';
|
|
9
|
+
import { isValidMimeType, normalizeCollectionsConfig } from './utilities/mimeTypes.js';
|
|
10
10
|
import { deepMergeSimple } from './utils/deepMergeSimple.js';
|
|
11
11
|
const altTextHealthWidgetDefinition = {
|
|
12
12
|
slug: 'alt-text-health',
|
|
@@ -30,11 +30,34 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
30
30
|
}
|
|
31
31
|
const locales = config.localization ? config.localization.locales.map((localeConfig)=>typeof localeConfig === 'string' ? localeConfig : localeConfig.code) : [];
|
|
32
32
|
const enableHealthCheck = incomingPluginConfig.healthCheck !== false;
|
|
33
|
-
const normalizedCollections = normalizeCollectionsConfig(incomingPluginConfig.collections
|
|
33
|
+
const normalizedCollections = normalizeCollectionsConfig(incomingPluginConfig.collections, {
|
|
34
|
+
imageThumbnailMimeType: incomingPluginConfig.imageThumbnailMimeType
|
|
35
|
+
});
|
|
36
|
+
// A declared thumbnail MIME type replaces the per-document source check, so a
|
|
37
|
+
// wrong one fails at boot rather than as a silently missing guard or a 500 per
|
|
38
|
+
// image.
|
|
39
|
+
const supportedMimeTypes = incomingPluginConfig.resolver.supportedMimeTypes;
|
|
40
|
+
for (const collection of normalizedCollections){
|
|
41
|
+
const declared = collection.imageThumbnailMimeType;
|
|
42
|
+
if (declared === undefined) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!isValidMimeType(declared)) {
|
|
46
|
+
throw new Error(`The alt-text plugin is configured with imageThumbnailMimeType "${declared}" for the "${collection.slug}" collection, ` + 'but that is not a valid MIME type. Expected something like "image/webp".');
|
|
47
|
+
}
|
|
48
|
+
if (supportedMimeTypes && !supportedMimeTypes.includes(declared)) {
|
|
49
|
+
throw new Error(`The alt-text plugin is configured with imageThumbnailMimeType "${declared}" for the "${collection.slug}" collection, ` + `but the "${incomingPluginConfig.resolver.key}" resolver does not support it. ` + `Supported types: ${supportedMimeTypes.join(', ')}. ` + "Either change the transformation in getImageThumbnail, or remove the declaration to fall back to checking each document's own mime type.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
34
52
|
const access = incomingPluginConfig.access ?? (({ req })=>!!req.user);
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
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;
|
|
38
61
|
const pluginConfig = {
|
|
39
62
|
access,
|
|
40
63
|
collections: normalizedCollections,
|
|
@@ -43,6 +66,7 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
43
66
|
getImageThumbnail: incomingPluginConfig.getImageThumbnail,
|
|
44
67
|
healthCheck: enableHealthCheck,
|
|
45
68
|
healthCheckAccess,
|
|
69
|
+
healthCheckBaseFilter: healthCheckConfig.baseFilter,
|
|
46
70
|
locale: incomingPluginConfig.locale,
|
|
47
71
|
locales,
|
|
48
72
|
maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,
|
|
@@ -57,6 +81,9 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
57
81
|
entry.slug,
|
|
58
82
|
entry
|
|
59
83
|
]));
|
|
84
|
+
// Collected while collections are mapped and flushed in `onInit`: no Payload instance —
|
|
85
|
+
// and therefore no logger — exists while the config is still being built.
|
|
86
|
+
const configWarnings = [];
|
|
60
87
|
// Ensure collections array exists
|
|
61
88
|
config.collections = config.collections || [];
|
|
62
89
|
// Map over collections and inject AI alt text fields into specified ones
|
|
@@ -64,13 +91,16 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
64
91
|
const altTextCollectionConfig = collectionConfigBySlug.get(collectionConfig.slug);
|
|
65
92
|
if (altTextCollectionConfig) {
|
|
66
93
|
if (!collectionConfig.upload) {
|
|
67
|
-
|
|
94
|
+
configWarnings.push(`AI Alt Text Plugin: Collection "${collectionConfig.slug}" is not an upload collection. Skipping field injection.`);
|
|
68
95
|
return collectionConfig;
|
|
69
96
|
}
|
|
70
97
|
const defaultFields = [
|
|
71
98
|
altTextField({
|
|
72
99
|
localized: Boolean(config.localization),
|
|
73
|
-
|
|
100
|
+
// When the collection declares what getImageThumbnail delivers, the
|
|
101
|
+
// document's own mime type says nothing about whether generation can
|
|
102
|
+
// succeed — so don't let the admin UI disable the button on it.
|
|
103
|
+
supportedMimeTypes: altTextCollectionConfig.imageThumbnailMimeType ? undefined : pluginConfig.resolver.supportedMimeTypes,
|
|
74
104
|
trackedMimeTypes: altTextCollectionConfig.mimeTypes,
|
|
75
105
|
validate: altTextCollectionConfig.validate
|
|
76
106
|
}),
|
|
@@ -168,6 +198,12 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
168
198
|
i18n: {
|
|
169
199
|
...config.i18n,
|
|
170
200
|
translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {})
|
|
201
|
+
},
|
|
202
|
+
onInit: async (payload)=>{
|
|
203
|
+
for (const warning of configWarnings){
|
|
204
|
+
payload.logger.warn(warning);
|
|
205
|
+
}
|
|
206
|
+
await config.onInit?.(payload);
|
|
171
207
|
}
|
|
172
208
|
};
|
|
173
209
|
};
|
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 { 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\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 supportedMimeTypes: 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","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","access","req","user","healthCheckAccess","pluginConfig","fieldsOverride","getImageThumbnail","locale","maxBulkGenerateConcurrency","maxBulkGenerateIds","resolver","length","Error","collectionConfigBySlug","Map","entry","collectionConfig","altTextCollectionConfig","get","upload","console","warn","defaultFields","localized","Boolean","supportedMimeTypes","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 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","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,65 @@
|
|
|
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 — never true in local development, and not
|
|
51
|
+
* true for private buckets. Sending the bytes removes that whole class of
|
|
52
|
+
* failure for the price of one extra download, and supplies the `media_type`
|
|
53
|
+
* that a base64 image block requires and a URL cannot carry.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { anthropicResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
58
|
+
*
|
|
59
|
+
* anthropicResolver({
|
|
60
|
+
* apiKey: process.env.ANTHROPIC_API_KEY,
|
|
61
|
+
* model: 'claude-opus-5', // optional, this is the default
|
|
62
|
+
* })
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare const anthropicResolver: ({ apiKey, baseUrl, effort, instructions, model, timeoutMs, }: AnthropicResolverConfig) => AltTextResolver;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createVisionResolver, VisionProviderError } from './createVisionResolver.js';
|
|
2
|
+
/**
|
|
3
|
+
* Image formats the Messages API accepts.
|
|
4
|
+
*
|
|
5
|
+
* Narrower than what an upload collection may hold — SVG and AVIF are missing,
|
|
6
|
+
* so the endpoint rejects those documents and their generate button stays
|
|
7
|
+
* disabled instead of failing at the provider.
|
|
8
|
+
*
|
|
9
|
+
* @see https://platform.claude.com/docs/en/build-with-claude/vision
|
|
10
|
+
*/ const SUPPORTED_MIME_TYPES = [
|
|
11
|
+
'image/jpeg',
|
|
12
|
+
'image/png',
|
|
13
|
+
'image/gif',
|
|
14
|
+
'image/webp'
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Claude's 10 MB ceiling is measured on the base64 payload, which inflates the
|
|
18
|
+
* raw bytes by roughly 4/3 — so the guard has to sit at ~7.5 MB of raw image to
|
|
19
|
+
* mean 10 MB on the wire. Checking the raw length against 10 MB would wave
|
|
20
|
+
* through a 9 MB photo that arrives as ~12 MB and is rejected by the provider,
|
|
21
|
+
* costing the download and replacing a readable message with a raw 400.
|
|
22
|
+
*/ const MAX_IMAGE_BYTES = Math.floor(10 * 1024 * 1024 * 3 / 4);
|
|
23
|
+
/**
|
|
24
|
+
* Room for one alt text and its keywords per locale, plus the thinking tokens
|
|
25
|
+
* Claude spends before answering. A budget sized for the answer alone would be
|
|
26
|
+
* exhausted while reasoning, and the response would be cut off mid-JSON.
|
|
27
|
+
*/ const MAX_TOKENS_PER_LOCALE = 2000;
|
|
28
|
+
/**
|
|
29
|
+
* Creates a Claude-based resolver for alt text generation.
|
|
30
|
+
*
|
|
31
|
+
* The image is downloaded and sent as bytes. Claude can fetch an image URL
|
|
32
|
+
* itself, but that path is not dependable for a CMS: it requires the file to be
|
|
33
|
+
* reachable from the public internet — never true in local development, and not
|
|
34
|
+
* true for private buckets. Sending the bytes removes that whole class of
|
|
35
|
+
* failure for the price of one extra download, and supplies the `media_type`
|
|
36
|
+
* that a base64 image block requires and a URL cannot carry.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { anthropicResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
41
|
+
*
|
|
42
|
+
* anthropicResolver({
|
|
43
|
+
* apiKey: process.env.ANTHROPIC_API_KEY,
|
|
44
|
+
* model: 'claude-opus-5', // optional, this is the default
|
|
45
|
+
* })
|
|
46
|
+
* ```
|
|
47
|
+
*/ export const anthropicResolver = ({ apiKey, baseUrl = 'https://api.anthropic.com', effort, instructions, model = 'claude-opus-5', timeoutMs = 30_000 })=>createVisionResolver({
|
|
48
|
+
apiKey,
|
|
49
|
+
generate: async ({ filename, image, instructions: resolvedInstructions, maxTokens, responseSchema, signal })=>{
|
|
50
|
+
if (!image) {
|
|
51
|
+
throw new Error('The image was not downloaded');
|
|
52
|
+
}
|
|
53
|
+
const response = await fetch(`${baseUrl}/v1/messages`, {
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
max_tokens: maxTokens,
|
|
56
|
+
messages: [
|
|
57
|
+
{
|
|
58
|
+
content: [
|
|
59
|
+
// Claude works best when the image comes before the text.
|
|
60
|
+
{
|
|
61
|
+
type: 'image',
|
|
62
|
+
source: {
|
|
63
|
+
type: 'base64',
|
|
64
|
+
data: image.base64,
|
|
65
|
+
media_type: image.mediaType
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
...filename ? [
|
|
69
|
+
{
|
|
70
|
+
type: 'text',
|
|
71
|
+
text: filename
|
|
72
|
+
}
|
|
73
|
+
] : []
|
|
74
|
+
],
|
|
75
|
+
role: 'user'
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
model,
|
|
79
|
+
// `format` constrains the response to the schema the plugin needs;
|
|
80
|
+
// `effort` caps how long Claude thinks before producing it. Only sent
|
|
81
|
+
// when configured: some models reject the field outright.
|
|
82
|
+
output_config: {
|
|
83
|
+
...effort ? {
|
|
84
|
+
effort
|
|
85
|
+
} : {},
|
|
86
|
+
format: {
|
|
87
|
+
type: 'json_schema',
|
|
88
|
+
schema: responseSchema
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
// The instructions are an operator instruction, not a turn in the
|
|
92
|
+
// conversation, so they travel as the top-level system prompt.
|
|
93
|
+
system: resolvedInstructions
|
|
94
|
+
}),
|
|
95
|
+
headers: {
|
|
96
|
+
'anthropic-version': '2023-06-01',
|
|
97
|
+
'content-type': 'application/json',
|
|
98
|
+
'x-api-key': apiKey
|
|
99
|
+
},
|
|
100
|
+
method: 'POST',
|
|
101
|
+
signal
|
|
102
|
+
});
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
// Bounded: unbounded provider text would land in the log as-is.
|
|
105
|
+
const body = (await response.text().catch(()=>'')).slice(0, 500);
|
|
106
|
+
throw new VisionProviderError({
|
|
107
|
+
body,
|
|
108
|
+
label: 'Anthropic',
|
|
109
|
+
status: response.status
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const message = await response.json();
|
|
113
|
+
// A refusal and a truncated answer both arrive as a 200 with unusable
|
|
114
|
+
// content, so they are named rather than surfacing as a JSON parse error.
|
|
115
|
+
if (message.stop_reason === 'refusal') {
|
|
116
|
+
throw new Error('Claude declined to describe this image');
|
|
117
|
+
}
|
|
118
|
+
if (message.stop_reason === 'max_tokens') {
|
|
119
|
+
throw new Error(`Claude ran out of tokens before finishing the alt text (max_tokens: ${maxTokens})`);
|
|
120
|
+
}
|
|
121
|
+
const text = message.content?.find((block)=>block.type === 'text')?.text;
|
|
122
|
+
if (typeof text !== 'string') {
|
|
123
|
+
throw new Error('No result from Anthropic');
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(text);
|
|
127
|
+
} catch {
|
|
128
|
+
throw new Error('Claude returned a response that was not valid JSON');
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
inlineImage: true,
|
|
132
|
+
instructions,
|
|
133
|
+
key: 'anthropic',
|
|
134
|
+
label: 'Anthropic',
|
|
135
|
+
maxImageBytes: MAX_IMAGE_BYTES,
|
|
136
|
+
maxTokensPerLocale: MAX_TOKENS_PER_LOCALE,
|
|
137
|
+
supportedMimeTypes: SUPPORTED_MIME_TYPES,
|
|
138
|
+
timeoutMs
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/resolvers/anthropic.ts"],"sourcesContent":["import type { VisionInstructions } from './createVisionResolver.js'\nimport type { AltTextResolver } from './types.js'\n\nimport { createVisionResolver, VisionProviderError } from './createVisionResolver.js'\n\nexport type AnthropicResolverConfig = {\n /** Anthropic API key for authentication */\n apiKey: string\n /**\n * Base URL of the Anthropic API.\n * @default 'https://api.anthropic.com'\n */\n baseUrl?: string\n /**\n * Caps how long Claude thinks before answering. Lower effort still thinks on a\n * difficult image, just less than a higher setting would.\n *\n * Describing an image is not a reasoning-heavy task, so `'low'` keeps the\n * spend down on the models that accept it. Omitted, the field is not sent and\n * Claude uses its default (`'high'`) — which also keeps models without effort\n * support, such as `claude-haiku-4-5`, usable.\n */\n effort?: 'high' | 'low' | 'max' | 'medium' | 'xhigh'\n /**\n * Builds the instructions from the default ones, e.g. to append a house style\n * rule. Sent as the system prompt, separately from the image.\n *\n * @default ({ defaultInstructions }) => defaultInstructions\n */\n instructions?: VisionInstructions\n /**\n * The Claude model to use for alt text generation.\n *\n * Must be able to read images. `claude-sonnet-5` is the cheaper choice for a\n * large media library; `claude-haiku-4-5` works too, but only without\n * `effort`.\n *\n * @default 'claude-opus-5'\n */\n model?: string\n /**\n * Abort after this many milliseconds. Covers downloading the image and the\n * message call together.\n * @default 30000\n */\n timeoutMs?: number\n}\n\n/**\n * Image formats the Messages API accepts.\n *\n * Narrower than what an upload collection may hold — SVG and AVIF are missing,\n * so the endpoint rejects those documents and their generate button stays\n * disabled instead of failing at the provider.\n *\n * @see https://platform.claude.com/docs/en/build-with-claude/vision\n */\nconst SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']\n\n/**\n * Claude's 10 MB ceiling is measured on the base64 payload, which inflates the\n * raw bytes by roughly 4/3 — so the guard has to sit at ~7.5 MB of raw image to\n * mean 10 MB on the wire. Checking the raw length against 10 MB would wave\n * through a 9 MB photo that arrives as ~12 MB and is rejected by the provider,\n * costing the download and replacing a readable message with a raw 400.\n */\nconst MAX_IMAGE_BYTES = Math.floor((10 * 1024 * 1024 * 3) / 4)\n\n/**\n * Room for one alt text and its keywords per locale, plus the thinking tokens\n * Claude spends before answering. A budget sized for the answer alone would be\n * exhausted while reasoning, and the response would be cut off mid-JSON.\n */\nconst MAX_TOKENS_PER_LOCALE = 2000\n\ntype AnthropicMessage = {\n content?: { text?: string; type: string }[]\n stop_reason?: string\n}\n\n/**\n * Creates a Claude-based resolver for alt text generation.\n *\n * The image is downloaded and sent as bytes. Claude can fetch an image URL\n * itself, but that path is not dependable for a CMS: it requires the file to be\n * reachable from the public internet — never true in local development, and not\n * true for private buckets. Sending the bytes removes that whole class of\n * failure for the price of one extra download, and supplies the `media_type`\n * that a base64 image block requires and a URL cannot carry.\n *\n * @example\n * ```typescript\n * import { anthropicResolver } from '@jhb.software/payload-alt-text-plugin'\n *\n * anthropicResolver({\n * apiKey: process.env.ANTHROPIC_API_KEY,\n * model: 'claude-opus-5', // optional, this is the default\n * })\n * ```\n */\nexport const anthropicResolver = ({\n apiKey,\n baseUrl = 'https://api.anthropic.com',\n effort,\n instructions,\n model = 'claude-opus-5',\n timeoutMs = 30_000,\n}: AnthropicResolverConfig): AltTextResolver =>\n createVisionResolver({\n apiKey,\n generate: async ({\n filename,\n image,\n instructions: resolvedInstructions,\n maxTokens,\n responseSchema,\n signal,\n }) => {\n if (!image) {\n throw new Error('The image was not downloaded')\n }\n\n const response = await fetch(`${baseUrl}/v1/messages`, {\n body: JSON.stringify({\n max_tokens: maxTokens,\n messages: [\n {\n content: [\n // Claude works best when the image comes before the text.\n {\n type: 'image',\n source: { type: 'base64', data: image.base64, media_type: image.mediaType },\n },\n ...(filename ? [{ type: 'text', text: filename }] : []),\n ],\n role: 'user',\n },\n ],\n model,\n // `format` constrains the response to the schema the plugin needs;\n // `effort` caps how long Claude thinks before producing it. Only sent\n // when configured: some models reject the field outright.\n output_config: {\n ...(effort ? { effort } : {}),\n format: { type: 'json_schema', schema: responseSchema },\n },\n // The instructions are an operator instruction, not a turn in the\n // conversation, so they travel as the top-level system prompt.\n system: resolvedInstructions,\n }),\n headers: {\n 'anthropic-version': '2023-06-01',\n 'content-type': 'application/json',\n 'x-api-key': apiKey,\n },\n method: 'POST',\n signal,\n })\n\n if (!response.ok) {\n // Bounded: unbounded provider text would land in the log as-is.\n const body = (await response.text().catch(() => '')).slice(0, 500)\n\n throw new VisionProviderError({ body, label: 'Anthropic', status: response.status })\n }\n\n const message = (await response.json()) as AnthropicMessage\n\n // A refusal and a truncated answer both arrive as a 200 with unusable\n // content, so they are named rather than surfacing as a JSON parse error.\n if (message.stop_reason === 'refusal') {\n throw new Error('Claude declined to describe this image')\n }\n\n if (message.stop_reason === 'max_tokens') {\n throw new Error(\n `Claude ran out of tokens before finishing the alt text (max_tokens: ${maxTokens})`,\n )\n }\n\n const text = message.content?.find((block) => block.type === 'text')?.text\n\n if (typeof text !== 'string') {\n throw new Error('No result from Anthropic')\n }\n\n try {\n return JSON.parse(text)\n } catch {\n throw new Error('Claude returned a response that was not valid JSON')\n }\n },\n inlineImage: true,\n instructions,\n key: 'anthropic',\n label: 'Anthropic',\n maxImageBytes: MAX_IMAGE_BYTES,\n maxTokensPerLocale: MAX_TOKENS_PER_LOCALE,\n supportedMimeTypes: SUPPORTED_MIME_TYPES,\n timeoutMs,\n })\n"],"names":["createVisionResolver","VisionProviderError","SUPPORTED_MIME_TYPES","MAX_IMAGE_BYTES","Math","floor","MAX_TOKENS_PER_LOCALE","anthropicResolver","apiKey","baseUrl","effort","instructions","model","timeoutMs","generate","filename","image","resolvedInstructions","maxTokens","responseSchema","signal","Error","response","fetch","body","JSON","stringify","max_tokens","messages","content","type","source","data","base64","media_type","mediaType","text","role","output_config","format","schema","system","headers","method","ok","catch","slice","label","status","message","json","stop_reason","find","block","parse","inlineImage","key","maxImageBytes","maxTokensPerLocale","supportedMimeTypes"],"mappings":"AAGA,SAASA,oBAAoB,EAAEC,mBAAmB,QAAQ,4BAA2B;AA6CrF;;;;;;;;CAQC,GACD,MAAMC,uBAAuB;IAAC;IAAc;IAAa;IAAa;CAAa;AAEnF;;;;;;CAMC,GACD,MAAMC,kBAAkBC,KAAKC,KAAK,CAAC,AAAC,KAAK,OAAO,OAAO,IAAK;AAE5D;;;;CAIC,GACD,MAAMC,wBAAwB;AAO9B;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,oBAAoB,CAAC,EAChCC,MAAM,EACNC,UAAU,2BAA2B,EACrCC,MAAM,EACNC,YAAY,EACZC,QAAQ,eAAe,EACvBC,YAAY,MAAM,EACM,GACxBb,qBAAqB;QACnBQ;QACAM,UAAU,OAAO,EACfC,QAAQ,EACRC,KAAK,EACLL,cAAcM,oBAAoB,EAClCC,SAAS,EACTC,cAAc,EACdC,MAAM,EACP;YACC,IAAI,CAACJ,OAAO;gBACV,MAAM,IAAIK,MAAM;YAClB;YAEA,MAAMC,WAAW,MAAMC,MAAM,GAAGd,QAAQ,YAAY,CAAC,EAAE;gBACrDe,MAAMC,KAAKC,SAAS,CAAC;oBACnBC,YAAYT;oBACZU,UAAU;wBACR;4BACEC,SAAS;gCACP,0DAA0D;gCAC1D;oCACEC,MAAM;oCACNC,QAAQ;wCAAED,MAAM;wCAAUE,MAAMhB,MAAMiB,MAAM;wCAAEC,YAAYlB,MAAMmB,SAAS;oCAAC;gCAC5E;mCACIpB,WAAW;oCAAC;wCAAEe,MAAM;wCAAQM,MAAMrB;oCAAS;iCAAE,GAAG,EAAE;6BACvD;4BACDsB,MAAM;wBACR;qBACD;oBACDzB;oBACA,mEAAmE;oBACnE,sEAAsE;oBACtE,0DAA0D;oBAC1D0B,eAAe;wBACb,GAAI5B,SAAS;4BAAEA;wBAAO,IAAI,CAAC,CAAC;wBAC5B6B,QAAQ;4BAAET,MAAM;4BAAeU,QAAQrB;wBAAe;oBACxD;oBACA,kEAAkE;oBAClE,+DAA+D;oBAC/DsB,QAAQxB;gBACV;gBACAyB,SAAS;oBACP,qBAAqB;oBACrB,gBAAgB;oBAChB,aAAalC;gBACf;gBACAmC,QAAQ;gBACRvB;YACF;YAEA,IAAI,CAACE,SAASsB,EAAE,EAAE;gBAChB,gEAAgE;gBAChE,MAAMpB,OAAO,AAAC,CAAA,MAAMF,SAASc,IAAI,GAAGS,KAAK,CAAC,IAAM,GAAE,EAAGC,KAAK,CAAC,GAAG;gBAE9D,MAAM,IAAI7C,oBAAoB;oBAAEuB;oBAAMuB,OAAO;oBAAaC,QAAQ1B,SAAS0B,MAAM;gBAAC;YACpF;YAEA,MAAMC,UAAW,MAAM3B,SAAS4B,IAAI;YAEpC,sEAAsE;YACtE,0EAA0E;YAC1E,IAAID,QAAQE,WAAW,KAAK,WAAW;gBACrC,MAAM,IAAI9B,MAAM;YAClB;YAEA,IAAI4B,QAAQE,WAAW,KAAK,cAAc;gBACxC,MAAM,IAAI9B,MACR,CAAC,oEAAoE,EAAEH,UAAU,CAAC,CAAC;YAEvF;YAEA,MAAMkB,OAAOa,QAAQpB,OAAO,EAAEuB,KAAK,CAACC,QAAUA,MAAMvB,IAAI,KAAK,SAASM;YAEtE,IAAI,OAAOA,SAAS,UAAU;gBAC5B,MAAM,IAAIf,MAAM;YAClB;YAEA,IAAI;gBACF,OAAOI,KAAK6B,KAAK,CAAClB;YACpB,EAAE,OAAM;gBACN,MAAM,IAAIf,MAAM;YAClB;QACF;QACAkC,aAAa;QACb5C;QACA6C,KAAK;QACLT,OAAO;QACPU,eAAetD;QACfuD,oBAAoBpD;QACpBqD,oBAAoBzD;QACpBW;IACF,GAAE"}
|