@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/resolvers/mistral.ts"],"sourcesContent":["import type { VisionInstructions } from './createVisionResolver.js'\nimport type { AltTextResolver } from './types.js'\n\nimport { createVisionResolver, VisionProviderError } from './createVisionResolver.js'\n\nexport type MistralResolverConfig = {\n /** Mistral API key for authentication */\n apiKey: string\n /**\n * Base URL of the Mistral API.\n * @default 'https://api.mistral.ai/v1'\n */\n baseUrl?: string\n /**\n * Builds the instructions from the default ones, e.g. to append a house style\n * rule. Sent as the system message, separately from the image.\n *\n * @default ({ defaultInstructions }) => defaultInstructions\n */\n instructions?: VisionInstructions\n /**\n * The vision-capable Mistral model to use for alt text generation.\n *\n * Must be able to read images — `mistral-medium-latest`,\n * `mistral-large-latest`, `mistral-small-latest` and the `ministral-*` models\n * all are.\n *\n * @default 'mistral-medium-latest'\n */\n model?: string\n /**\n * Abort after this many milliseconds. Covers downloading the image and the\n * completion call together.\n * @default 30000\n */\n timeoutMs?: number\n}\n\n/**\n * Image formats the Mistral 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://docs.mistral.ai/capabilities/vision/\n */\nconst SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']\n\n/**\n * Creates a Mistral-based resolver for alt text generation.\n *\n * The image is downloaded and sent as bytes rather than handed to Mistral as a\n * URL. Mistral's own fetcher requires a publicly reachable file — never true in\n * local development, and not true for private buckets — and some hosts refuse it\n * outright, which surfaces as `File could not be fetched from url` (error 3310).\n *\n * @example\n * ```typescript\n * import { mistralResolver } from '@jhb.software/payload-alt-text-plugin'\n *\n * mistralResolver({\n * apiKey: process.env.MISTRAL_API_KEY,\n * model: 'mistral-medium-latest', // optional, this is the default\n * })\n * ```\n */\nexport const mistralResolver = ({\n apiKey,\n baseUrl = 'https://api.mistral.ai/v1',\n instructions,\n model = 'mistral-medium-latest',\n timeoutMs = 30_000,\n}: MistralResolverConfig): 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}/chat/completions`, {\n body: JSON.stringify({\n max_tokens: maxTokens,\n messages: [\n { content: resolvedInstructions, role: 'system' },\n {\n content: [\n { type: 'image_url', image_url: image.dataUri },\n ...(filename ? [{ type: 'text', text: filename }] : []),\n ],\n role: 'user',\n },\n ],\n model,\n response_format: {\n type: 'json_schema',\n json_schema: { name: 'data', schema: responseSchema, strict: true },\n },\n }),\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },\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: 'Mistral', status: response.status })\n }\n\n const completion = (await response.json()) as {\n choices?: { finish_reason?: string; message?: { content?: unknown } }[]\n }\n const choice = completion.choices?.[0]\n\n if (choice?.finish_reason === 'length') {\n throw new Error(\n `Mistral ran out of tokens before finishing the alt text (max_tokens: ${maxTokens})`,\n )\n }\n\n const content = choice?.message?.content\n\n if (typeof content !== 'string') {\n throw new Error('No result from Mistral')\n }\n\n try {\n return JSON.parse(content)\n } catch {\n throw new Error('Mistral returned a response that was not valid JSON')\n }\n },\n inlineImage: true,\n instructions,\n key: 'mistral',\n label: 'Mistral',\n // Mistral rejects images above 20 MB.\n maxImageBytes: 20 * 1024 * 1024,\n supportedMimeTypes: SUPPORTED_MIME_TYPES,\n timeoutMs,\n })\n"],"names":["createVisionResolver","VisionProviderError","SUPPORTED_MIME_TYPES","mistralResolver","apiKey","baseUrl","instructions","model","timeoutMs","generate","filename","image","resolvedInstructions","maxTokens","responseSchema","signal","Error","response","fetch","body","JSON","stringify","max_tokens","messages","content","role","type","image_url","dataUri","text","response_format","json_schema","name","schema","strict","headers","Authorization","method","ok","catch","slice","label","status","completion","json","choice","choices","finish_reason","message","parse","inlineImage","key","maxImageBytes","supportedMimeTypes"],"mappings":"AAGA,SAASA,oBAAoB,EAAEC,mBAAmB,QAAQ,4BAA2B;AAmCrF;;;;;;;;CAQC,GACD,MAAMC,uBAAuB;IAAC;IAAc;IAAa;IAAa;CAAa;AAEnF;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMC,kBAAkB,CAAC,EAC9BC,MAAM,EACNC,UAAU,2BAA2B,EACrCC,YAAY,EACZC,QAAQ,uBAAuB,EAC/BC,YAAY,MAAM,EACI,GACtBR,qBAAqB;QACnBI;QACAK,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,GAAGb,QAAQ,iBAAiB,CAAC,EAAE;gBAC1Dc,MAAMC,KAAKC,SAAS,CAAC;oBACnBC,YAAYT;oBACZU,UAAU;wBACR;4BAAEC,SAASZ;4BAAsBa,MAAM;wBAAS;wBAChD;4BACED,SAAS;gCACP;oCAAEE,MAAM;oCAAaC,WAAWhB,MAAMiB,OAAO;gCAAC;mCAC1ClB,WAAW;oCAAC;wCAAEgB,MAAM;wCAAQG,MAAMnB;oCAAS;iCAAE,GAAG,EAAE;6BACvD;4BACDe,MAAM;wBACR;qBACD;oBACDlB;oBACAuB,iBAAiB;wBACfJ,MAAM;wBACNK,aAAa;4BAAEC,MAAM;4BAAQC,QAAQnB;4BAAgBoB,QAAQ;wBAAK;oBACpE;gBACF;gBACAC,SAAS;oBAAEC,eAAe,CAAC,OAAO,EAAEhC,QAAQ;oBAAE,gBAAgB;gBAAmB;gBACjFiC,QAAQ;gBACRtB;YACF;YAEA,IAAI,CAACE,SAASqB,EAAE,EAAE;gBAChB,gEAAgE;gBAChE,MAAMnB,OAAO,AAAC,CAAA,MAAMF,SAASY,IAAI,GAAGU,KAAK,CAAC,IAAM,GAAE,EAAGC,KAAK,CAAC,GAAG;gBAE9D,MAAM,IAAIvC,oBAAoB;oBAAEkB;oBAAMsB,OAAO;oBAAWC,QAAQzB,SAASyB,MAAM;gBAAC;YAClF;YAEA,MAAMC,aAAc,MAAM1B,SAAS2B,IAAI;YAGvC,MAAMC,SAASF,WAAWG,OAAO,EAAE,CAAC,EAAE;YAEtC,IAAID,QAAQE,kBAAkB,UAAU;gBACtC,MAAM,IAAI/B,MACR,CAAC,qEAAqE,EAAEH,UAAU,CAAC,CAAC;YAExF;YAEA,MAAMW,UAAUqB,QAAQG,SAASxB;YAEjC,IAAI,OAAOA,YAAY,UAAU;gBAC/B,MAAM,IAAIR,MAAM;YAClB;YAEA,IAAI;gBACF,OAAOI,KAAK6B,KAAK,CAACzB;YACpB,EAAE,OAAM;gBACN,MAAM,IAAIR,MAAM;YAClB;QACF;QACAkC,aAAa;QACb5C;QACA6C,KAAK;QACLV,OAAO;QACP,sCAAsC;QACtCW,eAAe,KAAK,OAAO;QAC3BC,oBAAoBnD;QACpBM;IACF,GAAE"}
|
|
@@ -1,22 +1,51 @@
|
|
|
1
|
+
import type { VisionInstructions } from './createVisionResolver.js';
|
|
1
2
|
import type { AltTextResolver } from './types.js';
|
|
2
3
|
export type OpenAIResolverConfig = {
|
|
3
4
|
/** OpenAI API key for authentication */
|
|
4
5
|
apiKey: string;
|
|
5
6
|
/**
|
|
6
|
-
* Base URL for the OpenAI-compatible API.
|
|
7
|
+
* Base URL for the OpenAI-compatible API, including the version segment.
|
|
7
8
|
* Use this to point at alternative providers (e.g. Azure, Nebius, local inference).
|
|
8
|
-
* @default
|
|
9
|
+
* @default 'https://api.openai.com/v1'
|
|
9
10
|
*/
|
|
10
11
|
baseUrl?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Builds the instructions from the default ones, e.g. to append a house style
|
|
14
|
+
* rule. Sent as the system message, separately from the image.
|
|
15
|
+
*
|
|
16
|
+
* @default ({ defaultInstructions }) => defaultInstructions
|
|
17
|
+
*/
|
|
18
|
+
instructions?: VisionInstructions;
|
|
11
19
|
/**
|
|
12
20
|
* The OpenAI LLM model to use for alt text generation.
|
|
13
21
|
* @default 'gpt-4.1-nano'
|
|
14
22
|
*/
|
|
15
23
|
model?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The MIME types the provider accepts for the image URL.
|
|
26
|
+
*
|
|
27
|
+
* Defaults to the formats documented for OpenAI's vision models. Override it
|
|
28
|
+
* when pointing `baseUrl` at another provider whose accepted formats differ —
|
|
29
|
+
* the person choosing the provider is the one who knows.
|
|
30
|
+
*
|
|
31
|
+
* @default ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
|
32
|
+
*/
|
|
33
|
+
supportedMimeTypes?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Abort after this many milliseconds, covering the completion call and the
|
|
36
|
+
* retries the factory makes within it.
|
|
37
|
+
* @default 30000
|
|
38
|
+
*/
|
|
39
|
+
timeoutMs?: number;
|
|
16
40
|
};
|
|
17
41
|
/**
|
|
18
42
|
* Creates an OpenAI-based resolver for alt text generation.
|
|
19
43
|
*
|
|
44
|
+
* The thumbnail URL is handed to OpenAI, which fetches it itself — so the URL
|
|
45
|
+
* has to be reachable from the public internet. Behind a private bucket or in
|
|
46
|
+
* local development, reach for a resolver that inlines the bytes instead
|
|
47
|
+
* (`mistralResolver`, `anthropicResolver`).
|
|
48
|
+
*
|
|
20
49
|
* @example
|
|
21
50
|
* ```typescript
|
|
22
51
|
* import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
@@ -35,4 +64,4 @@ export type OpenAIResolverConfig = {
|
|
|
35
64
|
* })
|
|
36
65
|
* ```
|
|
37
66
|
*/
|
|
38
|
-
export declare const openAIResolver: (
|
|
67
|
+
export declare const openAIResolver: ({ apiKey, baseUrl, instructions, model, supportedMimeTypes, timeoutMs, }: OpenAIResolverConfig) => AltTextResolver;
|
package/dist/resolvers/openAI.js
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* This is a temporary drop in replacement for the zodResponseFormat from openai/helpers/zod.ts
|
|
9
|
-
* because of issue https://github.com/openai/openai-node/issues/1576
|
|
10
|
-
*/ function zodResponseFormat(zodObject, name, props) {
|
|
11
|
-
return makeParseableResponseFormat({
|
|
12
|
-
type: 'json_schema',
|
|
13
|
-
json_schema: {
|
|
14
|
-
...props,
|
|
15
|
-
name,
|
|
16
|
-
schema: z.toJSONSchema(zodObject, {
|
|
17
|
-
target: 'draft-7'
|
|
18
|
-
}),
|
|
19
|
-
strict: true
|
|
20
|
-
}
|
|
21
|
-
}, (content)=>zodObject.parse(JSON.parse(content)));
|
|
22
|
-
}
|
|
1
|
+
import { createVisionResolver, VisionProviderError } from './createVisionResolver.js';
|
|
2
|
+
/** @see https://platform.openai.com/docs/guides/images-vision */ const OPENAI_SUPPORTED_MIME_TYPES = [
|
|
3
|
+
'image/jpeg',
|
|
4
|
+
'image/png',
|
|
5
|
+
'image/gif',
|
|
6
|
+
'image/webp'
|
|
7
|
+
];
|
|
23
8
|
/**
|
|
24
9
|
* Creates an OpenAI-based resolver for alt text generation.
|
|
25
10
|
*
|
|
11
|
+
* The thumbnail URL is handed to OpenAI, which fetches it itself — so the URL
|
|
12
|
+
* has to be reachable from the public internet. Behind a private bucket or in
|
|
13
|
+
* local development, reach for a resolver that inlines the bytes instead
|
|
14
|
+
* (`mistralResolver`, `anthropicResolver`).
|
|
15
|
+
*
|
|
26
16
|
* @example
|
|
27
17
|
* ```typescript
|
|
28
18
|
* import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
@@ -40,39 +30,15 @@ import { z } from 'zod';
|
|
|
40
30
|
* model: 'Qwen/Qwen2.5-VL-72B-Instruct',
|
|
41
31
|
* })
|
|
42
32
|
* ```
|
|
43
|
-
*/ export const openAIResolver = (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const getClient = ()=>openai ??= new OpenAI({
|
|
50
|
-
apiKey,
|
|
51
|
-
baseURL: baseUrl
|
|
52
|
-
});
|
|
53
|
-
return {
|
|
54
|
-
key: 'openai',
|
|
55
|
-
resolve: async ({ filename, imageThumbnailUrl, locale })=>{
|
|
56
|
-
try {
|
|
57
|
-
const modelResponseSchema = z.object({
|
|
58
|
-
altText: z.string().describe('A concise, descriptive alt text for the image'),
|
|
59
|
-
keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
|
|
60
|
-
});
|
|
61
|
-
const response = await getClient().chat.completions.parse({
|
|
62
|
-
max_completion_tokens: 150,
|
|
33
|
+
*/ export const openAIResolver = ({ apiKey, baseUrl = 'https://api.openai.com/v1', instructions, model = 'gpt-4.1-nano', supportedMimeTypes = OPENAI_SUPPORTED_MIME_TYPES, timeoutMs = 30_000 })=>createVisionResolver({
|
|
34
|
+
apiKey,
|
|
35
|
+
generate: async ({ filename, imageThumbnailUrl, instructions: resolvedInstructions, maxTokens, responseSchema, signal })=>{
|
|
36
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
max_completion_tokens: maxTokens,
|
|
63
39
|
messages: [
|
|
64
40
|
{
|
|
65
|
-
content:
|
|
66
|
-
You are an expert at analyzing images and creating descriptive image alt text.
|
|
67
|
-
|
|
68
|
-
Please analyze the given image and provide the following:
|
|
69
|
-
- A concise, descriptive alt text (1-2 sentences) as "altText". Focus on the subject, action, and setting. Avoid phrases like 'Image of', 'A picture of', or 'Photo showing'. Be specific and include relevant details like location or context if visible. Make no assumptions.
|
|
70
|
-
- A list of keywords that describe the content (e.g., ["Camel", "Palm trees", "Desert"]) as "keywords"
|
|
71
|
-
|
|
72
|
-
If a context is provided, use it to enhance the alt text.
|
|
73
|
-
|
|
74
|
-
Format your response as a JSON object. You must respond in the ${locale} language.
|
|
75
|
-
`,
|
|
41
|
+
content: resolvedInstructions,
|
|
76
42
|
role: 'system'
|
|
77
43
|
},
|
|
78
44
|
{
|
|
@@ -94,101 +60,54 @@ import { z } from 'zod';
|
|
|
94
60
|
}
|
|
95
61
|
],
|
|
96
62
|
model,
|
|
97
|
-
response_format:
|
|
63
|
+
response_format: {
|
|
64
|
+
type: 'json_schema',
|
|
65
|
+
json_schema: {
|
|
66
|
+
name: 'data',
|
|
67
|
+
schema: responseSchema,
|
|
68
|
+
strict: true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
headers: {
|
|
73
|
+
Authorization: `Bearer ${apiKey}`,
|
|
74
|
+
'Content-Type': 'application/json'
|
|
75
|
+
},
|
|
76
|
+
method: 'POST',
|
|
77
|
+
signal
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
// Bounded: unbounded provider text would land in the log as-is.
|
|
81
|
+
const body = (await response.text().catch(()=>'')).slice(0, 500);
|
|
82
|
+
throw new VisionProviderError({
|
|
83
|
+
body,
|
|
84
|
+
label: 'OpenAI',
|
|
85
|
+
status: response.status
|
|
98
86
|
});
|
|
99
|
-
const result = response.choices[0]?.message?.parsed;
|
|
100
|
-
if (!result) {
|
|
101
|
-
return {
|
|
102
|
-
error: 'No result from OpenAI',
|
|
103
|
-
success: false
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
result,
|
|
108
|
-
success: true
|
|
109
|
-
};
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.error('Error generating alt text:', error);
|
|
112
|
-
return {
|
|
113
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
114
|
-
success: false
|
|
115
|
-
};
|
|
116
87
|
}
|
|
117
|
-
|
|
118
|
-
|
|
88
|
+
const completion = await response.json();
|
|
89
|
+
const choice = completion.choices?.[0];
|
|
90
|
+
// A budget exhausted mid-JSON otherwise reaches JSON.parse and reads as
|
|
91
|
+
// "Unexpected end of JSON input" in the admin panel, which tells an editor
|
|
92
|
+
// nothing about what to change.
|
|
93
|
+
if (choice?.finish_reason === 'length') {
|
|
94
|
+
throw new Error(`OpenAI ran out of tokens before finishing the alt text (max_completion_tokens: ${maxTokens})`);
|
|
95
|
+
}
|
|
96
|
+
const content = choice?.message?.content;
|
|
97
|
+
if (typeof content !== 'string') {
|
|
98
|
+
throw new Error('No result from OpenAI');
|
|
99
|
+
}
|
|
119
100
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
altText: z.string().describe('A concise, descriptive alt text for the image'),
|
|
124
|
-
keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
|
|
125
|
-
})
|
|
126
|
-
])));
|
|
127
|
-
const response = await getClient().chat.completions.parse({
|
|
128
|
-
max_completion_tokens: 300,
|
|
129
|
-
messages: [
|
|
130
|
-
{
|
|
131
|
-
content: `
|
|
132
|
-
You are an expert at analyzing images and creating descriptive image alt text.
|
|
133
|
-
|
|
134
|
-
Please analyze the given image and provide the following in ${locales.join(', ')}:
|
|
135
|
-
- A concise, localized descriptive alt text (1-2 sentences) as "altText". Focus on the subject, action, and setting. Avoid phrases like 'Image of', 'A picture of', or 'Photo showing'. Be specific and include relevant details like location or context if visible. Make no assumptions.
|
|
136
|
-
- A localized list of keywords that describe the content (e.g., ["Camel", "Palm trees", "Desert"]) as "keywords"
|
|
137
|
-
|
|
138
|
-
If a context is provided, use it to enhance the alt text.
|
|
139
|
-
|
|
140
|
-
Format your response as a JSON object with ${locales.join(', ')} keys, each containing "altText" and "keywords".
|
|
141
|
-
`,
|
|
142
|
-
role: 'system'
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
content: [
|
|
146
|
-
{
|
|
147
|
-
type: 'image_url',
|
|
148
|
-
image_url: {
|
|
149
|
-
url: imageThumbnailUrl
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
...filename ? [
|
|
153
|
-
{
|
|
154
|
-
type: 'text',
|
|
155
|
-
text: filename
|
|
156
|
-
}
|
|
157
|
-
] : []
|
|
158
|
-
],
|
|
159
|
-
role: 'user'
|
|
160
|
-
}
|
|
161
|
-
],
|
|
162
|
-
model,
|
|
163
|
-
response_format: zodResponseFormat(modelResponseSchema, 'data')
|
|
164
|
-
});
|
|
165
|
-
const result = response.choices[0]?.message?.parsed;
|
|
166
|
-
if (!result) {
|
|
167
|
-
return {
|
|
168
|
-
error: 'No result from OpenAI',
|
|
169
|
-
success: false
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
results: result,
|
|
174
|
-
success: true
|
|
175
|
-
};
|
|
176
|
-
} catch (error) {
|
|
177
|
-
console.error('Error generating bulk alt text:', error);
|
|
178
|
-
return {
|
|
179
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
180
|
-
success: false
|
|
181
|
-
};
|
|
101
|
+
return JSON.parse(content);
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error('OpenAI returned a response that was not valid JSON');
|
|
182
104
|
}
|
|
183
105
|
},
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
]
|
|
191
|
-
};
|
|
192
|
-
};
|
|
106
|
+
instructions,
|
|
107
|
+
key: 'openai',
|
|
108
|
+
label: 'OpenAI',
|
|
109
|
+
supportedMimeTypes,
|
|
110
|
+
timeoutMs
|
|
111
|
+
});
|
|
193
112
|
|
|
194
113
|
//# sourceMappingURL=openAI.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/resolvers/openAI.ts"],"sourcesContent":["import type { AutoParseableResponseFormat } from 'openai/lib/parser.mjs'\nimport type { ChatCompletionContentPartText } from 'openai/resources/chat/completions.mjs'\nimport type { ResponseFormatJSONSchema } from 'openai/resources/shared.mjs'\n\nimport OpenAI from 'openai'\nimport { makeParseableResponseFormat } from 'openai/lib/parser.mjs'\nimport { z } from 'zod'\n\nimport type {\n AltTextBulkResolverArgs,\n AltTextBulkResolverResponse,\n AltTextResolver,\n AltTextResolverArgs,\n AltTextResolverResponse,\n} from './types.js'\n\nexport type OpenAIResolverConfig = {\n /** OpenAI API key for authentication */\n apiKey: string\n /**\n * Base URL for the OpenAI-compatible API.\n * Use this to point at alternative providers (e.g. Azure, Nebius, local inference).\n * @default undefined — the OpenAI SDK defaults to 'https://api.openai.com/v1'\n */\n baseUrl?: string\n /**\n * The OpenAI LLM model to use for alt text generation.\n * @default 'gpt-4.1-nano'\n */\n model?: string\n}\n\n/**\n * Creates a chat completion `JSONSchema` response format object from\n * the given Zod schema.\n *\n * This is a temporary drop in replacement for the zodResponseFormat from openai/helpers/zod.ts\n * because of issue https://github.com/openai/openai-node/issues/1576\n */\nfunction zodResponseFormat<ZodInput extends z.ZodType>(\n zodObject: ZodInput,\n name: string,\n props?: Omit<ResponseFormatJSONSchema.JSONSchema, 'name' | 'schema' | 'strict'>,\n): AutoParseableResponseFormat<z.infer<ZodInput>> {\n return makeParseableResponseFormat(\n {\n type: 'json_schema',\n json_schema: {\n ...props,\n name,\n schema: z.toJSONSchema(zodObject, { target: 'draft-7' }),\n strict: true,\n },\n },\n (content) => zodObject.parse(JSON.parse(content)),\n )\n}\n\n/**\n * Creates an OpenAI-based resolver for alt text generation.\n *\n * @example\n * ```typescript\n * import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'\n *\n * // OpenAI\n * openAIResolver({\n * apiKey: process.env.OPENAI_API_KEY,\n * model: 'gpt-4.1-mini', // optional, defaults to 'gpt-4.1-nano'\n * })\n *\n * // OpenAI-compatible provider (e.g. Nebius)\n * openAIResolver({\n * apiKey: process.env.NEBIUS_API_KEY,\n * baseUrl: 'https://api.tokenfactory.us-central1.nebius.com/v1',\n * model: 'Qwen/Qwen2.5-VL-72B-Instruct',\n * })\n * ```\n */\nexport const openAIResolver = (config: OpenAIResolverConfig): AltTextResolver => {\n const { apiKey, baseUrl, model = 'gpt-4.1-nano' } = config\n\n // Build the client lazily (once, on first use): the `resolver` argument is\n // evaluated even when the plugin is disabled, so eager construction would\n // throw on a keyless `enabled: !!process.env.OPENAI_API_KEY` setup.\n let openai: OpenAI | undefined\n const getClient = (): OpenAI => (openai ??= new OpenAI({ apiKey, baseURL: baseUrl }))\n\n return {\n key: 'openai',\n resolve: async ({\n filename,\n imageThumbnailUrl,\n locale,\n }: AltTextResolverArgs): Promise<AltTextResolverResponse> => {\n try {\n const modelResponseSchema = z.object({\n altText: z.string().describe('A concise, descriptive alt text for the image'),\n keywords: z.array(z.string()).describe('Keywords that describe the content of the image'),\n })\n\n const response = await getClient().chat.completions.parse({\n max_completion_tokens: 150,\n messages: [\n {\n content: `\n You are an expert at analyzing images and creating descriptive image alt text.\n\n Please analyze the given image and provide the following:\n - A concise, descriptive alt text (1-2 sentences) as \"altText\". Focus on the subject, action, and setting. Avoid phrases like 'Image of', 'A picture of', or 'Photo showing'. Be specific and include relevant details like location or context if visible. Make no assumptions.\n - A list of keywords that describe the content (e.g., [\"Camel\", \"Palm trees\", \"Desert\"]) as \"keywords\"\n\n If a context is provided, use it to enhance the alt text.\n\n Format your response as a JSON object. You must respond in the ${locale} language.\n `,\n role: 'system',\n },\n {\n content: [\n {\n type: 'image_url',\n image_url: { url: imageThumbnailUrl },\n },\n ...(filename\n ? [\n {\n type: 'text',\n text: filename,\n } satisfies ChatCompletionContentPartText,\n ]\n : []),\n ],\n role: 'user',\n },\n ],\n model,\n response_format: zodResponseFormat(modelResponseSchema, 'data'),\n })\n\n const result = response.choices[0]?.message?.parsed\n\n if (!result) {\n return { error: 'No result from OpenAI', success: false }\n }\n\n return {\n result,\n success: true,\n }\n } catch (error) {\n console.error('Error generating alt text:', error)\n return {\n error: error instanceof Error ? error.message : 'Unknown error',\n success: false,\n }\n }\n },\n resolveBulk: async ({\n filename,\n imageThumbnailUrl,\n locales,\n }: AltTextBulkResolverArgs): Promise<AltTextBulkResolverResponse> => {\n try {\n const modelResponseSchema = z.object(\n Object.fromEntries(\n locales.map((locale) => [\n locale,\n z.object({\n altText: z.string().describe('A concise, descriptive alt text for the image'),\n keywords: z\n .array(z.string())\n .describe('Keywords that describe the content of the image'),\n }),\n ]),\n ),\n )\n\n const response = await getClient().chat.completions.parse({\n max_completion_tokens: 300,\n messages: [\n {\n content: `\n You are an expert at analyzing images and creating descriptive image alt text.\n\n Please analyze the given image and provide the following in ${locales.join(', ')}:\n - A concise, localized descriptive alt text (1-2 sentences) as \"altText\". Focus on the subject, action, and setting. Avoid phrases like 'Image of', 'A picture of', or 'Photo showing'. Be specific and include relevant details like location or context if visible. Make no assumptions.\n - A localized list of keywords that describe the content (e.g., [\"Camel\", \"Palm trees\", \"Desert\"]) as \"keywords\"\n\n If a context is provided, use it to enhance the alt text.\n\n Format your response as a JSON object with ${locales.join(', ')} keys, each containing \"altText\" and \"keywords\".\n `,\n role: 'system',\n },\n {\n content: [\n {\n type: 'image_url',\n image_url: { url: imageThumbnailUrl },\n },\n ...(filename\n ? [\n {\n type: 'text',\n text: filename,\n } satisfies ChatCompletionContentPartText,\n ]\n : []),\n ],\n role: 'user',\n },\n ],\n model,\n response_format: zodResponseFormat(modelResponseSchema, 'data'),\n })\n\n const result = response.choices[0]?.message?.parsed\n\n if (!result) {\n return { error: 'No result from OpenAI', success: false }\n }\n\n return {\n results: result,\n success: true,\n }\n } catch (error) {\n console.error('Error generating bulk alt text:', error)\n return {\n error: error instanceof Error ? error.message : 'Unknown error',\n success: false,\n }\n }\n },\n // https://platform.openai.com/docs/guides/images-vision\n supportedMimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],\n }\n}\n"],"names":["OpenAI","makeParseableResponseFormat","z","zodResponseFormat","zodObject","name","props","type","json_schema","schema","toJSONSchema","target","strict","content","parse","JSON","openAIResolver","config","apiKey","baseUrl","model","openai","getClient","baseURL","key","resolve","filename","imageThumbnailUrl","locale","modelResponseSchema","object","altText","string","describe","keywords","array","response","chat","completions","max_completion_tokens","messages","role","image_url","url","text","response_format","result","choices","message","parsed","error","success","console","Error","resolveBulk","locales","Object","fromEntries","map","join","results","supportedMimeTypes"],"mappings":"AAIA,OAAOA,YAAY,SAAQ;AAC3B,SAASC,2BAA2B,QAAQ,wBAAuB;AACnE,SAASC,CAAC,QAAQ,MAAK;AA0BvB;;;;;;CAMC,GACD,SAASC,kBACPC,SAAmB,EACnBC,IAAY,EACZC,KAA+E;IAE/E,OAAOL,4BACL;QACEM,MAAM;QACNC,aAAa;YACX,GAAGF,KAAK;YACRD;YACAI,QAAQP,EAAEQ,YAAY,CAACN,WAAW;gBAAEO,QAAQ;YAAU;YACtDC,QAAQ;QACV;IACF,GACA,CAACC,UAAYT,UAAUU,KAAK,CAACC,KAAKD,KAAK,CAACD;AAE5C;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMG,iBAAiB,CAACC;IAC7B,MAAM,EAAEC,MAAM,EAAEC,OAAO,EAAEC,QAAQ,cAAc,EAAE,GAAGH;IAEpD,2EAA2E;IAC3E,0EAA0E;IAC1E,oEAAoE;IACpE,IAAII;IACJ,MAAMC,YAAY,IAAeD,WAAW,IAAIrB,OAAO;YAAEkB;YAAQK,SAASJ;QAAQ;IAElF,OAAO;QACLK,KAAK;QACLC,SAAS,OAAO,EACdC,QAAQ,EACRC,iBAAiB,EACjBC,MAAM,EACc;YACpB,IAAI;gBACF,MAAMC,sBAAsB3B,EAAE4B,MAAM,CAAC;oBACnCC,SAAS7B,EAAE8B,MAAM,GAAGC,QAAQ,CAAC;oBAC7BC,UAAUhC,EAAEiC,KAAK,CAACjC,EAAE8B,MAAM,IAAIC,QAAQ,CAAC;gBACzC;gBAEA,MAAMG,WAAW,MAAMd,YAAYe,IAAI,CAACC,WAAW,CAACxB,KAAK,CAAC;oBACxDyB,uBAAuB;oBACvBC,UAAU;wBACR;4BACE3B,SAAS,CAAC;;;;;;;;;2EASmD,EAAEe,OAAO;UAC1E,CAAC;4BACGa,MAAM;wBACR;wBACA;4BACE5B,SAAS;gCACP;oCACEN,MAAM;oCACNmC,WAAW;wCAAEC,KAAKhB;oCAAkB;gCACtC;mCACID,WACA;oCACE;wCACEnB,MAAM;wCACNqC,MAAMlB;oCACR;iCACD,GACD,EAAE;6BACP;4BACDe,MAAM;wBACR;qBACD;oBACDrB;oBACAyB,iBAAiB1C,kBAAkB0B,qBAAqB;gBAC1D;gBAEA,MAAMiB,SAASV,SAASW,OAAO,CAAC,EAAE,EAAEC,SAASC;gBAE7C,IAAI,CAACH,QAAQ;oBACX,OAAO;wBAAEI,OAAO;wBAAyBC,SAAS;oBAAM;gBAC1D;gBAEA,OAAO;oBACLL;oBACAK,SAAS;gBACX;YACF,EAAE,OAAOD,OAAO;gBACdE,QAAQF,KAAK,CAAC,8BAA8BA;gBAC5C,OAAO;oBACLA,OAAOA,iBAAiBG,QAAQH,MAAMF,OAAO,GAAG;oBAChDG,SAAS;gBACX;YACF;QACF;QACAG,aAAa,OAAO,EAClB5B,QAAQ,EACRC,iBAAiB,EACjB4B,OAAO,EACiB;YACxB,IAAI;gBACF,MAAM1B,sBAAsB3B,EAAE4B,MAAM,CAClC0B,OAAOC,WAAW,CAChBF,QAAQG,GAAG,CAAC,CAAC9B,SAAW;wBACtBA;wBACA1B,EAAE4B,MAAM,CAAC;4BACPC,SAAS7B,EAAE8B,MAAM,GAAGC,QAAQ,CAAC;4BAC7BC,UAAUhC,EACPiC,KAAK,CAACjC,EAAE8B,MAAM,IACdC,QAAQ,CAAC;wBACd;qBACD;gBAIL,MAAMG,WAAW,MAAMd,YAAYe,IAAI,CAACC,WAAW,CAACxB,KAAK,CAAC;oBACxDyB,uBAAuB;oBACvBC,UAAU;wBACR;4BACE3B,SAAS,CAAC;;;kEAG0C,EAAE0C,QAAQI,IAAI,CAAC,MAAM;;;;;;iDAMtC,EAAEJ,QAAQI,IAAI,CAAC,MAAM;IAClE,CAAC;4BACSlB,MAAM;wBACR;wBACA;4BACE5B,SAAS;gCACP;oCACEN,MAAM;oCACNmC,WAAW;wCAAEC,KAAKhB;oCAAkB;gCACtC;mCACID,WACA;oCACE;wCACEnB,MAAM;wCACNqC,MAAMlB;oCACR;iCACD,GACD,EAAE;6BACP;4BACDe,MAAM;wBACR;qBACD;oBACDrB;oBACAyB,iBAAiB1C,kBAAkB0B,qBAAqB;gBAC1D;gBAEA,MAAMiB,SAASV,SAASW,OAAO,CAAC,EAAE,EAAEC,SAASC;gBAE7C,IAAI,CAACH,QAAQ;oBACX,OAAO;wBAAEI,OAAO;wBAAyBC,SAAS;oBAAM;gBAC1D;gBAEA,OAAO;oBACLS,SAASd;oBACTK,SAAS;gBACX;YACF,EAAE,OAAOD,OAAO;gBACdE,QAAQF,KAAK,CAAC,mCAAmCA;gBACjD,OAAO;oBACLA,OAAOA,iBAAiBG,QAAQH,MAAMF,OAAO,GAAG;oBAChDG,SAAS;gBACX;YACF;QACF;QACA,wDAAwD;QACxDU,oBAAoB;YAAC;YAAc;YAAa;YAAa;SAAa;IAC5E;AACF,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../src/resolvers/openAI.ts"],"sourcesContent":["import type { VisionInstructions } from './createVisionResolver.js'\nimport type { AltTextResolver } from './types.js'\n\nimport { createVisionResolver, VisionProviderError } from './createVisionResolver.js'\n\nexport type OpenAIResolverConfig = {\n /** OpenAI API key for authentication */\n apiKey: string\n /**\n * Base URL for the OpenAI-compatible API, including the version segment.\n * Use this to point at alternative providers (e.g. Azure, Nebius, local inference).\n * @default 'https://api.openai.com/v1'\n */\n baseUrl?: string\n /**\n * Builds the instructions from the default ones, e.g. to append a house style\n * rule. Sent as the system message, separately from the image.\n *\n * @default ({ defaultInstructions }) => defaultInstructions\n */\n instructions?: VisionInstructions\n /**\n * The OpenAI LLM model to use for alt text generation.\n * @default 'gpt-4.1-nano'\n */\n model?: string\n /**\n * The MIME types the provider accepts for the image URL.\n *\n * Defaults to the formats documented for OpenAI's vision models. Override it\n * when pointing `baseUrl` at another provider whose accepted formats differ —\n * the person choosing the provider is the one who knows.\n *\n * @default ['image/jpeg', 'image/png', 'image/gif', 'image/webp']\n */\n supportedMimeTypes?: string[]\n /**\n * Abort after this many milliseconds, covering the completion call and the\n * retries the factory makes within it.\n * @default 30000\n */\n timeoutMs?: number\n}\n\n/** @see https://platform.openai.com/docs/guides/images-vision */\nconst OPENAI_SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']\n\n/**\n * Creates an OpenAI-based resolver for alt text generation.\n *\n * The thumbnail URL is handed to OpenAI, which fetches it itself — so the URL\n * has to be reachable from the public internet. Behind a private bucket or in\n * local development, reach for a resolver that inlines the bytes instead\n * (`mistralResolver`, `anthropicResolver`).\n *\n * @example\n * ```typescript\n * import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'\n *\n * // OpenAI\n * openAIResolver({\n * apiKey: process.env.OPENAI_API_KEY,\n * model: 'gpt-4.1-mini', // optional, defaults to 'gpt-4.1-nano'\n * })\n *\n * // OpenAI-compatible provider (e.g. Nebius)\n * openAIResolver({\n * apiKey: process.env.NEBIUS_API_KEY,\n * baseUrl: 'https://api.tokenfactory.us-central1.nebius.com/v1',\n * model: 'Qwen/Qwen2.5-VL-72B-Instruct',\n * })\n * ```\n */\nexport const openAIResolver = ({\n apiKey,\n baseUrl = 'https://api.openai.com/v1',\n instructions,\n model = 'gpt-4.1-nano',\n supportedMimeTypes = OPENAI_SUPPORTED_MIME_TYPES,\n timeoutMs = 30_000,\n}: OpenAIResolverConfig): AltTextResolver =>\n createVisionResolver({\n apiKey,\n generate: async ({\n filename,\n imageThumbnailUrl,\n instructions: resolvedInstructions,\n maxTokens,\n responseSchema,\n signal,\n }) => {\n const response = await fetch(`${baseUrl}/chat/completions`, {\n body: JSON.stringify({\n max_completion_tokens: maxTokens,\n messages: [\n { content: resolvedInstructions, role: 'system' },\n {\n content: [\n { type: 'image_url', image_url: { url: imageThumbnailUrl } },\n ...(filename ? [{ type: 'text', text: filename }] : []),\n ],\n role: 'user',\n },\n ],\n model,\n response_format: {\n type: 'json_schema',\n json_schema: { name: 'data', schema: responseSchema, strict: true },\n },\n }),\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },\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: 'OpenAI', status: response.status })\n }\n\n const completion = (await response.json()) as {\n choices?: { finish_reason?: string; message?: { content?: unknown } }[]\n }\n const choice = completion.choices?.[0]\n\n // A budget exhausted mid-JSON otherwise reaches JSON.parse and reads as\n // \"Unexpected end of JSON input\" in the admin panel, which tells an editor\n // nothing about what to change.\n if (choice?.finish_reason === 'length') {\n throw new Error(\n `OpenAI ran out of tokens before finishing the alt text (max_completion_tokens: ${maxTokens})`,\n )\n }\n\n const content = choice?.message?.content\n\n if (typeof content !== 'string') {\n throw new Error('No result from OpenAI')\n }\n\n try {\n return JSON.parse(content)\n } catch {\n throw new Error('OpenAI returned a response that was not valid JSON')\n }\n },\n instructions,\n key: 'openai',\n label: 'OpenAI',\n supportedMimeTypes,\n timeoutMs,\n })\n"],"names":["createVisionResolver","VisionProviderError","OPENAI_SUPPORTED_MIME_TYPES","openAIResolver","apiKey","baseUrl","instructions","model","supportedMimeTypes","timeoutMs","generate","filename","imageThumbnailUrl","resolvedInstructions","maxTokens","responseSchema","signal","response","fetch","body","JSON","stringify","max_completion_tokens","messages","content","role","type","image_url","url","text","response_format","json_schema","name","schema","strict","headers","Authorization","method","ok","catch","slice","label","status","completion","json","choice","choices","finish_reason","Error","message","parse","key"],"mappings":"AAGA,SAASA,oBAAoB,EAAEC,mBAAmB,QAAQ,4BAA2B;AAyCrF,+DAA+D,GAC/D,MAAMC,8BAA8B;IAAC;IAAc;IAAa;IAAa;CAAa;AAE1F;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,MAAMC,iBAAiB,CAAC,EAC7BC,MAAM,EACNC,UAAU,2BAA2B,EACrCC,YAAY,EACZC,QAAQ,cAAc,EACtBC,qBAAqBN,2BAA2B,EAChDO,YAAY,MAAM,EACG,GACrBT,qBAAqB;QACnBI;QACAM,UAAU,OAAO,EACfC,QAAQ,EACRC,iBAAiB,EACjBN,cAAcO,oBAAoB,EAClCC,SAAS,EACTC,cAAc,EACdC,MAAM,EACP;YACC,MAAMC,WAAW,MAAMC,MAAM,GAAGb,QAAQ,iBAAiB,CAAC,EAAE;gBAC1Dc,MAAMC,KAAKC,SAAS,CAAC;oBACnBC,uBAAuBR;oBACvBS,UAAU;wBACR;4BAAEC,SAASX;4BAAsBY,MAAM;wBAAS;wBAChD;4BACED,SAAS;gCACP;oCAAEE,MAAM;oCAAaC,WAAW;wCAAEC,KAAKhB;oCAAkB;gCAAE;mCACvDD,WAAW;oCAAC;wCAAEe,MAAM;wCAAQG,MAAMlB;oCAAS;iCAAE,GAAG,EAAE;6BACvD;4BACDc,MAAM;wBACR;qBACD;oBACDlB;oBACAuB,iBAAiB;wBACfJ,MAAM;wBACNK,aAAa;4BAAEC,MAAM;4BAAQC,QAAQlB;4BAAgBmB,QAAQ;wBAAK;oBACpE;gBACF;gBACAC,SAAS;oBAAEC,eAAe,CAAC,OAAO,EAAEhC,QAAQ;oBAAE,gBAAgB;gBAAmB;gBACjFiC,QAAQ;gBACRrB;YACF;YAEA,IAAI,CAACC,SAASqB,EAAE,EAAE;gBAChB,gEAAgE;gBAChE,MAAMnB,OAAO,AAAC,CAAA,MAAMF,SAASY,IAAI,GAAGU,KAAK,CAAC,IAAM,GAAE,EAAGC,KAAK,CAAC,GAAG;gBAE9D,MAAM,IAAIvC,oBAAoB;oBAAEkB;oBAAMsB,OAAO;oBAAUC,QAAQzB,SAASyB,MAAM;gBAAC;YACjF;YAEA,MAAMC,aAAc,MAAM1B,SAAS2B,IAAI;YAGvC,MAAMC,SAASF,WAAWG,OAAO,EAAE,CAAC,EAAE;YAEtC,wEAAwE;YACxE,2EAA2E;YAC3E,gCAAgC;YAChC,IAAID,QAAQE,kBAAkB,UAAU;gBACtC,MAAM,IAAIC,MACR,CAAC,+EAA+E,EAAElC,UAAU,CAAC,CAAC;YAElG;YAEA,MAAMU,UAAUqB,QAAQI,SAASzB;YAEjC,IAAI,OAAOA,YAAY,UAAU;gBAC/B,MAAM,IAAIwB,MAAM;YAClB;YAEA,IAAI;gBACF,OAAO5B,KAAK8B,KAAK,CAAC1B;YACpB,EAAE,OAAM;gBACN,MAAM,IAAIwB,MAAM;YAClB;QACF;QACA1C;QACA6C,KAAK;QACLV,OAAO;QACPjC;QACAC;IACF,GAAE"}
|
|
@@ -14,6 +14,16 @@ export type AltTextResult = {
|
|
|
14
14
|
export type AltTextResolverArgs = {
|
|
15
15
|
/** Optional filename for additional context */
|
|
16
16
|
filename?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The format served at `imageThumbnailUrl`, when the collection declares one
|
|
19
|
+
* via the plugin's `imageThumbnailMimeType` option. Undefined otherwise.
|
|
20
|
+
*
|
|
21
|
+
* Resolvers that hand the URL to the provider can ignore this. Resolvers that
|
|
22
|
+
* inline the bytes need it — Anthropic image blocks require `media_type`,
|
|
23
|
+
* Gemini's `inline_data` requires `mime_type`, and neither can be sniffed from
|
|
24
|
+
* a URL.
|
|
25
|
+
*/
|
|
26
|
+
imageThumbnailMimeType?: string;
|
|
17
27
|
/** URL of the image thumbnail (must be publicly accessible) */
|
|
18
28
|
imageThumbnailUrl: string;
|
|
19
29
|
/** Target locale for the generated alt text */
|
|
@@ -27,6 +37,11 @@ export type AltTextResolverArgs = {
|
|
|
27
37
|
export type AltTextBulkResolverArgs = {
|
|
28
38
|
/** Optional filename for additional context */
|
|
29
39
|
filename?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The format served at `imageThumbnailUrl`, when the collection declares one
|
|
42
|
+
* via `imageThumbnailMimeType`. See {@link AltTextResolverArgs.imageThumbnailMimeType}.
|
|
43
|
+
*/
|
|
44
|
+
imageThumbnailMimeType?: string;
|
|
30
45
|
/** URL of the image thumbnail (must be publicly accessible) */
|
|
31
46
|
imageThumbnailUrl: string;
|
|
32
47
|
/** Target locales for the generated alt texts */
|
|
@@ -65,6 +80,14 @@ export type AltTextResolver = {
|
|
|
65
80
|
resolve: (args: AltTextResolverArgs) => Promise<AltTextResolverResponse>;
|
|
66
81
|
/** Generate alt text for a single image in multiple locales (bulk operation) */
|
|
67
82
|
resolveBulk: (args: AltTextBulkResolverArgs) => Promise<AltTextBulkResolverResponse>;
|
|
68
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Formats the provider accepts for the bytes served at `imageThumbnailUrl`.
|
|
85
|
+
*
|
|
86
|
+
* When set, the endpoints reject documents whose stored `mimeType` is not in
|
|
87
|
+
* this list — a conservative proxy, since the resolver never sees the stored
|
|
88
|
+
* file. A project whose `getImageThumbnail` transcodes should declare the
|
|
89
|
+
* delivered format via the plugin's `imageThumbnailMimeType` option, which
|
|
90
|
+
* replaces the proxy with a one-time check against this list at config load.
|
|
91
|
+
*/
|
|
69
92
|
supportedMimeTypes?: string[];
|
|
70
93
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/resolvers/types.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\n/**\n * Result of generating alt text for a single image.\n */\nexport type AltTextResult = {\n /** Concise descriptive alt text (1-2 sentences) */\n altText: string\n /** Keywords describing the image content */\n keywords: string[]\n}\n\n/**\n * Arguments passed to the resolver for single image generation.\n */\nexport type AltTextResolverArgs = {\n /** Optional filename for additional context */\n filename?: string\n /** URL of the image thumbnail (must be publicly accessible) */\n imageThumbnailUrl: string\n /** Target locale for the generated alt text */\n locale: string\n /** Payload request object for logging */\n req: PayloadRequest\n}\n\n/**\n * Arguments passed to the resolver for bulk/multi-locale generation.\n */\nexport type AltTextBulkResolverArgs = {\n /** Optional filename for additional context */\n filename?: string\n /** URL of the image thumbnail (must be publicly accessible) */\n imageThumbnailUrl: string\n /** Target locales for the generated alt texts */\n locales: string[]\n /** Payload request object for logging */\n req: PayloadRequest\n}\n\n/**\n * Response from single image alt text generation.\n */\nexport type AltTextResolverResponse =\n
|
|
1
|
+
{"version":3,"sources":["../../src/resolvers/types.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\n/**\n * Result of generating alt text for a single image.\n */\nexport type AltTextResult = {\n /** Concise descriptive alt text (1-2 sentences) */\n altText: string\n /** Keywords describing the image content */\n keywords: string[]\n}\n\n/**\n * Arguments passed to the resolver for single image generation.\n */\nexport type AltTextResolverArgs = {\n /** Optional filename for additional context */\n filename?: string\n /**\n * The format served at `imageThumbnailUrl`, when the collection declares one\n * via the plugin's `imageThumbnailMimeType` option. Undefined otherwise.\n *\n * Resolvers that hand the URL to the provider can ignore this. Resolvers that\n * inline the bytes need it — Anthropic image blocks require `media_type`,\n * Gemini's `inline_data` requires `mime_type`, and neither can be sniffed from\n * a URL.\n */\n imageThumbnailMimeType?: string\n /** URL of the image thumbnail (must be publicly accessible) */\n imageThumbnailUrl: string\n /** Target locale for the generated alt text */\n locale: string\n /** Payload request object for logging */\n req: PayloadRequest\n}\n\n/**\n * Arguments passed to the resolver for bulk/multi-locale generation.\n */\nexport type AltTextBulkResolverArgs = {\n /** Optional filename for additional context */\n filename?: string\n /**\n * The format served at `imageThumbnailUrl`, when the collection declares one\n * via `imageThumbnailMimeType`. See {@link AltTextResolverArgs.imageThumbnailMimeType}.\n */\n imageThumbnailMimeType?: string\n /** URL of the image thumbnail (must be publicly accessible) */\n imageThumbnailUrl: string\n /** Target locales for the generated alt texts */\n locales: string[]\n /** Payload request object for logging */\n req: PayloadRequest\n}\n\n/**\n * Response from single image alt text generation.\n */\nexport type AltTextResolverResponse =\n { error?: string; success: false } | { result: AltTextResult; success: true }\n\n/**\n * Response from bulk/multi-locale alt text generation.\n */\nexport type AltTextBulkResolverResponse =\n { error?: string; success: false } | { results: Record<string, AltTextResult>; success: true }\n\n/**\n * Alt text resolver interface.\n * Implement this to create custom resolvers for different providers.\n */\nexport type AltTextResolver = {\n /** Unique key identifying this resolver */\n key: string\n /** Generate alt text for a single image in one locale */\n resolve: (args: AltTextResolverArgs) => Promise<AltTextResolverResponse>\n /** Generate alt text for a single image in multiple locales (bulk operation) */\n resolveBulk: (args: AltTextBulkResolverArgs) => Promise<AltTextBulkResolverResponse>\n /**\n * Formats the provider accepts for the bytes served at `imageThumbnailUrl`.\n *\n * When set, the endpoints reject documents whose stored `mimeType` is not in\n * this list — a conservative proxy, since the resolver never sees the stored\n * file. A project whose `getImageThumbnail` transcodes should declare the\n * delivered format via the plugin's `imageThumbnailMimeType` option, which\n * replaces the proxy with a one-time check against this list at config load.\n */\n supportedMimeTypes?: string[]\n}\n"],"names":[],"mappings":"AAmEA;;;CAGC,GACD,WAiBC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import { de } from './de.js'\nimport { en } from './en.js'\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type GenericTranslationsObject = {\n [key: string]: GenericTranslationsObject | string\n}\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type NestedKeysStripped<T> = T extends object\n ? {\n [K in keyof T]-?: K extends string\n ? T[K] extends object\n ? `${K}:${NestedKeysStripped<T[K]>}`\n : `${StripCountVariants<K>}`\n : never\n }[keyof T]\n : ''\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type StripCountVariants<TKey> = TKey extends\n
|
|
1
|
+
{"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import { de } from './de.js'\nimport { en } from './en.js'\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type GenericTranslationsObject = {\n [key: string]: GenericTranslationsObject | string\n}\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type NestedKeysStripped<T> = T extends object\n ? {\n [K in keyof T]-?: K extends string\n ? T[K] extends object\n ? `${K}:${NestedKeysStripped<T[K]>}`\n : `${StripCountVariants<K>}`\n : never\n }[keyof T]\n : ''\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type StripCountVariants<TKey> = TKey extends\n `${infer Base}_many` | `${infer Base}_one` | `${infer Base}_other`\n ? Base\n : TKey\n\nexport const translations = {\n de,\n en,\n}\n\nexport type PluginAltTextTranslations = GenericTranslationsObject\n\nexport type PluginAltTextTranslationKeys = NestedKeysStripped<PluginAltTextTranslations>\n"],"names":["de","en","translations"],"mappings":"AAAA,SAASA,EAAE,QAAQ,UAAS;AAC5B,SAASC,EAAE,QAAQ,UAAS;AAwB5B,OAAO,MAAMC,eAAe;IAC1BF;IACAC;AACF,EAAC"}
|
|
@@ -1,7 +1,64 @@
|
|
|
1
|
-
import type { Field, PayloadRequest } from 'payload';
|
|
1
|
+
import type { CollectionSlug, Field, PayloadRequest, Where } from 'payload';
|
|
2
2
|
import type { AltTextResolver } from '../resolvers/types.js';
|
|
3
3
|
import type { AltTextCollectionConfig, IncomingCollectionsConfig, NormalizedAltTextCollectionConfig } from '../utilities/mimeTypes.js';
|
|
4
4
|
export type { AltTextCollectionConfig, NormalizedAltTextCollectionConfig };
|
|
5
|
+
/**
|
|
6
|
+
* Builds the thumbnail URL the resolver fetches. Must be publicly reachable.
|
|
7
|
+
*
|
|
8
|
+
* May be async, so the URL can be signed on demand (S3 presigning, short-lived
|
|
9
|
+
* CDN tokens).
|
|
10
|
+
*
|
|
11
|
+
* @param doc The upload document to build the URL for.
|
|
12
|
+
* @param args.collection The slug of the collection `doc` belongs to — use it to
|
|
13
|
+
* build different URLs per collection (e.g. a Cloudinary transformation for one
|
|
14
|
+
* collection and a plain S3 URL for another).
|
|
15
|
+
* @param args.req The request the generation runs under.
|
|
16
|
+
*/
|
|
17
|
+
export type GetImageThumbnail = (doc: Record<string, unknown>, args: {
|
|
18
|
+
collection: CollectionSlug;
|
|
19
|
+
req: PayloadRequest;
|
|
20
|
+
}) => Promise<string> | string;
|
|
21
|
+
/**
|
|
22
|
+
* Narrows the health scan of one collection to a subset of its documents — in a
|
|
23
|
+
* multi-tenant CMS, to the tenant the request is for. The returned constraint is
|
|
24
|
+
* ANDed onto the scan's MIME type filter.
|
|
25
|
+
*
|
|
26
|
+
* Called once per configured collection, so collections that do not carry the
|
|
27
|
+
* constraining field (a media library shared across tenants, say) can return `{}`
|
|
28
|
+
* instead of being queried for a field they do not have.
|
|
29
|
+
*
|
|
30
|
+
* Return `{}` to scan the collection whole.
|
|
31
|
+
*
|
|
32
|
+
* @param args.collection The slug of the collection being scanned.
|
|
33
|
+
* @param args.req The request the scan runs for.
|
|
34
|
+
*/
|
|
35
|
+
export type AltTextHealthBaseFilter = (args: {
|
|
36
|
+
collection: CollectionSlug;
|
|
37
|
+
req: PayloadRequest;
|
|
38
|
+
}) => Promise<Where> | Where;
|
|
39
|
+
/** Configuration of the alt text health feature. */
|
|
40
|
+
export type AltTextHealthCheckConfig = {
|
|
41
|
+
/**
|
|
42
|
+
* Access control for the health report — both the REST endpoint and the dashboard
|
|
43
|
+
* widget, which hides itself when this denies.
|
|
44
|
+
*
|
|
45
|
+
* Use it to restrict the collection-wide report more strictly than the
|
|
46
|
+
* per-document generate endpoints (e.g. to admins).
|
|
47
|
+
*
|
|
48
|
+
* @default the plugin's `access`
|
|
49
|
+
*/
|
|
50
|
+
access?: (args: {
|
|
51
|
+
req: PayloadRequest;
|
|
52
|
+
}) => boolean | Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Narrows what the report counts. See {@link AltTextHealthBaseFilter}.
|
|
55
|
+
*
|
|
56
|
+
* This is not access control: it scopes the aggregate, it does not decide who
|
|
57
|
+
* may see it. Use `access` for that, and note the report is always filtered to
|
|
58
|
+
* the collections the requesting user can read.
|
|
59
|
+
*/
|
|
60
|
+
baseFilter?: AltTextHealthBaseFilter;
|
|
61
|
+
};
|
|
5
62
|
/** Configuration options for the alt text plugin. */
|
|
6
63
|
export type IncomingAltTextPluginConfig = {
|
|
7
64
|
/**
|
|
@@ -36,31 +93,36 @@ export type IncomingAltTextPluginConfig = {
|
|
|
36
93
|
defaultFields: Field[];
|
|
37
94
|
}) => Field[];
|
|
38
95
|
/**
|
|
39
|
-
*
|
|
40
|
-
* This URL will be sent to the LLM for analysis.
|
|
96
|
+
* Builds the image URL sent to the resolver. See {@link GetImageThumbnail}.
|
|
41
97
|
*
|
|
42
98
|
* @remarks
|
|
43
|
-
* -
|
|
44
|
-
* -
|
|
99
|
+
* - Prefer a thumbnail/preview size over the original (e.g. from the sizes field)
|
|
100
|
+
* - When the URL transcodes, declare the delivered format via
|
|
101
|
+
* `imageThumbnailMimeType` so source formats the resolver does not accept are
|
|
102
|
+
* not rejected
|
|
45
103
|
*/
|
|
46
|
-
getImageThumbnail:
|
|
104
|
+
getImageThumbnail: GetImageThumbnail;
|
|
47
105
|
/**
|
|
48
106
|
* Controls the alt text health feature (REST endpoint, cache revalidation hooks, and dashboard widget).
|
|
49
107
|
*
|
|
50
108
|
* - `false` disables the entire feature.
|
|
51
|
-
* - `true` enables it, gated by `access
|
|
52
|
-
* -
|
|
53
|
-
* with that access check — use this to restrict the collection-wide report
|
|
54
|
-
* more strictly than the per-document generate endpoints (e.g. to admins).
|
|
55
|
-
*
|
|
56
|
-
* Regardless of the gate, the report is always filtered to the collections the
|
|
57
|
-
* requesting user can read.
|
|
109
|
+
* - `true` enables it, gated by `access` and covering every document.
|
|
110
|
+
* - An object enables it and configures it. See {@link AltTextHealthCheckConfig}.
|
|
58
111
|
*
|
|
59
112
|
* @default true
|
|
60
113
|
*/
|
|
61
|
-
healthCheck?:
|
|
62
|
-
|
|
63
|
-
|
|
114
|
+
healthCheck?: AltTextHealthCheckConfig | boolean;
|
|
115
|
+
/**
|
|
116
|
+
* The MIME type `getImageThumbnail` delivers, for every configured collection.
|
|
117
|
+
*
|
|
118
|
+
* Declaring it takes a document's stored `mimeType` out of the decision of
|
|
119
|
+
* whether alt text can be generated. Collections may override it or opt out with
|
|
120
|
+
* `null` — see {@link AltTextCollectionConfig.imageThumbnailMimeType} for the
|
|
121
|
+
* rationale and the `f_auto` caveat.
|
|
122
|
+
*
|
|
123
|
+
* @example 'image/webp'
|
|
124
|
+
*/
|
|
125
|
+
imageThumbnailMimeType?: string;
|
|
64
126
|
/**
|
|
65
127
|
* The locale to generate alt texts in when localization is disabled.
|
|
66
128
|
*
|
|
@@ -93,7 +155,11 @@ export type AltTextPluginConfig = {
|
|
|
93
155
|
access: (args: {
|
|
94
156
|
req: PayloadRequest;
|
|
95
157
|
}) => boolean | Promise<boolean>;
|
|
96
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Collections with resolved MIME type filters and resolved delivered thumbnail
|
|
160
|
+
* MIME types. The plugin-level `imageThumbnailMimeType` is folded into these
|
|
161
|
+
* entries during normalization, so this is the only place to read it from.
|
|
162
|
+
*/
|
|
97
163
|
collections: NormalizedAltTextCollectionConfig[];
|
|
98
164
|
/** Whether the plugin is enabled. */
|
|
99
165
|
enabled: boolean;
|
|
@@ -102,13 +168,15 @@ export type AltTextPluginConfig = {
|
|
|
102
168
|
defaultFields: Field[];
|
|
103
169
|
}) => Field[];
|
|
104
170
|
/** Function to get the thumbnail URL of an image document. */
|
|
105
|
-
getImageThumbnail:
|
|
171
|
+
getImageThumbnail: GetImageThumbnail;
|
|
106
172
|
/** Whether alt text health tracking is enabled. */
|
|
107
173
|
healthCheck: boolean;
|
|
108
174
|
/** Access control for the health endpoint. Defaults to `access`. */
|
|
109
175
|
healthCheckAccess: (args: {
|
|
110
176
|
req: PayloadRequest;
|
|
111
177
|
}) => boolean | Promise<boolean>;
|
|
178
|
+
/** Narrows what the health report counts. See {@link AltTextHealthBaseFilter}. */
|
|
179
|
+
healthCheckBaseFilter?: AltTextHealthBaseFilter;
|
|
112
180
|
/** The locale to generate alt texts in when localization is disabled. */
|
|
113
181
|
locale?: string;
|
|
114
182
|
/** The locales to generate alt texts for. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/types/AltTextPluginConfig.ts"],"sourcesContent":["import type { Field, PayloadRequest } from 'payload'\n\nimport type { AltTextResolver } from '../resolvers/types.js'\nimport type {\n AltTextCollectionConfig,\n IncomingCollectionsConfig,\n NormalizedAltTextCollectionConfig,\n} from '../utilities/mimeTypes.js'\n\nexport type { AltTextCollectionConfig, NormalizedAltTextCollectionConfig }\n\n/** Configuration options for the alt text plugin. */\nexport type IncomingAltTextPluginConfig = {\n /**\n * Custom access control for plugin endpoints.\n * Return `true` to allow access, `false` to deny.\n *\n * @default ({ req }) => !!req.user — requires authentication\n */\n access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections to enable the plugin for.\n *\n * Each entry may be a bare collection slug or an object with a `slug` and an\n * optional `mimeTypes` array restricting which MIME types are tracked,\n * validated, and generated. Bare slugs default to `['image/*']`.\n *\n * @example\n * ```typescript\n * collections: [\n * 'images', // shorthand — defaults to ['image/*']\n * { slug: 'media', mimeTypes: ['image/*', 'application/pdf'] },\n * ]\n * ```\n */\n collections: IncomingCollectionsConfig\n\n /** Whether the plugin is enabled. */\n enabled?: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /**\n *
|
|
1
|
+
{"version":3,"sources":["../../src/types/AltTextPluginConfig.ts"],"sourcesContent":["import type { CollectionSlug, Field, PayloadRequest, Where } from 'payload'\n\nimport type { AltTextResolver } from '../resolvers/types.js'\nimport type {\n AltTextCollectionConfig,\n IncomingCollectionsConfig,\n NormalizedAltTextCollectionConfig,\n} from '../utilities/mimeTypes.js'\n\nexport type { AltTextCollectionConfig, NormalizedAltTextCollectionConfig }\n\n/**\n * Builds the thumbnail URL the resolver fetches. Must be publicly reachable.\n *\n * May be async, so the URL can be signed on demand (S3 presigning, short-lived\n * CDN tokens).\n *\n * @param doc The upload document to build the URL for.\n * @param args.collection The slug of the collection `doc` belongs to — use it to\n * build different URLs per collection (e.g. a Cloudinary transformation for one\n * collection and a plain S3 URL for another).\n * @param args.req The request the generation runs under.\n */\nexport type GetImageThumbnail = (\n doc: Record<string, unknown>,\n args: { collection: CollectionSlug; req: PayloadRequest },\n) => Promise<string> | string\n\n/**\n * Narrows the health scan of one collection to a subset of its documents — in a\n * multi-tenant CMS, to the tenant the request is for. The returned constraint is\n * ANDed onto the scan's MIME type filter.\n *\n * Called once per configured collection, so collections that do not carry the\n * constraining field (a media library shared across tenants, say) can return `{}`\n * instead of being queried for a field they do not have.\n *\n * Return `{}` to scan the collection whole.\n *\n * @param args.collection The slug of the collection being scanned.\n * @param args.req The request the scan runs for.\n */\nexport type AltTextHealthBaseFilter = (args: {\n collection: CollectionSlug\n req: PayloadRequest\n}) => Promise<Where> | Where\n\n/** Configuration of the alt text health feature. */\nexport type AltTextHealthCheckConfig = {\n /**\n * Access control for the health report — both the REST endpoint and the dashboard\n * widget, which hides itself when this denies.\n *\n * Use it to restrict the collection-wide report more strictly than the\n * per-document generate endpoints (e.g. to admins).\n *\n * @default the plugin's `access`\n */\n access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Narrows what the report counts. See {@link AltTextHealthBaseFilter}.\n *\n * This is not access control: it scopes the aggregate, it does not decide who\n * may see it. Use `access` for that, and note the report is always filtered to\n * the collections the requesting user can read.\n */\n baseFilter?: AltTextHealthBaseFilter\n}\n\n/** Configuration options for the alt text plugin. */\nexport type IncomingAltTextPluginConfig = {\n /**\n * Custom access control for plugin endpoints.\n * Return `true` to allow access, `false` to deny.\n *\n * @default ({ req }) => !!req.user — requires authentication\n */\n access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections to enable the plugin for.\n *\n * Each entry may be a bare collection slug or an object with a `slug` and an\n * optional `mimeTypes` array restricting which MIME types are tracked,\n * validated, and generated. Bare slugs default to `['image/*']`.\n *\n * @example\n * ```typescript\n * collections: [\n * 'images', // shorthand — defaults to ['image/*']\n * { slug: 'media', mimeTypes: ['image/*', 'application/pdf'] },\n * ]\n * ```\n */\n collections: IncomingCollectionsConfig\n\n /** Whether the plugin is enabled. */\n enabled?: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /**\n * Builds the image URL sent to the resolver. See {@link GetImageThumbnail}.\n *\n * @remarks\n * - Prefer a thumbnail/preview size over the original (e.g. from the sizes field)\n * - When the URL transcodes, declare the delivered format via\n * `imageThumbnailMimeType` so source formats the resolver does not accept are\n * not rejected\n */\n getImageThumbnail: GetImageThumbnail\n\n /**\n * Controls the alt text health feature (REST endpoint, cache revalidation hooks, and dashboard widget).\n *\n * - `false` disables the entire feature.\n * - `true` enables it, gated by `access` and covering every document.\n * - An object enables it and configures it. See {@link AltTextHealthCheckConfig}.\n *\n * @default true\n */\n healthCheck?: AltTextHealthCheckConfig | boolean\n\n /**\n * The MIME type `getImageThumbnail` delivers, for every configured collection.\n *\n * Declaring it takes a document's stored `mimeType` out of the decision of\n * whether alt text can be generated. Collections may override it or opt out with\n * `null` — see {@link AltTextCollectionConfig.imageThumbnailMimeType} for the\n * rationale and the `f_auto` caveat.\n *\n * @example 'image/webp'\n */\n imageThumbnailMimeType?: string\n\n /**\n * The locale to generate alt texts in when localization is disabled.\n *\n * Required when localization is disabled, ignored when localization is enabled.\n * @example 'en'\n */\n locale?: string\n\n /**\n * Maximum number of concurrent API requests for bulk generate operations.\n *\n * @default 16\n */\n maxBulkGenerateConcurrency?: number\n\n /**\n * Maximum number of image IDs accepted in a single bulk generate request.\n * Requests exceeding this are rejected with `400`. Duplicate IDs are collapsed\n * before the limit is applied, so each image counts once.\n *\n * Raise it for large libraries that need to process more images per request.\n *\n * @default 100\n */\n maxBulkGenerateIds?: number\n\n /** The resolver to use for generating alt text (e.g., openAIResolver) */\n resolver: AltTextResolver\n}\n\n/** Configuration of the alt text plugin after defaults have been applied. */\nexport type AltTextPluginConfig = {\n /** Access control for plugin endpoints. */\n access: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections with resolved MIME type filters and resolved delivered thumbnail\n * MIME types. The plugin-level `imageThumbnailMimeType` is folded into these\n * entries during normalization, so this is the only place to read it from.\n */\n collections: NormalizedAltTextCollectionConfig[]\n\n /** Whether the plugin is enabled. */\n enabled: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /** Function to get the thumbnail URL of an image document. */\n getImageThumbnail: GetImageThumbnail\n\n /** Whether alt text health tracking is enabled. */\n healthCheck: boolean\n\n /** Access control for the health endpoint. Defaults to `access`. */\n healthCheckAccess: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /** Narrows what the health report counts. See {@link AltTextHealthBaseFilter}. */\n healthCheckBaseFilter?: AltTextHealthBaseFilter\n\n /** The locale to generate alt texts in when localization is disabled. */\n locale?: string\n\n /** The locales to generate alt texts for. */\n locales: string[]\n\n /** Maximum number of concurrent API requests for bulk generate operations. */\n maxBulkGenerateConcurrency: number\n\n /** Maximum number of image IDs accepted per bulk generate request. */\n maxBulkGenerateIds: number\n\n /** The resolver to use for generating alt text */\n resolver: AltTextResolver\n}\n"],"names":[],"mappings":"AAuKA,2EAA2E,GAC3E,WA2CC"}
|